feat: Enhance service manager with zinit socket discovery and systemd fallback
- Improve Linux support by automatically discovering zinit sockets using environment variables and common paths. - Add fallback to systemd if no zinit server is detected. - Enhance README with detailed instructions for zinit usage, including custom socket path configuration. - Add example demonstrating zinit socket discovery. - Add logging to show socket discovery process. - Add unit tests for service manager creation and socket discovery.
This commit is contained in:
		
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -62,4 +62,5 @@ docusaurus.config.ts | ||||
| sidebars.ts | ||||
|  | ||||
| tsconfig.json | ||||
| Cargo.toml.bak | ||||
| Cargo.toml.bak | ||||
| for_augment | ||||
| @@ -1,13 +1,17 @@ | ||||
| // Basic Service Manager Usage Example | ||||
| // | ||||
| // This example demonstrates the basic API of the service manager. | ||||
| // It works on both macOS (launchctl) and Linux (zinit). | ||||
| // It works on both macOS (launchctl) and Linux (zinit/systemd). | ||||
| // | ||||
| // Prerequisites: | ||||
| // | ||||
| // Linux: Make sure zinit is running: | ||||
| // Linux: The service manager will automatically discover running zinit servers | ||||
| //        or fall back to systemd. To use zinit, start it with: | ||||
| //   zinit -s /tmp/zinit.sock init | ||||
| // | ||||
| //   You can also specify a custom socket path: | ||||
| //   export ZINIT_SOCKET_PATH=/your/custom/path/zinit.sock | ||||
| // | ||||
| // macOS: No additional setup required (uses launchctl). | ||||
| // | ||||
| // Usage: | ||||
|   | ||||
| @@ -36,6 +36,7 @@ rhai = ["dep:rhai"] | ||||
| tokio-test = "0.4" | ||||
| rhai = { workspace = true } | ||||
| tempfile = { workspace = true } | ||||
| env_logger = "0.10" | ||||
|  | ||||
| [[test]] | ||||
| name = "zinit_integration_tests" | ||||
|   | ||||
| @@ -159,15 +159,34 @@ herodo your_service_script.rhai | ||||
|  | ||||
| ## Prerequisites | ||||
|  | ||||
| ### Linux (zinit) | ||||
| ### Linux (zinit/systemd) | ||||
|  | ||||
| Make sure zinit is installed and running: | ||||
| The service manager automatically discovers running zinit servers and falls back to systemd if none are found. | ||||
|  | ||||
| **For zinit (recommended):** | ||||
|  | ||||
| ```bash | ||||
| # Start zinit with default socket | ||||
| zinit -s /tmp/zinit.sock init | ||||
|  | ||||
| # Or with a custom socket path | ||||
| zinit -s /var/run/zinit.sock init | ||||
| ``` | ||||
|  | ||||
| **Socket Discovery:** | ||||
| The service manager will automatically find running zinit servers by checking: | ||||
| 1. `ZINIT_SOCKET_PATH` environment variable (if set) | ||||
| 2. Common socket locations: `/var/run/zinit.sock`, `/tmp/zinit.sock`, `/run/zinit.sock`, `./zinit.sock` | ||||
|  | ||||
| **Custom socket path:** | ||||
| ```bash | ||||
| # Set custom socket path | ||||
| export ZINIT_SOCKET_PATH=/your/custom/path/zinit.sock | ||||
| ``` | ||||
|  | ||||
| **Systemd fallback:** | ||||
| If no zinit server is detected, the service manager automatically falls back to systemd. | ||||
|  | ||||
| ### macOS (launchctl) | ||||
|  | ||||
| No additional setup required - uses the built-in launchctl system. | ||||
|   | ||||
| @@ -10,6 +10,9 @@ use std::thread; | ||||
| use std::time::Duration; | ||||
|  | ||||
| fn main() { | ||||
|     // Initialize logging to see socket discovery in action | ||||
|     env_logger::init(); | ||||
|  | ||||
|     let manager = match create_service_manager() { | ||||
|         Ok(manager) => manager, | ||||
|         Err(e) => { | ||||
|   | ||||
| @@ -4,6 +4,9 @@ use std::thread; | ||||
| use std::time::Duration; | ||||
|  | ||||
| fn main() { | ||||
|     // Initialize logging to see socket discovery in action | ||||
|     env_logger::init(); | ||||
|  | ||||
|     // 1. Create a service manager for the current platform | ||||
|     let manager = match create_service_manager() { | ||||
|         Ok(manager) => manager, | ||||
|   | ||||
							
								
								
									
										47
									
								
								service_manager/examples/socket_discovery_test.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								service_manager/examples/socket_discovery_test.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,47 @@ | ||||
| //! Socket Discovery Test | ||||
| //! | ||||
| //! This example demonstrates the zinit socket discovery functionality. | ||||
| //! It shows how the service manager finds available zinit sockets. | ||||
|  | ||||
| use sal_service_manager::create_service_manager; | ||||
|  | ||||
| fn main() { | ||||
|     // Initialize logging to see socket discovery in action | ||||
|     env_logger::init(); | ||||
|      | ||||
|     println!("=== Zinit Socket Discovery Test ==="); | ||||
|     println!("This test demonstrates how the service manager discovers zinit sockets."); | ||||
|     println!(); | ||||
|      | ||||
|     // Test environment variable | ||||
|     if let Ok(socket_path) = std::env::var("ZINIT_SOCKET_PATH") { | ||||
|         println!("🔍 ZINIT_SOCKET_PATH environment variable set to: {}", socket_path); | ||||
|     } else { | ||||
|         println!("🔍 ZINIT_SOCKET_PATH environment variable not set"); | ||||
|     } | ||||
|     println!(); | ||||
|      | ||||
|     println!("🚀 Creating service manager..."); | ||||
|     match create_service_manager() { | ||||
|         Ok(_manager) => { | ||||
|             println!("✅ Service manager created successfully!"); | ||||
|              | ||||
|             #[cfg(target_os = "macos")] | ||||
|             println!("📱 Platform: macOS - Using launchctl"); | ||||
|              | ||||
|             #[cfg(target_os = "linux")] | ||||
|             println!("🐧 Platform: Linux - Check logs above for socket discovery details"); | ||||
|         } | ||||
|         Err(e) => { | ||||
|             println!("❌ Failed to create service manager: {}", e); | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     println!(); | ||||
|     println!("=== Test Complete ==="); | ||||
|     println!(); | ||||
|     println!("To test zinit socket discovery on Linux:"); | ||||
|     println!("1. Start zinit: zinit -s /tmp/zinit.sock init"); | ||||
|     println!("2. Run with logging: RUST_LOG=debug cargo run --example socket_discovery_test -p sal-service-manager"); | ||||
|     println!("3. Or set custom path: ZINIT_SOCKET_PATH=/custom/path.sock RUST_LOG=debug cargo run --example socket_discovery_test -p sal-service-manager"); | ||||
| } | ||||
| @@ -100,6 +100,89 @@ 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<String> { | ||||
|     // 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 | ||||
| @@ -108,7 +191,12 @@ pub mod rhai; | ||||
| /// # Returns | ||||
| /// | ||||
| /// Returns a Result containing the service manager or an error if initialization fails. | ||||
| /// On Linux, if zinit is not available, it will automatically fall back to systemd. | ||||
| /// 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 | ||||
| /// | ||||
| @@ -122,22 +210,28 @@ pub fn create_service_manager() -> Result<Box<dyn ServiceManager>, ServiceManage | ||||
|     } | ||||
|     #[cfg(target_os = "linux")] | ||||
|     { | ||||
|         // Try zinit first (preferred), fall back to systemd if it fails | ||||
|         let socket_path = "/tmp/zinit.sock"; | ||||
|         match ZinitServiceManager::new(socket_path) { | ||||
|             Ok(zinit_manager) => { | ||||
|                 log::debug!("Using zinit service manager"); | ||||
|                 Ok(Box::new(zinit_manager)) | ||||
|             } | ||||
|             Err(zinit_error) => { | ||||
|                 log::warn!( | ||||
|                     "Zinit service manager failed, falling back to systemd: {}", | ||||
|                     zinit_error | ||||
|                 ); | ||||
|                 // Fallback to systemd | ||||
|                 Ok(Box::new(SystemdServiceManager::new())) | ||||
|         // 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")))] | ||||
|     { | ||||
| @@ -163,3 +257,45 @@ pub fn create_zinit_service_manager( | ||||
| pub fn create_systemd_service_manager() -> Box<dyn ServiceManager> { | ||||
|     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" | ||||
|         ); | ||||
|     } | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user