...
This commit is contained in:
134
pkg2/heroscript/handlerfactory/rustclients/src/fakehandler.rs
Normal file
134
pkg2/heroscript/handlerfactory/rustclients/src/fakehandler.rs
Normal file
@@ -0,0 +1,134 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::{Client, Result, ClientError};
|
||||
|
||||
/// Response from the fake handler
|
||||
#[derive(Debug, Serialize, Deserialize, Default)]
|
||||
pub struct FakeResponse {
|
||||
#[serde(default)]
|
||||
pub message: String,
|
||||
#[serde(default)]
|
||||
pub status: String,
|
||||
#[serde(default)]
|
||||
pub code: i32,
|
||||
}
|
||||
|
||||
/// Client for the fake handler
|
||||
pub struct FakeHandlerClient {
|
||||
client: Client,
|
||||
}
|
||||
|
||||
impl FakeHandlerClient {
|
||||
/// Create a new fake handler client
|
||||
pub fn new(socket_path: &str) -> Self {
|
||||
Self {
|
||||
client: Client::new(socket_path),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the connection timeout
|
||||
pub fn with_timeout(mut self, timeout: Duration) -> Self {
|
||||
self.client = self.client.with_timeout(timeout);
|
||||
self
|
||||
}
|
||||
|
||||
/// Connect to the server
|
||||
pub fn connect(&self) -> Result<()> {
|
||||
self.client.connect()
|
||||
}
|
||||
|
||||
/// Close the connection
|
||||
pub fn close(&self) -> Result<()> {
|
||||
self.client.close()
|
||||
}
|
||||
|
||||
/// Return a success message
|
||||
pub fn return_success(&self, message: Option<&str>) -> Result<String> {
|
||||
let mut script = "!!fake.return_success".to_string();
|
||||
|
||||
if let Some(msg) = message {
|
||||
script.push_str(&format!(" message:'{}'", msg));
|
||||
}
|
||||
|
||||
self.client.send_command(&script)
|
||||
}
|
||||
|
||||
/// Return an error message
|
||||
pub fn return_error(&self, message: Option<&str>) -> Result<String> {
|
||||
let mut script = "!!fake.return_error".to_string();
|
||||
|
||||
if let Some(msg) = message {
|
||||
script.push_str(&format!(" message:'{}'", msg));
|
||||
}
|
||||
|
||||
// This will return a ClientError::ServerError with the error message
|
||||
self.client.send_command(&script)
|
||||
}
|
||||
|
||||
/// Return a JSON response
|
||||
pub fn return_json(&self, message: Option<&str>, status: Option<&str>, code: Option<i32>) -> Result<FakeResponse> {
|
||||
let mut script = "!!fake.return_json".to_string();
|
||||
|
||||
if let Some(msg) = message {
|
||||
script.push_str(&format!(" message:'{}'", msg));
|
||||
}
|
||||
|
||||
if let Some(status_val) = status {
|
||||
script.push_str(&format!(" status:'{}'", status_val));
|
||||
}
|
||||
|
||||
if let Some(code_val) = code {
|
||||
script.push_str(&format!(" code:{}", code_val));
|
||||
}
|
||||
|
||||
let response = self.client.send_command(&script)?;
|
||||
|
||||
// Parse the JSON response
|
||||
match serde_json::from_str::<FakeResponse>(&response) {
|
||||
Ok(result) => Ok(result),
|
||||
Err(e) => Err(ClientError::JsonError(e)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return an invalid JSON response
|
||||
pub fn return_invalid_json(&self) -> Result<FakeResponse> {
|
||||
let script = "!!fake.return_invalid_json";
|
||||
let response = self.client.send_command(&script)?;
|
||||
|
||||
// This should fail with a JSON parsing error
|
||||
match serde_json::from_str::<FakeResponse>(&response) {
|
||||
Ok(result) => Ok(result),
|
||||
Err(e) => Err(ClientError::JsonError(e)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return an empty response
|
||||
pub fn return_empty(&self) -> Result<String> {
|
||||
let script = "!!fake.return_empty";
|
||||
self.client.send_command(&script)
|
||||
}
|
||||
|
||||
/// Return a large response
|
||||
pub fn return_large(&self, size: Option<i32>) -> Result<String> {
|
||||
let mut script = "!!fake.return_large".to_string();
|
||||
|
||||
if let Some(size_val) = size {
|
||||
script.push_str(&format!(" size:{}", size_val));
|
||||
}
|
||||
|
||||
self.client.send_command(&script)
|
||||
}
|
||||
|
||||
/// Return a malformed error message
|
||||
pub fn return_malformed_error(&self) -> Result<String> {
|
||||
let script = "!!fake.return_malformed_error";
|
||||
self.client.send_command(&script)
|
||||
}
|
||||
|
||||
/// Get help information
|
||||
pub fn help(&self) -> Result<String> {
|
||||
let script = "!!fake.help";
|
||||
self.client.send_command(&script)
|
||||
}
|
||||
}
|
242
pkg2/heroscript/handlerfactory/rustclients/src/lib.rs
Normal file
242
pkg2/heroscript/handlerfactory/rustclients/src/lib.rs
Normal file
@@ -0,0 +1,242 @@
|
||||
use std::io::{Read, Write};
|
||||
use std::os::unix::net::UnixStream;
|
||||
use std::time::Duration;
|
||||
use thiserror::Error;
|
||||
use std::fmt;
|
||||
use std::error::Error as StdError;
|
||||
|
||||
mod processmanager;
|
||||
mod fakehandler;
|
||||
|
||||
pub use processmanager::ProcessManagerClient;
|
||||
pub use fakehandler::FakeHandlerClient;
|
||||
|
||||
/// Standard error response from the telnet server
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ServerError {
|
||||
pub message: String,
|
||||
pub raw_response: String,
|
||||
}
|
||||
|
||||
impl fmt::Display for ServerError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.message)
|
||||
}
|
||||
}
|
||||
|
||||
impl StdError for ServerError {}
|
||||
|
||||
/// Error type for the client
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ClientError {
|
||||
#[error("IO error: {0}")]
|
||||
IoError(#[from] std::io::Error),
|
||||
|
||||
#[error("JSON parsing error: {0}")]
|
||||
JsonError(#[from] serde_json::Error),
|
||||
|
||||
#[error("Connection error: {0}")]
|
||||
ConnectionError(String),
|
||||
|
||||
#[error("Command error: {0}")]
|
||||
CommandError(String),
|
||||
|
||||
#[error("Server error: {0}")]
|
||||
ServerError(String),
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, ClientError>;
|
||||
|
||||
/// A client for connecting to a Unix socket server with improved error handling
|
||||
pub struct Client {
|
||||
socket_path: String,
|
||||
timeout: Duration,
|
||||
secret: Option<String>,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
/// Create a new Unix socket client
|
||||
pub fn new(socket_path: &str) -> Self {
|
||||
Self {
|
||||
socket_path: socket_path.to_string(),
|
||||
timeout: Duration::from_secs(10),
|
||||
secret: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the connection timeout
|
||||
pub fn with_timeout(mut self, timeout: Duration) -> Self {
|
||||
self.timeout = timeout;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the authentication secret
|
||||
pub fn with_secret(mut self, secret: &str) -> Self {
|
||||
self.secret = Some(secret.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
/// Connect to the Unix socket and return the stream
|
||||
fn connect_socket(&self) -> Result<UnixStream> {
|
||||
println!("DEBUG: Opening new connection to {}", self.socket_path);
|
||||
// Connect to the socket
|
||||
let stream = UnixStream::connect(&self.socket_path)
|
||||
.map_err(|e| ClientError::ConnectionError(format!("Failed to connect to socket {}: {}", self.socket_path, e)))?;
|
||||
|
||||
// Set read timeout
|
||||
stream.set_read_timeout(Some(self.timeout))?;
|
||||
stream.set_write_timeout(Some(self.timeout))?;
|
||||
|
||||
// Read welcome message
|
||||
let mut buffer = [0; 4096];
|
||||
match stream.try_clone()?.read(&mut buffer) {
|
||||
Ok(n) => {
|
||||
let welcome = String::from_utf8_lossy(&buffer[0..n]);
|
||||
if !welcome.contains("Welcome") {
|
||||
return Err(ClientError::ConnectionError("Invalid welcome message".to_string()));
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
return Err(ClientError::IoError(e));
|
||||
}
|
||||
}
|
||||
|
||||
// Authenticate if a secret is provided
|
||||
if let Some(secret) = &self.secret {
|
||||
self.authenticate_stream(&stream, secret)?;
|
||||
}
|
||||
|
||||
Ok(stream)
|
||||
}
|
||||
|
||||
/// Authenticate with the server using the provided stream
|
||||
fn authenticate_stream(&self, stream: &UnixStream, secret: &str) -> Result<()> {
|
||||
let mut stream_clone = stream.try_clone()?;
|
||||
let auth_command = format!("auth {}\n\n", secret);
|
||||
|
||||
// Send the auth command
|
||||
stream_clone.write_all(auth_command.as_bytes())
|
||||
.map_err(|e| ClientError::CommandError(format!("Failed to send auth command: {}", e)))?;
|
||||
stream_clone.flush()
|
||||
.map_err(|e| ClientError::CommandError(format!("Failed to flush auth command: {}", e)))?;
|
||||
|
||||
// Add a small delay to ensure the server has time to process the command
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
|
||||
// Read the response
|
||||
let mut buffer = [0; 4096];
|
||||
let n = stream_clone.read(&mut buffer)
|
||||
.map_err(|e| ClientError::CommandError(format!("Failed to read auth response: {}", e)))?;
|
||||
|
||||
if n == 0 {
|
||||
return Err(ClientError::ConnectionError("Connection closed by server during authentication".to_string()));
|
||||
}
|
||||
|
||||
let response = String::from_utf8_lossy(&buffer[0..n]).to_string();
|
||||
|
||||
// Check for authentication success
|
||||
if response.contains("Authentication successful") || response.contains("authenticated") {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ClientError::ServerError(format!("Authentication failed: {}", response)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Send a command to the server and get the response
|
||||
pub fn send_command(&self, command: &str) -> Result<String> {
|
||||
// Connect to the socket for this command
|
||||
let mut stream = self.connect_socket()?;
|
||||
|
||||
// Ensure command ends with double newlines to execute it
|
||||
let command = if command.ends_with("\n\n") {
|
||||
command.to_string()
|
||||
} else if command.ends_with('\n') {
|
||||
format!("{}\n", command)
|
||||
} else {
|
||||
format!("{}\n\n", command)
|
||||
};
|
||||
|
||||
// Send the command
|
||||
stream.write_all(command.as_bytes())
|
||||
.map_err(|e| ClientError::CommandError(format!("Failed to send command: {}", e)))?;
|
||||
stream.flush()
|
||||
.map_err(|e| ClientError::CommandError(format!("Failed to flush command: {}", e)))?;
|
||||
|
||||
// Add a small delay to ensure the server has time to process the command
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
|
||||
// Read the response
|
||||
let mut buffer = [0; 8192]; // Use a larger buffer for large responses
|
||||
let n = stream.read(&mut buffer)
|
||||
.map_err(|e| ClientError::CommandError(format!("Failed to read response: {}", e)))?;
|
||||
|
||||
if n == 0 {
|
||||
return Err(ClientError::ConnectionError("Connection closed by server".to_string()));
|
||||
}
|
||||
|
||||
let response = String::from_utf8_lossy(&buffer[0..n]).to_string();
|
||||
|
||||
// Remove the prompt if present
|
||||
let response = response.trim_end_matches("> ").trim().to_string();
|
||||
|
||||
// Check for standard error format
|
||||
if response.starts_with("Error:") {
|
||||
return Err(ClientError::ServerError(response));
|
||||
}
|
||||
|
||||
// Close the connection by dropping the stream
|
||||
println!("DEBUG: Closing connection to {}", self.socket_path);
|
||||
drop(stream);
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
/// Send a command and parse the JSON response
|
||||
pub fn send_command_json<T: serde::de::DeserializeOwned>(&self, command: &str) -> Result<T> {
|
||||
let response = self.send_command(command)?;
|
||||
|
||||
// If the response is empty, return an error
|
||||
if response.trim().is_empty() {
|
||||
return Err(ClientError::CommandError("Empty response".to_string()));
|
||||
}
|
||||
|
||||
// Handle "action not supported" errors specially
|
||||
if response.contains("action not supported") {
|
||||
return Err(ClientError::ServerError(response));
|
||||
}
|
||||
|
||||
// Try to parse the JSON response
|
||||
match serde_json::from_str::<T>(&response) {
|
||||
Ok(result) => Ok(result),
|
||||
Err(e) => {
|
||||
// If parsing fails, check if it's an error message
|
||||
if response.starts_with("Error:") || response.contains("error") || response.contains("failed") {
|
||||
Err(ClientError::ServerError(response))
|
||||
} else {
|
||||
Err(ClientError::JsonError(e))
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// For backward compatibility
|
||||
pub fn connect(&self) -> Result<()> {
|
||||
// Just verify we can connect
|
||||
let stream = self.connect_socket()?;
|
||||
drop(stream);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// For backward compatibility
|
||||
pub fn close(&self) -> Result<()> {
|
||||
// No-op since we don't maintain a persistent connection
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Authenticate with the server - kept for backward compatibility
|
||||
pub fn authenticate(&self, secret: &str) -> Result<()> {
|
||||
// Create a temporary connection to authenticate
|
||||
let stream = self.connect_socket()?;
|
||||
self.authenticate_stream(&stream, secret)
|
||||
}
|
||||
}
|
164
pkg2/heroscript/handlerfactory/rustclients/src/processmanager.rs
Normal file
164
pkg2/heroscript/handlerfactory/rustclients/src/processmanager.rs
Normal file
@@ -0,0 +1,164 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::{Client, Result};
|
||||
|
||||
/// Information about a process
|
||||
#[derive(Debug, Serialize, Deserialize, Default)]
|
||||
pub struct ProcessInfo {
|
||||
#[serde(default)]
|
||||
pub name: String,
|
||||
#[serde(default)]
|
||||
pub command: String,
|
||||
#[serde(default)]
|
||||
pub status: String,
|
||||
#[serde(default)]
|
||||
pub pid: i32,
|
||||
#[serde(default)]
|
||||
pub start_time: String,
|
||||
#[serde(default)]
|
||||
pub uptime: String,
|
||||
#[serde(default)]
|
||||
pub cpu: String,
|
||||
#[serde(default)]
|
||||
pub memory: String,
|
||||
#[serde(default)]
|
||||
pub cron: Option<String>,
|
||||
#[serde(default)]
|
||||
pub job_id: Option<String>,
|
||||
}
|
||||
|
||||
/// Client for the process manager
|
||||
pub struct ProcessManagerClient {
|
||||
client: Client,
|
||||
}
|
||||
|
||||
impl ProcessManagerClient {
|
||||
/// Create a new process manager client
|
||||
pub fn new(socket_path: &str) -> Self {
|
||||
Self {
|
||||
client: Client::new(socket_path),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the connection timeout
|
||||
pub fn with_timeout(mut self, timeout: Duration) -> Self {
|
||||
self.client = self.client.with_timeout(timeout);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the authentication secret
|
||||
pub fn with_secret(mut self, secret: &str) -> Self {
|
||||
self.client = self.client.with_secret(secret);
|
||||
self
|
||||
}
|
||||
|
||||
/// Connect to the server
|
||||
pub fn connect(&self) -> Result<()> {
|
||||
self.client.connect()
|
||||
}
|
||||
|
||||
/// Close the connection
|
||||
pub fn close(&self) -> Result<()> {
|
||||
self.client.close()
|
||||
}
|
||||
|
||||
/// Start a new process
|
||||
pub fn start(&self, name: &str, command: &str, log_enabled: bool, deadline: Option<i32>, cron: Option<&str>, job_id: Option<&str>) -> Result<String> {
|
||||
let mut script = format!("!!process.start name:'{}' command:'{}' log:{}", name, command, log_enabled);
|
||||
|
||||
if let Some(deadline_val) = deadline {
|
||||
script.push_str(&format!(" deadline:{}", deadline_val));
|
||||
}
|
||||
|
||||
if let Some(cron_val) = cron {
|
||||
script.push_str(&format!(" cron:'{}'", cron_val));
|
||||
}
|
||||
|
||||
if let Some(job_id_val) = job_id {
|
||||
script.push_str(&format!(" job_id:'{}'", job_id_val));
|
||||
}
|
||||
|
||||
self.client.send_command(&script)
|
||||
}
|
||||
|
||||
/// Stop a running process
|
||||
pub fn stop(&self, name: &str) -> Result<String> {
|
||||
let script = format!("!!process.stop name:'{}'", name);
|
||||
self.client.send_command(&script)
|
||||
}
|
||||
|
||||
/// Restart a process
|
||||
pub fn restart(&self, name: &str) -> Result<String> {
|
||||
let script = format!("!!process.restart name:'{}'", name);
|
||||
self.client.send_command(&script)
|
||||
}
|
||||
|
||||
/// Delete a process
|
||||
pub fn delete(&self, name: &str) -> Result<String> {
|
||||
let script = format!("!!process.delete name:'{}'", name);
|
||||
self.client.send_command(&script)
|
||||
}
|
||||
|
||||
/// List all processes
|
||||
pub fn list(&self) -> Result<Vec<ProcessInfo>> {
|
||||
let script = "!!process.list format:'json'";
|
||||
let response = self.client.send_command(&script)?;
|
||||
|
||||
// Handle empty responses
|
||||
if response.trim().is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
// Try to parse the response as JSON
|
||||
match serde_json::from_str::<Vec<ProcessInfo>>(&response) {
|
||||
Ok(processes) => Ok(processes),
|
||||
Err(_) => {
|
||||
// If parsing as a list fails, try parsing as a single ProcessInfo
|
||||
match serde_json::from_str::<ProcessInfo>(&response) {
|
||||
Ok(process) => Ok(vec![process]),
|
||||
Err(_) => {
|
||||
// If both parsing attempts fail, check if it's a "No processes found" message
|
||||
if response.contains("No processes found") {
|
||||
Ok(Vec::new())
|
||||
} else {
|
||||
// Otherwise, try to send it as JSON
|
||||
self.client.send_command_json(&script)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the status of a specific process
|
||||
pub fn status(&self, name: &str) -> Result<ProcessInfo> {
|
||||
let script = format!("!!process.status name:'{}' format:'json'", name);
|
||||
|
||||
// Use the send_command_json method which handles JSON parsing with better error handling
|
||||
self.client.send_command_json(&script)
|
||||
}
|
||||
|
||||
/// Get the logs of a specific process
|
||||
pub fn logs(&self, name: &str, lines: Option<i32>) -> Result<String> {
|
||||
let mut script = format!("!!process.logs name:'{}'", name);
|
||||
|
||||
if let Some(lines_val) = lines {
|
||||
script.push_str(&format!(" lines:{}", lines_val));
|
||||
}
|
||||
|
||||
self.client.send_command(&script)
|
||||
}
|
||||
|
||||
/// Set the logs path for the process manager
|
||||
pub fn set_logs_path(&self, path: &str) -> Result<String> {
|
||||
let script = format!("!!process.set_logs_path path:'{}'", path);
|
||||
self.client.send_command(&script)
|
||||
}
|
||||
|
||||
/// Get help information for the process manager
|
||||
pub fn help(&self) -> Result<String> {
|
||||
let script = "!!process.help";
|
||||
self.client.send_command(&script)
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user