Compare commits
	
		
			2 Commits
		
	
	
		
			ef8cc74d2b
			...
			46ad848e7e
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 46ad848e7e | ||
|  | b4e370b668 | 
							
								
								
									
										22
									
								
								service_manager/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								service_manager/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | ||||
| [package] | ||||
| name = "sal-service-manager" | ||||
| version = "0.1.0" | ||||
| edition = "2021" | ||||
|  | ||||
| [dependencies] | ||||
| async-trait = "0.1" | ||||
| thiserror = "1.0" | ||||
| tokio = { workspace = true } | ||||
| log = { workspace = true } | ||||
| serde = { workspace = true } | ||||
| serde_json = { workspace = true, optional = true } | ||||
|  | ||||
| zinit_client = { package = "sal-zinit-client", path = "../zinit_client", optional = true } | ||||
|  | ||||
| [target.'cfg(target_os = "macos")'.dependencies] | ||||
| # macOS-specific dependencies for launchctl | ||||
| plist = "1.6" | ||||
|  | ||||
| [features] | ||||
| default = [] | ||||
| zinit = ["dep:zinit_client", "dep:serde_json"] | ||||
							
								
								
									
										54
									
								
								service_manager/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								service_manager/README.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,54 @@ | ||||
| # Service Manager | ||||
|  | ||||
| This crate provides a unified interface for managing system services across different platforms. | ||||
| It abstracts the underlying service management system (like `launchctl` on macOS or `systemd` on Linux), | ||||
| allowing you to start, stop, and monitor services with a consistent API. | ||||
|  | ||||
| ## Features | ||||
|  | ||||
| - A `ServiceManager` trait defining a common interface for service operations. | ||||
| - Platform-specific implementations for: | ||||
|   - macOS (`launchctl`) | ||||
|   - Linux (`systemd`) | ||||
| - A factory function `create_service_manager` that returns the appropriate manager for the current platform. | ||||
|  | ||||
| ## Usage | ||||
|  | ||||
| Add this to your `Cargo.toml`: | ||||
|  | ||||
| ```toml | ||||
| [dependencies] | ||||
| service_manager = { path = "../service_manager" } | ||||
| ``` | ||||
|  | ||||
| Here is an example of how to use the `ServiceManager`: | ||||
|  | ||||
| ```rust,no_run | ||||
| use service_manager::{create_service_manager, ServiceConfig}; | ||||
| use std::collections::HashMap; | ||||
|  | ||||
| fn main() -> Result<(), Box<dyn std::error::Error>> { | ||||
|     let service_manager = create_service_manager(); | ||||
|  | ||||
|     let config = ServiceConfig { | ||||
|         name: "my-service".to_string(), | ||||
|         binary_path: "/usr/local/bin/my-service-executable".to_string(), | ||||
|         args: vec!["--config".to_string(), "/etc/my-service.conf".to_string()], | ||||
|         working_directory: Some("/var/tmp".to_string()), | ||||
|         environment: HashMap::new(), | ||||
|         auto_restart: true, | ||||
|     }; | ||||
|  | ||||
|     // Start a new service | ||||
|     service_manager.start(&config)?; | ||||
|  | ||||
|     // Get the status of the service | ||||
|     let status = service_manager.status("my-service")?; | ||||
|     println!("Service status: {:?}", status); | ||||
|  | ||||
|     // Stop the service | ||||
|     service_manager.stop("my-service")?; | ||||
|  | ||||
|     Ok(()) | ||||
| } | ||||
| ``` | ||||
							
								
								
									
										399
									
								
								service_manager/src/launchctl.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										399
									
								
								service_manager/src/launchctl.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,399 @@ | ||||
