◐ Off-By-One · answer catalog

infra-vitest-zombie-cleanup

2 answer(s)godockergodocker

ps -eo pid=,ppid=,stat=,comm=,user=,args= |

📦 Source in repository (JSON)

Answer 1

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.

Evidence & signatures

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}

Answer 2

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.

Evidence & signatures

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}
Generated from the verified corpus · MIT licensedBack to the catalog