Skip to content

Reap the native process tree when a Windows test hangs#595

Merged
xroche merged 8 commits into
masterfrom
windows-watchdog-reap
Jul 16, 2026
Merged

Reap the native process tree when a Windows test hangs#595
xroche merged 8 commits into
masterfrom
windows-watchdog-reap

Conversation

@xroche

@xroche xroche commented Jul 16, 2026

Copy link
Copy Markdown
Owner

Master's Windows x64 leg fails intermittently with "the hosted runner lost communication with the server": the job runs its full 45-minute budget, then the runner dies with no logs uploaded. It's flaky, not a regression (the same commits pass on re-run), but it hides real failures and burns CI minutes.

The cause is the per-test watchdog. Each test ran under timeout 600, which can't reap a native httrack.exe: MSYS signals don't reach non-Cygwin processes, as testlib.sh already notes for the python server. So when a crawl wedged, timeout killed only the outer bash and left the engine spinning, and the orphans piled up until the runner starved. The failure-path traced re-run had no watchdog at all, so a deterministic hang went straight to the cap.

run_with_timeout/kill_tree in testlib.sh replace timeout: they poll for the deadline and end the whole tree by Windows PID with taskkill //T (on POSIX, in its own process group, signalling the group). A hang now fails as a bounded 124 with everything reaped. 57_watchdog.test guards it: exit codes pass through, an overrun reports 124 near the deadline, and a native or orphaned grandchild is confirmed dead (I checked the oracle fails a watchdog that reaps only the direct child). It doesn't address why the engine's own --max-time=120 sometimes fails to fire on a stuck x64 socket, the deeper bug behind the wedge.

xroche and others added 3 commits July 16, 2026 07:31
The Windows x64 leg intermittently died with "the hosted runner lost
communication with the server": a crawl wedged and the timeout(1)
watchdog around each test could not kill it. MSYS signals don't reach a
native httrack.exe, so timeout reaped only the outer bash and left the
engine and its python server spinning; the orphans accumulated until the
runner starved. The failure-path traced re-run had no watchdog at all,
so a deterministic hang burned the full 45-minute job budget.

Replace timeout(1) with run_with_timeout/kill_tree in testlib.sh: it
polls for the deadline and, on Windows, ends the whole tree by Windows
PID via taskkill //T; on POSIX it runs the job in its own process group
and signals the group. A hang now fails as 124 in bounded time with the
tree reaped, rather than taking the runner down.

57_watchdog.test proves it: exit codes pass through, an overrun reports
124 near the deadline, and a native (Windows) or orphaned (POSIX)
grandchild is confirmed dead. It runs on every platform under make check
and in the Windows suite.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
…ously

The tree-reap assertions were negative-only: the Windows branch trusted
`tasklist` reporting zero PING.EXE, which also holds if ping never
launched or the filter were mistyped, and the POSIX branch trusted the
marker being absent, which also holds if the grandchild never ran. Since
the Windows path can't be exercised off a Windows runner, a vacuous pass
there would go unnoticed.

Prove the mechanism first: on Windows, confirm a live ping is visible to
the filter and that kill_tree reaps it before asserting the grandchild is
gone; on POSIX, have the grandchild mark that it ran so marker-absent
means reaped, not never-started.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
The Windows positive control fired in CI: a live ping.exe was running yet
the survivor count came back zero. The workflow exports
MSYS2_ARG_CONV_EXCL='*', which stops MSYS from folding the `//FI ... //NH`
switches down to `/FI ... /NH`, so tasklist saw a mangled filter, matched
nothing, and every `-eq 0` check passed without testing anything. Scan the
unfiltered tasklist for the image name instead, which takes no switch args.

This is exactly the vacuous pass the positive control was added to catch.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
@xroche xroche closed this Jul 16, 2026
@xroche xroche reopened this Jul 16, 2026
xroche and others added 5 commits July 16, 2026 10:12
Signed-off-by: Xavier Roche <roche@httrack.com>
…e count

The tasklist filter fix worked (a live ping was detected), but counting
every ping.exe globally raced the still-dying ping left by the timing
sub-test just above, so the survivor count came back non-zero. Match the
grandchild's exact Windows PID instead (tasklist column 2), and have it
record that PID so a non-empty value doubles as the positive control.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
… out

The bash -c backgrounded the ping and exited immediately after recording
its PID, so run_with_timeout saw it finish in a blink and returned 0, not
124 (and the orphaned ping was never the watchdog's to reap). Add `wait`
so the bash blocks on the ping past the deadline, which is what makes the
timeout fire and taskkill //T reap the whole tree.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
A real watchdog defect the new test exposed, not a test-only bug. The
workflow sets MSYS_NO_PATHCONV=1 and MSYS2_ARG_CONV_EXCL='*' (so httrack
sees URL paths unmangled), which makes MSYS pass every argument verbatim.
`taskkill //F //T //PID` therefore reached taskkill with the switches
still doubled, was rejected, and did nothing (masked by `|| true`); only
the direct child died, via kill -9 translating to TerminateProcess on its
winpid. A native grandchild (the wedged httrack.exe in production) would
have survived. Use single-slash switches, which pass through cleanly under
no-conversion, so taskkill /T actually reaps the tree.

Real crawls never hang, so this path was never exercised by the green
runs; 58_watchdog.test is what drove it out.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
@xroche
xroche merged commit eb46d8d into master Jul 16, 2026
37 of 38 checks passed
@xroche
xroche deleted the windows-watchdog-reap branch July 16, 2026 15:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant