diff --git a/cli/src/connection.rs b/cli/src/connection.rs index d532943..f749dbb 100644 --- a/cli/src/connection.rs +++ b/cli/src/connection.rs @@ -119,7 +119,7 @@ fn get_pid_path(session: &str) -> PathBuf { } /// Clean up stale socket and PID files for a session -fn cleanup_stale_files(session: &str) { +pub fn cleanup_stale_files(session: &str) { let pid_path = get_pid_path(session); let _ = fs::remove_file(&pid_path); let stream_path = get_socket_dir().join(format!("{}.stream", session)); @@ -429,6 +429,21 @@ pub fn ensure_daemon(session: &str, opts: &DaemonOptions) -> Result 500 { let mut end = 500; diff --git a/cli/src/main.rs b/cli/src/main.rs index 979f592..e43d6a7 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -21,7 +21,7 @@ use windows_sys::Win32::Foundation::CloseHandle; use windows_sys::Win32::System::Threading::{OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION}; use commands::{gen_id, parse_command, ParseError}; -use connection::{ensure_daemon, get_socket_dir, send_command, DaemonOptions}; +use connection::{cleanup_stale_files, ensure_daemon, get_socket_dir, send_command, DaemonOptions}; use flags::{clean_args, parse_flags, Flags}; use install::run_install; use output::{ @@ -380,7 +380,7 @@ fn run_dashboard_stop(json_mode: bool) { fn run_close_all(flags: &Flags) { let socket_dir = get_socket_dir(); - let mut sessions: Vec = Vec::new(); + let mut sessions: Vec<(String, u32)> = Vec::new(); if let Ok(entries) = fs::read_dir(&socket_dir) { for entry in entries.flatten() { @@ -409,9 +409,33 @@ fn run_close_all(flags: &Flags) { } }; if running { - sessions.push(session_name.to_string()); + sessions.push((session_name.to_string(), pid)); + } else { + // Process is gone but stale files remain; clean them up + cleanup_stale_files(session_name); } } + } else { + // PID file exists but is unreadable; clean up stale files + cleanup_stale_files(session_name); + } + } + } + } + + // Also scan for orphaned .sock files without corresponding .pid files + #[cfg(unix)] + if let Ok(entries) = fs::read_dir(&socket_dir) { + for entry in entries.flatten() { + let name = entry.file_name().to_string_lossy().to_string(); + if let Some(session_name) = name.strip_suffix(".sock") { + if session_name.is_empty() { + continue; + } + let pid_path = socket_dir.join(format!("{}.pid", session_name)); + if !pid_path.exists() { + // Orphaned socket file with no PID file; remove it + cleanup_stale_files(session_name); } } } @@ -432,7 +456,7 @@ fn run_close_all(flags: &Flags) { let mut closed: Vec = Vec::new(); let mut failed: Vec<(String, String)> = Vec::new(); - for session in &sessions { + for (session, pid) in &sessions { let cmd = json!({ "id": gen_id(), "action": "close" }); match send_command(cmd, session) { Ok(resp) if resp.success => closed.push(session.clone()), @@ -440,7 +464,25 @@ fn run_close_all(flags: &Flags) { let err = resp.error.unwrap_or_else(|| "Unknown error".to_string()); failed.push((session.clone(), err)); } - Err(e) => failed.push((session.clone(), e.to_string())), + Err(_) => { + // Daemon is unreachable despite its process existing. + // Force-kill the process and clean up stale files so future + // sessions are not poisoned. + #[cfg(unix)] + unsafe { + libc::kill(*pid as i32, libc::SIGKILL); + } + #[cfg(windows)] + unsafe { + let handle = OpenProcess(1, 0, *pid); // PROCESS_TERMINATE = 1 + if handle != 0 { + windows_sys::Win32::System::Threading::TerminateProcess(handle, 1); + CloseHandle(handle); + } + } + cleanup_stale_files(session); + closed.push(session.clone()); + } } }