D194 - grep -q reading a Windows program deadlocks the harness, and it hid as a timeout bug for hours

The symptom the user saw: a Kyty window sitting on screen doing nothing for ten minutes, against a stated budget of 100 seconds. The measured version: sweep runs of 278s, 352s and 216s with TIMEOUT=100, then a 60-second run still going at five minutes.

Every wrong diagnosis on the way there was a plausible one. The wait loop recomputes elapsed from the wall clock and tests it at the top, so the loop was correct. alive calls tasklist, which costs about two seconds warm and eighty-five cold, so I throttled it to once every ten seconds - a real improvement that fixed nothing, because the loop was not running slowly, it was not running at all.

bash -x with a timestamped PS4 settled it in one run:

+[1787753398] alive fc_script
+[1787753399] tasklist
+[1787753400] grep -qi '^fc_script\.exe'

and then 148 lines of trace and no line 149, ever.

The mechanism

grep -q exits the moment it matches and closes its end of the pipe. A POSIX writer gets SIGPIPE and dies, which is why this idiom is safe everywhere it is normally written. tasklist.exe is a Windows program: no SIGPIPE, so it blocks forever writing the rest of its output into a reader that is gone. The loop stops mid-iteration and never re-tests its own budget, so the timeout is not late - it is unreachable.

The same failure hung apt for thirty-nine minutes earlier the same day, from the other end: that time I was the reader that went away, via a | tail -3. Twice in one day, in opposite roles, and I did not recognise the second one.

Fix

Four call sites across run-emulator.sh and sweep-emulators.sh collapse into one helper that never abandons a pipe:

out=$(MSYS_NO_PATHCONV=1 tasklist /FI "IMAGENAME eq $1.exe" /NH 2>/dev/null | tr 'A-Z' 'a-z')

$(...) reads to EOF, so the writer always finishes. /FI makes the output a line or two rather than two hundred, which also removes most of the cost the throttle was added for. MSYS_NO_PATHCONV because Git Bash otherwise rewrites /FI into a Windows path and the filter silently stops filtering.

Verified: Kyty now stops at 60s against a 60s budget.

The general rule

Do not pipe a Windows program into anything that can exit early - grep -q, head, tail, read. Capture with $(...) and match on the string. The failure mode is not a slow command or a wrong answer, it is a harness that stops measuring while continuing to look busy, which is the same class of silent measurement loss as D181.

Status: derived - mechanism traced, fix measured against the budget it was missing.