diff --git a/cli/src/connection.rs b/cli/src/connection.rs index f749dbb..d3c0e8b 100644 --- a/cli/src/connection.rs +++ b/cli/src/connection.rs @@ -118,10 +118,16 @@ fn get_pid_path(session: &str) -> PathBuf { get_socket_dir().join(format!("{}.pid", session)) } +fn get_version_path(session: &str) -> PathBuf { + get_socket_dir().join(format!("{}.version", session)) +} + /// Clean up stale socket and PID files for a session pub fn cleanup_stale_files(session: &str) { let pid_path = get_pid_path(session); let _ = fs::remove_file(&pid_path); + let version_path = get_version_path(session); + let _ = fs::remove_file(&version_path); let stream_path = get_socket_dir().join(format!("{}.stream", session)); let _ = fs::remove_file(&stream_path); @@ -306,6 +312,68 @@ fn apply_daemon_env(cmd: &mut Command, session: &str, opts: &DaemonOptions) { } } +/// Check if the running daemon's version matches this CLI binary. +/// Returns false when the version file is missing — an unversioned daemon +/// is most likely a stale leftover from before version tracking was added +/// (or from the Node.js era), and silently reusing it is the exact bug +/// this check exists to prevent. The one-time cost of an unnecessary +/// restart on the first upgrade is preferable to silent failures. +fn daemon_version_matches(session: &str) -> bool { + let version_path = get_version_path(session); + match fs::read_to_string(&version_path) { + Ok(v) => v.trim() == env!("CARGO_PKG_VERSION"), + Err(_) => false, + } +} + +/// Kill a running daemon by reading its PID file and sending a kill signal. +fn kill_stale_daemon(session: &str) { + // Remove the socket first so no new connections reach the old daemon + #[cfg(unix)] + { + let socket_path = get_socket_path(session); + let _ = fs::remove_file(&socket_path); + } + + let pid_path = get_pid_path(session); + if let Ok(pid_str) = fs::read_to_string(&pid_path) { + if let Ok(pid) = pid_str.trim().parse::() { + #[cfg(unix)] + { + unsafe { + libc::kill(pid as i32, libc::SIGTERM); + } + // Wait up to 1s for graceful shutdown, then force-kill + for _ in 0..10 { + thread::sleep(Duration::from_millis(100)); + if unsafe { libc::kill(pid as i32, 0) } != 0 { + break; + } + } + // Force-kill if still alive + if unsafe { libc::kill(pid as i32, 0) } == 0 { + unsafe { + libc::kill(pid as i32, libc::SIGKILL); + } + thread::sleep(Duration::from_millis(100)); + } + } + #[cfg(windows)] + { + let _ = Command::new("taskkill") + .args(["/PID", &pid.to_string(), "/F"]) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status(); + thread::sleep(Duration::from_millis(500)); + } + } + } + + // Clean up leftover files regardless + cleanup_stale_files(session); +} + pub fn ensure_daemon(session: &str, opts: &DaemonOptions) -> Result { // Socket connectivity is the sole liveness check — no PID check — so // callers in a different PID namespace (e.g. unshare) can still reuse @@ -316,9 +384,20 @@ pub fn ensure_daemon(session: &str, opts: &DaemonOptions) -> Result