fix(core): stop counting unreaped processes as running - #166
Closed
neumie wants to merge 1 commit into
Closed
Conversation
`is_process_alive` was a bare `kill(pid, 0)` on unix, which succeeds for a zombie — a process that has exited but whose parent has not reaped it. The desktop holds its spawned daemon's `Child` handle without waiting on it, so an exited UI-owned daemon stayed "alive" to every pid probe until that handle was finally reaped. Two consequences. On a clean quit, `wait_for_pid_exit` could never observe its own child's exit, so shutdown sat out the full 3s timeout, logged a bogus "did not exit gracefully" and SIGKILLed a corpse. And a crashed daemon still looked live to `running_daemon_in`, sending startup down the attach branch into a hard error instead of respawning. Every caller means *running* — a zombie answers no requests, serves no port, and holds no lock (its fds close at exit) — so zombies now count as dead. Windows already behaved this way: its process handle is signalled at exit. macOS has no direct zombie predicate, so this asks `proc_pidinfo` for the BSD record, which the kernel refuses with ESRCH once the process is gone. Apple's wrapper collapses the kernel's -1 into a return of 0 and reports the reason through errno, so the return value alone cannot separate "pid is gone" from "the call failed" — EPERM and ENOMEM also return 0. Only ESRCH counts as exited; anything else keeps the pre-fix answer, since declaring a live daemon dead and respawning over it is the worse error. Linux reads the state character from /proc/<pid>/stat, as bytes rather than a String — `comm` is the raw executable name and need not be valid UTF-8. Other unix targets keep the old behaviour. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y4Aq3X2s6ftafXzmqaN3A8
Member
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
okena_core::process::is_process_alivewas a barelibc::kill(pid, 0) == 0on unix. That succeeds for a zombie — a process that has exited but whose parent has not reaped it, so its pid stays in the table.The desktop holds its spawned daemon's
std::process::Childwithout waiting on it, so an exited UI-owned daemon stayed "alive" to every pid probe until that handle was finally reaped. Two consequences:hand_off_ui_owned_daemoncallswait_for_pid_exit(daemon.pid, 3s), which polls onlyis_process_alive— so it could never observe its own child's exit. Every graceful shutdown sat out the full 3s, then loggedUI-owned daemon pid N did not exit gracefully; reaping by pidand SIGKILLed a corpse.running_daemon_inreported it live, sending startup down the attach branch intoLocal daemon pid N did not respond to /health in timeinstead of falling through and respawning.Every caller here means running — a zombie answers no requests, serves no port, and holds no lock (its fds close at exit). So zombies now count as dead. Windows already behaved this way: its process handle is signalled at exit, which is exactly the semantic unix was missing.
macOS
There is no direct zombie predicate, so this asks
proc_pidinfo(PROC_PIDTBSDINFO)for the BSD record, which the kernel refuses once the process is gone. Measured on this machine:ESRCHESRCHENOMEMEPERMApple's libc wrapper collapses the kernel's
-1into a return of0and reports the reason through errno, so the return value alone cannot separate "pid is gone" from "the call failed" — an earlier revision of this patch keyed on== 0and would have readENOMEM/EPERMas dead. OnlyESRCHcounts as exited; anything else keeps the pre-fix answer, since declaring a live daemon dead and respawning over the top of it is the worse error. errno is cleared before the call so an unrelated earlier failure can't be mistaken for ours.Linux
Reads the state character from
/proc/<pid>/stat— as bytes rather than aString, sincecommis the raw executable name and need not be valid UTF-8. The scan starts after the last)becausecommis parenthesised and may itself contain spaces and parentheses.Other unix targets (iOS, Android) keep the old behaviour.
Tests
Full build clean, clippy clean workspace-wide, 1367 tests pass. A 21-agent adversarial review returned zero confirmed findings; the
ESRCHgating and the/procbyte read above came out of true-but-refuted observations it surfaced.Notes
Based on #157 (
refactorx/full-headless). Independent of #164 and #165 — this is the follow-up called out in #165's description.🤖 Generated with Claude Code
https://claude.ai/code/session_01Y4Aq3X2s6ftafXzmqaN3A8