This commit is contained in:
2025-05-23 15:28:30 +04:00
parent 92b9c356b8
commit 0e545e56de
144 changed files with 294 additions and 1907 deletions

View 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)
}
}

View 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)
}
}

View 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)
}
}