Context
Follow-up from review of PR #774.
internal/background/process_posix.go:47-51 (terminateOwnedProcess) always returns leaderAlreadyExited = false, unlike its Windows counterpart in internal/background/process_windows.go, which computes alreadyExited from GetExitCodeProcess before attempting termination. TerminateCommand (internal/background/terminate.go) uses that flag to decide whether a termination error is safe to suppress once the reap has independently succeeded.
Reproduction
Confirmed empirically (WSL Ubuntu, Go 1.26.5 linux/amd64): start a child as its own process-group leader, let it become a zombie, make ps unresolvable (e.g. PATH=""), then call TerminateCommand.
Result:
TerminateCommand elapsed=406ms err="process <pid> did not exit after SIGKILL"
ProcessState: exit status 0
The reap succeeds cleanly, but TerminateCommand still returns a spurious termination error — because signalTargetRunning (internal/execution/process_unix.go) falls back to the raw kill(pid, 0) result when ps is unavailable or unparseable, and kill(0) succeeds against a zombie. The tree is genuinely already dead, but the code can't distinguish that from "still alive" without ps, so it burns both grace periods and reports failure.
This matters because that error string is surfaced verbatim in zero daemon start's failure message when startAndAwaitDaemonProcess's cleanup path hits it.
Why this isn't a simple fix
Using "the reap succeeded" (or "the leader was already a zombie before termination began") as a stand-in leaderAlreadyExited is NOT safe: a zombie leader can still have a live orphaned child in the same process group (see TestTerminateCommandKillsChildAfterLeaderExits), and only ps//proc can currently tell that group-liveness apart from "unknown." Using the reap result to suppress the error would risk masking that genuinely-alive-descendant case too.
A correct fix likely needs either:
- A portable way to positively confirm the group has zero non-zombie members without
ps (e.g. /proc/<pid>/stat scanning on Linux, a Darwin-specific equivalent, falling back to today's conservative behavior only where no such mechanism exists), or
- A deliberate, documented decision to accept the rare spurious-failure report as the price of never risking a false "success," with a clearer error message that says the liveness check couldn't run rather than asserting "did not exit after SIGKILL".
Requires design discussion before implementation. Not blocking ps is present on all Unix targets we ship for today.
Context
Follow-up from review of PR #774.
internal/background/process_posix.go:47-51(terminateOwnedProcess) always returnsleaderAlreadyExited = false, unlike its Windows counterpart ininternal/background/process_windows.go, which computesalreadyExitedfromGetExitCodeProcessbefore attempting termination.TerminateCommand(internal/background/terminate.go) uses that flag to decide whether a termination error is safe to suppress once the reap has independently succeeded.Reproduction
Confirmed empirically (WSL Ubuntu, Go 1.26.5 linux/amd64): start a child as its own process-group leader, let it become a zombie, make
psunresolvable (e.g.PATH=""), then callTerminateCommand.Result:
The reap succeeds cleanly, but
TerminateCommandstill returns a spurious termination error — becausesignalTargetRunning(internal/execution/process_unix.go) falls back to the rawkill(pid, 0)result whenpsis unavailable or unparseable, andkill(0)succeeds against a zombie. The tree is genuinely already dead, but the code can't distinguish that from "still alive" withoutps, so it burns both grace periods and reports failure.This matters because that error string is surfaced verbatim in
zero daemon start's failure message whenstartAndAwaitDaemonProcess's cleanup path hits it.Why this isn't a simple fix
Using "the reap succeeded" (or "the leader was already a zombie before termination began") as a stand-in
leaderAlreadyExitedis NOT safe: a zombie leader can still have a live orphaned child in the same process group (seeTestTerminateCommandKillsChildAfterLeaderExits), and onlyps//proccan currently tell that group-liveness apart from "unknown." Using the reap result to suppress the error would risk masking that genuinely-alive-descendant case too.A correct fix likely needs either:
ps(e.g./proc/<pid>/statscanning on Linux, a Darwin-specific equivalent, falling back to today's conservative behavior only where no such mechanism exists), orRequires design discussion before implementation. Not blocking
psis present on all Unix targets we ship for today.