igneum/tools/ci/kill-by-name-check.sh
igneum-labs 11c4426a96 Kill by exact command line or pid file, never by a name: tools/ci/kill-by-name-check.sh in the gate; the 36 pgrep/pkill literals in the tree fixed
The fleet's 22:09 UK incident (a Mac-side pkill -f <log file name> matched nothing, the roll-everything script lived on and wiped a held box) and the day's two pgrep self-matches are one class. The check flags pgrep -f / pkill -f with a plain literal (every one on a line), any pgrep/pkill on a file-name shape, and ps | grep with a literal; it allows the bracket form, -x, -F pidfile, kill $(cat pidfile), a variable and a full path; 11 banned and 16 allowed shapes in its self-test; 0.15 s over the tree. The 25 pkill -f sp1-gpu-server inside bash -c bodies (which matched the calling bash) are pkill -x; the other 11 literals take the bracket form; prover-socket-check accepts both. Row R in the record; the CLAUDE.md rule names the check and covers pkill and file names.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-06 22:10:31 +00:00

99 lines
6.7 KiB
Bash
Executable file

#!/usr/bin/env bash
# Kill or find a process by exact command line or pid file, never by a name (CLAUDE.md, 6 October 2026).
#
# Two incidents the same day: a `pgrep -f "<pattern>"` whose literal sat in the calling shell's own command line matched
# itself, so the check always passed and no node ever started (twice, lunchtime and 17:1x UTC); and at 22:09 UK a Mac-side
# `pkill -f <log file name>` matched nothing (the name was a shell redirect, not part of any command line), the
# roll-everything script lived on and wiped a box it had been told to hold.
#
# Rule, as this check reads it, over every script in the tree (sh, bash, mjs, js, py, ps1; comments skipped):
# 1. `pgrep -f` or `pkill -f` with a plain literal pattern is flagged. Allowed: the bracket form (`[i]gneumd`, which never
# matches its own command line), `-x` on a binary name, `pkill -F <pidfile>` / `kill $(cat <pidfile>)`, and a pattern
# that is a variable or starts with a slash (an anchored full path: "the binary's full path and first argument").
# 2. Any pgrep/pkill pattern, bracketed or not, whose literal looks like a FILE NAME (an extension such as .log, .txt,
# .json, .jsonl, .out, .err, .pid, .csv, .md, .toml, .yml) is flagged: a file name is a redirect or an argument, and a
# redirect is never on a command line.
# 3. `ps ... | grep <literal>` without the bracket form is flagged (the same self-match).
#
# tools/ci/kill-by-name-check.sh # exit 1 with file:line and the reason
# tools/ci/kill-by-name-check.sh --self-test # fires on each banned shape, passes each allowed one
set -euo pipefail
check_line() { # <line> -> prints the reason, returns 1, when the line carries a banned shape; returns 0 otherwise
local line="$1" code pat
code="${line%%#*}" # a comment is not code (a line that starts with // or * is a comment too)
[[ "$code" =~ ^[[:space:]]*(//|\*|/\*) ]] && return 0
[[ "$line" =~ ^[[:space:]]*(//|\*|/\*) ]] && return 0
# every pgrep/pkill -f on the line, not only the first (`pkill -f "[i]gneum-prove-host"; pkill -f prove-shard.sh` hid its second)
local rest="$code" m
while [[ "$rest" =~ (^|[^A-Za-z0-9_./-])(pgrep|pkill)([[:space:]]+-[A-Za-z0-9]+)*[[:space:]]+-[A-Za-z]*f[A-Za-z]*[[:space:]]+(\"[^\"]*\"|\'[^\']*\'|[^[:space:]|;\)]+) ]]; do
m="${BASH_REMATCH[0]}"; pat="${BASH_REMATCH[4]}"; pat="${pat#[\"\']}"; pat="${pat%[\"\']}"
rest="${rest#*"$m"}"
if [[ "$pat" =~ \.(log|txt|jsonl?|out|err|pid|csv|md|toml|ya?ml|lock)(\"|\'|$|[^A-Za-z0-9]) ]]; then echo "pkill/pgrep -f on a file name ($pat): a file name is a redirect or an argument, never the command line; use a pid file (tools/fleet/fleet-bg.sh) or the binary's full path"; return 1; fi
[[ "$pat" == *'$'* ]] && continue # a variable: the caller anchored it (reviewed by hand)
[[ "$pat" == /* ]] && continue # an anchored full path
[[ "$pat" =~ ^\[.\] ]] && continue # the bracket form never matches its own command line
echo "pgrep/pkill -f with a plain literal ($pat): it matches the calling shell's own command line; use the bracket form ([${pat:0:1}]${pat:1}), -x on the binary name, or a pid file"; return 1
done
if [[ "$code" =~ (^|[^A-Za-z0-9_./-])(pgrep|pkill)[[:space:]]+(-[A-Za-z0-9]+[[:space:]]+)*(\"[^\"]*\"|\'[^\']*\'|[^[:space:]|;\)-][^[:space:]|;\)]*) ]]; then
pat="${BASH_REMATCH[4]}"; pat="${pat#[\"\']}"; pat="${pat%[\"\']}"
if [[ "$pat" =~ \.(log|txt|jsonl?|out|err|pid|csv|md|toml|ya?ml|lock)($|[^A-Za-z0-9]) ]] && [[ "$line" != *"-F "* ]]; then echo "pkill/pgrep on a file name ($pat): a file name is never a process name; use pkill -F <pidfile> or the binary's full path"; return 1; fi
fi
if [[ "$code" =~ (^|[^A-Za-z0-9_])ps[[:space:]][^|]*\|[[:space:]]*grep[[:space:]]+(-[A-Za-z]+[[:space:]]+)*(\"[^\"]*\"|\'[^\']*\'|[^[:space:]|;\)]+) ]]; then
pat="${BASH_REMATCH[3]}"; pat="${pat#[\"\']}"; pat="${pat%[\"\']}"
[[ "$pat" == *'$'* ]] && return 0
[[ "$pat" =~ ^\[.\] ]] && return 0
[[ "$pat" == grep ]] && return 0 # `grep -v grep`
echo "ps | grep with a plain literal ($pat): it matches the grep itself; use the bracket form ([${pat:0:1}]${pat:1}) or pgrep -x"; return 1
fi
return 0
}
if [ "${1:-}" = "--self-test" ]; then
fails=0
bad=(
'pkill -f igneum-roll.log'
'pkill -f "fleet-wave-3.log" || true'
'pgrep -f igneumd >/dev/null && exit 0'
"pkill -f 'node tools/fleet/wave.mjs'"
'if pgrep -f "igneum-miner --pool" >/dev/null; then echo up; fi'
'pkill -9 -f run-shard.out'
'ps aux | grep igneumd | grep -v grep'
'ps -ef | grep "igneum-miner" | awk "{print \$2}"'
'pkill node-1.pid'
"bash -c 'pkill -f \"[i]gneum-prove-host\"; pkill -f prove-shard.sh; true'"
"pgrep -fl 'igneumd --' | grep -v Wallet"
)
good=(
'pgrep -f "[i]gneumd" >/dev/null'
"pkill -f '[n]ode tools/fleet/wave.mjs'"
'pgrep -x igneumd'
'pkill -x igneum-miner'
'pkill -F /srv/hands/node1.pid'
'kill "$(cat "$PIDFILE")"'
'pkill -f "$EXACT_CMD"'
'pkill -f /opt/igneum/bin/igneumd'
'pgrep -f "/srv/fleet/bin/igneum-miner --pool"'
'# pgrep -f igneumd is banned (a comment)'
'// pkill -f wave.log in a comment'
'ps aux | grep "[i]gneumd"'
'ps aux | grep -v grep | wc -l'
'echo "the pgrep rule"'
"bash -c 'pkill -f \"[i]gneum-prove-host\"; pkill -f \"[p]rove-shard.sh\"; true'"
"pgrep -fl '[i]gneumd --' | grep -v Wallet"
)
for l in "${bad[@]}"; do if check_line "$l" >/dev/null; then echo "self-test failed: accepted: $l"; fails=1; fi; done
for l in "${good[@]}"; do if ! out="$(check_line "$l")"; then echo "self-test failed: rejected: $l ($out)"; fails=1; fi; done
[ "$fails" = 0 ] && echo "self-test passed: ${#bad[@]} banned shapes fail (a log or out file name under pkill/pgrep, a plain literal under -f, the second pkill on a line, ps | grep with a literal); ${#good[@]} allowed shapes pass (bracket form, -x, -F pidfile, kill \$(cat pidfile), a variable, a full path, comments)"
exit $fails
fi
cd "$(git rev-parse --show-toplevel)"
fail=0; n=0
# only the lines that name the commands (git grep is a second over the tree; a bash loop over every line was a minute)
while IFS= read -r hit; do
f="${hit%%:*}"; rest="${hit#*:}"; ln="${rest%%:*}"; line="${rest#*:}"; n=$((n + 1))
if ! why="$(check_line "$line")"; then echo "kill-by-name: $f:$ln: $why"; fail=1; fi
done < <(git grep -nE '(^|[^A-Za-z0-9_./-])(pgrep|pkill)([[:space:]]|$)|(^|[^A-Za-z0-9_])ps[[:space:]][^|]*\|[[:space:]]*grep' -- '*.sh' '*.bash' '*.mjs' '*.js' '*.py' '*.ps1' '*.bat' ':!vendor/**' ':!**/node_modules/**' ':!tools/ci/kill-by-name-check.sh' || true)
[ "$fail" = 0 ] && echo "kill-by-name: $n pgrep/pkill/ps-grep lines, none kills or finds a process by a plain name or a file name"
exit $fail