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