fix(windows): _process_alive misreports process state - #150
Open
tradewithmeai wants to merge 1 commit into
Open
Conversation
os.kill(pid, 0) is the POSIX idiom for "does this process exist", but it does
not carry to Windows, where os.kill is implemented over OpenProcess rather
than signals. Measured on Windows 11 / CPython 3.13, the old implementation
had two distinct faults:
1. A PID that never existed raised OSError(WinError 87, "The parameter is
incorrect") rather than ProcessLookupError, so it matched neither except
clause and propagated out of what is only a status check.
2. A recently-exited PID did not raise at all. A Windows process handle
stays openable while any handle to it remains, so os.kill(pid, 0)
succeeded and the function returned True — reporting a dead service as
running.
The second is the more damaging of the two: it is silent, and it makes
`cce services status` claim a crashed service is healthy. The first at least
announces itself.
Non-positive PIDs were also accepted. On POSIX os.kill(0, 0) signals the
caller's entire process group and returns cleanly, so pid=0 reported "alive".
psutil is already a core dependency (pyproject.toml) and is already imported
by config.py, so use it instead of hand-rolling a ctypes OpenProcess path.
Zombies are excluded deliberately: on POSIX a reaped-but-not-collected child
still "exists" while not running, which is fault 2 wearing a different hat.
Tests: three added alongside the existing max-PID case —
- an actually-exited child reports dead (the case the max-PID test cannot
reach, and the one that was silently wrong)
- a known-running child reports alive (positive control)
- 0 and negative PIDs report dead
Verified by mutation: against the previous implementation 5 of the tests in
this file fail; against the fix all 23 pass. This also turns
tests/test_cli_smoke.py::test_services green, which was failing for the same
reason.
tradewithmeai
requested review from
fazleelahhee and
rajkumarsakthivel
as code owners
July 28, 2026 22:59
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.
Fixes #149.
What's wrong
_process_alive()usesos.kill(pid, 0), the POSIX existence idiom. On Windowsos.killis implemented overOpenProcessrather than signals, and the function returns the wrong answer in two different ways — measured on Windows 11 / CPython 3.13:OSError(WinError 87)— propagates out of a status checkFalseTrueFalse0/ negativeTrueFalseThe middle row is the reason this is worth fixing. A Windows process handle stays openable while any handle to it remains, so
os.kill(pid, 0)on a just-exited PID doesn't raise —cce services statusreports a crashed service as healthy, with nothing to notice. TheWinError 87crash is the loud symptom; this is the quiet one.Non-positive PIDs slipped through too: on POSIX
os.kill(0, 0)signals the caller's whole process group and returns cleanly, sopid=0reported alive.The change
psutil>=5.9is already a core dependency and already imported byconfig.py, so this uses it rather than hand-rolling a ctypesOpenProcess+WaitForSingleObjectpath:Zombies are excluded deliberately: on POSIX a reaped-but-not-collected child still "exists" while not running, which is the same wrong answer in a different shape.
Tests
Three added alongside the existing max-PID case:
test_process_alive_dead_piduses PID 2**22, beyond the max PID on Linux/macOS, so it only exercises "never existed"; the silent wrong answer lives in the exited-process path.Falsefor everything.Verified by mutation rather than by watching them pass: against the previous implementation 5 tests in this file fail; against the fix all 23 pass.
CI
This also turns
tests/test_cli_smoke.py::test_servicesgreen — it was failing for the same root cause.One thing worth flagging separately:
ci.ymlruns the Windows job with|| true, so both of these failures were being swallowed rather than surfaced. That mask was hiding a real defect, not runner flakiness. Removing it is out of scope here, but you may want to once Windows is green.