Import #776's teardown half: decide on the root's liveness, not on taskkill's message - #15
Merged
Merged
Conversation
sendBrokerShutdown waited forever when the broker accepted the connection but never replied, so the SessionEnd hook never returned (observed as "Hook cancelled"). It now gives up after a timeout and destroys the socket; a regression test covers a mute broker. terminateProcessTree treated any non-zero taskkill status as a failure. On Windows, taskkill /T enumerates the tree and then terminates each entry; a short-lived descendant (git/cmd helpers spawned by the worker) that exits in between makes taskkill report "The operation attempted is not supported" with status 128 even though the root process was killed. The root's liveness is now checked before treating that as an error, which fixes the flaky cancel integration test (reproduced 7/8 runs). Also make the unix endpoint path POSIX-joined and adapt Windows test fixtures: skip the broken-symlink case without Developer Mode, use a node.cmd shim instead of a symlink, and propagate USERPROFILE. Full suite on Windows 11 / Node 26.1.0: 93 passed, 0 failed, 1 skipped. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Review of the previous commit found that terminateProcessTree could
report delivered: true for a Windows process that was already gone
before the call: looksLikeMissingProcessMessage only recognised English
taskkill messages, so on a localised system (French "introuvable") the
"not found" status fell through to the liveness check and was taken as
a successful kill.
terminateProcessTree now probes the root with process.kill(pid, 0)
before running taskkill. A root that is already absent returns
{ attempted: false, delivered: false, method: null } without invoking
taskkill, in every system language; looksLikeMissingProcessMessage and
the text-based branch are removed. EPERM on the probe still means the
process exists, so taskkill runs as before. The post-taskkill liveness
check and the ENOENT fallback are unchanged.
Comments now state the scope of delivered: it describes the root only,
like the process-group SIGTERM on other platforms, and does not prove
that every descendant is gone. Narrow TOCTOU windows (root exiting on
its own between probe and taskkill, PID reuse before the second probe)
are accepted; none of the callers reads the return value.
Tests: French taskkill output with liveness probes, taskkill skipped
when the root is already absent, EPERM on the probe not treated as
absent, ENOENT fallback preserved, non-Windows path unchanged; existing
mocks now lock the killImpl(pid, 0) call. Against the previous
process.mjs, 4 of the 8 tests fail.
Windows 11 / Node 26.1.0, run from native cmd.exe: process.test.mjs
8 passed; cancel integration test 3/3; full suite 98 tests, 97 passed,
0 failed, 1 skipped.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…l's message Taken for terminateProcessTree(); the PR's other two changes are not. What it fixes here. Our win32 branch trusted taskkill's exit status and, failing that, matched its message. Status 128 already covered "no such process" in any language, so the localization hole the PR leads with was closed here — but any *other* non-zero status threw at the caller, and taskkill produces one routinely: /T walks the tree and then terminates each entry, so a short-lived git or cmd helper that exits in between makes it report a failure although the root did die. A throw out of terminateProcessTree lands in teardown paths that have no way to tell that apart from a real failure. Now the root is probed before taskkill is spawned at all — a root that is provably gone returns attempted: false and costs no process — and after a non-zero status the root's own liveness decides. `delivered` accordingly describes the root, exactly like the process-group SIGTERM on other platforms; it never claimed to prove every descendant was gone. Changes on top of the import: - their isProcessAlive() is dropped for this fork's isPidAlive(), and the new gate is isProvablyGone(): only ESRCH proves absence. Their reading (anything but ESRCH is alive) is right for "should I kill", but the inverse question here is "may I skip the kill", and skipping it on a root we merely cannot read leaks the whole tree - two of our tests now inject killImpl. Without it they call the real process.kill(1234, 0), so their outcome depended on whether pid 1234 happened to exist on the machine running the suite Not taken: `path.posix.join` in createBrokerEndpoint (the unix branch is unreachable on Windows, so it changes nothing here), and the 2s timeout in sendBrokerShutdown (ours is already bounded, with refused/unreachable semantics and a retry). Their two test-portability fixes are kept: they let the suite run on a Windows host without Developer Mode, and change no behavior. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UXfvnjSC72HsM6EEPVt2Tg
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.
One new PR upstream since the last sweep. It bundles three changes; one is worth having here, and the other two are not.
347/347, full suite run twice.
tscclean.What was wrong here
Our Windows branch trusted taskkill's exit status and, failing that, matched its message. The localization hole the PR leads with —
looksLikeMissingProcessMessageonly recognising English, so a French "introuvable" fell through — was already closed here: we carry thestatus === 128check, which is language-independent.The part that was broken is the one their commit message buries: any other non-zero status threw at the caller, and taskkill produces one routinely.
/Twalks the tree and then terminates each entry, so a short-livedgitorcmdhelper that exits in between makes taskkill report a failure although the root did die. A throw out ofterminateProcessTree()lands in teardown paths that cannot tell that apart from a real failure.Now:
{attempted: false, delivered: false, method: null}and costs no processdeliveredaccordingly describes the root, exactly like the process-group SIGTERM on other platforms. It never proved every descendant was gone, and now says soChanges on top of the import
isProcessAlive()is dropped for this fork'sisPidAlive(), and the new gate isisProvablyGone(): onlyESRCHproves absence. Their reading (anything but ESRCH is alive) is the right one for "should I kill"; the question at this gate is the inverse — "may I skip the kill" — and skipping it on a root we merely cannot read leaks the whole treekillImpl. Without it they call the realprocess.kill(1234, 0), so their outcome depended on whether pid 1234 happened to exist on the machine running the suite. That is exactly the class of environment-dependent test this fork has spent two rounds removingNot taken
path.posix.joinincreateBrokerEndpointunix:branch is unreachable on Windows, so on every platform this fork runs it produces the same stringsendBrokerShutdownrefused/unreachablesemantics and a retry — strictly ahead of itTheir two test-portability fixes are kept, though: a
.cmdshim instead of a symlink for the fakenode, and a skip when Windows refuses to create the broken-symlink fixture without Developer Mode. They change no behavior and let the suite run on a real Windows host — which is the one check this fork still cannot do from here.Sabotage matrix
🤖 Generated with Claude Code
https://claude.ai/code/session_01UXfvnjSC72HsM6EEPVt2Tg
Generated by Claude Code