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, pub working_directory: Option, pub environment: HashMap, pub auto_restart: bool, } #[derive(Debug, Clone, PartialEq)] pub enum ServiceStatus { Running, Stopped, Failed, Unknown, } pub trait ServiceManager: Send + Sync { /// Check if a service exists fn exists(&self, service_name: &str) -> Result; /// 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 fn start_and_confirm( &self, config: &ServiceConfig, timeout_secs: u64, ) -> Result<(), ServiceManagerError>; /// Start an existing service and wait for confirmation that it's running or failed 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; /// Get logs for a service fn logs(&self, service_name: &str, lines: Option) -> Result; /// List all managed services fn list(&self) -> Result, 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; mod zinit; pub use zinit::ZinitServiceManager; #[cfg(feature = "rhai")] pub mod rhai; /// Discover available zinit socket paths /// /// This function checks for zinit sockets in the following order: /// 1. Environment variable ZINIT_SOCKET_PATH (if set) /// 2. Common socket locations with connectivity testing /// /// # Returns /// /// Returns the first working socket path found, or None if no working zinit server is detected. #[cfg(target_os = "linux")] fn discover_zinit_socket() -> Option { // First check environment variable if let Ok(env_socket_path) = std::env::var("ZINIT_SOCKET_PATH") { log::debug!("Checking ZINIT_SOCKET_PATH: {}", env_socket_path); if test_zinit_socket(&env_socket_path) { log::info!( "Using zinit socket from ZINIT_SOCKET_PATH: {}", env_socket_path ); return Some(env_socket_path); } else { log::warn!( "ZINIT_SOCKET_PATH specified but socket is not accessible: {}", env_socket_path ); } } // Try common socket locations let common_paths = [ "/var/run/zinit.sock", "/tmp/zinit.sock", "/run/zinit.sock", "./zinit.sock", ]; log::debug!("Discovering zinit socket from common locations..."); for path in &common_paths { log::debug!("Testing socket path: {}", path); if test_zinit_socket(path) { log::info!("Found working zinit socket at: {}", path); return Some(path.to_string()); } } log::debug!("No working zinit socket found"); None } /// Test if a zinit socket is accessible and responsive /// /// This function attempts to create a ZinitServiceManager and perform a basic /// connectivity test by listing services. #[cfg(target_os = "linux")] fn test_zinit_socket(socket_path: &str) -> bool { // Check if socket file exists first if !std::path::Path::new(socket_path).exists() { log::debug!("Socket file does not exist: {}", socket_path); return false; } // Try to create a manager and test basic connectivity match ZinitServiceManager::new(socket_path) { Ok(manager) => { // Test basic connectivity by trying to list services match manager.list() { Ok(_) => { log::debug!("Socket {} is responsive", socket_path); true } Err(e) => { log::debug!("Socket {} exists but not responsive: {}", socket_path, e); false } } } Err(e) => { log::debug!("Failed to create manager for socket {}: {}", socket_path, e); false } } } /// Create a service manager appropriate for the current platform /// /// - On macOS: Uses launchctl for service management /// - On Linux: Uses zinit for service management with systemd fallback /// /// # Returns /// /// Returns a Result containing the service manager or an error if initialization fails. /// On Linux, it first tries to discover a working zinit socket. If no zinit server is found, /// it will fall back to systemd. /// /// # Environment Variables /// /// - `ZINIT_SOCKET_PATH`: Specifies the zinit socket path (Linux only) /// /// # Errors /// /// Returns `ServiceManagerError` if: /// - The platform is not supported (Windows, etc.) /// - Service manager initialization fails on all available backends pub fn create_service_manager() -> Result, ServiceManagerError> { #[cfg(target_os = "macos")] { Ok(Box::new(LaunchctlServiceManager::new())) } #[cfg(target_os = "linux")] { // Try to discover a working zinit socket if let Some(socket_path) = discover_zinit_socket() { match ZinitServiceManager::new(&socket_path) { Ok(zinit_manager) => { log::info!("Using zinit service manager with socket: {}", socket_path); return Ok(Box::new(zinit_manager)); } Err(zinit_error) => { log::warn!( "Failed to create zinit manager for discovered socket {}: {}", socket_path, zinit_error ); } } } else { log::info!("No running zinit server detected. To use zinit, start it with: zinit -s /tmp/zinit.sock init"); } // Fallback to systemd log::info!("Falling back to systemd service manager"); Ok(Box::new(SystemdServiceManager::new())) } #[cfg(not(any(target_os = "macos", target_os = "linux")))] { Err(ServiceManagerError::Other( "Service manager not implemented for this platform".to_string(), )) } } /// Create a service manager for zinit with a custom socket path /// /// This is useful when zinit is running with a non-default socket path pub fn create_zinit_service_manager( socket_path: &str, ) -> Result, ServiceManagerError> { Ok(Box::new(ZinitServiceManager::new(socket_path)?)) } /// Create a service manager for systemd (Linux alternative) /// /// This creates a systemd-based service manager as an alternative to zinit on Linux #[cfg(target_os = "linux")] pub fn create_systemd_service_manager() -> Box { Box::new(SystemdServiceManager::new()) } #[cfg(test)] mod tests { use super::*; #[test] fn test_create_service_manager() { // This test ensures the service manager can be created without panicking let result = create_service_manager(); assert!(result.is_ok(), "Service manager creation should succeed"); } #[cfg(target_os = "linux")] #[test] fn test_socket_discovery_with_env_var() { // Test that environment variable is respected std::env::set_var("ZINIT_SOCKET_PATH", "/test/path.sock"); // The discover function should check the env var first // Since the socket doesn't exist, it should return None, but we can't test // the actual discovery logic without a real socket std::env::remove_var("ZINIT_SOCKET_PATH"); } #[cfg(target_os = "linux")] #[test] fn test_socket_discovery_without_env_var() { // Ensure env var is not set std::env::remove_var("ZINIT_SOCKET_PATH"); // The discover function should try common paths // Since no zinit is running, it should return None let result = discover_zinit_socket(); // This is expected to be None in test environment assert!( result.is_none(), "Should return None when no zinit server is running" ); } }