| use crate::{ServiceConfig, ServiceManager, ServiceManagerError, ServiceStatus}; | ||||
| use async_trait::async_trait; | ||||
| use serde::{Deserialize, Serialize}; | ||||
| use std::collections::HashMap; | ||||
| use std::path::PathBuf; | ||||
| use tokio::process::Command; | ||||
|  | ||||
| #[derive(Debug)] | ||||
| pub struct LaunchctlServiceManager { | ||||
|     service_prefix: String, | ||||
| } | ||||
|  | ||||
| #[derive(Serialize, Deserialize)] | ||||
| struct LaunchDaemon { | ||||
|     #[serde(rename = "Label")] | ||||
|     label: String, | ||||
|     #[serde(rename = "ProgramArguments")] | ||||
|     program_arguments: Vec<String>, | ||||
|     #[serde(rename = "WorkingDirectory", skip_serializing_if = "Option::is_none")] | ||||
|     working_directory: Option<String>, | ||||
|     #[serde(rename = "EnvironmentVariables", skip_serializing_if = "Option::is_none")] | ||||
|     environment_variables: Option<HashMap<String, String>>, | ||||
|     #[serde(rename = "KeepAlive", skip_serializing_if = "Option::is_none")] | ||||
|     keep_alive: Option<bool>, | ||||
|     #[serde(rename = "RunAtLoad")] | ||||
|     run_at_load: bool, | ||||
|     #[serde(rename = "StandardOutPath", skip_serializing_if = "Option::is_none")] | ||||
|     standard_out_path: Option<String>, | ||||
|     #[serde(rename = "StandardErrorPath", skip_serializing_if = "Option::is_none")] | ||||
|     standard_error_path: Option<String>, | ||||
| } | ||||
|  | ||||
| impl LaunchctlServiceManager { | ||||
|     pub fn new() -> Self { | ||||
|         Self { | ||||
|             service_prefix: "tf.ourworld.circles".to_string(), | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fn get_service_label(&self, service_name: &str) -> String { | ||||
|         format!("{}.{}", self.service_prefix, service_name) | ||||
|     } | ||||
|  | ||||
|     fn get_plist_path(&self, service_name: &str) -> PathBuf { | ||||
|         let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string()); | ||||
|         PathBuf::from(home) | ||||
|             .join("Library") | ||||
|             .join("LaunchAgents") | ||||
|             .join(format!("{}.plist", self.get_service_label(service_name))) | ||||
|     } | ||||
|  | ||||
|     fn get_log_path(&self, service_name: &str) -> PathBuf { | ||||
|         let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string()); | ||||
|         PathBuf::from(home) | ||||
|             .join("Library") | ||||
|             .join("Logs") | ||||
|             .join("circles") | ||||
|             .join(format!("{}.log", service_name)) | ||||
|     } | ||||
|  | ||||
|     async fn create_plist(&self, config: &ServiceConfig) -> Result<(), ServiceManagerError> { | ||||
|         let label = self.get_service_label(&config.name); | ||||
|         let plist_path = self.get_plist_path(&config.name); | ||||
|         let log_path = self.get_log_path(&config.name); | ||||
|  | ||||
|         // Ensure the LaunchAgents directory exists | ||||
|         if let Some(parent) = plist_path.parent() { | ||||
|             tokio::fs::create_dir_all(parent).await?; | ||||
|         } | ||||
|  | ||||
|         // Ensure the logs directory exists | ||||
|         if let Some(parent) = log_path.parent() { | ||||
|             tokio::fs::create_dir_all(parent).await?; | ||||
|         } | ||||
|  | ||||
|         let mut program_arguments = vec![config.binary_path.clone()]; | ||||
|         program_arguments.extend(config.args.clone()); | ||||
|  | ||||
|         let launch_daemon = LaunchDaemon { | ||||
|             label: label.clone(), | ||||
|             program_arguments, | ||||
|             working_directory: config.working_directory.clone(), | ||||
|             environment_variables: if config.environment.is_empty() { | ||||
|                 None | ||||
|             } else { | ||||
|                 Some(config.environment.clone()) | ||||
|             }, | ||||
|             keep_alive: if config.auto_restart { Some(true) } else { None }, | ||||
|             run_at_load: true, | ||||
|             standard_out_path: Some(log_path.to_string_lossy().to_string()), | ||||
|             standard_error_path: Some(log_path.to_string_lossy().to_string()), | ||||
|         }; | ||||
|  | ||||
|         let mut plist_content = Vec::new(); | ||||
|         plist::to_writer_xml(&mut plist_content, &launch_daemon) | ||||
|             .map_err(|e| ServiceManagerError::Other(format!("Failed to serialize plist: {}", e)))?; | ||||
|         let plist_content = String::from_utf8(plist_content) | ||||
|             .map_err(|e| ServiceManagerError::Other(format!("Failed to convert plist to string: {}", e)))?; | ||||
|  | ||||
|         tokio::fs::write(&plist_path, plist_content).await?; | ||||
|  | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     async fn run_launchctl(&self, args: &[&str]) -> Result<String, ServiceManagerError> { | ||||
|         let output = Command::new("launchctl") | ||||
|             .args(args) | ||||
|             .output() | ||||
|             .await?; | ||||
|  | ||||
|         if !output.status.success() { | ||||
|             let stderr = String::from_utf8_lossy(&output.stderr); | ||||
|             return Err(ServiceManagerError::Other(format!( | ||||
|                 "launchctl command failed: {}", | ||||
|                 stderr | ||||
|             ))); | ||||
|         } | ||||
|  | ||||
|         Ok(String::from_utf8_lossy(&output.stdout).to_string()) | ||||
|     } | ||||
|  | ||||
|     async fn wait_for_service_status(&self, service_name: &str, timeout_secs: u64) -> Result<(), ServiceManagerError> { | ||||
|         use tokio::time::{sleep, Duration, timeout}; | ||||
|          | ||||
|         let timeout_duration = Duration::from_secs(timeout_secs); | ||||
|         let poll_interval = Duration::from_millis(500); | ||||
|          | ||||
|         let result = timeout(timeout_duration, async { | ||||
|             loop { | ||||
|                 match self.status(service_name) { | ||||
|                     Ok(ServiceStatus::Running) => { | ||||
|                         return Ok(()); | ||||
|                     } | ||||
|                     Ok(ServiceStatus::Failed) => { | ||||
|                         // Service failed, get error details from logs | ||||
|                         let logs = self.logs(service_name, Some(20)).unwrap_or_default(); | ||||
|                         let error_msg = if logs.is_empty() { | ||||
|                             "Service failed to start (no logs available)".to_string() | ||||
|                         } else { | ||||
|                             // Extract error lines from logs | ||||
|                             let error_lines: Vec<&str> = logs | ||||
|                                 .lines() | ||||
|                                 .filter(|line| line.to_lowercase().contains("error") || line.to_lowercase().contains("failed")) | ||||
|                                 .take(3) | ||||
|                                 .collect(); | ||||
|                              | ||||
|                             if error_lines.is_empty() { | ||||
|                                 format!("Service failed to start. Recent logs:\n{}", | ||||
|                                     logs.lines().rev().take(5).collect::<Vec<_>>().into_iter().rev().collect::<Vec<_>>().join("\n")) | ||||
|                             } else { | ||||
|                                 format!("Service failed to start. Errors:\n{}", error_lines.join("\n")) | ||||
|                             } | ||||
|                         }; | ||||
|                         return Err(ServiceManagerError::StartFailed(service_name.to_string(), error_msg)); | ||||
|                     } | ||||
|                     Ok(ServiceStatus::Stopped) | Ok(ServiceStatus::Unknown) => { | ||||
|                         // Still starting, continue polling | ||||
|                         sleep(poll_interval).await; | ||||
|                     } | ||||
|                     Err(ServiceManagerError::ServiceNotFound(_)) => { | ||||
|                         return Err(ServiceManagerError::ServiceNotFound(service_name.to_string())); | ||||
|                     } | ||||
|                     Err(e) => { | ||||
|                         return Err(e); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         }).await; | ||||
|          | ||||
|         match result { | ||||
|             Ok(Ok(())) => Ok(()), | ||||
|             Ok(Err(e)) => Err(e), | ||||
|             Err(_) => Err(ServiceManagerError::StartFailed( | ||||
|                 service_name.to_string(), | ||||
|                 format!("Service did not start within {} seconds", timeout_secs) | ||||
|             )), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[async_trait] | ||||
| impl ServiceManager for LaunchctlServiceManager { | ||||
|     fn exists(&self, service_name: &str) -> Result<bool, ServiceManagerError> { | ||||
|         let plist_path = self.get_plist_path(service_name); | ||||
|         Ok(plist_path.exists()) | ||||
|     } | ||||
|  | ||||
|     fn start(&self, config: &ServiceConfig) -> Result<(), ServiceManagerError> { | ||||
|         // For synchronous version, we'll use blocking operations | ||||
|         let rt = tokio::runtime::Runtime::new().map_err(|e| ServiceManagerError::Other(e.to_string()))?; | ||||
|         rt.block_on(async { | ||||
|             let label = self.get_service_label(&config.name); | ||||
|              | ||||
|             // Check if service is already loaded | ||||
|             let list_output = self.run_launchctl(&["list"]).await?; | ||||
|             if list_output.contains(&label) { | ||||
|                 return Err(ServiceManagerError::ServiceAlreadyExists(config.name.clone())); | ||||
|             } | ||||
|  | ||||
|             // Create the plist file | ||||
|             self.create_plist(config).await?; | ||||
|  | ||||
|             // Load the service | ||||
|             let plist_path = self.get_plist_path(&config.name); | ||||
|             self.run_launchctl(&["load", &plist_path.to_string_lossy()]) | ||||
|                 .await | ||||
|                 .map_err(|e| ServiceManagerError::StartFailed(config.name.clone(), e.to_string()))?; | ||||
|  | ||||
|             Ok(()) | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     fn start_existing(&self, service_name: &str) -> Result<(), ServiceManagerError> { | ||||
|         let rt = tokio::runtime::Runtime::new().map_err(|e| ServiceManagerError::Other(e.to_string()))?; | ||||
|         rt.block_on(async { | ||||
|             let label = self.get_service_label(service_name); | ||||
|             let plist_path = self.get_plist_path(service_name); | ||||
|              | ||||
|             // Check if plist file exists | ||||
|             if !plist_path.exists() { | ||||
|                 return Err(ServiceManagerError::ServiceNotFound(service_name.to_string())); | ||||
|             } | ||||
|              | ||||
|             // Check if service is already loaded and running | ||||
|             let list_output = self.run_launchctl(&["list"]).await?; | ||||
|             if list_output.contains(&label) { | ||||
|                 // Service is loaded, check if it's running | ||||
|                 match self.status(service_name)? { | ||||
|                     ServiceStatus::Running => { | ||||
|                         return Ok(()); // Already running, nothing to do | ||||
|                     } | ||||
|                     _ => { | ||||
|                         // Service is loaded but not running, try to start it | ||||
|                         self.run_launchctl(&["start", &label]) | ||||
|                             .await | ||||
|                             .map_err(|e| ServiceManagerError::StartFailed(service_name.to_string(), e.to_string()))?; | ||||
|                         return Ok(()); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|              | ||||
|             // Service is not loaded, load it | ||||
|             self.run_launchctl(&["load", &plist_path.to_string_lossy()]) | ||||
|                 .await | ||||
|                 .map_err(|e| ServiceManagerError::StartFailed(service_name.to_string(), e.to_string()))?; | ||||
|  | ||||
|             Ok(()) | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     async fn start_and_confirm(&self, config: &ServiceConfig, timeout_secs: u64) -> Result<(), ServiceManagerError> { | ||||
|         // First start the service | ||||
|         self.start(config)?; | ||||
|          | ||||
|         // Then wait for confirmation | ||||
|         self.wait_for_service_status(&config.name, timeout_secs).await | ||||
|     } | ||||
|  | ||||
|     async fn run(&self, config: &ServiceConfig, timeout_secs: u64) -> Result<(), ServiceManagerError> { | ||||
|         self.start_and_confirm(config, timeout_secs).await | ||||
|     } | ||||
|  | ||||
|     async fn start_existing_and_confirm(&self, service_name: &str, timeout_secs: u64) -> Result<(), ServiceManagerError> { | ||||
|         // First start the existing service | ||||
|         self.start_existing(service_name)?; | ||||
|          | ||||
|         // Then wait for confirmation | ||||
|         self.wait_for_service_status(service_name, timeout_secs).await | ||||
|     } | ||||
|  | ||||
|     fn stop(&self, service_name: &str) -> Result<(), ServiceManagerError> { | ||||
|         let rt = tokio::runtime::Runtime::new().map_err(|e| ServiceManagerError::Other(e.to_string()))?; | ||||
|         rt.block_on(async { | ||||
|             let _label = self.get_service_label(service_name); | ||||
|             let plist_path = self.get_plist_path(service_name); | ||||
|  | ||||
|             // Unload the service | ||||
|             self.run_launchctl(&["unload", &plist_path.to_string_lossy()]) | ||||
|                 .await | ||||
|                 .map_err(|e| ServiceManagerError::StopFailed(service_name.to_string(), e.to_string()))?; | ||||
|  | ||||
|             Ok(()) | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     fn restart(&self, service_name: &str) -> Result<(), ServiceManagerError> { | ||||
|         // For launchctl, we stop and start | ||||
|         if let Err(e) = self.stop(service_name) { | ||||
|             // If stop fails because service doesn't exist, that's ok for restart | ||||
|             if !matches!(e, ServiceManagerError::ServiceNotFound(_)) { | ||||
|                 return Err(ServiceManagerError::RestartFailed(service_name.to_string(), e.to_string())); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // We need the config to restart, but we don't have it stored | ||||
|         // For now, return an error - in a real implementation we might store configs | ||||
|         Err(ServiceManagerError::RestartFailed( | ||||
|             service_name.to_string(), | ||||
|             "Restart requires re-providing service configuration".to_string(), | ||||
|         )) | ||||
|     } | ||||
|  | ||||
|     fn status(&self, service_name: &str) -> Result<ServiceStatus, ServiceManagerError> { | ||||
|         let rt = tokio::runtime::Runtime::new().map_err(|e| ServiceManagerError::Other(e.to_string()))?; | ||||
|         rt.block_on(async { | ||||
|             let label = self.get_service_label(service_name); | ||||
|             let plist_path = self.get_plist_path(service_name); | ||||
|              | ||||
|             // First check if the plist file exists | ||||
|             if !plist_path.exists() { | ||||
|                 return Err(ServiceManagerError::ServiceNotFound(service_name.to_string())); | ||||
|             } | ||||
|              | ||||
|             let list_output = self.run_launchctl(&["list"]).await?; | ||||
|              | ||||
|             if !list_output.contains(&label) { | ||||
|                 return Ok(ServiceStatus::Stopped); | ||||
|             } | ||||
|  | ||||
|             // Get detailed status | ||||
|             match self.run_launchctl(&["list", &label]).await { | ||||
|                 Ok(output) => { | ||||
|                     if output.contains("\"PID\" = ") { | ||||
|                         Ok(ServiceStatus::Running) | ||||
|                     } else if output.contains("\"LastExitStatus\" = ") { | ||||
|                         Ok(ServiceStatus::Failed) | ||||
|                     } else { | ||||
|                         Ok(ServiceStatus::Unknown) | ||||
|                     } | ||||
|                 } | ||||
|                 Err(_) => Ok(ServiceStatus::Stopped), | ||||
|             } | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     fn logs(&self, service_name: &str, lines: Option<usize>) -> Result<String, ServiceManagerError> { | ||||
|         let rt = tokio::runtime::Runtime::new().map_err(|e| ServiceManagerError::Other(e.to_string()))?; | ||||
|         rt.block_on(async { | ||||
|             let log_path = self.get_log_path(service_name); | ||||
|              | ||||
|             if !log_path.exists() { | ||||
|                 return Ok(String::new()); | ||||
|             } | ||||
|  | ||||
|             match lines { | ||||
|                 Some(n) => { | ||||
|                     let output = Command::new("tail") | ||||
|                         .args(&["-n", &n.to_string(), &log_path.to_string_lossy()]) | ||||
|                         .output() | ||||
|                         .await?; | ||||
|                     Ok(String::from_utf8_lossy(&output.stdout).to_string()) | ||||
|                 } | ||||
|                 None => { | ||||
|                     let content = tokio::fs::read_to_string(&log_path).await?; | ||||
|                     Ok(content) | ||||
|                 } | ||||
|             } | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     fn list(&self) -> Result<Vec<String>, ServiceManagerError> { | ||||
|         let rt = tokio::runtime::Runtime::new().map_err(|e| ServiceManagerError::Other(e.to_string()))?; | ||||
|         rt.block_on(async { | ||||
|             let list_output = self.run_launchctl(&["list"]).await?; | ||||
|              | ||||
|             let services: Vec<String> = list_output | ||||
|                 .lines() | ||||
|                 .filter_map(|line| { | ||||
|                     if line.contains(&self.service_prefix) { | ||||
|                         // Extract service name from label | ||||
|                         line.split_whitespace() | ||||
|                             .last() | ||||
|                             .and_then(|label| label.strip_prefix(&format!("{}.", self.service_prefix))) | ||||
|                             .map(|s| s.to_string()) | ||||
|                     } else { | ||||
|                         None | ||||
|                     } | ||||
|                 }) | ||||
|                 .collect(); | ||||
|  | ||||
|             Ok(services) | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     fn remove(&self, service_name: &str) -> Result<(), ServiceManagerError> { | ||||
|         // Stop the service first | ||||
|         let _ = self.stop(service_name); | ||||
|  | ||||
|         // Remove the plist file | ||||
|         let rt = tokio::runtime::Runtime::new().map_err(|e| ServiceManagerError::Other(e.to_string()))?; | ||||
|         rt.block_on(async { | ||||
|             let plist_path = self.get_plist_path(service_name); | ||||
|             if plist_path.exists() { | ||||
|                 tokio::fs::remove_file(&plist_path).await?; | ||||
|             } | ||||
|             Ok(()) | ||||
|         }) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										112
									
								
								service_manager/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								service_manager/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,112 @@ | ||||
| use async_trait::async_trait; | ||||
| use std::collections::HashMap; | ||||
| use thiserror::Error; | ||||
|  | ||||
| #[derive(Error, Debug)] | ||||
| pub enum ServiceManagerError { | ||||
|     #[error("Service '{0}' not found")] | ||||
|     ServiceNotFound(String), | ||||
|     #[error("Service '{0}' already exists")] | ||||
|     ServiceAlreadyExists(String), | ||||
|     #[error("Failed to start service '{0}': {1}")] | ||||
|     StartFailed(String, String), | ||||
|     #[error("Failed to stop service '{0}': {1}")] | ||||
|     StopFailed(String, String), | ||||
|     #[error("Failed to restart service '{0}': {1}")] | ||||
|     RestartFailed(String, String), | ||||
|     #[error("Failed to get logs for service '{0}': {1}")] | ||||
|     LogsFailed(String, String), | ||||
|     #[error("IO error: {0}")] | ||||
|     IoError(#[from] std::io::Error), | ||||
|     #[error("Service manager error: {0}")] | ||||
|     Other(String), | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone)] | ||||
| pub struct ServiceConfig { | ||||
|     pub name: String, | ||||
|     pub binary_path: String, | ||||
|     pub args: Vec<String>, | ||||
|     pub working_directory: Option<String>, | ||||
|     pub environment: HashMap<String, String>, | ||||
|     pub auto_restart: bool, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone)] | ||||
| pub enum ServiceStatus { | ||||
|     Running, | ||||
|     Stopped, | ||||
|     Failed, | ||||
|     Unknown, | ||||
| } | ||||
|  | ||||
| #[async_trait] | ||||
| pub trait ServiceManager: Send + Sync { | ||||
|     /// Check if a service exists | ||||
|     fn exists(&self, service_name: &str) -> Result<bool, ServiceManagerError>; | ||||
|      | ||||
|     /// Start a service with the given configuration | ||||
|     fn start(&self, config: &ServiceConfig) -> Result<(), ServiceManagerError>; | ||||
|      | ||||
|     /// Start an existing service by name (load existing plist/config) | ||||
|     fn start_existing(&self, service_name: &str) -> Result<(), ServiceManagerError>; | ||||
|      | ||||
|     /// Start a service and wait for confirmation that it's running or failed | ||||
|     async fn start_and_confirm(&self, config: &ServiceConfig, timeout_secs: u64) -> Result<(), ServiceManagerError>; | ||||
|      | ||||
|     /// Start a service and wait for confirmation that it's running or failed | ||||
|     async fn run(&self, config: &ServiceConfig, timeout_secs: u64) -> Result<(), ServiceManagerError>; | ||||
|      | ||||
|     /// Start an existing service and wait for confirmation that it's running or failed | ||||
|     async fn start_existing_and_confirm(&self, service_name: &str, timeout_secs: u64) -> Result<(), ServiceManagerError>; | ||||
|      | ||||
|     /// Stop a service by name | ||||
|     fn stop(&self, service_name: &str) -> Result<(), ServiceManagerError>; | ||||
|      | ||||
|     /// Restart a service by name | ||||
|     fn restart(&self, service_name: &str) -> Result<(), ServiceManagerError>; | ||||
|      | ||||
|     /// Get the status of a service | ||||
|     fn status(&self, service_name: &str) -> Result<ServiceStatus, ServiceManagerError>; | ||||
|      | ||||
|     /// Get logs for a service | ||||
|     fn logs(&self, service_name: &str, lines: Option<usize>) -> Result<String, ServiceManagerError>; | ||||
|      | ||||
|     /// List all managed services | ||||
|     fn list(&self) -> Result<Vec<String>, ServiceManagerError>; | ||||
|      | ||||
|     /// Remove a service configuration (stop if running) | ||||
|     fn remove(&self, service_name: &str) -> Result<(), ServiceManagerError>; | ||||
| } | ||||
|  | ||||
| // Platform-specific implementations | ||||
| #[cfg(target_os = "macos")] | ||||
| mod launchctl; | ||||
| #[cfg(target_os = "macos")] | ||||
| pub use launchctl::LaunchctlServiceManager; | ||||
|  | ||||
| #[cfg(target_os = "linux")] | ||||
| mod systemd; | ||||
| #[cfg(target_os = "linux")] | ||||
| pub use systemd::SystemdServiceManager; | ||||
|  | ||||
| #[cfg(feature = "zinit")] | ||||
| mod zinit; | ||||
| #[cfg(feature = "zinit")] | ||||
| pub use zinit::ZinitServiceManager; | ||||
|  | ||||
| // Factory function to create the appropriate service manager for the platform | ||||
| pub fn create_service_manager() -> Box<dyn ServiceManager> { | ||||
|     #[cfg(target_os = "macos")] | ||||
|     { | ||||
|         Box::new(LaunchctlServiceManager::new()) | ||||
|     } | ||||
|     #[cfg(target_os = "linux")] | ||||
|     { | ||||
|         Box::new(SystemdServiceManager::new()) | ||||
|     } | ||||
|     #[cfg(not(any(target_os = "macos", target_os = "linux")))] | ||||
|     { | ||||
|         compile_error!("Service manager not implemented for this platform") | ||||
|     } | ||||
| } | ||||
							
								
								
									
										42
									
								
								service_manager/src/systemd.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								service_manager/src/systemd.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,42 @@ | ||||
| use crate::{ServiceConfig, ServiceManager, ServiceManagerError, ServiceStatus}; | ||||
| use async_trait::async_trait; | ||||
|  | ||||
| #[derive(Debug)] | ||||
| pub struct SystemdServiceManager; | ||||
|  | ||||
| impl SystemdServiceManager { | ||||
|     pub fn new() -> Self { | ||||
|         Self | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[async_trait] | ||||
| impl ServiceManager for SystemdServiceManager { | ||||
|     async fn start(&self, _config: &ServiceConfig) -> Result<(), ServiceManagerError> { | ||||
|         Err(ServiceManagerError::Other("Systemd implementation not yet complete".to_string())) | ||||
|     } | ||||
|  | ||||
|     async fn stop(&self, _service_name: &str) -> Result<(), ServiceManagerError> { | ||||
|         Err(ServiceManagerError::Other("Systemd implementation not yet complete".to_string())) | ||||
|     } | ||||
|  | ||||
|     async fn restart(&self, _service_name: &str) -> Result<(), ServiceManagerError> { | ||||
|         Err(ServiceManagerError::Other("Systemd implementation not yet complete".to_string())) | ||||
|     } | ||||
|  | ||||
|     async fn status(&self, _service_name: &str) -> Result<ServiceStatus, ServiceManagerError> { | ||||
|         Err(ServiceManagerError::Other("Systemd implementation not yet complete".to_string())) | ||||
|     } | ||||
|  | ||||
|     async fn logs(&self, _service_name: &str, _lines: Option<usize>) -> Result<String, ServiceManagerError> { | ||||
|         Err(ServiceManagerError::Other("Systemd implementation not yet complete".to_string())) | ||||
|     } | ||||
|  | ||||
|     async fn list(&self) -> Result<Vec<String>, ServiceManagerError> { | ||||
|         Err(ServiceManagerError::Other("Systemd implementation not yet complete".to_string())) | ||||
|     } | ||||
|  | ||||
|     async fn remove(&self, _service_name: &str) -> Result<(), ServiceManagerError> { | ||||
|         Err(ServiceManagerError::Other("Systemd implementation not yet complete".to_string())) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										122
									
								
								service_manager/src/zinit.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										122
									
								
								service_manager/src/zinit.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,122 @@ | ||||
| use crate::{ServiceConfig, ServiceManager, ServiceManagerError, ServiceStatus}; | ||||
| use async_trait::async_trait; | ||||
| use serde_json::json; | ||||
| use std::sync::Arc; | ||||
| use zinit_client::{get_zinit_client, ServiceStatus as ZinitServiceStatus, ZinitClientWrapper}; | ||||
|  | ||||
| pub struct ZinitServiceManager { | ||||
|     client: Arc<ZinitClientWrapper>, | ||||
| } | ||||
|  | ||||
| impl ZinitServiceManager { | ||||
|     pub fn new(socket_path: &str) -> Result<Self, ServiceManagerError> { | ||||
|         // This is a blocking call to get the async client. | ||||
|         // We might want to make this async in the future if the constructor can be async. | ||||
|         let client = tokio::runtime::Runtime::new() | ||||
|             .unwrap() | ||||
|             .block_on(get_zinit_client(socket_path)) | ||||
|             .map_err(|e| ServiceManagerError::Other(e.to_string()))?; | ||||
|         Ok(ZinitServiceManager { client }) | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[async_trait] | ||||
| impl ServiceManager for ZinitServiceManager { | ||||
|     fn exists(&self, service_name: &str) -> Result<bool, ServiceManagerError> { | ||||
|         let status_res = self.status(service_name); | ||||
|         match status_res { | ||||
|             Ok(_) => Ok(true), | ||||
|             Err(ServiceManagerError::ServiceNotFound(_)) => Ok(false), | ||||
|             Err(e) => Err(e), | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fn start(&self, config: &ServiceConfig) -> Result<(), ServiceManagerError> { | ||||
|         let service_config = json!({ | ||||
|             "exec": config.binary_path, | ||||
|             "args": config.args, | ||||
|             "working_directory": config.working_directory, | ||||
|             "env": config.environment, | ||||
|             "restart": config.auto_restart, | ||||
|         }); | ||||
|  | ||||
|         tokio::runtime::Runtime::new() | ||||
|             .unwrap() | ||||
|             .block_on(self.client.create_service(&config.name, service_config)) | ||||
|             .map_err(|e| ServiceManagerError::StartFailed(config.name.clone(), e.to_string()))?; | ||||
|  | ||||
|         self.start_existing(&config.name) | ||||
|     } | ||||
|  | ||||
|     fn start_existing(&self, service_name: &str) -> Result<(), ServiceManagerError> { | ||||
|         tokio::runtime::Runtime::new() | ||||
|             .unwrap() | ||||
|             .block_on(self.client.start(service_name)) | ||||
|             .map_err(|e| ServiceManagerError::StartFailed(service_name.to_string(), e.to_string())) | ||||
|     } | ||||
|  | ||||
|     async fn start_and_confirm(&self, config: &ServiceConfig, _timeout_secs: u64) -> Result<(), ServiceManagerError> { | ||||
|         self.start(config) | ||||
|     } | ||||
|  | ||||
|     async fn run(&self, config: &ServiceConfig, _timeout_secs: u64) -> Result<(), ServiceManagerError> { | ||||
|         self.start(config) | ||||
|     } | ||||
|  | ||||
|     async fn start_existing_and_confirm(&self, service_name: &str, _timeout_secs: u64) -> Result<(), ServiceManagerError> { | ||||
|         self.start_existing(service_name) | ||||
|     } | ||||
|  | ||||
|     fn stop(&self, service_name: &str) -> Result<(), ServiceManagerError> { | ||||
|         tokio::runtime::Runtime::new() | ||||
|             .unwrap() | ||||
|             .block_on(self.client.stop(service_name)) | ||||
|             .map_err(|e| ServiceManagerError::StopFailed(service_name.to_string(), e.to_string())) | ||||
|     } | ||||
|  | ||||
|     fn restart(&self, service_name: &str) -> Result<(), ServiceManagerError> { | ||||
|         tokio::runtime::Runtime::new() | ||||
|             .unwrap() | ||||
|             .block_on(self.client.restart(service_name)) | ||||
|             .map_err(|e| ServiceManagerError::RestartFailed(service_name.to_string(), e.to_string())) | ||||
|     } | ||||
|  | ||||
|     fn status(&self, service_name: &str) -> Result<ServiceStatus, ServiceManagerError> { | ||||
|         let status: ZinitServiceStatus = tokio::runtime::Runtime::new() | ||||
|             .unwrap() | ||||
|             .block_on(self.client.status(service_name)) | ||||
|             .map_err(|e| ServiceManagerError::Other(e.to_string()))?; | ||||
|  | ||||
|         let service_status = match status { | ||||
|             ZinitServiceStatus::Running(_) => crate::ServiceStatus::Running, | ||||
|             ZinitServiceStatus::Stopped => crate::ServiceStatus::Stopped, | ||||
|             ZinitServiceStatus::Failed(_) => crate::ServiceStatus::Failed, | ||||
|             ZinitServiceStatus::Waiting(_) => crate::ServiceStatus::Unknown, | ||||
|         }; | ||||
|         Ok(service_status) | ||||
|     } | ||||
|  | ||||
|     fn logs(&self, service_name: &str, _lines: Option<usize>) -> Result<String, ServiceManagerError> { | ||||
|         let logs = tokio::runtime::Runtime::new() | ||||
|             .unwrap() | ||||
|             .block_on(self.client.logs(Some(service_name.to_string()))) | ||||
|             .map_err(|e| ServiceManagerError::LogsFailed(service_name.to_string(), e.to_string()))?; | ||||
|         Ok(logs.join("\n")) | ||||
|     } | ||||
|  | ||||
|     fn list(&self) -> Result<Vec<String>, ServiceManagerError> { | ||||
|         let services = tokio::runtime::Runtime::new() | ||||
|             .unwrap() | ||||
|             .block_on(self.client.list()) | ||||
|             .map_err(|e| ServiceManagerError::Other(e.to_string()))?; | ||||
|         Ok(services.keys().cloned().collect()) | ||||
|     } | ||||
|  | ||||
|     fn remove(&self, service_name: &str) -> Result<(), ServiceManagerError> { | ||||
|         let _ = self.stop(service_name); // Best effort to stop before removing | ||||
|         tokio::runtime::Runtime::new() | ||||
|             .unwrap() | ||||
|             .block_on(self.client.delete_service(service_name)) | ||||
|             .map_err(|e| ServiceManagerError::Other(e.to_string())) | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user