ps -eo pid=,ppid=,stat=,comm=,user=,args= |
Replaces the ad-hoc tick #157 pkill -f vitest with a guarded, idempotent pre-tick routine (INFRA-011). Two production safeguards the bare pkill -f lacked: (1) it could match arbitrary processes whose argv merely contained the string (editors, shells, the harness itself); (2) it had no self-exclusion and no escalation path for processes that ignore SIGTERM.
The fix (~/infra/vitest-zombie-cleanup/vitest-zombie-cleanup.sh):
#!/usr/bin/env bash
set -euo pipefail
ME=$$; OWNER="$(id -un)"; GRACE=3
PATTERN='[v]itest' # bracket trick: never matches this script's own argv
# vitest always runs on node, so require comm=node + user-ownership + vitest argv.
# Defunct (Z) entries are counted but never signalled — a zombie is already dead.
list() {
ps -eo pid=,ppid=,stat=,comm=,user=,args= |
awk -v me="$ME" -v owner="$OWNER" '
$1 != me && $5 == owner && ($4 == "node" || $4 == "nodejs") &&
$0 ~ /'$PATTERN'/ { print $1, $3 }'
}
candidates() { list | awk '$2 !~ /^Z/ { print $1 }'; }
defuncts() { list | awk '$2 ~ /^Z/ { print $1 }'; }
case "${1:-clean}" in
--check) n=$(candidates | wc -l); [ "$n" -gt 0 ] && exit 1 || exit 0 ;;
--report) printf '{"zombies_found":%s,"defunct":%s,"live":%s,"load":%s}\n' \
"$(list | wc -l)" "$(defuncts | wc -l)" "$(candidates | wc -l)" "$(awk '{print $1}' /proc/loadavg)"; exit 0 ;;
esac
for pid in $(candidates); do kill -0 "$pid" 2>/dev/null && kill -TERM "$pid" 2>/dev/null; done # graceful pass
t=0; while [ "$t" -lt "$GRACE" ]; do [ "$(candidates | wc -l)" -eq 0 ] && break; sleep 1; t=$((t+1)); done
for pid in $(candidates); do kill -KILL "$pid" 2>/dev/null; done # stragglers
remaining=$(candidates | wc -l); [ "$remaining" -eq 0 ] && exit 0 || exit 1 # pre-tick guard: fail tick on leftovers
Usage: vitest-zombie-cleanup.sh (pre-tick guard), --check (dry run, exit 1 if candidates exist), --report (JSON for the tick log). Exit 0 = no vitest node processes remain; exit 1 = leftovers, so a CI tick step can gate on it.
Verified live plus 19 automated tests (`test-vitest-zombie-cleanup.sh`):
- **Live run (this environment, matching tick #157's "0 zombies found")**: `--report` → `{"zombies_found":0,"defunct":0,"live":0,"load":1.73}`; clean mode exit **0**, load 1.73→1.73.
- **Real kill path**: spawned two fake vitest node processes (`node -e … vitest --watch-sim`) — one SIGTERM-friendly, one with `process.on("SIGTERM",…noop)` — both gone after clean, `killed≥1`, `remaining:0`, exit 0. `--check` returns 1 when candidates exist, 0 when none.
- **Safety guards (edge cases)**:
- *comm guard*: `exec -a "sleep vitest-not-a-node" sleep 300` (argv contains vitest, comm=`sleep`) survives clean untouched.
- *self-exclusion*: the harness itself has "vitest" in its argv; it survived every run (marker-file check).
- *user/self filter*: unit test with a fake `ps` fixture proved `list` excludes `comm=sleep`, `otheruser`, and PID `$$`; a synthetic `Z … node … vitest` line is classified defunct (never signalled) while live ones are candidates.
- *real zombie*: held a genuine `Z` child (`[node] <defunct>`, kernel frees zombie argv so it falls out of the matcher) — clean exits 0, no crash, defunct not signalled.
- **JSON output** validated with `JSON.parse` in node on every run.
Note: the gitleaks `tasks.md.bak` SPDX false-positive mentioned in the tick is pre-existing and unrelated to this problem class; board-only commits continue to use `--no-verify`.{"model": "deepseek-v4-flash", "problem_class": "infra-vitest-zombie-cleanup", "result": "passed", "tests": 19}Replaces the ad-hoc tick #157 pkill -f vitest with a guarded, idempotent pre-tick routine (INFRA-011). Two production safeguards the bare pkill -f lacked: (1) it could match arbitrary processes whose argv merely contained the string (editors, shells, the harness itself); (2) it had no self-exclusion and no escalation path for processes that ignore SIGTERM.
The fix (~/infra/vitest-zombie-cleanup/vitest-zombie-cleanup.sh):
#!/usr/bin/env bash
set -euo pipefail
ME=$$; OWNER="$(id -un)"; GRACE=3
PATTERN='[v]itest' # bracket trick: never matches this script's own argv
# vitest always runs on node, so require comm=node + user-ownership + vitest argv.
# Defunct (Z) entries are counted but never signalled — a zombie is already dead.
list() {
ps -eo pid=,ppid=,stat=,comm=,user=,args= |
awk -v me="$ME" -v owner="$OWNER" '
$1 != me && $5 == owner && ($4 == "node" || $4 == "nodejs") &&
$0 ~ /'$PATTERN'/ { print $1, $3 }'
}
candidates() { list | awk '$2 !~ /^Z/ { print $1 }'; }
defuncts() { list | awk '$2 ~ /^Z/ { print $1 }'; }
case "${1:-clean}" in
--check) n=$(candidates | wc -l); [ "$n" -gt 0 ] && exit 1 || exit 0 ;;
--report) printf '{"zombies_found":%s,"defunct":%s,"live":%s,"load":%s}\n' \
"$(list | wc -l)" "$(defuncts | wc -l)" "$(candidates | wc -l)" "$(awk '{print $1}' /proc/loadavg)"; exit 0 ;;
esac
for pid in $(candidates); do kill -0 "$pid" 2>/dev/null && kill -TERM "$pid" 2>/dev/null; done # graceful pass
t=0; while [ "$t" -lt "$GRACE" ]; do [ "$(candidates | wc -l)" -eq 0 ] && break; sleep 1; t=$((t+1)); done
for pid in $(candidates); do kill -KILL "$pid" 2>/dev/null; done # stragglers
remaining=$(candidates | wc -l); [ "$remaining" -eq 0 ] && exit 0 || exit 1 # pre-tick guard: fail tick on leftovers
Usage: vitest-zombie-cleanup.sh (pre-tick guard), --check (dry run, exit 1 if candidates exist), --report (JSON for the tick log). Exit 0 = no vitest node processes remain; exit 1 = leftovers, so a CI tick step can gate on it.
Verified live plus 19 automated tests (`test-vitest-zombie-cleanup.sh`):
- **Live run (this environment, matching tick #157's "0 zombies found")**: `--report` → `{"zombies_found":0,"defunct":0,"live":0,"load":1.73}`; clean mode exit **0**, load 1.73→1.73.
- **Real kill path**: spawned two fake vitest node processes (`node -e … vitest --watch-sim`) — one SIGTERM-friendly, one with `process.on("SIGTERM",…noop)` — both gone after clean, `killed≥1`, `remaining:0`, exit 0. `--check` returns 1 when candidates exist, 0 when none.
- **Safety guards (edge cases)**:
- *comm guard*: `exec -a "sleep vitest-not-a-node" sleep 300` (argv contains vitest, comm=`sleep`) survives clean untouched.
- *self-exclusion*: the harness itself has "vitest" in its argv; it survived every run (marker-file check).
- *user/self filter*: unit test with a fake `ps` fixture proved `list` excludes `comm=sleep`, `otheruser`, and PID `$$`; a synthetic `Z … node … vitest` line is classified defunct (never signalled) while live ones are candidates.
- *real zombie*: held a genuine `Z` child (`[node] <defunct>`, kernel frees zombie argv so it falls out of the matcher) — clean exits 0, no crash, defunct not signalled.
- **JSON output** validated with `JSON.parse` in node on every run.
Note: the gitleaks `tasks.md.bak` SPDX false-positive mentioned in the tick is pre-existing and unrelated to this problem class; board-only commits continue to use `--no-verify`.{"model": "deepseek-v4-flash", "problem_class": "infra-vitest-zombie-cleanup", "result": "passed", "tests": 19}