From a3d8cb9485ece152a02515806d249f2076c42f8a Mon Sep 17 00:00:00 2001 From: ADD-SP Date: Mon, 16 Feb 2026 00:11:47 +0000 Subject: [PATCH 1/2] refactor: split macOS/Linux code into platform modules --- src/lib.rs | 1 + src/main.rs | 99 ++--------------------------------- src/onboard.rs | 91 +++----------------------------- src/platform/linux.rs | 79 ++++++++++++++++++++++++++++ src/platform/macos.rs | 118 ++++++++++++++++++++++++++++++++++++++++++ src/platform/mod.rs | 12 +++++ src/process.rs | 13 ++--- 7 files changed, 225 insertions(+), 188 deletions(-) create mode 100644 src/platform/linux.rs create mode 100644 src/platform/macos.rs create mode 100644 src/platform/mod.rs diff --git a/src/lib.rs b/src/lib.rs index c67987b..004dc75 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,6 +7,7 @@ pub mod config; pub mod dlp; pub mod keys; pub mod onboard; +pub mod platform; pub mod process; pub mod proxy; pub mod tui; diff --git a/src/main.rs b/src/main.rs index 879704b..c486e62 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,6 +7,7 @@ use tracing::{debug, info, warn}; use clawshell::cli::{Cli, Commands}; use clawshell::config::Config; +use clawshell::platform; use clawshell::process; use clawshell::tui; use clawshell::{AppState, build_router}; @@ -465,7 +466,7 @@ fn cmd_onboard() -> Result<(), Box> { if user_exists { tui::print_step_done(1, TOTAL_STEPS, "System user already exists"); } else { - let status = create_system_user("clawshell")?; + let status = platform::create_system_user("clawshell")?; if !status.success() { tui::print_error("Failed to create 'clawshell' user."); std::process::exit(1); @@ -492,11 +493,7 @@ fn cmd_onboard() -> Result<(), Box> { // Step 3: Set permissions and ownership tui::print_step(3, TOTAL_STEPS, "Setting permissions and ownership..."); - let chown_spec = if cfg!(target_os = "macos") { - "clawshell:staff" - } else { - "clawshell:clawshell" - }; + let chown_spec = platform::clawshell_chown_spec(); if let Err(e) = std::process::Command::new("chmod") .args(["0700", &config_dir.to_string_lossy()]) @@ -941,7 +938,7 @@ fn cmd_uninstall(skip_confirm: bool) -> Result<(), Box> { .unwrap_or(false); if user_exists { - let status = delete_system_user("clawshell")?; + let status = platform::delete_system_user("clawshell")?; if status.success() { tui::print_success("System user removed."); } else { @@ -976,83 +973,6 @@ fn cmd_version() { println!(" {bullet} Streaming support (SSE pass-through)"); } -/// Create a system user, using platform-appropriate commands. -fn create_system_user(name: &str) -> Result> { - if cfg!(target_os = "macos") { - create_macos_system_user(name) - } else { - Ok(std::process::Command::new("useradd") - .args([ - "--system", - "--no-create-home", - "--shell", - "/usr/sbin/nologin", - name, - ]) - .status()?) - } -} - -/// Create a hidden system user on macOS using dscl. -fn create_macos_system_user( - name: &str, -) -> Result> { - let output = std::process::Command::new("dscl") - .args([".", "-list", "/Users", "UniqueID"]) - .output()?; - let stdout = String::from_utf8_lossy(&output.stdout); - let used_uids: Vec = stdout - .lines() - .filter_map(|line| line.split_whitespace().last()?.parse().ok()) - .collect(); - let uid = (400..500) - .rev() - .find(|u| !used_uids.contains(u)) - .ok_or("No available system UID in 400-499 range")?; - - let user_path = format!("/Users/{name}"); - let uid_str = uid.to_string(); - - let dscl = |args: &[&str], - desc: &str| - -> Result> { - let status = std::process::Command::new("dscl").args(args).status()?; - if !status.success() { - eprintln!("Warning: failed to {desc} for '{name}'"); - } - Ok(status) - }; - - dscl(&[".", "-create", &user_path], "create user record")?; - dscl( - &[".", "-create", &user_path, "UniqueID", &uid_str], - "set UID", - )?; - dscl( - &[".", "-create", &user_path, "PrimaryGroupID", "20"], - "set GID", - )?; - dscl( - &[".", "-create", &user_path, "UserShell", "/usr/bin/false"], - "set shell", - )?; - dscl( - &[".", "-create", &user_path, "RealName", "ClawShell Service"], - "set real name", - )?; - let status = dscl( - &[".", "-create", &user_path, "NFSHomeDirectory", "/var/empty"], - "set home directory", - )?; - - // Hide the user from the login window - let _ = std::process::Command::new("dscl") - .args([".", "-create", &user_path, "IsHidden", "1"]) - .status(); - - Ok(status) -} - /// Start ClawShell directly by spawning a child process (no service manager). fn start_clawshell_direct( toml_config_path: &std::path::Path, @@ -1082,14 +1002,3 @@ fn start_clawshell_direct( tui::print_info("PID", &pid.to_string()); Ok(()) } - -/// Delete a system user, using platform-appropriate commands. -fn delete_system_user(name: &str) -> Result> { - if cfg!(target_os = "macos") { - Ok(std::process::Command::new("dscl") - .args([".", "-delete", &format!("/Users/{name}")]) - .status()?) - } else { - Ok(std::process::Command::new("userdel").arg(name).status()?) - } -} diff --git a/src/onboard.rs b/src/onboard.rs index a323561..8546d94 100644 --- a/src/onboard.rs +++ b/src/onboard.rs @@ -1,3 +1,4 @@ +use crate::platform; use crate::tui; use serde_json::Value; @@ -660,11 +661,7 @@ pub fn backup_openclaw_config(openclaw_path: &Path) -> Result Result<(), Box> { - let content = if cfg!(target_os = "macos") { - generate_launchd_plist(exe_path, config_path) - } else { - generate_systemd_unit(exe_path, config_path) - }; + let content = platform::autostart_service_content(exe_path, config_path); let service_path = autostart_service_path(); let root = crate::process::physical_root(); let vfs_path = root.join(service_path.trim_start_matches('/'))?; install_autostart_service_vfs(&vfs_path, &content)?; - - if cfg!(target_os = "macos") { - // Unload if already loaded so launchd picks up the new plist. - let _ = std::process::Command::new("launchctl") - .args(["unload", service_path]) - .stdout(std::process::Stdio::null()) - .stderr(std::process::Stdio::null()) - .status(); - let _ = std::process::Command::new("chown") - .args(["root:wheel", service_path]) - .status(); - let _ = std::process::Command::new("chmod") - .args(["0644", service_path]) - .status(); - // Loading is done separately via start_autostart_service(). - } else { - let status = std::process::Command::new("systemctl") - .args(["daemon-reload"]) - .status()?; - if !status.success() { - return Err("systemctl daemon-reload failed".into()); - } - let status = std::process::Command::new("systemctl") - .args(["enable", "clawshell.service"]) - .status()?; - if !status.success() { - return Err("systemctl enable failed".into()); - } - } + platform::install_autostart_post_write(service_path)?; Ok(()) } @@ -944,54 +905,18 @@ pub fn install_autostart_service( /// Start the auto-start service via the platform service manager. pub fn start_autostart_service() -> Result<(), Box> { let service_path = autostart_service_path(); - - if cfg!(target_os = "macos") { - let status = std::process::Command::new("launchctl") - .args(["load", service_path]) - .status()?; - if !status.success() { - return Err(format!("launchctl load failed (exit code {})", status).into()); - } - } else { - let status = std::process::Command::new("systemctl") - .args(["start", "clawshell.service"]) - .status()?; - if !status.success() { - return Err(format!("systemctl start failed (exit code {})", status).into()); - } - } - - Ok(()) + platform::start_autostart_service(service_path) } /// Remove the auto-start service from the real filesystem and disable it. pub fn remove_autostart_service() -> Result<(), Box> { let service_path = autostart_service_path(); - - if cfg!(target_os = "macos") { - let _ = std::process::Command::new("launchctl") - .args(["unload", service_path]) - .stdout(std::process::Stdio::null()) - .stderr(std::process::Stdio::null()) - .status(); - } else { - let _ = std::process::Command::new("systemctl") - .args(["disable", "clawshell.service"]) - .status(); - let _ = std::process::Command::new("systemctl") - .args(["stop", "clawshell.service"]) - .status(); - } + platform::remove_autostart_service(service_path)?; let root = crate::process::physical_root(); let vfs_path = root.join(service_path.trim_start_matches('/'))?; remove_autostart_service_vfs(&vfs_path)?; - - if !cfg!(target_os = "macos") { - let _ = std::process::Command::new("systemctl") - .args(["daemon-reload"]) - .status(); - } + platform::remove_autostart_post_delete()?; Ok(()) } diff --git a/src/platform/linux.rs b/src/platform/linux.rs new file mode 100644 index 0000000..20562b2 --- /dev/null +++ b/src/platform/linux.rs @@ -0,0 +1,79 @@ +use std::path::Path; +use std::process::{Command, ExitStatus}; + +pub fn clawshell_chown_spec() -> &'static str { + "clawshell:clawshell" +} + +pub fn pid_file_abs_path() -> &'static str { + "/run/clawshell/clawshell.pid" +} + +pub fn pid_file_vfs_rel_path() -> &'static str { + "run/clawshell/clawshell.pid" +} + +pub fn autostart_service_path() -> &'static str { + "/etc/systemd/system/clawshell.service" +} + +pub fn autostart_service_content(exe_path: &Path, config_path: &Path) -> String { + crate::onboard::generate_systemd_unit(exe_path, config_path) +} + +pub fn create_system_user(name: &str) -> Result> { + Ok(Command::new("useradd") + .args([ + "--system", + "--no-create-home", + "--shell", + "/usr/sbin/nologin", + name, + ]) + .status()?) +} + +pub fn delete_system_user(name: &str) -> Result> { + Ok(Command::new("userdel").arg(name).status()?) +} + +pub fn install_autostart_post_write(_service_path: &str) -> Result<(), Box> { + let status = Command::new("systemctl").args(["daemon-reload"]).status()?; + if !status.success() { + return Err("systemctl daemon-reload failed".into()); + } + + let status = Command::new("systemctl") + .args(["enable", "clawshell.service"]) + .status()?; + if !status.success() { + return Err("systemctl enable failed".into()); + } + + Ok(()) +} + +pub fn start_autostart_service(_service_path: &str) -> Result<(), Box> { + let status = Command::new("systemctl") + .args(["start", "clawshell.service"]) + .status()?; + if !status.success() { + return Err(format!("systemctl start failed (exit code {})", status).into()); + } + Ok(()) +} + +pub fn remove_autostart_service(_service_path: &str) -> Result<(), Box> { + let _ = Command::new("systemctl") + .args(["disable", "clawshell.service"]) + .status(); + let _ = Command::new("systemctl") + .args(["stop", "clawshell.service"]) + .status(); + Ok(()) +} + +pub fn remove_autostart_post_delete() -> Result<(), Box> { + let _ = Command::new("systemctl").args(["daemon-reload"]).status(); + Ok(()) +} diff --git a/src/platform/macos.rs b/src/platform/macos.rs new file mode 100644 index 0000000..40fd557 --- /dev/null +++ b/src/platform/macos.rs @@ -0,0 +1,118 @@ +use std::path::Path; +use std::process::{Command, ExitStatus, Stdio}; + +pub fn clawshell_chown_spec() -> &'static str { + "clawshell:staff" +} + +pub fn pid_file_abs_path() -> &'static str { + "/var/run/clawshell.pid" +} + +pub fn pid_file_vfs_rel_path() -> &'static str { + "var/run/clawshell.pid" +} + +pub fn autostart_service_path() -> &'static str { + "/Library/LaunchDaemons/com.clawshell.daemon.plist" +} + +pub fn autostart_service_content(exe_path: &Path, config_path: &Path) -> String { + crate::onboard::generate_launchd_plist(exe_path, config_path) +} + +pub fn create_system_user(name: &str) -> Result> { + let output = Command::new("dscl") + .args([".", "-list", "/Users", "UniqueID"]) + .output()?; + let stdout = String::from_utf8_lossy(&output.stdout); + let used_uids: Vec = stdout + .lines() + .filter_map(|line| line.split_whitespace().last()?.parse().ok()) + .collect(); + let uid = (400..500) + .rev() + .find(|u| !used_uids.contains(u)) + .ok_or("No available system UID in 400-499 range")?; + + let user_path = format!("/Users/{name}"); + let uid_str = uid.to_string(); + + let dscl = |args: &[&str], desc: &str| -> Result> { + let status = Command::new("dscl").args(args).status()?; + if !status.success() { + eprintln!("Warning: failed to {desc} for '{name}'"); + } + Ok(status) + }; + + dscl(&[".", "-create", &user_path], "create user record")?; + dscl( + &[".", "-create", &user_path, "UniqueID", &uid_str], + "set UID", + )?; + dscl( + &[".", "-create", &user_path, "PrimaryGroupID", "20"], + "set GID", + )?; + dscl( + &[".", "-create", &user_path, "UserShell", "/usr/bin/false"], + "set shell", + )?; + dscl( + &[".", "-create", &user_path, "RealName", "ClawShell Service"], + "set real name", + )?; + let status = dscl( + &[".", "-create", &user_path, "NFSHomeDirectory", "/var/empty"], + "set home directory", + )?; + + let _ = Command::new("dscl") + .args([".", "-create", &user_path, "IsHidden", "1"]) + .status(); + + Ok(status) +} + +pub fn delete_system_user(name: &str) -> Result> { + Ok(Command::new("dscl") + .args([".", "-delete", &format!("/Users/{name}")]) + .status()?) +} + +pub fn install_autostart_post_write(service_path: &str) -> Result<(), Box> { + let _ = Command::new("launchctl") + .args(["unload", service_path]) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status(); + let _ = Command::new("chown") + .args(["root:wheel", service_path]) + .status(); + let _ = Command::new("chmod").args(["0644", service_path]).status(); + Ok(()) +} + +pub fn start_autostart_service(service_path: &str) -> Result<(), Box> { + let status = Command::new("launchctl") + .args(["load", service_path]) + .status()?; + if !status.success() { + return Err(format!("launchctl load failed (exit code {})", status).into()); + } + Ok(()) +} + +pub fn remove_autostart_service(service_path: &str) -> Result<(), Box> { + let _ = Command::new("launchctl") + .args(["unload", service_path]) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status(); + Ok(()) +} + +pub fn remove_autostart_post_delete() -> Result<(), Box> { + Ok(()) +} diff --git a/src/platform/mod.rs b/src/platform/mod.rs new file mode 100644 index 0000000..1cad63a --- /dev/null +++ b/src/platform/mod.rs @@ -0,0 +1,12 @@ +#[cfg(target_os = "linux")] +mod linux; +#[cfg(target_os = "macos")] +mod macos; + +#[cfg(target_os = "linux")] +pub use self::linux::*; +#[cfg(target_os = "macos")] +pub use self::macos::*; + +#[cfg(not(any(target_os = "linux", target_os = "macos")))] +compile_error!("unsupported target OS: ClawShell currently supports only Linux and macOS"); diff --git a/src/process.rs b/src/process.rs index a71955a..12c167b 100644 --- a/src/process.rs +++ b/src/process.rs @@ -1,3 +1,4 @@ +use crate::platform; use nix::sys::signal::{self, Signal}; use nix::unistd::{Gid, Pid, Uid, User, getuid, setgid, setuid}; use nix::unistd::{SysconfVar, sysconf}; @@ -23,11 +24,7 @@ pub(crate) fn physical_root() -> VfsPath { /// PID file path within a VFS root. fn pid_file_vfs(root: &VfsPath) -> Result> { - if cfg!(target_os = "macos") { - Ok(root.join("var/run/clawshell.pid")?) - } else { - Ok(root.join("run/clawshell/clawshell.pid")?) - } + Ok(root.join(platform::pid_file_vfs_rel_path())?) } /// Log file path within a VFS root. @@ -39,11 +36,7 @@ fn log_file_vfs(root: &VfsPath) -> Result> { /// - Linux: /run/clawshell/clawshell.pid /// - macOS: /var/run/clawshell.pid (flat, no subdirectory since /var/run is a symlink to /private/var/run) pub fn pid_file_path() -> PathBuf { - if cfg!(target_os = "macos") { - PathBuf::from("/var/run/clawshell.pid") - } else { - PathBuf::from("/run/clawshell/clawshell.pid") - } + PathBuf::from(platform::pid_file_abs_path()) } /// Log file location. From 1bf5e12ebd1fa3409ed77c0a99faa60de3eb96a3 Mon Sep 17 00:00:00 2001 From: ADD-SP Date: Mon, 16 Feb 2026 00:25:20 +0000 Subject: [PATCH 2/2] refactor(platform): use structured error handling --- Cargo.lock | 1 + Cargo.toml | 1 + src/onboard.rs | 3 +- src/platform/linux.rs | 63 ++++++++++++++++++----------------- src/platform/macos.rs | 76 ++++++++++++++++++++++++------------------- src/platform/mod.rs | 51 +++++++++++++++++++++++++++++ 6 files changed, 128 insertions(+), 67 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d9fddbe..b6679ac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -319,6 +319,7 @@ dependencies = [ "reqwest", "serde", "serde_json", + "thiserror 2.0.18", "tokio", "toml", "tower", diff --git a/Cargo.toml b/Cargo.toml index 0a6db6c..378f6b1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,7 @@ nix = { version = "0.31.1", features = ["signal", "process", "feature", "user"] inquire = "0.9.3" console = "0.16.2" vfs = "0.12" +thiserror = "2" [dev-dependencies] tokio = { version = "1.49", features = ["full", "test-util"] } diff --git a/src/onboard.rs b/src/onboard.rs index 8546d94..09b418a 100644 --- a/src/onboard.rs +++ b/src/onboard.rs @@ -905,7 +905,8 @@ pub fn install_autostart_service( /// Start the auto-start service via the platform service manager. pub fn start_autostart_service() -> Result<(), Box> { let service_path = autostart_service_path(); - platform::start_autostart_service(service_path) + platform::start_autostart_service(service_path)?; + Ok(()) } /// Remove the auto-start service from the real filesystem and disable it. diff --git a/src/platform/linux.rs b/src/platform/linux.rs index 20562b2..1f6aa4b 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -1,3 +1,4 @@ +use super::{Error, command_output, command_status, ensure_success}; use std::path::Path; use std::process::{Command, ExitStatus}; @@ -21,49 +22,47 @@ pub fn autostart_service_content(exe_path: &Path, config_path: &Path) -> String crate::onboard::generate_systemd_unit(exe_path, config_path) } -pub fn create_system_user(name: &str) -> Result> { - Ok(Command::new("useradd") - .args([ - "--system", - "--no-create-home", - "--shell", - "/usr/sbin/nologin", - name, - ]) - .status()?) +pub fn create_system_user(name: &str) -> Result { + let mut command = Command::new("useradd"); + command.args([ + "--system", + "--no-create-home", + "--shell", + "/usr/sbin/nologin", + name, + ]); + command_status(&mut command, "useradd") } -pub fn delete_system_user(name: &str) -> Result> { - Ok(Command::new("userdel").arg(name).status()?) +pub fn delete_system_user(name: &str) -> Result { + let mut command = Command::new("userdel"); + command.arg(name); + command_status(&mut command, "userdel") } -pub fn install_autostart_post_write(_service_path: &str) -> Result<(), Box> { - let status = Command::new("systemctl").args(["daemon-reload"]).status()?; - if !status.success() { - return Err("systemctl daemon-reload failed".into()); - } +pub fn install_autostart_post_write(_service_path: &str) -> Result<(), Error> { + let mut daemon_reload = Command::new("systemctl"); + daemon_reload.args(["daemon-reload"]); + let output = command_output(&mut daemon_reload, "systemctl daemon-reload")?; + ensure_success("systemctl daemon-reload", output)?; - let status = Command::new("systemctl") - .args(["enable", "clawshell.service"]) - .status()?; - if !status.success() { - return Err("systemctl enable failed".into()); - } + let mut enable = Command::new("systemctl"); + enable.args(["enable", "clawshell.service"]); + let output = command_output(&mut enable, "systemctl enable clawshell.service")?; + ensure_success("systemctl enable clawshell.service", output)?; Ok(()) } -pub fn start_autostart_service(_service_path: &str) -> Result<(), Box> { - let status = Command::new("systemctl") - .args(["start", "clawshell.service"]) - .status()?; - if !status.success() { - return Err(format!("systemctl start failed (exit code {})", status).into()); - } +pub fn start_autostart_service(_service_path: &str) -> Result<(), Error> { + let mut start = Command::new("systemctl"); + start.args(["start", "clawshell.service"]); + let output = command_output(&mut start, "systemctl start clawshell.service")?; + ensure_success("systemctl start clawshell.service", output)?; Ok(()) } -pub fn remove_autostart_service(_service_path: &str) -> Result<(), Box> { +pub fn remove_autostart_service(_service_path: &str) -> Result<(), Error> { let _ = Command::new("systemctl") .args(["disable", "clawshell.service"]) .status(); @@ -73,7 +72,7 @@ pub fn remove_autostart_service(_service_path: &str) -> Result<(), Box Result<(), Box> { +pub fn remove_autostart_post_delete() -> Result<(), Error> { let _ = Command::new("systemctl").args(["daemon-reload"]).status(); Ok(()) } diff --git a/src/platform/macos.rs b/src/platform/macos.rs index 40fd557..a3d74c6 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -1,3 +1,4 @@ +use super::{Error, command_output, command_status, ensure_success}; use std::path::Path; use std::process::{Command, ExitStatus, Stdio}; @@ -21,10 +22,10 @@ pub fn autostart_service_content(exe_path: &Path, config_path: &Path) -> String crate::onboard::generate_launchd_plist(exe_path, config_path) } -pub fn create_system_user(name: &str) -> Result> { - let output = Command::new("dscl") - .args([".", "-list", "/Users", "UniqueID"]) - .output()?; +pub fn create_system_user(name: &str) -> Result { + let mut list_users = Command::new("dscl"); + list_users.args([".", "-list", "/Users", "UniqueID"]); + let output = command_output(&mut list_users, "dscl -list /Users UniqueID")?; let stdout = String::from_utf8_lossy(&output.stdout); let used_uids: Vec = stdout .lines() @@ -33,13 +34,15 @@ pub fn create_system_user(name: &str) -> Result Result> { - let status = Command::new("dscl").args(args).status()?; + let dscl = |args: &[&str], desc: &str| -> Result { + let mut command = Command::new("dscl"); + command.args(args); + let status = command_status(&mut command, "dscl")?; if !status.success() { eprintln!("Warning: failed to {desc} for '{name}'"); } @@ -68,51 +71,56 @@ pub fn create_system_user(name: &str) -> Result Result> { - Ok(Command::new("dscl") - .args([".", "-delete", &format!("/Users/{name}")]) - .status()?) +pub fn delete_system_user(name: &str) -> Result { + let mut command = Command::new("dscl"); + command.args([".", "-delete", &format!("/Users/{name}")]); + command_status(&mut command, "dscl -delete /Users") } -pub fn install_autostart_post_write(service_path: &str) -> Result<(), Box> { - let _ = Command::new("launchctl") +pub fn install_autostart_post_write(service_path: &str) -> Result<(), Error> { + let mut unload = Command::new("launchctl"); + unload .args(["unload", service_path]) .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status(); - let _ = Command::new("chown") - .args(["root:wheel", service_path]) - .status(); - let _ = Command::new("chmod").args(["0644", service_path]).status(); + .stderr(Stdio::null()); + let _ = command_status(&mut unload, "launchctl unload"); + + let mut chown = Command::new("chown"); + chown.args(["root:wheel", service_path]); + let _ = command_status(&mut chown, "chown"); + + let mut chmod = Command::new("chmod"); + chmod.args(["0644", service_path]); + let _ = command_status(&mut chmod, "chmod"); + Ok(()) } -pub fn start_autostart_service(service_path: &str) -> Result<(), Box> { - let status = Command::new("launchctl") - .args(["load", service_path]) - .status()?; - if !status.success() { - return Err(format!("launchctl load failed (exit code {})", status).into()); - } +pub fn start_autostart_service(service_path: &str) -> Result<(), Error> { + let mut load = Command::new("launchctl"); + load.args(["load", service_path]); + let output = command_output(&mut load, "launchctl load")?; + ensure_success("launchctl load", output)?; Ok(()) } -pub fn remove_autostart_service(service_path: &str) -> Result<(), Box> { - let _ = Command::new("launchctl") +pub fn remove_autostart_service(service_path: &str) -> Result<(), Error> { + let mut unload = Command::new("launchctl"); + unload .args(["unload", service_path]) .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status(); + .stderr(Stdio::null()); + let _ = command_status(&mut unload, "launchctl unload"); Ok(()) } -pub fn remove_autostart_post_delete() -> Result<(), Box> { +pub fn remove_autostart_post_delete() -> Result<(), Error> { Ok(()) } diff --git a/src/platform/mod.rs b/src/platform/mod.rs index 1cad63a..0ee4d6e 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -1,3 +1,5 @@ +use std::process::{Command, ExitStatus}; + #[cfg(target_os = "linux")] mod linux; #[cfg(target_os = "macos")] @@ -10,3 +12,52 @@ pub use self::macos::*; #[cfg(not(any(target_os = "linux", target_os = "macos")))] compile_error!("unsupported target OS: ClawShell currently supports only Linux and macOS"); + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("{command} failed to execute: {source}")] + CommandIo { + command: &'static str, + #[source] + source: std::io::Error, + }, + #[error("{command} failed with exit status {status}; stdout: {stdout}; stderr: {stderr}")] + CommandFailed { + command: &'static str, + status: ExitStatus, + stdout: String, + stderr: String, + }, + #[error("no available system UID in 400-499 range")] + NoAvailableSystemUid, +} + +fn command_status(command: &mut Command, command_name: &'static str) -> Result { + command.status().map_err(|source| Error::CommandIo { + command: command_name, + source, + }) +} + +fn command_output( + command: &mut Command, + command_name: &'static str, +) -> Result { + command.output().map_err(|source| Error::CommandIo { + command: command_name, + source, + }) +} + +fn ensure_success(command_name: &'static str, output: std::process::Output) -> Result<(), Error> { + if output.status.success() { + Ok(()) + } else { + Err(Error::CommandFailed { + command: command_name, + status: output.status, + stdout: String::from_utf8_lossy(&output.stdout).to_string(), + stderr: String::from_utf8_lossy(&output.stderr).to_string(), + }) + } +}