fix(scripts): row 211 R5 busy check literal listing, flat form, sourced word walk (#1709)
From the GPT review of R4 (row211-r4-gpt.md, N1 and N2), for #1709 and #426: - slot-busy.sh (N1): gone() lists the proc root with ls -f -1 --quoting-style=literal, so QUOTING_STYLE can't quote a pid out of the listing, and the script exports LC_ALL=C before any tool runs. A failed or denied listing is still never gone. - slot-busy.sh (N2): a command line holding quotes, backslashes, shell operators or uneven whitespace gets a flat form, made in awk. The snapshot rule reads the flat form. The general rules read the flat form, then the raw text. A wrapper's text goes through an awk word walk: every word of a source or . span counts, quoted text is read again, and the wrapper's own snapshot is skipped. More than 32 walk levels, an awk failure or bad output is exit 2, "cannot classify". - test-git-guard.sh: 48 new cases, 153 in all. The new cases cover QUOTING_STYLE, the locale, the rule shapes, depth, awk failure, and live wrappers for the review's 13 shapes plus a CLEAR control. - mutants.sh: sites R5 moved are updated, and 27 new mutants bring the total to 57, all killed. RED 119/34 on fd1aa8cc8's slot-busy.sh, GREEN 153/0. The hook and the installer are unchanged.
This commit is contained in:
1 parent
fd1aa8cc81
commit
afc176de02
3 files changed
+325
-54
No files matched your search
@@ -75,16 +75,16 @@ run differing-hook-replaced $I '[ "$FORCE" = 1 ] || die 2 "refused: a different'
|
||||
run replace-in-place $I 'mv -f -- "$tmp" "$HOOK" ||' 'cat -- "$tmp" > "$HOOK" && rm -f -- "$tmp" ||'
|
||||
run symlink-followed $I 'if [ -L "$HOOK" ]; then die 2' 'if false; then die 2'
|
||||
run ancestor-error-passes $H ' *) refuse "$rref: the ancestor check failed' ' *) : "$rref: the ancestor check failed'
|
||||
run snapshot-matched $S $' if [[ "$c" =~ $RE_SNAPSHOT ]]; then sourced "${c:${#BASH_REMATCH[0]}}"; return; fi\n' ''
|
||||
run snapshot-matched $S $' if [[ " $n " =~ $RE_SNAPSHOT ]]; then sourced "$1"; return; fi\n' ''
|
||||
run ancestors-matched $S ' [ -z "${SKIP[$pid]:-}" ] || continue' ' :'
|
||||
run no-node-rule $S 'if [[ "$c" =~ $RE_NODE ]]; then echo "node --test"; return 0; fi' ':'
|
||||
run no-compose-rule $S 'if [[ "$c" =~ $RE_COMPOSE ]]; then echo "docker compose"; return 0; fi' ':'
|
||||
run no-node-rule $S 'if [[ "$1" =~ $RE_NODE ]]; then echo "node --test"; return 0; fi' ':'
|
||||
run no-compose-rule $S 'if [[ "$1" =~ $RE_COMPOSE ]]; then echo "docker compose"; return 0; fi' ':'
|
||||
# R3, the GPT review (F1 to F5 and the object-name length check).
|
||||
run unreadable-read-clear $S ' gone "$PROC" "$pid" && continue' ' continue'
|
||||
run vanished-is-error $S $' gone "$PROC" "$pid" && continue\n' ''
|
||||
run read-errors-ignored $S '[ "$errors" = 0 ] || {' 'true || {'
|
||||
run self-read-unchecked $S ' || { echo "slot-busy: error: cannot read its own process" >&2; exit 2; }' ' || p=""'
|
||||
run snapshot-substring $S '[[ "$c" =~ $RE_SNAPSHOT ]]' '[[ "$c" == *"/shell-snapshots/snapshot-"* ]]'
|
||||
run snapshot-substring $S '[[ " $n " =~ $RE_SNAPSHOT ]]' '[[ " $n " == *"/shell-snapshots/snapshot-"* ]]'
|
||||
run tag-replace-passes $H 'if [ "${rref#refs/tags/}" != "$rref" ] && [ "$lsha" != "$rsha" ]; then' 'if false; then'
|
||||
run hooks-dir-symlink-followed $I 'own_hooks() {' 'own_hooks() { return 0;'
|
||||
run short-sha-accepted $H ' if ! idlen "$lsha" || ! idlen "$rsha"; then' ' if false; then'
|
||||
@@ -92,10 +92,38 @@ run short-sha-accepted $H ' if ! idlen "$lsha" || ! idlen "$rsha"; then' '
|
||||
run ancestor-read-unchecked $S $' gone /proc "$p" && break\n echo "slot-busy: error: cannot read the status of ancestor $p" >&2; exit 2' ' break'
|
||||
run exited-ancestor-error $S ' gone /proc "$p" && break' ' :'
|
||||
run denied-lookup-vanished $S ' gone "$PROC" "$pid" && continue' ' [ -e "$d" ] || continue'
|
||||
run denied-listing-gone $S 'names=$(ls -f -- "$1" 2>/dev/null) || return 1' 'names=$(ls -f -- "$1" 2>/dev/null) || return 0'
|
||||
run denied-listing-gone $S 'names=$(ls -f -1 --quoting-style=literal -- "$1" 2>/dev/null) || return 1' 'names=$(ls -f -1 --quoting-style=literal -- "$1" 2>/dev/null) || return 0'
|
||||
run dir-check-skips $S $' pid=${d##*/}\n' $' [ -d "$d" ] || continue\n pid=${d##*/}\n'
|
||||
run sourced-test-hidden $S 'then sourced "${c:${#BASH_REMATCH[0]}}"; return; fi' 'then return 1; fi'
|
||||
run sourced-first-only $S ' rest=${rest#*"$m"}' ' break'
|
||||
run dot-source-missed $S '(source|\\.)[[:space:]]+' '(source)[[:space:]]+'
|
||||
run sourced-test-hidden $S 'then sourced "$1"; return; fi' 'then return 1; fi'
|
||||
run sourced-first-only $S '{ if (!depth && !bq) span = 0 }' '{ if (!depth && !bq) { if (span) return 1 } }'
|
||||
run dot-source-missed $S 'w == "source" || w == "."' 'w == "source"'
|
||||
run installer-fixed-tmp $I ' tmp=$(mktemp "$HOOKS/.pre-push.install-git-hooks.XXXXXX") || die 1 "cannot create a temporary file in $HOOKS"' ' tmp="$HOOKS/.pre-push.install-git-hooks.$$"'
|
||||
# R5 (the GPT review of R4: N1 and N2). Sites R5 moved are edited above.
|
||||
run ls-env-quoting $S '-1 --quoting-style=literal --' '-1 --'
|
||||
run locale-kept $S $'export LC_ALL=C\n' ''
|
||||
run quotes-kept $S 'gsub(/["\047\\]/, "", t); ' ''
|
||||
run operators-kept $S 'gsub(/[[:space:];&|(){}<>`]+/, " ", t)' 'gsub(/[[:space:]]+/, " ", t)'
|
||||
run snapshot-single-space $S '<>`]+/, " ", t)' '<>`]/, " ", t)'
|
||||
run flat-skipped $S ' if [[ "$n" == *$SPECIAL* || "$n" == *" "* ]]; then' ' if false; then'
|
||||
run flat-double-blank-missed $S ' || "$n" == *" "* ]]; then' ' ]]; then'
|
||||
run special-tab-missed $S '<>`\t\n\r\v\f]'"'" '<>`\n\r\v\f]'"'"
|
||||
run flat-failure-passes $S '[[ "$n" == F* ]] || return 2' '[[ "$n" == F* ]] || true'
|
||||
run flat-sentinel-unchecked $S 'n=$(flat "$1") && [[ "$n" == F* ]] || return 2' 'n=$(flat "$1") || return 2'
|
||||
run raw-rules-dropped $S 'rules " $n " || rules " $1 "' 'rules " $n "'
|
||||
run flat-rules-dropped $S 'rules " $n " || rules " $1 "' 'rules " $1 "'
|
||||
run source-arg-first-word $S 'span = span || w == "source" || w == "."' 'span = w == "source" || w == "."'
|
||||
run no-requote $S 'if (q && w ~ AGAIN) {' 'if (0) {'
|
||||
run quoted-span-reset $S 'if (open) span = 1' 'span = open'
|
||||
run span-ends-in-parens $S '{ if (!depth && !bq) span = 0 }' '{ span = 0 }'
|
||||
run backtick-ignored $S 'bq = !bq' 'bq = bq'
|
||||
run sq-kept $S 'if (c == SQ) {' 'if (0) {'
|
||||
run dq-kept $S '} else if (c == DQ) {' '} else if (0) {'
|
||||
run dollar-quote-glued $S '} else if (c == "$" && index(SQ DQ, substr(s, i + 1, 1))) {' '} else if (0) {'
|
||||
run backslash-kept $S '} else if (c == BS) {' '} else if (0) {'
|
||||
run snapshot-path-checked $S 'if (snap == "" && w ~ SNAP) snap = w' 'if (0) snap = w'
|
||||
run snapshot-repeat-checked $S 'else if (w != snap) {' 'else if (1) {'
|
||||
run depth-unbounded $S 'if (lvl > maxlvl) return 2' 'if (0) return 2'
|
||||
run awk-failure-none $S "') || return 2" "') || return 1"
|
||||
run lexer-garbage-none $S '*) return 2 ;; esac' '*) return 1 ;; esac'
|
||||
run classify-error-passes $S '[ "$rc" != 1 ] || continue' '[ "$rc" = 0 ] || continue'
|
||||
exit "$bad"
|
||||
+142
-42
@@ -7,26 +7,33 @@
|
||||
#
|
||||
# It prints one BUSY line per matching process, or one CLEAR line. Exit codes:
|
||||
# 0 CLEAR, 1 BUSY, 2 its own error, which includes a process or an ancestor
|
||||
# it can't read that isn't confirmed gone (it still prints any BUSY lines,
|
||||
# never CLEAR). Gone means a listing of the proc root that succeeds and lacks
|
||||
# the pid. Chain it on the exit code, never on a filter of its output:
|
||||
# it can't read that isn't confirmed gone, and a wrapper's text it can't
|
||||
# classify (it still prints any BUSY lines, never CLEAR). Gone means a
|
||||
# literal listing of the proc root that succeeds and lacks the pid. Chain it
|
||||
# on the exit code, never on a filter of its output:
|
||||
# bash scripts/slot-busy.sh > LOG && gate (set -o pipefail if piped)
|
||||
# never `bash scripts/slot-busy.sh | tail -1 && gate`, which runs while BUSY.
|
||||
#
|
||||
# A process matches when its command line holds `node ... --test`, a
|
||||
# `*gate*.sh`, `mutant`, a `test-*.sh` or `docker compose`/`docker-compose`.
|
||||
# It skips itself and its ancestors (a gate script may run the check), and an
|
||||
# agent harness's shell-snapshot wrapper (`bash -c 'source .../shell-snapshots/
|
||||
# snapshot-*.sh ...'`), whose command line is the agent's command text; that
|
||||
# shell's real children are still matched, and so is any other command line
|
||||
# that only names a shell-snapshots path. A wrapper whose text after the
|
||||
# snapshot source also sources (`source` or `.`) a gate, test or mutant
|
||||
# script runs it in its own shell, so that wrapper is BUSY. It reads only
|
||||
# /proc/<pid>/cmdline, the cwd link, its own and its ancestors'
|
||||
# /proc/<pid>/status and, after a failed read, the proc root's listing, never
|
||||
# a file under a seat's directory.
|
||||
# The rules read the command line as it is and in a flat form: quotes and
|
||||
# backslashes removed, and each run of blanks and the characters ; & | ( ) { }
|
||||
# < > ` made one space. It skips itself and its ancestors (a gate script may
|
||||
# run the check), and an agent harness's shell-snapshot wrapper (`bash -c
|
||||
# 'source .../shell-snapshots/snapshot-*.sh ...'`, matched in the flat form),
|
||||
# whose command line is the agent's command text; that shell's real children
|
||||
# are still matched, and so is any other command line that only names a
|
||||
# shell-snapshots path. A wrapper that sources (`source` or `.`) a gate, test
|
||||
# or mutant script runs it in its own shell, so that wrapper is BUSY. Every
|
||||
# word from the `source` or `.` to the next ; & | or newline counts, and
|
||||
# quoted text is read again as shell text. It reads only /proc/<pid>/cmdline,
|
||||
# the cwd link, its own and its ancestors' /proc/<pid>/status and, after a
|
||||
# failed read, the proc root's listing, never a file under a seat's directory.
|
||||
# --proc-root names a stand-in for /proc in the selftests.
|
||||
set -uo pipefail
|
||||
# Every tool reads bytes the same way whatever the caller's locale: under a
|
||||
# UTF-8 locale a byte that isn't UTF-8 would end a [^ /]* run early.
|
||||
export LC_ALL=C
|
||||
|
||||
PROC=/proc
|
||||
while [ $# -gt 0 ]; do
|
||||
@@ -40,10 +47,12 @@ done
|
||||
|
||||
# gone ROOT PID: true only when ROOT lists and PID isn't in the listing. A
|
||||
# failed or denied listing is false: a lookup that fails can't tell an exited
|
||||
# process from one it may not see. `ls -f` reads the names only, no stat.
|
||||
# process from one it may not see. `ls -f` reads the names only, no stat. The
|
||||
# flags fix the format: one name per line, never quoted, whatever
|
||||
# QUOTING_STYLE holds (a quoted name would read as a missing pid).
|
||||
gone() {
|
||||
local names
|
||||
names=$(ls -f -- "$1" 2>/dev/null) || return 1
|
||||
names=$(ls -f -1 --quoting-style=literal -- "$1" 2>/dev/null) || return 1
|
||||
[[ $'\n'"$names"$'\n' != *$'\n'"$2"$'\n'* ]]
|
||||
}
|
||||
|
||||
@@ -67,34 +76,119 @@ RE_NODE='[ /]node[^ ]* (.* )?--test '
|
||||
RE_GATE='[ /_-]gate[^ /]*\.sh '
|
||||
RE_TEST='[ /]test-[^ /]*\.sh '
|
||||
RE_COMPOSE='[ /]docker( compose|-compose) '
|
||||
# The wrapper's shape: a shell, its options with -c, then `source` (or `.`)
|
||||
# of a shell-snapshots/snapshot-*.sh file as the first command.
|
||||
# The wrapper's shape, in the flat form: a shell, its options with -c, then
|
||||
# `source` (or `.`) of a shell-snapshots/snapshot-*.sh file as the first
|
||||
# command.
|
||||
RE_SNAPSHOT='^ (/[^ ]*/)?(ba|z)?sh( -[a-zA-Z]+)* -[a-zA-Z]*c[a-zA-Z]*( -[a-zA-Z]+)* (source|\.) [^ ]*/shell-snapshots/snapshot-[^ /]*\.sh '
|
||||
# A sourced file in the wrapper's text: `source` or `.` after the start, a
|
||||
# blank, a shell operator or a quote, then blanks, an optional `--`, an
|
||||
# optional opening quote and the path, up to a blank, operator or quote.
|
||||
RE_SOURCED="(^|[[:space:];&|(){}\`'\"])(source|\\.)[[:space:]]+(--[[:space:]]+)?['\"]?([^[:space:];&|<>(){}\`'\"]+)"
|
||||
sourced() { # $1: the wrapper's text after the snapshot source. Prints the rule.
|
||||
local rest=$1 m f
|
||||
while [[ "$rest" =~ $RE_SOURCED ]]; do
|
||||
# Saved first: the rule tests below reset BASH_REMATCH.
|
||||
m=${BASH_REMATCH[0]} f=" ${BASH_REMATCH[4]} "
|
||||
if [[ "$f" =~ $RE_GATE ]]; then echo "sourced gate"; return 0; fi
|
||||
if [[ "$f" == *mutant* ]]; then echo "sourced mutant"; return 0; fi
|
||||
if [[ "$f" =~ $RE_TEST ]]; then echo "sourced test script"; return 0; fi
|
||||
rest=${rest#*"$m"}
|
||||
done
|
||||
|
||||
# sourced TEXT: a wrapper's whole command line. Prints the rule when a
|
||||
# `source` or `.` in it names a gate, test or mutant script; returns 1 when
|
||||
# none does, 2 when the text can't be classified.
|
||||
#
|
||||
# The awk walk reads the text in words the way the shell splits them: a
|
||||
# blank, newline or one of ; & | ( ) < > ` ends a word; '...' and "..." are
|
||||
# parts of a word with the quotes removed (a backslash inside "..." keeps the
|
||||
# next character in the word); a backslash keeps the next character; $'...'
|
||||
# and $"..." are read as quotes. A quote that never closes runs to the end of
|
||||
# the text. Quoted text that holds a blank, an operator, a quote or a
|
||||
# backslash is walked again as shell text (eval, bash -c and the harness's
|
||||
# '\'' forms), and a span left open at its end stays open after the word.
|
||||
# A span starts after a `source` or `.` word and ends at ; & | or a newline
|
||||
# outside parentheses and backticks; every word in it is checked, so
|
||||
# redirections, $(...) and `...` parts and later arguments count. The first
|
||||
# shell-snapshots/snapshot-*.sh word in a span is the harness's own snapshot,
|
||||
# skipped whatever its name; a later word equal to it is skipped too. Text
|
||||
# quoted more than 32 levels deep, a failed awk or output that is neither a
|
||||
# rule nor "none" returns 2.
|
||||
sourced() {
|
||||
local out
|
||||
out=$(printf '%s' "$1" | awk -v maxlvl=32 '
|
||||
BEGIN {
|
||||
RS = "\001"; SQ = "\047"; DQ = "\""; BS = "\\"
|
||||
SEP = " \t\n;&|()<>`"; STOP = SEP SQ DQ BS "$"
|
||||
AGAIN = "[[:space:];&|()<>`" SQ DQ "\\\\]"
|
||||
GATE = "(^|[/_-])gate[^/]*\\.sh$"; TEST = "(^|/)test-[^/]*\\.sh$"
|
||||
SNAP = "/shell-snapshots/snapshot-[^/]*\\.sh$"
|
||||
}
|
||||
{ text = (NR > 1) ? text RS $0 : $0 }
|
||||
function walk(s, span, lvl, n, i, j, c, w, q, had, r, depth, bq) {
|
||||
if (lvl > maxlvl) return 2
|
||||
n = length(s); i = 1
|
||||
for (;;) {
|
||||
w = ""; q = had = 0
|
||||
while ((c = substr(s, i, 1)) != "" && !index(SEP, c)) {
|
||||
had = 1
|
||||
if (c == SQ) {
|
||||
for (j = i + 1; j <= n && substr(s, j, 1) != SQ; j++) ;
|
||||
w = w substr(s, i + 1, j - i - 1); i = j + 1; q = 1
|
||||
} else if (c == DQ) {
|
||||
for (j = i + 1; j <= n && (c = substr(s, j, 1)) != DQ; j++) if (c == BS) j++
|
||||
w = w substr(s, i + 1, j - i - 1); i = j + 1; q = 1
|
||||
} else if (c == "$" && index(SQ DQ, substr(s, i + 1, 1))) {
|
||||
i++
|
||||
} else if (c == BS) {
|
||||
c = substr(s, i + 1, 1); if (c != "\n") w = w c; i += 2; q = 1
|
||||
} else {
|
||||
for (j = i + 1; j <= n && !index(STOP, substr(s, j, 1)); j++) ;
|
||||
w = w substr(s, i, j - i); i = j
|
||||
}
|
||||
}
|
||||
if (had) {
|
||||
if (q && w ~ AGAIN) {
|
||||
r = walk(w, span, lvl + 1)
|
||||
if (r != 1) return r
|
||||
if (open) span = 1
|
||||
}
|
||||
if (span) {
|
||||
if (snap == "" && w ~ SNAP) snap = w
|
||||
else if (w != snap) {
|
||||
if (w ~ GATE) { rule = "sourced gate"; return 0 }
|
||||
if (index(w, "mutant")) { rule = "sourced mutant"; return 0 }
|
||||
if (w ~ TEST) { rule = "sourced test script"; return 0 }
|
||||
}
|
||||
}
|
||||
span = span || w == "source" || w == "."
|
||||
}
|
||||
if (c == "") { open = span; return 1 }
|
||||
i++
|
||||
if (c == ";" || c == "&" || c == "|" || c == "\n") { if (!depth && !bq) span = 0 }
|
||||
else if (c == "(") depth++
|
||||
else if (c == ")") { if (depth) depth-- }
|
||||
else if (c == "`") bq = !bq
|
||||
}
|
||||
}
|
||||
END { r = walk(text, 0, 1); print (r == 0 ? rule : r == 1 ? "none" : "deep") }
|
||||
') || return 2
|
||||
case $out in none) return 1 ;; "sourced "*) echo "$out"; return 0 ;; *) return 2 ;; esac
|
||||
}
|
||||
rules() { # $1: a command line with a blank at each end. Prints the rule.
|
||||
if [[ "$1" =~ $RE_NODE ]]; then echo "node --test"; return 0; fi
|
||||
if [[ "$1" =~ $RE_GATE ]]; then echo "gate"; return 0; fi
|
||||
if [[ "$1" == *mutant* ]]; then echo "mutant"; return 0; fi
|
||||
if [[ "$1" =~ $RE_TEST ]]; then echo "test script"; return 0; fi
|
||||
if [[ "$1" =~ $RE_COMPOSE ]]; then echo "docker compose"; return 0; fi
|
||||
return 1
|
||||
}
|
||||
match() { # $1: the command line, NULs as spaces. Prints the rule that matched.
|
||||
local c=" $1 "
|
||||
if [[ "$c" =~ $RE_SNAPSHOT ]]; then sourced "${c:${#BASH_REMATCH[0]}}"; return; fi
|
||||
if [[ "$c" =~ $RE_NODE ]]; then echo "node --test"; return 0; fi
|
||||
if [[ "$c" =~ $RE_GATE ]]; then echo "gate"; return 0; fi
|
||||
if [[ "$c" == *mutant* ]]; then echo "mutant"; return 0; fi
|
||||
if [[ "$c" =~ $RE_TEST ]]; then echo "test script"; return 0; fi
|
||||
if [[ "$c" =~ $RE_COMPOSE ]]; then echo "docker compose"; return 0; fi
|
||||
return 1
|
||||
# The characters the flat form changes, other than a single space. A command
|
||||
# line with none of them and no two blanks in a row is its own flat form.
|
||||
SPECIAL=$'[\"\'\\\\;&|(){}<>`\t\n\r\v\f]'
|
||||
# flat TEXT: prints F, then TEXT in the flat form. awk, not bash: a bash
|
||||
# ${s//x/y} costs time per match, and a 2 MB line full of quotes took minutes.
|
||||
flat() {
|
||||
printf '%s' "$1" | awk '
|
||||
BEGIN { RS = "\001" }
|
||||
{ t = (NR > 1) ? t RS $0 : $0 }
|
||||
END { gsub(/["\047\\]/, "", t); gsub(/[[:space:];&|(){}<>`]+/, " ", t); printf "F%s", t }
|
||||
'
|
||||
}
|
||||
match() { # $1: the command line, NULs as spaces. Prints the rule; 1 none, 2 unclassified.
|
||||
local n=$1
|
||||
if [[ "$n" == *$SPECIAL* || "$n" == *" "* ]]; then
|
||||
n=$(flat "$1") && [[ "$n" == F* ]] || return 2
|
||||
n=${n#F}
|
||||
fi
|
||||
if [[ " $n " =~ $RE_SNAPSHOT ]]; then sourced "$1"; return; fi
|
||||
rules " $n " || rules " $1 "
|
||||
}
|
||||
|
||||
seen=0
|
||||
@@ -119,13 +213,19 @@ for d in "$PROC"/[0-9]*; do
|
||||
fi
|
||||
cmd=${cmd% }
|
||||
[ -n "$cmd" ] || continue
|
||||
rule=$(match "$cmd") || continue
|
||||
rule=$(match "$cmd"); rc=$?
|
||||
[ "$rc" != 1 ] || continue
|
||||
if [ "$rc" != 0 ]; then
|
||||
echo "slot-busy: error: cannot classify the command line of $d" >&2
|
||||
errors=$((errors + 1))
|
||||
continue
|
||||
fi
|
||||
cwd=$(readlink "$d/cwd" 2>/dev/null) || cwd="?"
|
||||
printf 'BUSY %s %s cwd=%s %s\n' "$pid" "$rule" "$cwd" "${cmd:0:200}"
|
||||
busy=1
|
||||
done
|
||||
[ "$seen" -ge 1 ] || { echo "slot-busy: error: no processes under $PROC" >&2; exit 2; }
|
||||
[ "$errors" = 0 ] || { echo "slot-busy: error: $errors process(es) could not be read, so the host isn't known to be idle" >&2; exit 2; }
|
||||
[ "$errors" = 0 ] || { echo "slot-busy: error: $errors process(es) could not be read or classified, so the host isn't known to be idle" >&2; exit 2; }
|
||||
if [ "$busy" = 1 ]; then exit 1; fi
|
||||
echo "CLEAR"
|
||||
exit 0
|
||||
+147
-4
@@ -273,7 +273,7 @@ rule() { # $1: expected rule, rest: argv
|
||||
local want=$1; shift
|
||||
rm -rf "${P:?}/4194420"; fakeproc 4194420 /seat/b "$@"
|
||||
out=$(busyrun); rc=$?
|
||||
check "busy on: $* ($want)" "$([ "$rc" = 1 ] && has "$out" "BUSY 4194420 $want "; echo $?)"
|
||||
check "busy on: ${*//$'\n'/\\n} ($want)" "$([ "$rc" = 1 ] && has "$out" "BUSY 4194420 $want "; echo $?)"
|
||||
}
|
||||
rule "node --test" /usr/bin/node --experimental-strip-types --test a.test.ts
|
||||
rule "gate" bash /mnt/scratch/sage-gate.sh 253
|
||||
@@ -297,6 +297,58 @@ rule "sourced test script" /bin/bash -c "$SNAP && source \"scripts/test-x.sh\" a
|
||||
rule "sourced test script" /bin/bash -c "$SNAP && source .venv/bin/activate && . scripts/test-x.sh"
|
||||
rule "sourced gate" /usr/bin/bash -c "source /home/u/.claude/shell-snapshots/snapshot-bash-2-ab.sh 2>/dev/null || true && eval '. ./gate.sh 253' && pwd -P >| /home/u/cwd"
|
||||
rule "sourced mutant" /bin/bash -c "$SNAP && cd /seat && eval 'source agents/x/work/queue-1/mutants.sh M3'"
|
||||
# R5 (N2): every word up to the next operator counts, not only the first; and
|
||||
# quoted, escaped and nested forms read the way the shell reads them.
|
||||
rule "sourced test script" /bin/bash -c "$SNAP && source 2>/dev/null /seat/scripts/test-x.sh"
|
||||
rule "sourced test script" /bin/bash -c "$SNAP && source \$(cd scripts && pwd)/test-x.sh"
|
||||
rule "sourced test script" /bin/bash -c "$SNAP && source \`cd scripts; pwd\`/test-x.sh"
|
||||
rule "sourced test script" /bin/bash -c "$SNAP && source \"/seat/a;b (c)/scripts/test-x.sh\""
|
||||
rule "sourced test script" /bin/bash -c "$SNAP && eval 'source \"/seat/a;b/scripts/test-x.sh\"'"
|
||||
rule "sourced test script" /bin/bash -c "$SNAP && eval \"source \\\"/seat/dir with spaces/scripts/test-x.sh\\\"\""
|
||||
rule "sourced test script" /usr/bin/bash -c "$SNAP 2>/dev/null || true && eval 'source '\\''scripts/test-x.sh'\\''' && pwd -P >| /home/u/cwd"
|
||||
rule "sourced test script" /bin/bash -c "$SNAP && eval \$'source scripts/test-x.sh'"
|
||||
rule "sourced test script" /bin/bash -c "$SNAP && cat <<EOF"$'\n'"don't"$'\n'"EOF"$'\n'"source \"it's dir/scripts/test-x.sh\""
|
||||
# An unclosed quote runs to the end of the text and is read again there.
|
||||
rule "sourced test script" /bin/bash -c "$SNAP && echo \"it's && source /seat/scripts/test-x.sh"
|
||||
# The general rules read the flat form too, and still read the raw one.
|
||||
rule "test script" /bin/bash -c "bash scripts/test-x.sh;echo done"
|
||||
rule "gate" bash "/seat/gate;1.sh"
|
||||
# Two blanks in a row make one in the flat form, with no other character to
|
||||
# flatten.
|
||||
rule "docker compose" /bin/sh -c "docker compose -p x up"
|
||||
rule "docker compose" /bin/sh -c $'docker\tcompose -p x up'
|
||||
rm -rf "${P:?}/4194420"
|
||||
idle() { # argv: a process that must leave the check CLEAR
|
||||
rm -rf "${P:?}/4194421"; fakeproc 4194421 /seat/c "$@"
|
||||
out=$(busyrun); rc=$?
|
||||
check "not busy: ${*//$'\n'/\\n}" "$([ "$rc" = 0 ] && [ "$out" = CLEAR ]; echo $?)"
|
||||
rm -rf "${P:?}/4194421"
|
||||
}
|
||||
# The harness's own snapshot path is skipped, quoted or not, whatever its name.
|
||||
idle /bin/bash -c "source /home/u/.claude/shell-snapshots/snapshot-bash-3-gate.sh && echo hi"
|
||||
idle /bin/bash -c "source \"/home/u/.claude/shell-snapshots/snapshot-bash-3-gate.sh\" && echo hi"
|
||||
idle /bin/bash -c "source /home/u/.claude/shell-snapshots/snapshot-bash-3-gate.sh && . /home/u/.claude/shell-snapshots/snapshot-bash-3-gate.sh && echo hi"
|
||||
# Locale: a byte that isn't UTF-8 must not end a name under a UTF-8 locale.
|
||||
rm -rf "${P:?}/4194420"; fakeproc 4194420 /seat/b bash $'scripts/test-\xffx.sh'
|
||||
out=$(LC_ALL=C.UTF-8 bash "$BUSY" --proc-root "$P" 2>&1); rc=$?
|
||||
check "under LC_ALL=C.UTF-8, a test script name holding the byte 0xff: exit 1" \
|
||||
"$([ "$rc" = 1 ] && has "$out" "BUSY 4194420 test script "; echo $?)"
|
||||
# Quoting that nests deeper than the walk reads is an error, never CLEAR. Each
|
||||
# level escapes backslashes and double quotes and wraps the text in quotes.
|
||||
nest() { # $1: levels. Sets w.
|
||||
w='source scripts/test-x.sh'
|
||||
for _ in $(seq "$1"); do w=$(printf '%s' "$w" | sed 's/[\\"]/\\&/g'); w="\"$w\""; done
|
||||
}
|
||||
nest 16
|
||||
rm -rf "${P:?}/4194420"; fakeproc 4194420 /seat/b /bin/bash -c "$SNAP && eval $w"
|
||||
out=$(busyrun); rc=$?
|
||||
check "a sourced test under 16 escaped quoting levels (${#w} bytes): exit 1, sourced test script" \
|
||||
"$([ "$rc" = 1 ] && has "$out" "BUSY 4194420 sourced test script "; echo $?)"
|
||||
nest 17
|
||||
rm -rf "${P:?}/4194420"; fakeproc 4194420 /seat/b /bin/bash -c "$SNAP && eval $w"
|
||||
out=$(busyrun); rc=$?
|
||||
check "quoting 17 escaped levels deep (${#w} bytes): exit 2, cannot classify, no CLEAR" \
|
||||
"$([ "$rc" = 2 ] && has "$out" "cannot classify the command line of $P/4194420" && ! has "$out" CLEAR; echo $?)"
|
||||
rm -rf "${P:?}/4194420"
|
||||
|
||||
echo "== slot-busy, read errors =="
|
||||
@@ -362,9 +414,9 @@ fi
|
||||
exec $(command -v awk) "\$@"
|
||||
EOF
|
||||
chmod +x "$SANDBOX/awkn/awk"
|
||||
awkn() { # $1: fail, empty or exited. Runs the check on the idle fake /proc.
|
||||
echo "$1" > "$SANDBOX/awkn/mode"; rm -f "$SANDBOX/awkn/n"
|
||||
out=$(PATH="$SANDBOX/awkn:$PATH" bash "$BUSY" --proc-root "$P" 2>&1); rc=$?
|
||||
awkn() { # $1: fail, empty or exited; rest: NAME=VALUE for the check's environment.
|
||||
echo "$1" > "$SANDBOX/awkn/mode"; rm -f "$SANDBOX/awkn/n"; shift
|
||||
out=$(env "$@" PATH="$SANDBOX/awkn:$PATH" bash "$BUSY" --proc-root "$P" 2>&1); rc=$?
|
||||
calls=$(cat "$SANDBOX/awkn/n" 2>/dev/null || echo 0)
|
||||
}
|
||||
awkn fail
|
||||
@@ -377,6 +429,48 @@ awkn exited
|
||||
check "an ancestor that has exited (its pid gone from /proc) ends the walk: exit 0 and CLEAR" \
|
||||
"$([ "$calls" -ge 3 ] && [ "$rc" = 0 ] && [ "$out" = CLEAR ]; echo $?)"
|
||||
|
||||
# R5 (N1): the gone check reads the proc root's listing whatever the caller's
|
||||
# QUOTING_STYLE is. A quoted name would read as a vanished pid, and both
|
||||
# failed reads above would turn into CLEAR.
|
||||
for qs in shell-always c escape shell-escape-always locale; do
|
||||
out=$(QUOTING_STYLE=$qs PATH="$SANDBOX/failtr:$PATH" bash "$BUSY" --proc-root "$P" 2>&1); rc=$?
|
||||
check "QUOTING_STYLE=$qs: a failing cmdline read with the pid still listed: exit 2, no CLEAR" \
|
||||
"$([ "$rc" = 2 ] && has "$out" "cannot read $P/" && ! has "$out" CLEAR; echo $?)"
|
||||
awkn fail QUOTING_STYLE=$qs
|
||||
check "QUOTING_STYLE=$qs: a failing ancestor status read with the pid still listed: exit 2, no CLEAR" \
|
||||
"$([ "$calls" -ge 2 ] && [ "$rc" = 2 ] && has "$out" "cannot read the status of ancestor" && ! has "$out" CLEAR; echo $?)"
|
||||
done
|
||||
|
||||
# The flat form and the walk over a wrapper's text are awk programs. If
|
||||
# either awk fails, or prints nothing, the command line can't be classified:
|
||||
# exit 2, never CLEAR. This awk fails or goes silent for one program only
|
||||
# (the walk has `function walk`, the flat form `gsub`).
|
||||
mkdir -p "$SANDBOX/lexawk"
|
||||
cat > "$SANDBOX/lexawk/awk" <<EOF
|
||||
#!/bin/sh
|
||||
m=\$(cat "$SANDBOX/lexawk/mode")
|
||||
case "\$*" in
|
||||
*"function walk"*) case \$m in walk-fail) cat > /dev/null; exit 1 ;; walk-empty) cat > /dev/null; exit 0 ;; esac ;;
|
||||
*gsub*) case \$m in flat-fail) cat > /dev/null; exit 1 ;; flat-empty) cat > /dev/null; exit 0 ;; esac ;;
|
||||
esac
|
||||
exec $(command -v awk) "\$@"
|
||||
EOF
|
||||
chmod +x "$SANDBOX/lexawk/awk"
|
||||
lexawk() { # $1: mode, $2: label, rest: argv of a process that is BUSY
|
||||
local mode=$1 label=$2; shift 2
|
||||
fakeproc 4194460 /seat/w "$@"
|
||||
echo "$mode" > "$SANDBOX/lexawk/mode"
|
||||
out=$(PATH="$SANDBOX/lexawk:$PATH" bash "$BUSY" --proc-root "$P" 2>&1); rc=$?
|
||||
check "$label: exit 2, cannot classify, no CLEAR" \
|
||||
"$([ "$rc" = 2 ] && has "$out" "cannot classify the command line of $P/4194460" && ! has "$out" CLEAR; echo $?)"
|
||||
rm -rf "${P:?}/4194460"
|
||||
}
|
||||
lexawk walk-fail "the wrapper walk's awk exits 1" /bin/bash -c "$SNAP && source /seat/scripts/test-x.sh"
|
||||
lexawk walk-empty "the wrapper walk's awk prints nothing" /bin/bash -c "$SNAP && source /seat/scripts/test-x.sh"
|
||||
# Only the flat form names this one (`.sh;` ends no rule's match as it is).
|
||||
lexawk flat-fail "the flat form's awk exits 1" /bin/bash -c "bash scripts/test-x.sh;echo done"
|
||||
lexawk flat-empty "the flat form's awk prints nothing" /bin/bash -c "bash scripts/test-x.sh;echo done"
|
||||
|
||||
# A lookup that is denied isn't a process that vanished. This tr makes the
|
||||
# proc root unreadable once the pid directory is listed, then fails the read;
|
||||
# the pid directory and its node --test cmdline stay where they are.
|
||||
@@ -497,6 +591,55 @@ check "the wrapper itself has no BUSY line" "$(! printf '%s\n' "$out" | grep -q
|
||||
for _ in $(seq 50); do kill -0 "$WRAP" 2>/dev/null || break; sleep 0.1; done
|
||||
if ! kill -0 "$WRAP" 2>/dev/null; then wait "$WRAP" 2>/dev/null; KIDS=""; fi
|
||||
|
||||
# R5 (N2): the 13 shapes from the R4 review's Check 2, each a live wrapper
|
||||
# that runs the sourced file in its own shell, with no child. S is the
|
||||
# snapshot, SF a snapshot that holds `false` (so `||` runs the test), T the
|
||||
# test. The sourced file blocks as test-x.sh does above.
|
||||
S="$SANDBOX/shell-snapshots/snapshot-x.sh"
|
||||
SF="$SANDBOX/shell-snapshots/snapshot-f.sh"
|
||||
T="$SANDBOX/scripts/test-x.sh"
|
||||
echo false > "$SF"
|
||||
mkdir -p "$SANDBOX/wrap/scripts" "$SANDBOX/dir with spaces/scripts"
|
||||
for f in wrap/scripts/test-x.sh wrap/gate.sh wrap/mutants.sh "dir with spaces/scripts/test-x.sh"; do
|
||||
cp "$T" "$SANDBOX/$f"
|
||||
done
|
||||
live() { # $1: shape number, $2: expected rule or CLEAR, $3: label, $4: the wrapper's text
|
||||
local n=$1 want=$2 label=$3 lp
|
||||
rm -f "$SANDBOX/wrap/ready"
|
||||
(cd "$SANDBOX/wrap" && exec bash -c "$4") &
|
||||
WRAP=$!; KIDS="$KIDS $WRAP"
|
||||
for _ in $(seq 50); do [ -e "$SANDBOX/wrap/ready" ] && break; sleep 0.1; done
|
||||
kids=$(cat "/proc/$WRAP/task/$WRAP/children") && kids="none$kids"
|
||||
lp="$SANDBOX/lproc-s$n"; mkdir -p "$lp"; ln -s "/proc/$WRAP" "$lp/$WRAP"
|
||||
out=$(bash "$BUSY" --proc-root "$lp" 2>&1); rc=$?
|
||||
if [ "$want" = CLEAR ]; then
|
||||
check "live shape $n, $label, no child: exit 0 and CLEAR" \
|
||||
"$([ -e "$SANDBOX/wrap/ready" ] && [ "$kids" = none ] && [ "$rc" = 0 ] && [ "$out" = CLEAR ]; echo $?)"
|
||||
else
|
||||
check "live shape $n, $label, no child: exit 1, $want" \
|
||||
"$([ -e "$SANDBOX/wrap/ready" ] && [ "$kids" = none ] && [ "$rc" = 1 ] && ! has "$out" CLEAR \
|
||||
&& printf '%s\n' "$out" | grep -q "^BUSY $WRAP $want cwd=$SANDBOX/wrap "; echo $?)"
|
||||
fi
|
||||
kill "$WRAP" 2>/dev/null; wait "$WRAP" 2>/dev/null; KIDS=""
|
||||
}
|
||||
live 1 "sourced test script" 'source S && source T' "source $S && source $T"
|
||||
live 2 "sourced test script" "source S && .<TAB><TAB>'scripts/test-x.sh'" "source $S && ."$'\t\t'"'scripts/test-x.sh'"
|
||||
live 3 "sourced test script" 'source S ; source T ; :' "source $S ; source $T ; :"
|
||||
live 4 "sourced test script" 'source SF || source T' "source $SF || source $T"
|
||||
live 5 "sourced gate" 'source S && . "./gate.sh"' "source $S && . \"./gate.sh\""
|
||||
live 6 "sourced mutant" 'source S && source ./mutants.sh' "source $S && source ./mutants.sh"
|
||||
live 7 "sourced test script" 'source S && source ".../dir with spaces/scripts/test-x.sh"' \
|
||||
"source $S && source \"$SANDBOX/dir with spaces/scripts/test-x.sh\""
|
||||
live 8 "sourced test script" 'source S && "source" T' "source $S && \"source\" $T"
|
||||
live 9 "sourced test script" 'source S;source T;:' "source $S;source $T;:"
|
||||
live 10 "sourced test script" 'source S;. T;:' "source $S;. $T;:"
|
||||
live 11 "sourced test script" 'source "S" && source "T"' "source \"$S\" && source \"$T\""
|
||||
live 12 "sourced test script" 'source<TAB>S && source "T"' "source"$'\t'"$S && source \"$T\""
|
||||
live 13 "sourced test script" 'source<SPACE><SPACE>S && source "T"' "source $S && source \"$T\""
|
||||
# A plain wrapper that blocks the same way but sources no test stays CLEAR.
|
||||
live 14 CLEAR 'a plain wrapper, the same blocking read inline' \
|
||||
"source $S && exec 3<>\"$SANDBOX/wrap/fifo\" && : > \"$SANDBOX/wrap/ready\" && read -t 60 -u 3 _"
|
||||
|
||||
echo "---"
|
||||
echo "git-guard selftests: $PASS passed, $FAIL failed"
|
||||
[ "$FAIL" -eq 0 ]
|
||||
Reference in new issue
Block a user