Skip to main content

opencode_cloud_core/platform/
mod.rs

1//! Platform-specific service manager abstraction
2//!
3//! This module provides a unified interface for registering the opencode-cloud
4//! service with platform-specific init systems (systemd on Linux, launchd on macOS).
5
6use std::path::PathBuf;
7
8use anyhow::Result;
9
10#[cfg(any(
11    target_os = "linux",
12    not(any(target_os = "linux", target_os = "macos"))
13))]
14use anyhow::anyhow;
15
16#[cfg(target_os = "linux")]
17mod systemd;
18
19#[cfg(target_os = "macos")]
20mod launchd;
21
22#[cfg(target_os = "linux")]
23pub use systemd::{SystemdManager, systemd_available};
24
25#[cfg(target_os = "macos")]
26pub use launchd::LaunchdManager;
27
28/// Configuration for service installation
29#[derive(Debug, Clone)]
30pub struct ServiceConfig {
31    /// Path to the executable to run
32    pub executable_path: PathBuf,
33
34    /// Number of restart attempts on crash
35    pub restart_retries: u32,
36
37    /// Seconds between restart attempts
38    pub restart_delay: u32,
39
40    /// Boot mode: "user" (starts on login) or "system" (starts on boot)
41    pub boot_mode: String,
42}
43
44/// Result of a service installation operation
45#[derive(Debug, Clone)]
46pub struct InstallResult {
47    /// Path to the service file that was created
48    pub service_file_path: PathBuf,
49
50    /// Name of the service (e.g., "opencode-cloud")
51    pub service_name: String,
52
53    /// Whether the service was started after installation
54    pub started: bool,
55
56    /// Whether root/sudo is required for this installation type
57    pub requires_root: bool,
58}
59
60/// Trait for platform-specific service managers
61///
62/// Implementations handle the details of registering services with
63/// systemd (Linux) or launchd (macOS).
64pub trait ServiceManager: Send + Sync {
65    /// Install the service with the given configuration
66    ///
67    /// Creates the service file and registers it with the init system.
68    /// Also starts the service immediately after registration.
69    fn install(&self, config: &ServiceConfig) -> Result<InstallResult>;
70
71    /// Uninstall the service
72    ///
73    /// Stops the service if running and removes the registration.
74    fn uninstall(&self) -> Result<()>;
75
76    /// Check if the service is currently installed
77    fn is_installed(&self) -> Result<bool>;
78
79    /// Get the path to the service file
80    fn service_file_path(&self) -> PathBuf;
81
82    /// Get the service name
83    fn service_name(&self) -> &str;
84}
85
86/// Get the appropriate service manager for the current platform
87///
88/// # Arguments
89/// * `boot_mode` - "user" for user-level service (default), "system" for system-level
90///
91/// Returns an error if the platform is not supported or if the
92/// service manager implementation is not yet available.
93pub fn get_service_manager(boot_mode: &str) -> Result<Box<dyn ServiceManager>> {
94    #[cfg(target_os = "linux")]
95    {
96        if !systemd::systemd_available() {
97            return Err(anyhow!(
98                "systemd not available on this system. \
99                 Service registration requires systemd as the init system."
100            ));
101        }
102        Ok(Box::new(systemd::SystemdManager::new(boot_mode)))
103    }
104    #[cfg(target_os = "macos")]
105    {
106        Ok(Box::new(launchd::LaunchdManager::new(boot_mode)))
107    }
108    #[cfg(not(any(target_os = "linux", target_os = "macos")))]
109    {
110        Err(anyhow!("Unsupported platform for service registration"))
111    }
112}
113
114/// Check if service registration is supported on the current platform
115///
116/// Returns true for Linux (systemd) and macOS (launchd).
117pub fn is_service_registration_supported() -> bool {
118    cfg!(any(target_os = "linux", target_os = "macos"))
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124
125    #[test]
126    fn test_service_config_creation() {
127        let config = ServiceConfig {
128            executable_path: PathBuf::from("/usr/local/bin/occ"),
129            restart_retries: 3,
130            restart_delay: 5,
131            boot_mode: "user".to_string(),
132        };
133
134        assert_eq!(config.executable_path, PathBuf::from("/usr/local/bin/occ"));
135        assert_eq!(config.restart_retries, 3);
136        assert_eq!(config.restart_delay, 5);
137        assert_eq!(config.boot_mode, "user");
138    }
139
140    #[test]
141    fn test_install_result_creation() {
142        let result = InstallResult {
143            service_file_path: PathBuf::from("/etc/systemd/user/opencode-cloud.service"),
144            service_name: "opencode-cloud".to_string(),
145            started: true,
146            requires_root: false,
147        };
148
149        assert_eq!(
150            result.service_file_path,
151            PathBuf::from("/etc/systemd/user/opencode-cloud.service")
152        );
153        assert_eq!(result.service_name, "opencode-cloud");
154        assert!(result.started);
155        assert!(!result.requires_root);
156    }
157
158    #[test]
159    fn test_is_service_registration_supported() {
160        // On macOS/Linux this should return true, on other platforms false
161        #[cfg(any(target_os = "linux", target_os = "macos"))]
162        assert!(is_service_registration_supported());
163
164        #[cfg(not(any(target_os = "linux", target_os = "macos")))]
165        assert!(!is_service_registration_supported());
166    }
167
168    #[test]
169    fn test_get_service_manager_behavior() {
170        let result = get_service_manager("user");
171
172        // On Linux with systemd: returns Ok(SystemdManager)
173        // On Linux without systemd: returns Err (systemd not available)
174        // On macOS: returns Ok(LaunchdManager)
175        // On other platforms: returns Err (unsupported)
176        #[cfg(target_os = "linux")]
177        {
178            // Result depends on whether systemd is available
179            // This test just verifies the function doesn't panic
180            let _ = result;
181        }
182        #[cfg(target_os = "macos")]
183        {
184            // LaunchdManager should be returned on macOS
185            assert!(result.is_ok());
186            let manager = result.unwrap();
187            assert_eq!(manager.service_name(), "com.opencode-cloud.service");
188        }
189        #[cfg(not(any(target_os = "linux", target_os = "macos")))]
190        {
191            assert!(result.is_err());
192        }
193    }
194
195    #[test]
196    fn test_get_service_manager_respects_boot_mode() {
197        // Test that boot_mode parameter is passed through
198        let user_result = get_service_manager("user");
199        let system_result = get_service_manager("system");
200
201        // Both should either succeed or fail based on platform support,
202        // but they should not panic
203        let _ = user_result;
204        let _ = system_result;
205    }
206}