From 419e631868e20263135b5b91f3adede0928cc65a Mon Sep 17 00:00:00 2001 From: ADD-SP Date: Mon, 16 Feb 2026 01:43:26 +0000 Subject: [PATCH] refactor(platform): encapsulate plateform-specific operations --- src/main.rs | 101 ++++++++++++++++++++++++------------------ src/onboard.rs | 27 ++++++++--- src/platform/linux.rs | 54 +++++++++++++++------- src/platform/macos.rs | 74 ++++++++++++++++--------------- src/platform/mod.rs | 14 +++--- 5 files changed, 163 insertions(+), 107 deletions(-) diff --git a/src/main.rs b/src/main.rs index c486e62..b5883bd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -466,9 +466,8 @@ fn cmd_onboard() -> Result<(), Box> { if user_exists { tui::print_step_done(1, TOTAL_STEPS, "System user already exists"); } else { - let status = platform::create_system_user("clawshell")?; - if !status.success() { - tui::print_error("Failed to create 'clawshell' user."); + if let Err(error) = platform::create_system_user("clawshell") { + tui::print_error(&format!("Failed to create 'clawshell' user: {error}")); std::process::exit(1); } tui::print_step_done(1, TOTAL_STEPS, "System user created"); @@ -493,32 +492,35 @@ fn cmd_onboard() -> Result<(), Box> { // Step 3: Set permissions and ownership tui::print_step(3, TOTAL_STEPS, "Setting permissions and ownership..."); - let chown_spec = platform::clawshell_chown_spec(); - - if let Err(e) = std::process::Command::new("chmod") - .args(["0700", &config_dir.to_string_lossy()]) - .status() - { - warn!(path = %config_dir.display(), error = %e, "Failed to chmod config directory"); + if let Err(error) = platform::set_mode(&config_dir, 0o700) { + warn!( + error = %error, + path = %config_dir.display(), + "Failed to set config directory permissions" + ); } - if let Err(e) = std::process::Command::new("chown") - .args(["-R", chown_spec, &config_dir.to_string_lossy()]) - .status() - { - warn!(path = %config_dir.display(), error = %e, "Failed to chown config directory"); + if let Err(error) = platform::set_owner(&config_dir, true) { + warn!( + error = %error, + path = %config_dir.display(), + "Failed to set config directory owner" + ); } - if let Err(e) = std::process::Command::new("chown") - .args(["-R", chown_spec, &log_dir_path.to_string_lossy()]) - .status() - { - warn!(path = %log_dir_path.display(), error = %e, "Failed to chown log directory"); + if let Err(error) = platform::set_owner(&log_dir_path, true) { + warn!( + error = %error, + path = %log_dir_path.display(), + "Failed to set log directory owner" + ); } - if let Some(pid_parent) = pid_path.parent() - && let Err(e) = std::process::Command::new("chown") - .args([chown_spec, &pid_parent.to_string_lossy()]) - .status() - { - warn!(path = %pid_parent.display(), error = %e, "Failed to chown PID directory"); + if let Some(pid_parent) = pid_path.parent() { + if let Err(error) = platform::set_owner(pid_parent, false) { + warn!( + error = %error, + path = %pid_parent.display(), + "Failed to set PID directory owner" + ); + } } tui::print_step_done(3, TOTAL_STEPS, "Permissions set"); @@ -548,18 +550,34 @@ fn cmd_onboard() -> Result<(), Box> { std::fs::write(&config_file, serde_json::to_string_pretty(&config_json)?)?; // Set permissions on config files - let _ = std::process::Command::new("chmod") - .args(["0600", &config_file.to_string_lossy()]) - .status(); - let _ = std::process::Command::new("chmod") - .args(["0600", &toml_config_path.to_string_lossy()]) - .status(); - let _ = std::process::Command::new("chown") - .args([chown_spec, &config_file.to_string_lossy()]) - .status(); - let _ = std::process::Command::new("chown") - .args([chown_spec, &toml_config_path.to_string_lossy()]) - .status(); + if let Err(error) = platform::set_mode(&config_file, 0o600) { + warn!( + error = %error, + path = %config_file.display(), + "Failed to set config.json permissions" + ); + } + if let Err(error) = platform::set_mode(&toml_config_path, 0o600) { + warn!( + error = %error, + path = %toml_config_path.display(), + "Failed to set clawshell.toml permissions" + ); + } + if let Err(error) = platform::set_owner(&config_file, false) { + warn!( + error = %error, + path = %config_file.display(), + "Failed to set config.json owner" + ); + } + if let Err(error) = platform::set_owner(&toml_config_path, false) { + warn!( + error = %error, + path = %toml_config_path.display(), + "Failed to set clawshell.toml owner" + ); + } tui::print_step_done(5, TOTAL_STEPS, "Configuration written"); // Step 6: OpenClaw config path was already asked in step 4 @@ -938,11 +956,10 @@ fn cmd_uninstall(skip_confirm: bool) -> Result<(), Box> { .unwrap_or(false); if user_exists { - let status = platform::delete_system_user("clawshell")?; - if status.success() { - tui::print_success("System user removed."); + if let Err(error) = platform::delete_system_user("clawshell") { + tui::print_warning(&format!("Failed to remove system user: {error}")); } else { - tui::print_warning(&format!("Failed to remove user (exit code: {status}).")); + tui::print_success("System user removed."); } } diff --git a/src/onboard.rs b/src/onboard.rs index 09b418a..7f6ba49 100644 --- a/src/onboard.rs +++ b/src/onboard.rs @@ -4,6 +4,7 @@ use crate::tui; use serde_json::Value; use std::io::{self, BufRead, Write}; use std::path::{Path, PathBuf}; +use tracing::warn; use vfs::VfsPath; /// API keys detected from an existing OpenClaw installation. @@ -660,11 +661,14 @@ pub fn backup_openclaw_config(openclaw_path: &Path) -> Result &'static str { "clawshell:clawshell" @@ -22,7 +22,7 @@ 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 { +pub fn create_system_user(name: &str) -> Result<(), Error> { let mut command = Command::new("useradd"); command.args([ "--system", @@ -34,7 +34,7 @@ pub fn create_system_user(name: &str) -> Result { command_status(&mut command, "useradd") } -pub fn delete_system_user(name: &str) -> Result { +pub fn delete_system_user(name: &str) -> Result<(), Error> { let mut command = Command::new("userdel"); command.arg(name); command_status(&mut command, "userdel") @@ -43,13 +43,11 @@ pub fn delete_system_user(name: &str) -> Result { 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)?; + command_status(&mut daemon_reload, "systemctl daemon-reload")?; 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)?; + command_status(&mut enable, "systemctl enable clawshell.service")?; Ok(()) } @@ -57,22 +55,44 @@ pub fn install_autostart_post_write(_service_path: &str) -> Result<(), Error> { 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)?; + command_status(&mut start, "systemctl start clawshell.service")?; Ok(()) } pub fn remove_autostart_service(_service_path: &str) -> Result<(), Error> { - let _ = Command::new("systemctl") - .args(["disable", "clawshell.service"]) - .status(); - let _ = Command::new("systemctl") - .args(["stop", "clawshell.service"]) - .status(); + let mut disable = Command::new("systemctl"); + disable.args(["disable", "clawshell.service"]); + command_status(&mut disable, "systemctl disable clawshell.service")?; + + let mut stop = Command::new("systemctl"); + stop.args(["stop", "clawshell.service"]); + command_status(&mut stop, "systemctl stop clawshell.service")?; + Ok(()) } pub fn remove_autostart_post_delete() -> Result<(), Error> { - let _ = Command::new("systemctl").args(["daemon-reload"]).status(); + let mut daemon_reload = Command::new("systemctl"); + daemon_reload.args(["daemon-reload"]); + command_status(&mut daemon_reload, "systemctl daemon-reload")?; Ok(()) } + +pub fn set_owner(path: &Path, recursive: bool) -> Result<(), Error> { + let mut command = Command::new("chown"); + if recursive { + command.arg("-R"); + } + let path_arg = path.to_string_lossy().into_owned(); + command.args([clawshell_chown_spec(), path_arg.as_str()]); + let op = if recursive { "chown -R" } else { "chown" }; + command_status(&mut command, op) +} + +pub fn set_mode(path: &Path, mode_bits: u32) -> Result<(), Error> { + let mode_str = format_octal_mode(mode_bits); + let path_arg = path.to_string_lossy().into_owned(); + let mut command = Command::new("chmod"); + command.args([mode_str.as_str(), path_arg.as_str()]); + command_status(&mut command, "chmod") +} diff --git a/src/platform/macos.rs b/src/platform/macos.rs index a3d74c6..0f63cc9 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -1,6 +1,6 @@ -use super::{Error, command_output, command_status, ensure_success}; +use super::{Error, command_output, command_status, format_octal_mode}; use std::path::Path; -use std::process::{Command, ExitStatus, Stdio}; +use std::process::Command; pub fn clawshell_chown_spec() -> &'static str { "clawshell:staff" @@ -22,7 +22,7 @@ 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 { +pub fn create_system_user(name: &str) -> Result<(), Error> { let mut list_users = Command::new("dscl"); list_users.args([".", "-list", "/Users", "UniqueID"]); let output = command_output(&mut list_users, "dscl -list /Users UniqueID")?; @@ -39,66 +39,55 @@ pub fn create_system_user(name: &str) -> Result { let user_path = format!("/Users/{name}"); let uid_str = uid.to_string(); - let dscl = |args: &[&str], desc: &str| -> Result { + let dscl = |args: &[&str], command_name: &'static str| -> Result<(), Error> { 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}'"); - } - Ok(status) + command_status(&mut command, command_name) }; - dscl(&[".", "-create", &user_path], "create user record")?; + dscl(&[".", "-create", &user_path], "dscl create user record")?; dscl( &[".", "-create", &user_path, "UniqueID", &uid_str], - "set UID", + "dscl set user UID", )?; dscl( &[".", "-create", &user_path, "PrimaryGroupID", "20"], - "set GID", + "dscl set user GID", )?; dscl( &[".", "-create", &user_path, "UserShell", "/usr/bin/false"], - "set shell", + "dscl set user shell", )?; dscl( &[".", "-create", &user_path, "RealName", "ClawShell Service"], - "set real name", + "dscl set user real name", )?; - let status = dscl( + dscl( &[".", "-create", &user_path, "NFSHomeDirectory", "/var/empty"], - "set home directory", + "dscl set user home directory", )?; let mut hide_user = Command::new("dscl"); hide_user.args([".", "-create", &user_path, "IsHidden", "1"]); - let _ = command_status(&mut hide_user, "dscl"); + command_status(&mut hide_user, "dscl hide user")?; - Ok(status) + Ok(()) } -pub fn delete_system_user(name: &str) -> Result { +pub fn delete_system_user(name: &str) -> Result<(), Error> { 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<(), Error> { - let mut unload = Command::new("launchctl"); - unload - .args(["unload", service_path]) - .stdout(Stdio::null()) - .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"); + command_status(&mut chown, "chown")?; let mut chmod = Command::new("chmod"); chmod.args(["0644", service_path]); - let _ = command_status(&mut chmod, "chmod"); + command_status(&mut chmod, "chmod")?; Ok(()) } @@ -106,21 +95,36 @@ pub fn install_autostart_post_write(service_path: &str) -> Result<(), Error> { 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)?; + command_status(&mut load, "launchctl load")?; Ok(()) } 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()); - let _ = command_status(&mut unload, "launchctl unload"); + unload.args(["unload", service_path]); + command_status(&mut unload, "launchctl unload")?; Ok(()) } pub fn remove_autostart_post_delete() -> Result<(), Error> { Ok(()) } + +pub fn set_owner(path: &Path, recursive: bool) -> Result<(), Error> { + let mut command = Command::new("chown"); + if recursive { + command.arg("-R"); + } + let path_arg = path.to_string_lossy().into_owned(); + command.args([clawshell_chown_spec(), path_arg.as_str()]); + let op = if recursive { "chown -R" } else { "chown" }; + command_status(&mut command, op) +} + +pub fn set_mode(path: &Path, mode_bits: u32) -> Result<(), Error> { + let mode_str = format_octal_mode(mode_bits); + let path_arg = path.to_string_lossy().into_owned(); + let mut command = Command::new("chmod"); + command.args([mode_str.as_str(), path_arg.as_str()]); + command_status(&mut command, "chmod") +} diff --git a/src/platform/mod.rs b/src/platform/mod.rs index 0ee4d6e..f54ebb5 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -32,13 +32,6 @@ pub enum Error { 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, @@ -49,7 +42,8 @@ fn command_output( }) } -fn ensure_success(command_name: &'static str, output: std::process::Output) -> Result<(), Error> { +fn command_status(command: &mut Command, command_name: &'static str) -> Result<(), Error> { + let output = command_output(command, command_name)?; if output.status.success() { Ok(()) } else { @@ -61,3 +55,7 @@ fn ensure_success(command_name: &'static str, output: std::process::Output) -> R }) } } + +fn format_octal_mode(mode_bits: u32) -> String { + format!("{:04o}", mode_bits) +}