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/// Returns an error if the platform is not supported or if the
89/// service manager implementation is not yet available.
90pub fn get_service_manager() -> Result<Box<dyn ServiceManager>> {
91    #[cfg(target_os = "linux")]
92    {
93        if !systemd::systemd_available() {
94            return Err(anyhow!(
95                "systemd not available on this system. \
96                 Service registration requires systemd as the init system."
97            ));
98        }
99        Ok(Box::new(systemd::SystemdManager::new("user")))
100    }
101    #[cfg(target_os = "macos")]
102    {
103        Ok(Box::new(launchd::LaunchdManager::new("user")))
104    }
105    #[cfg(not(any(target_os = "linux", target_os = "macos")))]
106    {
107        Err(anyhow!("Unsupported platform for service registration"))
108    }
109}
110
111/// Check if service registration is supported on the current platform
112///
113/// Returns true for Linux (systemd) and macOS (launchd).
114pub fn is_service_registration_supported() -> bool {
115    cfg!(any(target_os = "linux", target_os = "macos"))
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121
122    #[test]
123    fn test_service_config_creation() {
124        let config = ServiceConfig {
125            executable_path: PathBuf::from("/usr/local/bin/occ"),
126            restart_retries: 3,
127            restart_delay: 5,
128            boot_mode: "user".to_string(),
129        };
130
131        assert_eq!(config.executable_path, PathBuf::from("/usr/local/bin/occ"));
132        assert_eq!(config.restart_retries, 3);
133        assert_eq!(config.restart_delay, 5);
134        assert_eq!(config.boot_mode, "user");
135    }
136
137    #[test]
138    fn test_install_result_creation() {
139        let result = InstallResult {
140            service_file_path: PathBuf::from("/etc/systemd/user/opencode-cloud.service"),
141            service_name: "opencode-cloud".to_string(),
142            started: true,
143            requires_root: false,
144        };
145
146        assert_eq!(
147            result.service_file_path,
148            PathBuf::from("/etc/systemd/user/opencode-cloud.service")
149        );
150        assert_eq!(result.service_name, "opencode-cloud");
151        assert!(result.started);
152        assert!(!result.requires_root);
153    }
154
155    #[test]
156    fn test_is_service_registration_supported() {
157        // On macOS/Linux this should return true, on other platforms false
158        #[cfg(any(target_os = "linux", target_os = "macos"))]
159        assert!(is_service_registration_supported());
160
161        #[cfg(not(any(target_os = "linux", target_os = "macos")))]
162        assert!(!is_service_registration_supported());
163    }
164
165    #[test]
166    fn test_get_service_manager_behavior() {
167        let result = get_service_manager();
168
169        // On Linux with systemd: returns Ok(SystemdManager)
170        // On Linux without systemd: returns Err (systemd not available)
171        // On macOS: returns Ok(LaunchdManager)
172        // On other platforms: returns Err (unsupported)
173        #[cfg(target_os = "linux")]
174        {
175            // Result depends on whether systemd is available
176            // This test just verifies the function doesn't panic
177            let _ = result;
178        }
179        #[cfg(target_os = "macos")]
180        {
181            // LaunchdManager should be returned on macOS
182            assert!(result.is_ok());
183            let manager = result.unwrap();
184            assert_eq!(manager.service_name(), "com.opencode-cloud.service");
185        }
186        #[cfg(not(any(target_os = "linux", target_os = "macos")))]
187        {
188            assert!(result.is_err());
189        }
190    }
191}