From 37247587c4cffc524e8154ab2a59f6eaed40c813 Mon Sep 17 00:00:00 2001 From: Nathan Flurry Date: Sat, 7 Feb 2026 12:03:15 -0800 Subject: [PATCH] fix(daemon): find and stop orphaned daemons on macOS when PID file is missing --- server/packages/sandbox-agent/src/daemon.rs | 71 ++++++++++++++++----- 1 file changed, 54 insertions(+), 17 deletions(-) diff --git a/server/packages/sandbox-agent/src/daemon.rs b/server/packages/sandbox-agent/src/daemon.rs index c69c01f..377b3a1 100644 --- a/server/packages/sandbox-agent/src/daemon.rs +++ b/server/packages/sandbox-agent/src/daemon.rs @@ -349,25 +349,26 @@ pub fn start(cli: &CliConfig, host: &str, port: u16, token: Option<&str>) -> Res Ok(()) } +/// Find the PID of a process listening on the given port using lsof. #[cfg(unix)] -pub fn stop(host: &str, port: u16) -> Result<(), CliError> { - let pid_path = daemon_pid_path(host, port); +fn find_process_on_port(port: u16) -> Option { + let output = std::process::Command::new("lsof") + .args(["-i", &format!(":{port}"), "-t", "-sTCP:LISTEN"]) + .output() + .ok()?; - let pid = match read_pid(&pid_path) { - Some(pid) => pid, - None => { - eprintln!("daemon is not running (no PID file)"); - return Ok(()); - } - }; - - if !is_process_running(pid) { - eprintln!("daemon is not running (stale PID file)"); - let _ = remove_pid(&pid_path); - let _ = remove_version_file(host, port); - return Ok(()); + if !output.status.success() { + return None; } + let stdout = String::from_utf8_lossy(&output.stdout); + // lsof -t returns just the PID(s), one per line + stdout.lines().next()?.trim().parse::().ok() +} + +/// Stop a process by PID with SIGTERM then SIGKILL if needed. +#[cfg(unix)] +fn stop_process(pid: u32, host: &str, port: u16, pid_path: &Path) -> Result<(), CliError> { eprintln!("stopping daemon (PID {pid})..."); // SIGTERM @@ -379,7 +380,7 @@ pub fn stop(host: &str, port: u16) -> Result<(), CliError> { for _ in 0..50 { std::thread::sleep(Duration::from_millis(100)); if !is_process_running(pid) { - let _ = remove_pid(&pid_path); + let _ = remove_pid(pid_path); let _ = remove_version_file(host, port); eprintln!("daemon stopped"); return Ok(()); @@ -392,12 +393,48 @@ pub fn stop(host: &str, port: u16) -> Result<(), CliError> { libc::kill(pid as i32, libc::SIGKILL); } std::thread::sleep(Duration::from_millis(100)); - let _ = remove_pid(&pid_path); + let _ = remove_pid(pid_path); let _ = remove_version_file(host, port); eprintln!("daemon killed"); Ok(()) } +#[cfg(unix)] +pub fn stop(host: &str, port: u16) -> Result<(), CliError> { + let base_url = format!("http://{host}:{port}"); + let pid_path = daemon_pid_path(host, port); + + let pid = match read_pid(&pid_path) { + Some(pid) => pid, + None => { + // No PID file - but check if daemon is actually running via health check + // This can happen if PID file was deleted but daemon is still running + if check_health(&base_url, None)? { + eprintln!("daemon is running but PID file missing; finding process on port {port}..."); + if let Some(pid) = find_process_on_port(port) { + eprintln!("found daemon process {pid}"); + return stop_process(pid, host, port, &pid_path); + } else { + return Err(CliError::Server(format!( + "daemon is running on port {port} but cannot find PID" + ))); + } + } + eprintln!("daemon is not running (no PID file)"); + return Ok(()); + } + }; + + if !is_process_running(pid) { + eprintln!("daemon is not running (stale PID file)"); + let _ = remove_pid(&pid_path); + let _ = remove_version_file(host, port); + return Ok(()); + } + + stop_process(pid, host, port, &pid_path) +} + #[cfg(windows)] pub fn stop(host: &str, port: u16) -> Result<(), CliError> { let pid_path = daemon_pid_path(host, port);