@opencode-cloud/core 0.1.2 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.toml +13 -0
- package/README.md +172 -0
- package/core.darwin-arm64.node +0 -0
- package/package.json +1 -1
- package/src/config/schema.rs +62 -8
- package/src/docker/Dockerfile +444 -0
- package/src/docker/client.rs +84 -0
- package/src/docker/container.rs +317 -0
- package/src/docker/dockerfile.rs +41 -0
- package/src/docker/error.rs +79 -0
- package/src/docker/image.rs +502 -0
- package/src/docker/mod.rs +112 -0
- package/src/docker/progress.rs +401 -0
- package/src/docker/volume.rs +144 -0
- package/src/lib.rs +14 -0
- package/src/platform/launchd.rs +363 -0
- package/src/platform/mod.rs +191 -0
- package/src/platform/systemd.rs +346 -0
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
//! macOS launchd service manager implementation
|
|
2
|
+
//!
|
|
3
|
+
//! Manages launchd user agents and system daemons for service registration.
|
|
4
|
+
//! Uses plist serialization for property list generation and launchctl for
|
|
5
|
+
//! service lifecycle management.
|
|
6
|
+
|
|
7
|
+
use std::fs::{self, File};
|
|
8
|
+
use std::path::{Path, PathBuf};
|
|
9
|
+
use std::process::Command;
|
|
10
|
+
|
|
11
|
+
use anyhow::{Result, anyhow};
|
|
12
|
+
use serde::Serialize;
|
|
13
|
+
|
|
14
|
+
use super::{InstallResult, ServiceConfig, ServiceManager};
|
|
15
|
+
|
|
16
|
+
/// Service label used for launchd registration
|
|
17
|
+
const SERVICE_LABEL: &str = "com.opencode-cloud.service";
|
|
18
|
+
|
|
19
|
+
/// Plist structure for launchd service definition
|
|
20
|
+
#[derive(Serialize)]
|
|
21
|
+
#[serde(rename_all = "PascalCase")]
|
|
22
|
+
struct LaunchdPlist {
|
|
23
|
+
label: String,
|
|
24
|
+
program_arguments: Vec<String>,
|
|
25
|
+
run_at_load: bool,
|
|
26
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
27
|
+
keep_alive: Option<KeepAliveConfig>,
|
|
28
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
29
|
+
throttle_interval: Option<u32>,
|
|
30
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
31
|
+
standard_out_path: Option<String>,
|
|
32
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
33
|
+
standard_error_path: Option<String>,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/// KeepAlive configuration for restart behavior
|
|
37
|
+
#[derive(Serialize)]
|
|
38
|
+
#[serde(rename_all = "PascalCase")]
|
|
39
|
+
struct KeepAliveConfig {
|
|
40
|
+
/// Restart only on non-zero exit (false = restart on crash)
|
|
41
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
42
|
+
successful_exit: Option<bool>,
|
|
43
|
+
/// Restart on signal-based crash (SIGSEGV, etc.)
|
|
44
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
45
|
+
crashed: Option<bool>,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/// macOS launchd service manager
|
|
49
|
+
///
|
|
50
|
+
/// Manages launchd user agents (~/Library/LaunchAgents/) or system daemons
|
|
51
|
+
/// (/Library/LaunchDaemons/) depending on the boot mode.
|
|
52
|
+
pub struct LaunchdManager {
|
|
53
|
+
/// true = user mode (~/Library/LaunchAgents/), false = system (/Library/LaunchDaemons/)
|
|
54
|
+
user_mode: bool,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
impl LaunchdManager {
|
|
58
|
+
/// Create a new LaunchdManager
|
|
59
|
+
///
|
|
60
|
+
/// # Arguments
|
|
61
|
+
/// * `boot_mode` - "user" for user agents, "system" for system daemons
|
|
62
|
+
pub fn new(boot_mode: &str) -> Self {
|
|
63
|
+
Self {
|
|
64
|
+
user_mode: boot_mode != "system",
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/// Get the directory where service files are stored
|
|
69
|
+
fn service_dir(&self) -> PathBuf {
|
|
70
|
+
if self.user_mode {
|
|
71
|
+
directories::BaseDirs::new()
|
|
72
|
+
.expect("Could not determine base directories")
|
|
73
|
+
.home_dir()
|
|
74
|
+
.join("Library/LaunchAgents")
|
|
75
|
+
} else {
|
|
76
|
+
PathBuf::from("/Library/LaunchDaemons")
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/// Get the service label
|
|
81
|
+
fn label(&self) -> &str {
|
|
82
|
+
SERVICE_LABEL
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/// Get the log path for a given stream (stdout or stderr)
|
|
86
|
+
fn log_path(&self, stream: &str) -> PathBuf {
|
|
87
|
+
directories::BaseDirs::new()
|
|
88
|
+
.expect("Could not determine base directories")
|
|
89
|
+
.home_dir()
|
|
90
|
+
.join(format!("Library/Logs/opencode-cloud.{stream}.log"))
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/// Generate plist content from service configuration
|
|
94
|
+
fn generate_plist(&self, config: &ServiceConfig) -> LaunchdPlist {
|
|
95
|
+
LaunchdPlist {
|
|
96
|
+
label: self.label().to_string(),
|
|
97
|
+
program_arguments: vec![
|
|
98
|
+
config.executable_path.display().to_string(),
|
|
99
|
+
"start".to_string(),
|
|
100
|
+
"--no-daemon".to_string(),
|
|
101
|
+
],
|
|
102
|
+
run_at_load: true,
|
|
103
|
+
keep_alive: Some(KeepAliveConfig {
|
|
104
|
+
// Only restart on non-zero exit (crash)
|
|
105
|
+
successful_exit: Some(false),
|
|
106
|
+
// Restart on signal-based crash
|
|
107
|
+
crashed: Some(true),
|
|
108
|
+
}),
|
|
109
|
+
throttle_interval: Some(config.restart_delay),
|
|
110
|
+
standard_out_path: Some(self.log_path("stdout").display().to_string()),
|
|
111
|
+
standard_error_path: Some(self.log_path("stderr").display().to_string()),
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/// Get the current user's UID
|
|
117
|
+
fn get_user_id() -> Result<u32> {
|
|
118
|
+
let output = Command::new("id").arg("-u").output()?;
|
|
119
|
+
let uid_str = String::from_utf8_lossy(&output.stdout);
|
|
120
|
+
uid_str
|
|
121
|
+
.trim()
|
|
122
|
+
.parse()
|
|
123
|
+
.map_err(|e| anyhow!("Failed to parse UID: {}", e))
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
impl ServiceManager for LaunchdManager {
|
|
127
|
+
fn install(&self, config: &ServiceConfig) -> Result<InstallResult> {
|
|
128
|
+
// Check permissions for system-level install
|
|
129
|
+
if !self.user_mode {
|
|
130
|
+
let uid = get_user_id()?;
|
|
131
|
+
if uid != 0 {
|
|
132
|
+
return Err(anyhow!(
|
|
133
|
+
"System-level installation requires root. Run with sudo."
|
|
134
|
+
));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Create service directory if needed
|
|
139
|
+
let service_dir = self.service_dir();
|
|
140
|
+
if !service_dir.exists() {
|
|
141
|
+
fs::create_dir_all(&service_dir)?;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
let plist_path = self.service_file_path();
|
|
145
|
+
|
|
146
|
+
// If service is already loaded, bootout first
|
|
147
|
+
if plist_path.exists() {
|
|
148
|
+
// Ignore errors during bootout - service might not be running
|
|
149
|
+
let _ = self.bootout();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Generate and write plist
|
|
153
|
+
let plist = self.generate_plist(config);
|
|
154
|
+
let file = File::create(&plist_path)?;
|
|
155
|
+
plist::to_writer_xml(file, &plist)?;
|
|
156
|
+
|
|
157
|
+
// Bootstrap the service
|
|
158
|
+
self.bootstrap(&plist_path)?;
|
|
159
|
+
|
|
160
|
+
Ok(InstallResult {
|
|
161
|
+
service_file_path: plist_path,
|
|
162
|
+
service_name: self.label().to_string(),
|
|
163
|
+
started: true,
|
|
164
|
+
requires_root: !self.user_mode,
|
|
165
|
+
})
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
fn uninstall(&self) -> Result<()> {
|
|
169
|
+
let plist_path = self.service_file_path();
|
|
170
|
+
|
|
171
|
+
// Bootout service if running (ignore errors for idempotency)
|
|
172
|
+
let _ = self.bootout();
|
|
173
|
+
|
|
174
|
+
// Remove plist file if it exists
|
|
175
|
+
if plist_path.exists() {
|
|
176
|
+
fs::remove_file(&plist_path)?;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
Ok(())
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
fn is_installed(&self) -> Result<bool> {
|
|
183
|
+
Ok(self.service_file_path().exists())
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
fn service_file_path(&self) -> PathBuf {
|
|
187
|
+
self.service_dir().join(format!("{}.plist", self.label()))
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
fn service_name(&self) -> &str {
|
|
191
|
+
self.label()
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
impl LaunchdManager {
|
|
196
|
+
/// Bootstrap the service using modern launchctl syntax
|
|
197
|
+
fn bootstrap(&self, plist_path: &Path) -> Result<()> {
|
|
198
|
+
let output = if self.user_mode {
|
|
199
|
+
let uid = get_user_id()?;
|
|
200
|
+
let domain = format!("gui/{uid}");
|
|
201
|
+
Command::new("launchctl")
|
|
202
|
+
.args(["bootstrap", &domain, &plist_path.display().to_string()])
|
|
203
|
+
.output()?
|
|
204
|
+
} else {
|
|
205
|
+
Command::new("launchctl")
|
|
206
|
+
.args(["bootstrap", "system", &plist_path.display().to_string()])
|
|
207
|
+
.output()?
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
if !output.status.success() {
|
|
211
|
+
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
212
|
+
// Handle "already loaded" error gracefully - service is running
|
|
213
|
+
if stderr.contains("already loaded") || stderr.contains("service already loaded") {
|
|
214
|
+
return Ok(());
|
|
215
|
+
}
|
|
216
|
+
return Err(anyhow!("Failed to bootstrap service: {}", stderr.trim()));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
Ok(())
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/// Bootout the service using modern launchctl syntax
|
|
223
|
+
fn bootout(&self) -> Result<()> {
|
|
224
|
+
let output = if self.user_mode {
|
|
225
|
+
let uid = get_user_id()?;
|
|
226
|
+
let service_target = format!("gui/{uid}/{}", self.label());
|
|
227
|
+
Command::new("launchctl")
|
|
228
|
+
.args(["bootout", &service_target])
|
|
229
|
+
.output()?
|
|
230
|
+
} else {
|
|
231
|
+
let service_target = format!("system/{}", self.label());
|
|
232
|
+
Command::new("launchctl")
|
|
233
|
+
.args(["bootout", &service_target])
|
|
234
|
+
.output()?
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
if !output.status.success() {
|
|
238
|
+
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
239
|
+
// Handle "not found" error gracefully - service wasn't running
|
|
240
|
+
if stderr.contains("not find") || stderr.contains("No such") {
|
|
241
|
+
return Ok(());
|
|
242
|
+
}
|
|
243
|
+
return Err(anyhow!("Failed to bootout service: {}", stderr.trim()));
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
Ok(())
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
#[cfg(test)]
|
|
251
|
+
mod tests {
|
|
252
|
+
use super::*;
|
|
253
|
+
|
|
254
|
+
#[test]
|
|
255
|
+
fn test_launchd_manager_user_mode() {
|
|
256
|
+
let manager = LaunchdManager::new("user");
|
|
257
|
+
assert!(manager.user_mode);
|
|
258
|
+
assert_eq!(manager.label(), SERVICE_LABEL);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
#[test]
|
|
262
|
+
fn test_launchd_manager_system_mode() {
|
|
263
|
+
let manager = LaunchdManager::new("system");
|
|
264
|
+
assert!(!manager.user_mode);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
#[test]
|
|
268
|
+
fn test_service_dir_user_mode() {
|
|
269
|
+
let manager = LaunchdManager::new("user");
|
|
270
|
+
let service_dir = manager.service_dir();
|
|
271
|
+
assert!(
|
|
272
|
+
service_dir
|
|
273
|
+
.to_string_lossy()
|
|
274
|
+
.contains("Library/LaunchAgents")
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
#[test]
|
|
279
|
+
fn test_service_dir_system_mode() {
|
|
280
|
+
let manager = LaunchdManager::new("system");
|
|
281
|
+
let service_dir = manager.service_dir();
|
|
282
|
+
assert_eq!(service_dir, PathBuf::from("/Library/LaunchDaemons"));
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
#[test]
|
|
286
|
+
fn test_service_file_path() {
|
|
287
|
+
let manager = LaunchdManager::new("user");
|
|
288
|
+
let path = manager.service_file_path();
|
|
289
|
+
assert!(path.to_string_lossy().ends_with(".plist"));
|
|
290
|
+
assert!(
|
|
291
|
+
path.to_string_lossy()
|
|
292
|
+
.contains("com.opencode-cloud.service")
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
#[test]
|
|
297
|
+
fn test_log_path() {
|
|
298
|
+
let manager = LaunchdManager::new("user");
|
|
299
|
+
let stdout_path = manager.log_path("stdout");
|
|
300
|
+
let stderr_path = manager.log_path("stderr");
|
|
301
|
+
|
|
302
|
+
assert!(stdout_path.to_string_lossy().contains("Library/Logs"));
|
|
303
|
+
assert!(
|
|
304
|
+
stdout_path
|
|
305
|
+
.to_string_lossy()
|
|
306
|
+
.contains("opencode-cloud.stdout.log")
|
|
307
|
+
);
|
|
308
|
+
assert!(
|
|
309
|
+
stderr_path
|
|
310
|
+
.to_string_lossy()
|
|
311
|
+
.contains("opencode-cloud.stderr.log")
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
#[test]
|
|
316
|
+
fn test_generate_plist() {
|
|
317
|
+
let manager = LaunchdManager::new("user");
|
|
318
|
+
let config = ServiceConfig {
|
|
319
|
+
executable_path: PathBuf::from("/usr/local/bin/occ"),
|
|
320
|
+
restart_retries: 3,
|
|
321
|
+
restart_delay: 5,
|
|
322
|
+
boot_mode: "user".to_string(),
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
let plist = manager.generate_plist(&config);
|
|
326
|
+
|
|
327
|
+
assert_eq!(plist.label, SERVICE_LABEL);
|
|
328
|
+
assert_eq!(plist.program_arguments.len(), 3);
|
|
329
|
+
assert_eq!(plist.program_arguments[0], "/usr/local/bin/occ");
|
|
330
|
+
assert_eq!(plist.program_arguments[1], "start");
|
|
331
|
+
assert_eq!(plist.program_arguments[2], "--no-daemon");
|
|
332
|
+
assert!(plist.run_at_load);
|
|
333
|
+
assert!(plist.keep_alive.is_some());
|
|
334
|
+
assert_eq!(plist.throttle_interval, Some(5));
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
#[test]
|
|
338
|
+
fn test_plist_serialization() {
|
|
339
|
+
let manager = LaunchdManager::new("user");
|
|
340
|
+
let config = ServiceConfig {
|
|
341
|
+
executable_path: PathBuf::from("/usr/local/bin/occ"),
|
|
342
|
+
restart_retries: 3,
|
|
343
|
+
restart_delay: 5,
|
|
344
|
+
boot_mode: "user".to_string(),
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
let plist = manager.generate_plist(&config);
|
|
348
|
+
|
|
349
|
+
// Serialize to XML string to verify format
|
|
350
|
+
let mut buffer = Vec::new();
|
|
351
|
+
plist::to_writer_xml(&mut buffer, &plist).expect("Failed to serialize plist");
|
|
352
|
+
let xml = String::from_utf8(buffer).expect("Invalid UTF-8");
|
|
353
|
+
|
|
354
|
+
// Verify key elements are present
|
|
355
|
+
assert!(xml.contains("<key>Label</key>"));
|
|
356
|
+
assert!(xml.contains("<string>com.opencode-cloud.service</string>"));
|
|
357
|
+
assert!(xml.contains("<key>ProgramArguments</key>"));
|
|
358
|
+
assert!(xml.contains("<key>RunAtLoad</key>"));
|
|
359
|
+
assert!(xml.contains("<true/>"));
|
|
360
|
+
assert!(xml.contains("<key>KeepAlive</key>"));
|
|
361
|
+
assert!(xml.contains("<key>ThrottleInterval</key>"));
|
|
362
|
+
}
|
|
363
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
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
|
+
|
|
6
|
+
use std::path::PathBuf;
|
|
7
|
+
|
|
8
|
+
use anyhow::Result;
|
|
9
|
+
|
|
10
|
+
#[cfg(any(
|
|
11
|
+
target_os = "linux",
|
|
12
|
+
not(any(target_os = "linux", target_os = "macos"))
|
|
13
|
+
))]
|
|
14
|
+
use anyhow::anyhow;
|
|
15
|
+
|
|
16
|
+
#[cfg(target_os = "linux")]
|
|
17
|
+
mod systemd;
|
|
18
|
+
|
|
19
|
+
#[cfg(target_os = "macos")]
|
|
20
|
+
mod launchd;
|
|
21
|
+
|
|
22
|
+
#[cfg(target_os = "linux")]
|
|
23
|
+
pub use systemd::{SystemdManager, systemd_available};
|
|
24
|
+
|
|
25
|
+
#[cfg(target_os = "macos")]
|
|
26
|
+
pub use launchd::LaunchdManager;
|
|
27
|
+
|
|
28
|
+
/// Configuration for service installation
|
|
29
|
+
#[derive(Debug, Clone)]
|
|
30
|
+
pub 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)]
|
|
46
|
+
pub 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).
|
|
64
|
+
pub 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.
|
|
90
|
+
pub 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).
|
|
114
|
+
pub fn is_service_registration_supported() -> bool {
|
|
115
|
+
cfg!(any(target_os = "linux", target_os = "macos"))
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
#[cfg(test)]
|
|
119
|
+
mod 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
|
+
}
|