From 92a0699669e74d81fe48ec79a940bf95f349da20 Mon Sep 17 00:00:00 2001 From: Julien Desgats Date: Tue, 17 Feb 2026 19:11:30 +0000 Subject: [PATCH] remove all pidfile management logic this is not used anymore as we rely on OS process manager for lifecycle management --- src/main.rs | 132 ++----------------- src/platform/linux.rs | 8 -- src/platform/macos.rs | 8 -- src/process.rs | 295 +----------------------------------------- tests/cli_tests.rs | 14 -- 5 files changed, 13 insertions(+), 444 deletions(-) diff --git a/src/main.rs b/src/main.rs index c949e17..e0f3476 100644 --- a/src/main.rs +++ b/src/main.rs @@ -192,7 +192,7 @@ async fn main() -> Result<(), Box> { async fn cmd_start(config_path: &str, foreground: bool) -> Result<(), Box> { tui::print_banner("Start"); if foreground { - return cmd_start_inner(config_path, true).await; + return cmd_start_inner(config_path).await; } let path = PathBuf::from(config_path); @@ -209,23 +209,7 @@ async fn cmd_start(config_path: &str, foreground: bool) -> Result<(), Box Result<(), Box> { - // Check if already running (skip if PID file points to ourselves — daemon child) - let my_pid = std::process::id(); - if let Some(pid) = process::read_pid_file() { - if pid != my_pid && process::is_process_running(pid) { - tui::print_error(&format!("ClawShell is already running (PID: {pid})")); - std::process::exit(1); - } - if pid != my_pid { - // Stale PID file - process::remove_pid_file(); - } - } - +async fn cmd_start_inner(config_path: &str) -> Result<(), Box> { // Validate configuration let path = PathBuf::from(config_path); ensure_config_migrated(&path)?; @@ -234,44 +218,9 @@ async fn cmd_start_inner( tui::print_success("Configuration validated successfully."); - // Ensure runtime directories exist (for PID and log files) + // Ensure runtime directories exist (for log files) process::ensure_runtime_dirs()?; - if !foreground { - // Daemonize: fork a child process - use std::process::Command; - - let exe = std::env::current_exe()?; - let log_path = process::log_file_path(); - let log_file = std::fs::OpenOptions::new() - .create(true) - .append(true) - .open(&log_path)?; - let log_stderr = log_file.try_clone()?; - - let child = Command::new(exe) - .args(["start", "--config", config_path, "--foreground"]) - .stdout(log_file) - .stderr(log_stderr) - .stdin(std::process::Stdio::null()) - .spawn()?; - - let pid = child.id(); - - // Write PID file immediately so stop/restart can find the process - process::write_pid_file(pid)?; - - tui::print_success(&format!("ClawShell started in background (PID: {pid})")); - tui::print_info("Logs", &log_path.display().to_string()); - return Ok(()); - } - - // Foreground mode — write PID if not already recorded by the parent daemon - let pid = std::process::id(); - if process::read_pid_file() != Some(pid) { - process::write_pid_file(pid)?; - } - let env_filter: tracing_subscriber::EnvFilter = config .log_level .parse() @@ -314,7 +263,6 @@ async fn cmd_start_inner( }) .await?; - process::remove_pid_file(); info!("ClawShell shut down"); Ok(()) } @@ -332,11 +280,6 @@ fn cmd_stop() -> Result<(), Box> { println!("Stopping ClawShell via service manager..."); platform::service_stop()?; tui::print_success("ClawShell stopped successfully."); - if let Some(pid) = process::read_pid_file() - && !process::is_process_running(pid) - { - process::remove_pid_file(); - } tui::print_info("Logs", &process::log_file_path().display().to_string()); Ok(()) } @@ -346,24 +289,9 @@ fn cmd_status() -> Result<(), Box> { ensure_service_installed_for_lifecycle()?; if platform::service_is_running()? { - if let Some(pid) = process::read_pid_file() - && process::is_process_running(pid) - { - tui::print_success(&format!("ClawShell is running (PID: {pid})")); - if let Some(uptime) = process::get_process_uptime(pid) { - tui::print_info("Uptime", &uptime); - } - return Ok(()); - } - tui::print_success("ClawShell is running."); } else { tui::print_warning("ClawShell is not running."); - if let Some(pid) = process::read_pid_file() - && !process::is_process_running(pid) - { - process::remove_pid_file(); - } } Ok(()) } @@ -715,12 +643,6 @@ fn cmd_onboard() -> Result<(), Box> { std::fs::create_dir_all(&config_dir)?; std::fs::create_dir_all(&log_dir_path)?; - - // Also create runtime dirs for PID files - let pid_path = process::pid_file_path(); - if let Some(pid_parent) = pid_path.parent() { - std::fs::create_dir_all(pid_parent)?; - } tui::print_step_done(2, TOTAL_STEPS, "Directories created"); // Step 3: Set permissions and ownership @@ -747,15 +669,6 @@ fn cmd_onboard() -> Result<(), Box> { "Failed to set log directory owner" ); } - 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"); // Step 4: Ask the user for configuration details (TUI prompts) @@ -882,7 +795,7 @@ fn cmd_onboard() -> Result<(), Box> { } // Step 9: Start or skip ClawShell - let already_running = process::read_pid_file().is_some_and(process::is_process_running); + let already_running = platform::service_is_running().unwrap_or(false); if already_running { tui::print_step_done(9, TOTAL_STEPS, "ClawShell already running (skipped)"); @@ -1077,7 +990,6 @@ fn cmd_uninstall(skip_confirm: bool) -> Result<(), Box> { .parent() .map(|p| p.to_path_buf()) .unwrap_or_else(|| PathBuf::from("/var/log/clawshell")); - let pid_file = process::pid_file_path(); let service_path = std::path::Path::new(crate::onboard::autostart_service_path()); let service_exists = service_path.exists(); @@ -1086,7 +998,6 @@ fn cmd_uninstall(skip_confirm: bool) -> Result<(), Box> { tui::print_info("ClawShell", "Stop if running"); tui::print_info("Config dir", &config_dir.display().to_string()); tui::print_info("Log dir", &log_dir.display().to_string()); - tui::print_info("PID file", &pid_file.display().to_string()); if service_exists { tui::print_info("Service", &service_path.display().to_string()); } @@ -1146,43 +1057,19 @@ fn cmd_uninstall(skip_confirm: bool) -> Result<(), Box> { } } - // 2. Stop ClawShell if still running (e.g. started without service manager) - if let Some(pid) = process::read_pid_file() { - if process::is_process_running(pid) { - tui::print_info("PID", &pid.to_string()); - println!("Stopping ClawShell..."); - process::stop_process(pid)?; - tui::print_success("ClawShell stopped."); - } else { - process::remove_pid_file(); - } - } - - // 3. Remove PID file (in case stop_process didn't clean it) - if pid_file.exists() { - let _ = std::fs::remove_file(&pid_file); - tui::print_success("PID file removed."); - } - // Also remove the PID parent directory if it's a clawshell-specific dir - if let Some(pid_dir) = pid_file.parent() - && pid_dir.ends_with("clawshell") - { - let _ = std::fs::remove_dir(pid_dir); - } - - // 4. Remove log directory + // 2. Remove log directory if log_dir.exists() { std::fs::remove_dir_all(&log_dir)?; tui::print_success("Log directory removed."); } - // 5. Remove configuration directory + // 3. Remove configuration directory if config_dir.exists() { std::fs::remove_dir_all(&config_dir)?; tui::print_success("Configuration directory removed."); } - // 6. Remove the clawshell system user + // 4. Remove the clawshell system user let user_exists = std::process::Command::new("id") .arg("clawshell") .stdout(std::process::Stdio::null()) @@ -1199,7 +1086,7 @@ fn cmd_uninstall(skip_confirm: bool) -> Result<(), Box> { } } - // 7. Preserve the binary so users can still run clawshell later. + // 5. Preserve the binary so users can still run clawshell later. if exe_path.exists() { tui::print_info("Binary", &format!("Preserved at {}", exe_path.display())); } else { @@ -1251,7 +1138,6 @@ fn start_clawshell_direct( .spawn()?; let pid = child.id(); - process::write_pid_file(pid)?; - tui::print_info("PID", &pid.to_string()); + tui::print_info("Process ID", &pid.to_string()); Ok(()) } diff --git a/src/platform/linux.rs b/src/platform/linux.rs index 8b0d24d..60d0984 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -8,14 +8,6 @@ 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" } diff --git a/src/platform/macos.rs b/src/platform/macos.rs index d73851c..677c8ca 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -8,14 +8,6 @@ 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" } diff --git a/src/process.rs b/src/process.rs index 12c167b..3ba9bc3 100644 --- a/src/process.rs +++ b/src/process.rs @@ -1,11 +1,5 @@ -use crate::platform; -use nix::sys::signal::{self, Signal}; -use nix::unistd::{Gid, Pid, Uid, User, getuid, setgid, setuid}; -use nix::unistd::{SysconfVar, sysconf}; -use std::fs; -use std::io::Write as _; +use nix::unistd::{Gid, Uid, User, getuid, setgid, setuid}; use std::path::PathBuf; -use std::time::{Duration, SystemTime, UNIX_EPOCH}; use tracing::info; use vfs::VfsPath; @@ -22,23 +16,11 @@ pub(crate) fn physical_root() -> VfsPath { VfsPath::new(vfs::PhysicalFS::new("/")) } -/// PID file path within a VFS root. -fn pid_file_vfs(root: &VfsPath) -> Result> { - Ok(root.join(platform::pid_file_vfs_rel_path())?) -} - /// Log file path within a VFS root. fn log_file_vfs(root: &VfsPath) -> Result> { Ok(root.join("var/log/clawshell/clawshell.log")?) } -/// PID file location. -/// - 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 { - PathBuf::from(platform::pid_file_abs_path()) -} - /// Log file location. /// - Linux: /var/log/clawshell/clawshell.log /// - macOS: /var/log/clawshell/clawshell.log @@ -46,93 +28,18 @@ pub fn log_file_path() -> PathBuf { PathBuf::from("/var/log/clawshell/clawshell.log") } -/// Ensure the parent directories for PID and log files exist (VFS variant). +/// Ensure the parent directories for log files exist (VFS variant). pub(crate) fn ensure_runtime_dirs_vfs(root: &VfsPath) -> Result<(), Box> { - let pid_path = pid_file_vfs(root)?; - pid_path.parent().create_dir_all()?; let log_path = log_file_vfs(root)?; log_path.parent().create_dir_all()?; Ok(()) } -/// Ensure the parent directories for PID and log files exist. +/// Ensure the parent directories for log files exist. pub fn ensure_runtime_dirs() -> Result<(), Box> { ensure_runtime_dirs_vfs(&physical_root()) } -/// Write a PID file (VFS variant). -pub(crate) fn write_pid_file_vfs( - root: &VfsPath, - pid: u32, -) -> Result<(), Box> { - let path = pid_file_vfs(root)?; - path.create_file()?.write_all(pid.to_string().as_bytes())?; - Ok(()) -} - -pub fn write_pid_file(pid: u32) -> Result<(), Box> { - write_pid_file_vfs(&physical_root(), pid) -} - -/// Read the PID from the PID file (VFS variant). -pub(crate) fn read_pid_file_vfs(root: &VfsPath) -> Option { - let path = pid_file_vfs(root).ok()?; - path.read_to_string().ok()?.trim().parse().ok() -} - -pub fn read_pid_file() -> Option { - read_pid_file_vfs(&physical_root()) -} - -/// Remove the PID file (VFS variant). -pub(crate) fn remove_pid_file_vfs(root: &VfsPath) { - if let Ok(path) = pid_file_vfs(root) { - let _ = path.remove_file(); - } -} - -pub fn remove_pid_file() { - remove_pid_file_vfs(&physical_root()) -} - -fn to_pid(pid: u32) -> Result> { - let raw: i32 = pid - .try_into() - .map_err(|_| format!("PID {} exceeds i32::MAX", pid))?; - Ok(Pid::from_raw(raw)) -} - -pub fn is_process_running(pid: u32) -> bool { - to_pid(pid) - .map(|p| signal::kill(p, None).is_ok()) - .unwrap_or(false) -} - -pub fn stop_process(pid: u32) -> Result<(), Box> { - let nix_pid = to_pid(pid)?; - signal::kill(nix_pid, Signal::SIGTERM) - .map_err(|e| format!("Failed to send SIGTERM to process {}: {}", pid, e))?; - - // Wait for the process to exit (up to 10 seconds) - for _ in 0..100 { - if !is_process_running(pid) { - remove_pid_file(); - return Ok(()); - } - std::thread::sleep(Duration::from_millis(100)); - } - - // Force kill if still running - eprintln!( - "Process {} did not stop gracefully, sending SIGKILL...", - pid - ); - signal::kill(nix_pid, Signal::SIGKILL) - .map_err(|e| format!("Failed to send SIGKILL to process {}: {}", pid, e))?; - remove_pid_file(); - Ok(()) -} - /// Drop privileges from root to the `clawshell` system user. /// /// This resolves the `clawshell` user, then calls `setgid` followed by `setuid` @@ -157,86 +64,9 @@ pub fn drop_privileges() -> Result<(), Box> { Ok(()) } -pub fn get_process_uptime(pid: u32) -> Option { - let stat_path = format!("/proc/{}/stat", pid); - let stat = fs::read_to_string(&stat_path).ok()?; - let boot_time = get_boot_time()?; - let fields: Vec<&str> = stat.split_whitespace().collect(); - // Field 21 (0-indexed) is starttime in clock ticks - let start_ticks: u64 = fields.get(21)?.parse().ok()?; - let ticks_per_sec: u64 = sysconf(SysconfVar::CLK_TCK).ok()?? as u64; - let start_secs = boot_time + start_ticks / ticks_per_sec; - let now = SystemTime::now().duration_since(UNIX_EPOCH).ok()?.as_secs(); - let uptime_secs = now.saturating_sub(start_secs); - Some(format_duration(uptime_secs)) -} - -fn get_boot_time() -> Option { - let stat = fs::read_to_string("/proc/stat").ok()?; - for line in stat.lines() { - if let Some(rest) = line.strip_prefix("btime ") { - return rest.trim().parse().ok(); - } - } - None -} - -fn format_duration(secs: u64) -> String { - let days = secs / 86400; - let hours = (secs % 86400) / 3600; - let minutes = (secs % 3600) / 60; - let seconds = secs % 60; - - if days > 0 { - format!("{}d {}h {}m {}s", days, hours, minutes, seconds) - } else if hours > 0 { - format!("{}h {}m {}s", hours, minutes, seconds) - } else if minutes > 0 { - format!("{}m {}s", minutes, seconds) - } else { - format!("{}s", seconds) - } -} - #[cfg(test)] mod tests { use super::*; - use std::path::Path; - use std::process; - use vfs::MemoryFS; - - #[test] - fn test_format_duration_seconds() { - assert_eq!(format_duration(42), "42s"); - } - - #[test] - fn test_format_duration_minutes() { - assert_eq!(format_duration(125), "2m 5s"); - } - - #[test] - fn test_format_duration_hours() { - assert_eq!(format_duration(3661), "1h 1m 1s"); - } - - #[test] - fn test_format_duration_days() { - assert_eq!(format_duration(90061), "1d 1h 1m 1s"); - } - - #[test] - fn test_pid_file_path() { - let path = pid_file_path(); - let path_str = path.to_str().unwrap(); - assert!(path_str.contains("clawshell.pid")); - // Should be under /run or /var/run, not /tmp - assert!( - path_str.starts_with("/run/") || path_str.starts_with("/var/run"), - "PID path should be under /run or /var/run, got: {}", - path_str - ); - } #[test] fn test_log_file_path() { @@ -257,132 +87,15 @@ mod tests { assert_eq!(path_str, "/etc/clawshell/clawshell.toml"); } - #[test] - fn test_write_and_read_pid_file() { - let root = VfsPath::new(MemoryFS::new()); - ensure_runtime_dirs_vfs(&root).unwrap(); - - let test_pid = 12345u32; - write_pid_file_vfs(&root, test_pid).unwrap(); - let read_pid = read_pid_file_vfs(&root).unwrap(); - assert_eq!(read_pid, test_pid); - - remove_pid_file_vfs(&root); - assert!(read_pid_file_vfs(&root).is_none()); - } - - #[test] - fn test_is_process_running_self() { - let pid = process::id(); - assert!(is_process_running(pid)); - } - - #[test] - fn test_is_process_running_nonexistent() { - // PID 99999999 should not exist - assert!(!is_process_running(99999999)); - } - #[test] fn test_ensure_runtime_dirs() { - let root = VfsPath::new(MemoryFS::new()); + let root = VfsPath::new(vfs::MemoryFS::new()); ensure_runtime_dirs_vfs(&root).unwrap(); - let pid_parent = pid_file_vfs(&root).unwrap().parent(); - assert!(pid_parent.exists().unwrap()); let log_parent = log_file_vfs(&root).unwrap().parent(); assert!(log_parent.exists().unwrap()); } - #[test] - fn test_get_process_uptime_self() { - let pid = process::id(); - // On Linux with /proc, this should return Some - if Path::new("/proc/self/stat").exists() { - let uptime = get_process_uptime(pid); - assert!(uptime.is_some(), "Should be able to get uptime for self"); - let uptime_str = uptime.unwrap(); - // Should be a valid duration string (ends with 's') - assert!( - uptime_str.ends_with('s'), - "Uptime should end with 's': {}", - uptime_str - ); - } - } - - #[test] - fn test_get_process_uptime_nonexistent() { - let uptime = get_process_uptime(99999999); - assert!(uptime.is_none()); - } - - #[test] - fn test_get_boot_time() { - if Path::new("/proc/stat").exists() { - let boot_time = get_boot_time(); - assert!( - boot_time.is_some(), - "Should be able to read boot time from /proc/stat" - ); - assert!(boot_time.unwrap() > 0); - } - } - - #[test] - fn test_stop_process_nonexistent() { - // Stopping a nonexistent process should fail with an error - let result = stop_process(99999999); - assert!(result.is_err()); - } - - #[test] - fn test_stop_process_spawned_child() { - // Spawn a sleep process and then stop it - let child = process::Command::new("sleep").arg("60").spawn(); - if let Ok(mut child) = child { - let pid = child.id(); - assert!(is_process_running(pid)); - let result = stop_process(pid); - assert!(result.is_ok()); - // Wait for the child to be reaped - let _ = child.wait(); - } - } - - #[test] - fn test_format_duration_zero() { - assert_eq!(format_duration(0), "0s"); - } - - #[test] - fn test_format_duration_exactly_one_hour() { - assert_eq!(format_duration(3600), "1h 0m 0s"); - } - - #[test] - fn test_format_duration_exactly_one_day() { - assert_eq!(format_duration(86400), "1d 0h 0m 0s"); - } - - #[test] - fn test_to_pid_valid() { - let pid = to_pid(1234).unwrap(); - assert_eq!(pid.as_raw(), 1234); - } - - #[test] - fn test_to_pid_max_i32() { - let pid = to_pid(i32::MAX as u32).unwrap(); - assert_eq!(pid.as_raw(), i32::MAX); - } - - #[test] - fn test_to_pid_overflow() { - let result = to_pid(u32::MAX); - assert!(result.is_err()); - } - #[test] fn test_drop_privileges_no_clawshell_user() { // On non-root CI environments without a clawshell user, diff --git a/tests/cli_tests.rs b/tests/cli_tests.rs index 2d8e9b5..99397b2 100644 --- a/tests/cli_tests.rs +++ b/tests/cli_tests.rs @@ -35,16 +35,6 @@ base_url = "https://api.openai.com" std::fs::write(path, content).unwrap(); } -#[cfg(target_os = "linux")] -fn pid_file_path() -> std::path::PathBuf { - "/run/clawshell/clawshell.pid".into() -} - -#[cfg(target_os = "macos")] -fn pid_file_path() -> std::path::PathBuf { - "/var/run/clawshell.pid".into() -} - fn log_file_path() -> std::path::PathBuf { "/var/log/clawshell/clawshell.log".into() } @@ -111,8 +101,6 @@ fn test_version_output() { #[test] fn test_status_when_not_running() { - let _ = std::fs::remove_file(pid_file_path()); - if service_installed() { cmd() .arg("status") @@ -132,8 +120,6 @@ fn test_status_when_not_running() { #[test] fn test_stop_when_not_running() { - let _ = std::fs::remove_file(pid_file_path()); - if service_installed() { // Service lifecycle tests in service-installed environments require root/system setup. // Skip to keep CLI tests hermetic.