fix: treat EPERM from kill(pid, 0) as "process exists" in daemon liveness checks (#564)

Per POSIX, kill(pid, 0) returns EPERM when the process exists but the
caller lacks permission to signal it, and ESRCH when it does not exist.
The daemon liveness checks in both the Rust CLI and TypeScript daemon
treated any kill failure as "not running", which is incorrect when
running inside a macOS sandbox that restricts signal delivery to
(target self). This caused the CLI to delete the real daemon's socket
and PID files, then spawn a duplicate daemon.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
neilmix
2026-03-01 10:12:37 -06:00
committed by GitHub
co-authored by Claude Opus 4.6
parent 7238b7da4c
commit e912f541f2
3 changed files with 18 additions and 3 deletions
+7 -1
View File
@@ -159,7 +159,13 @@ fn is_daemon_running(session: &str) -> bool {
if let Ok(pid_str) = fs::read_to_string(&pid_path) {
if let Ok(pid) = pid_str.trim().parse::<i32>() {
unsafe {
return libc::kill(pid, 0) == 0;
if libc::kill(pid, 0) == 0 {
return true;
}
// EPERM means the process exists but we lack permission to
// signal it (e.g. inside a macOS sandbox). Only ESRCH means
// the process is genuinely gone.
return std::io::Error::last_os_error().raw_os_error() != Some(libc::ESRCH);
}
}
}
+5 -1
View File
@@ -170,7 +170,11 @@ fn run_session(args: &[String], session: &str, json_mode: bool) {
if let Ok(pid_str) = fs::read_to_string(&pid_path) {
if let Ok(pid) = pid_str.trim().parse::<u32>() {
#[cfg(unix)]
let running = unsafe { libc::kill(pid as i32, 0) == 0 };
let running = unsafe {
libc::kill(pid as i32, 0) == 0
|| std::io::Error::last_os_error().raw_os_error()
!= Some(libc::ESRCH)
};
#[cfg(windows)]
let running = unsafe {
let handle =
+6 -1
View File
@@ -262,7 +262,12 @@ export function isDaemonRunning(session?: string): boolean {
// Check if process exists (works on both Unix and Windows)
process.kill(pid, 0);
return true;
} catch {
} catch (err: unknown) {
// EPERM means the process exists but we lack permission to signal it
// (e.g. caller is inside a macOS sandbox). Only ESRCH means it's gone.
if (err instanceof Error && (err as NodeJS.ErrnoException).code === 'EPERM') {
return true;
}
// Process doesn't exist, clean up stale files
cleanupSocket(session);
return false;