fix: recover from stale daemon/socket state (#1136)

When a daemon is killed or crashes without cleaning up, stale .sock/.pid
files are left behind. Previously, `close --all` would fail to connect to
these zombie daemons and simply report an error, leaving the stale files
in place and poisoning all future sessions.

Three fixes:

1. `close --all` now force-kills unreachable daemon processes and removes
   all stale files (pid, sock, stream) instead of reporting failure. It
   also cleans up dead-but-lingering PID files during enumeration and
   scans for orphaned .sock files without corresponding .pid files.

2. `ensure_daemon` handles concurrent startup races: when a spawned
   daemon exits with "Address already in use" (another instance won the
   bind race), it checks whether the winner is accepting connections and
   piggybacks on it instead of failing.

3. `cleanup_stale_files` is now public so `close --all` can reuse it.

Fixes #1118

Co-authored-by: ctate <366502+ctate@users.noreply.github.com>
This commit is contained in:
Chris Tate
2026-04-04 10:54:46 -05:00
committed by GitHub
co-authored by ctate
parent c976212db4
commit 5e33672d08
2 changed files with 63 additions and 6 deletions
+16 -1
View File
@@ -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<DaemonResult
let _ = stderr.read_to_string(&mut stderr_output);
}
let stderr_trimmed = stderr_output.trim();
// If the daemon failed because another instance won the bind
// race ("Address already in use"), check whether that winner is
// now accepting connections and piggyback on it.
if stderr_trimmed.contains("Address already in use")
|| stderr_trimmed.contains("Failed to bind")
{
thread::sleep(Duration::from_millis(200));
if daemon_ready(session) {
return Ok(DaemonResult {
already_running: true,
});
}
}
if !stderr_trimmed.is_empty() {
let msg = if stderr_trimmed.len() > 500 {
let mut end = 500;