From 8387282270936ba2a4858b6e8d1ad3a5aacc4db3 Mon Sep 17 00:00:00 2001 From: TerrifiedBug Date: Fri, 21 Aug 2026 19:13:23 +0100 Subject: [PATCH] Kill an incumbent daemon that ignores SIGTERM MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The five-second wait ended by giving up, which cannot be what happens: the SIGTERM has already been delivered by then, so the incumbent dies as soon as it is unwedged, and the newcomer has just exited. The outcome was zero daemons and a log line claiming the old one still had the hotkey. Escalate to SIGKILL instead. Anything still holding the lock five seconds after a SIGTERM is wedged rather than busy — a clean exit is a few file writes — and the kernel drops the lock with the process, so the newcomer takes it and there is exactly one daemon either way. If the one killed was the login job, launchd brings it back and the two settle it the ordinary way; one of them holds the lock, never both. Verified: incumbent frozen with SIGSTOP, second daemon started — logs "ignored SIGTERM — killing it", the frozen pid is gone, and one process is left listening. --- Sources/yap/DaemonLock.swift | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/Sources/yap/DaemonLock.swift b/Sources/yap/DaemonLock.swift index f5c3f3c..e152f00 100644 --- a/Sources/yap/DaemonLock.swift +++ b/Sources/yap/DaemonLock.swift @@ -43,8 +43,18 @@ enum DaemonLock { /// the mic gain lease is given back. It exits 0, so `KeepAlive /// SuccessfulExit:false` leaves the job down instead of racing us for it. /// - /// False means someone else still owns the hotkey and this process must - /// not run. + /// Five seconds later, anything still holding the lock is wedged rather + /// than busy — a clean exit is a few file writes — and by then backing out + /// is not an option: the SIGTERM has been delivered, so the incumbent dies + /// the moment it recovers, and returning false here would leave the + /// machine with no daemon at all. So it escalates to SIGKILL, which the + /// kernel answers by dropping the lock with the process. If that daemon + /// was the login job, launchd sees the abnormal exit and brings it back, + /// and the two of us settle it the ordinary way — one of us ends up + /// holding the lock, never both. + /// + /// False means the hotkey could not be taken and this process must not + /// run: the only ways out above are a signal the kernel refused. static func claim() -> Bool { let path = Paths.daemonLock.path let handle = open(path, O_CREAT | O_RDWR, 0o600) @@ -81,7 +91,19 @@ enum DaemonLock { usleep(50_000) if take(handle) { return true } } - warn("pid \(holder) did not stop — leaving it the hotkey") + warn("pid \(holder) ignored SIGTERM — killing it") + if kill(holder, SIGKILL) != 0, errno != ESRCH { + warn("could not kill pid \(holder) (\(errnoText())) — leaving it the hotkey") + return false + } + // SIGKILL is not synchronous with the exit, and the lock goes when the + // process does, so this waits for the kernel rather than for the + // daemon. A second is orders of magnitude more than it takes. + for _ in 0..<20 { + usleep(50_000) + if take(handle) { return true } + } + warn("pid \(holder) still holds the hotkey after SIGKILL — not starting") return false }