Files
sausage/userland/brush/source/brush-shell/tests/cases/compat/signals.yaml
T
2026-11-11 22:04:16 +00:00

612 lines
23 KiB
YAML

name: "Signals"
# Signals sent to the shell itself, how it acts on them, and the dispositions the programs
# it starts inherit. Cases signal the shell from a program it runs (`sh -c 'kill … $PPID'`)
# or with `kill … $$`, so that each arrives at a known point.
incompatible_platforms: ["windows", "wasi"]
cases:
#
# When traps run
#
- name: "trap runs once for signals that arrive while a program runs"
known_failure: true # TODO(signals): signals aren't delivered to the shell
stdin: |
trap 'echo got' USR1
sh -c 'kill -USR1 $PPID; kill -USR1 $PPID; kill -USR1 $PPID; echo child-done'
echo done
- name: "trap runs for each signal that arrives between commands"
known_failure: true # TODO(signals): signals aren't delivered to the shell
stdin: |
trap 'echo got' USR1
kill -USR1 $$
kill -USR1 $$
echo done
- name: "trap runs again for its signal arriving during the trap"
known_failure: true # TODO(signals): signals aren't delivered to the shell
stdin: |
n=0
trap 'n=$((n + 1)); echo "in $n"; [ "$n" -lt 3 ] && kill -USR1 $$; echo "out $n"' USR1
kill -USR1 $$
echo done
# By job spec: brush's `$!` doesn't name a job that runs shell code (tracked in
# background_jobs.yaml). No `wait` follows the `kill`: under load, bash 5.3's can loop,
# reporting a job's process as not its child.
# brush nests a signal's trap 8 deep, and runs the rest in turn at the eighth level. (One
# line, so that the shell doesn't reach a safe point before reading the next.)
- name: "trap runs again for its signal, nested many deep"
known_failure: true # TODO(signals): signals aren't delivered to the shell
stdin: |
n=0; trap 'n=$((n + 1)); [ $n -lt 20 ] && kill -USR1 $$' USR1; kill -USR1 $$; echo "n=$n"
- name: "trapped signal interrupts wait"
known_failure: true # TODO(signals): signals aren't delivered to the shell
stdin: |
trap 'echo got' USR1
sleep 3 &
sh -c 'sleep 0.3; kill -USR1 $PPID' &
wait %1
echo "wait=$?"
kill %1
echo done
# The process substitution signals the shell while `read` waits for its line.
- name: "trapped signal doesn't interrupt read"
known_failure: true # TODO(signals): signals aren't delivered to the shell
stdin: |
trap 'echo got' USR1
exec 3< <(sleep 0.5; kill -USR1 $$; sleep 0.3; echo line)
read -r -u 3 x
echo "read=$? x=$x"
- name: "trapped signal interrupts read in POSIX mode"
known_failure: true # TODO(signals): signals aren't delivered to the shell
stdin: |
set -o posix
trap 'echo got' USR1
exec 3< <(sleep 0.5; kill -USR1 $$; sleep 0.3; echo line)
read -r -u 3 x
echo "read=$? x=$x"
#
# Default actions, non-interactive
#
- name: "SIGTERM ends a non-interactive shell, after its EXIT trap"
known_failure: true # TODO(signals): signals aren't delivered to the shell
stdin: |
trap 'echo exit-trap' EXIT
kill -TERM $$
echo after
- name: "SIGINT ends a non-interactive shell, after its EXIT trap"
known_failure: true # TODO(signals): signals aren't delivered to the shell
stdin: |
trap 'echo exit-trap' EXIT
kill -INT $$
echo after
- name: "SIGHUP ends a non-interactive shell, after its EXIT trap"
known_failure: true # TODO(signals): signals aren't delivered to the shell
stdin: |
trap 'echo exit-trap' EXIT
kill -HUP $$
echo after
- name: "SIGUSR1 ends a non-interactive shell, after its EXIT trap"
known_failure: true # TODO(signals): signals aren't delivered to the shell
stdin: |
trap 'echo exit-trap' EXIT
kill -USR1 $$
echo after
# Sent from outside, through the kernel; it doesn't wait for the program to end.
- name: "SIGTERM ends a shell waiting for a program at once"
known_failure: true # TODO(signals): signals aren't delivered to the shell
stdin: |
trap '[ -e late ] && echo late || echo early' EXIT
sh -c 'kill -TERM $PPID; sleep 0.5; : > late'
echo after
- name: "SIGTERM ends a shell blocked in read"
known_failure: true # TODO(signals): signals aren't delivered to the shell
stdin: |
trap 'echo "x=[$x]"' EXIT
exec 3< <(sleep 1; echo line)
sh -c 'sleep 0.3; kill -TERM $PPID' &
read -r -u 3 x
echo after
# Each line is longer than a pipe holds: `head` must read all of the first before it exits,
# and the second can't be written without a reader, so it fails however the two are timed.
- name: "SIGPIPE from a builtin's write ends a pipeline stage"
known_failure: true # TODO(signals): EPIPE doesn't end the shell, as SIGPIPE ends bash
stdin: |
i=0; while [ "$i" -lt 3 ]; do printf '%065536d\n' 0; i=$((i + 1)); done | head -n 1 > /dev/null
echo "status=${PIPESTATUS[0]}"
- name: "SIGPIPE from a builtin's write ends a loop writing to a pipeline"
skip: true # TODO(signals): hangs: the loop's failed writes don't end it
stdin: |
while :; do echo y; done | head -n 1
echo "status=${PIPESTATUS[*]}"
# The write errors go to /dev/null: brush words them differently. Each line is longer than
# what `head` and the pipe can hold between them, so that only the first write succeeds,
# however soon `head` exits.
- name: "SIGPIPE from a builtin's write runs the stage's PIPE trap"
known_failure: true # TODO(signals): signals aren't delivered to the shell
stdin: |
{ trap 'echo t >> log' PIPE; i=0; while [ "$i" -lt 3 ]; do printf '%0262144d\n' 0; echo "printf=$?" >> log; i=$((i + 1)); done; } 2>/dev/null | head -n 1 > /dev/null
echo "status=${PIPESTATUS[0]}"
cat log; rm log
# As above.
- name: "trap '' PIPE leaves a builtin's write failing"
known_failure: true # TODO(signals): signals aren't delivered to the shell
stdin: |
trap '' PIPE
{ i=0; while [ "$i" -lt 3 ]; do printf '%0262144d\n' 0; echo "printf=$?" >> log; i=$((i + 1)); done; } 2>/dev/null | head -n 1 > /dev/null
echo "status=${PIPESTATUS[0]}"
cat log; rm log
- name: "trap '' ignores SIGTERM"
known_failure: true # TODO(signals): signals aren't delivered to the shell
stdin: |
trap '' TERM
kill -TERM $$
echo after
# The inner shell starts with SIGUSR1 ignored, as programs of a shell that ignores it do.
- name: "a signal ignored when a non-interactive shell started can't be trapped"
known_failure: true # TODO(signals): signals aren't delivered to the shell
stdin: |
trap '' USR1
"$BASH" -c 'trap "echo trapped" USR1; trap -p USR1; trap - USR1; kill -USR1 $$; echo after'
- name: "SIGTERM ends the shell again after its trap is reset"
known_failure: true # TODO(signals): signals aren't delivered to the shell
stdin: |
trap 'echo t' TERM
trap - TERM
trap 'echo exit-trap' EXIT
kill -TERM $$
echo after
#
# A program that handles SIGINT
#
# The program signals the shell only once it has surely begun to wait for it: before then,
# a non-interactive bash, which catches SIGINT only while it waits, would die of it.
- name: "SIGINT a program handled doesn't end the shell"
skip: true # TODO(signals): doesn't pass reliably; the shell sometimes gets the SIGINT before it handles it
stdin: |
sh -c 'trap "" INT; sleep 0.1; kill -INT $PPID; echo child-done'
echo "after=$?"
- name: "SIGINT a program run by command handled doesn't end the shell"
stdin: |
command sh -c 'trap "" INT; sleep 0.1; kill -INT $PPID; echo child-done'
echo "after=$?"
- name: "SIGINT a program died of ends the shell"
known_failure: true # TODO(signals): signals aren't delivered to the shell
stdin: |
trap 'echo exit-trap' EXIT
sh -c 'kill -INT $PPID; kill -INT $$'
echo "after=$?"
- name: "INT trap runs though the program handled SIGINT"
known_failure: true # TODO(signals): signals aren't delivered to the shell
stdin: |
trap 'echo int-trap' INT
sh -c 'trap "" INT; sleep 0.1; kill -INT $PPID; echo child-done'
echo "after=$?"
- name: "INT trap runs when the program died of SIGINT"
known_failure: true # TODO(signals): signals aren't delivered to the shell
stdin: |
trap 'echo int-trap' INT
sh -c 'kill -INT $PPID; kill -INT $$'
echo "after=$?"
- name: "Programs inherit an ignored SIGINT after the shell first waits for one"
skip: true # TODO(signals): flaky; tokio's SIGINT listener replaces the inherited ignore, but a wait for a program sets it up only once its select! polls it (in random order), so the ignore survives when the first program has already exited
# The shell starts another one ignoring SIGINT; that one waits for one program, and then
# another sends itself SIGINT.
stdin: |
sh -c 'trap "" INT; exec "$0" -c "$1"' "$BASH" 'sh -c "exit 0"; sh -c "kill -INT \$\$; echo survived"'
- name: "SIGINT during a command substitution ends the shell"
stdin: |
x=$(kill -INT $$; echo inner)
echo "x=$x"
# The shell traps the signal, so the outcome doesn't depend on whether the signal arrives
# before the shell begins to wait for the subshell or while it waits: either way the trap
# runs once the subshell is done. (Untrapped, a non-interactive shell dies of a SIGINT
# that arrives before it begins to wait.)
- name: "signal sent to the shell isn't the subshell's"
known_failure: true # TODO(signals): a subshell shares its parent's signals
stdin: |
trap 'echo T' INT
( trap 'echo sub-int' INT; kill -INT $$; echo sub-after )
echo "after=$?"
- name: "a subshell resets the signal traps it inherits"
known_failure: true # TODO(signals): a subshell's own process isn't addressable apart from its parent's
ignore_stderr: true # bash also reports the subshell's death; that's job reporting
stdin: |
trap 'echo T' USR1
( kill -USR1 $BASHPID; echo sub-alive )
echo "after=$?"
- name: "a subshell's own trap handles a signal sent to it"
known_failure: true # TODO(signals): signals aren't delivered to the shell
stdin: |
( trap 'echo sub' USR1; kill -USR1 $BASHPID )
echo "after=$?"
# Another program sends it, so it reaches the shell through the kernel.
- name: "a signal sent to the shell's process isn't its command substitution's"
known_failure: true # TODO(signals): signals aren't delivered to the shell
stdin: |
trap 'echo trapped' USR2
x=$(/bin/kill -USR2 $$; echo sub)
echo "x=$x"
- name: "a trapped signal sent to the shell waits for its subshell"
known_failure: true # TODO(signals): signals aren't delivered to the shell
stdin: |
trap 'echo T' TERM
( kill -TERM $$; echo sub-after )
echo "after=$?"
# bash dies at once, and its subshell goes on without it.
- name: "a subshell goes on when the shell dies of a signal sent to it"
known_failure: true # TODO(signals): signals aren't delivered to the shell
stdin: |
( kill -TERM $$; echo sub-after )
echo "after=$?"
# The inner subshell writes to a file, shown once it's there: bash's outer subshell dies at
# once, and the shell's report would otherwise race the orphaned inner one's output.
- name: "a signal sent to a subshell waits for the subshell it runs"
known_failure: true # TODO(signals): signals aren't delivered to the shell
ignore_stderr: true # bash also reports the subshell's death; that's job reporting
stdin: |
( x=$BASHPID; ( kill -TERM $x; echo inner-after > inner.txt ); echo mid-after )
status=$?
until [ -s inner.txt ]; do :; done
cat inner.txt; rm inner.txt
echo "after=$status"
# bash ignores SIGQUIT, and an interactive bash SIGTERM; its subshells don't.
- name: "SIGQUIT ends a subshell, though the shell ignores it"
known_failure: true # TODO(signals): signals aren't delivered to the shell
ignore_stderr: true # bash also reports the subshell's death; that's job reporting
stdin: |
( kill -QUIT $BASHPID; echo not-reached )
echo "subshell=$?"
x=$(kill -QUIT $BASHPID; echo not-reached)
echo "cmdsubst=$? x=[$x]"
{ kill -QUIT $BASHPID; echo not-reached; } | cat
echo "stage=${PIPESTATUS[*]}"
trap '' QUIT
( kill -QUIT $BASHPID; echo ignored-reached )
echo "ignored=$?"
# A stage runs on a thread of its own, and the kernel may run the process's handler on
# another, after `kill` returns.
- name: "kill 0 in a pipeline stage ends the stage at once"
known_failure: true # TODO(signals): signals aren't delivered to the shell
ignore_stderr: true # bash may report the death of `cat`, which the signal reaches too, or not
stdin: |
trap 'echo T' USR1
f() { kill -USR1 0; echo reached > reached.txt; }
f | cat
echo "stage=${PIPESTATUS[0]}"
[ -e reached.txt ] && echo "reached"
- name: "a program's signal to the shell's group reaches its subshells"
known_failure: true # Deliberate (docs/explanation/signals.md, "Known differences"): it looks like a signal to the shell's process alone, which reaches no subshell
ignore_stderr: true # bash also reports the subshell's death; that's job reporting
args:
- "-i"
- "-c"
- |
f() { sh -c 'kill 0'; echo not-reached; }
f | cat
echo "status=${PIPESTATUS[*]}"
- name: "SIGTERM ends an interactive shell's subshell, though the shell ignores it"
known_failure: true # TODO(signals): signals aren't delivered to the shell
ignore_stderr: true # bash also reports the subshell's death; that's job reporting
args:
- "-i"
- "-c"
- |
( kill -TERM $BASHPID; echo not-reached )
echo "subshell=$?"
{ kill -TERM $BASHPID; echo not-reached; } | cat
echo "stage=${PIPESTATUS[*]}"
kill -TERM $$
echo "shell survives"
- name: "a subshell has a BASHPID of its own"
known_failure: true # TODO(signals): signals aren't delivered to the shell
stdin: |
[ "$BASHPID" = "$$" ] && echo same
( [ "$BASHPID" != "$$" ] && echo differ )
x=$(echo "$BASHPID")
[ "$x" != "$$" ] && echo differ
echo "$BASHPID" | { read -r y; [ "$y" != "$$" ] && echo differ; }
- name: "a foreground program's death by a signal is reported"
known_failure: true # TODO(jobs): bash reports, on stderr, a foreground program or subshell that died of a signal other than SIGINT or SIGPIPE
stdin: |
sh -c 'kill -TERM $$'
echo after
# Read with builtins only: a program would be one the shell waits for, which it catches
# SIGINT for.
- name: "a signal is caught only while the shell needs it"
incompatible_platforms: ["macos"]
known_failure: true # Deliberate (docs/explanation/signals.md, "Known differences"): brush's process collects the signals it catches, blocked, where bash's installs handlers, so /proc shows them blocked, not caught
stdin: |
cgt() {
local k v
while read -r k v; do
[ "$k" = SigCgt: ] && { local m=$((16#$v)); echo "$1 $2: $(( (m >> ($(kill -l $2) - 1)) & 1 ))"; }
done < /proc/$$/status
}
cgt start INT
/bin/true
cgt after-program INT
trap 'echo x' USR1
cgt trapped USR1
trap - USR1
cgt reset USR1
trap 'echo again' USR1
/bin/kill -USR1 $$
cgt retrapped USR1
# Another program sends it to the shell alone, while it waits for the pipeline's last stage.
- name: "a signal sent to the shell alone leaves its pipeline stages running"
known_failure: true # TODO(signals): signals aren't delivered to the shell
stdin: |
trap 'echo T' USR2
( sleep 0.2; /bin/kill -USR2 $$ ) &
echo in | { /bin/sleep 0.6; echo stage-done; }
echo "status=$?"
wait
- name: "a subshell keeps the signals its parent ignores"
known_failure: true # TODO(signals): signals aren't delivered to the shell
stdin: |
trap '' USR1
( kill -USR1 $BASHPID; echo sub-alive )
echo "after=$?"
- name: "kill signals a background job running shell code"
known_failure: true # TODO(signals): a job running shell code can't be signalled
stdin: |
# Bounded, so that a shell that can't kill it doesn't wait forever.
n=0; while [ "$n" -lt 20 ]; do sleep 0.1; n=$((n + 1)); done &
kill %1
wait %1
echo "status=$?"
#
# SIGCHLD traps
#
- name: "CHLD trap runs for each program that ends"
known_failure: true # TODO(signals): signals aren't delivered to the shell
stdin: |
trap 'echo chld' CHLD
/bin/true
/bin/true
echo done
- name: "CHLD trap runs for each background job that ends"
known_failure: true # TODO(signals): signals aren't delivered to the shell
stdin: |
trap 'echo chld' CHLD
sleep 0.1 &
sleep 0.2 &
wait
echo done
- name: "CHLD trap runs for each background job that ends, with job control"
known_failure: true # TODO(signals): signals aren't delivered to the shell
stdin: |
set -m
trap 'echo chld' CHLD
sleep 0.1 &
sleep 0.2 &
wait
echo done
- name: "CHLD trap interrupts wait in POSIX mode"
known_failure: true # TODO(signals): signals aren't delivered to the shell
stdin: |
set -o posix
trap 'echo chld' CHLD
sleep 0.1 &
sleep 0.5 &
wait
echo "wait=$?"
- name: "CHLD trap runs for each subshell and command substitution"
known_failure: true # TODO(signals): signals aren't delivered to the shell
stdin: |
trap 'echo chld' CHLD
(true)
echo $(echo a) $(echo b)
( /bin/true; /bin/true )
true
echo done
#
# Dispositions programs inherit (read from /proc, so Linux only)
#
- name: "programs inherit the signals the shell ignores"
known_failure: true # TODO(signals): programs inherit dispositions from the process
incompatible_platforms: ["macos"]
stdin: |
show() {
local hex=${1#*:}; hex=${hex//[[:space:]]/}
local m=$((16#$hex)) out="" s n
for s in INT QUIT USR1 PIPE TSTP TTIN TTOU; do
n=$(kill -l "$s"); out+="$s=$(( (m >> (n - 1)) & 1 )) "
done
echo "$2: $out"
}
grep SigIgn /proc/self/status > fg.txt; show "$(<fg.txt)" foreground
(grep SigIgn /proc/self/status > sub.txt); show "$(<sub.txt)" subshell
show "$(grep SigIgn /proc/self/status)" cmdsubst
trap '' USR1
grep SigIgn /proc/self/status > tr.txt; show "$(<tr.txt)" "trap '' USR1"
- name: "programs of async lists ignore SIGINT and SIGQUIT"
known_failure: true # TODO(signals): programs inherit dispositions from the process
incompatible_platforms: ["macos"]
stdin: |
show() {
local hex=${1#*:}; hex=${hex//[[:space:]]/}
local m=$((16#$hex)) out="" s n
for s in INT QUIT USR1 PIPE TSTP TTIN TTOU; do
n=$(kill -l "$s"); out+="$s=$(( (m >> (n - 1)) & 1 )) "
done
echo "$2: $out"
}
grep SigIgn /proc/self/status > bg.txt & wait; show "$(<bg.txt)" async
show "$(cat <(grep SigIgn /proc/self/status))" procsubst
coproc { grep SigIgn /proc/self/status; }
read -r line <&"${COPROC[0]}"
wait
show "$line" coproc
- name: "programs of substitutions ignore stop signals with job control"
incompatible_platforms: ["macos"]
known_failure: true # TODO(signals): programs inherit dispositions from the process
stdin: |
set -m
show() {
local hex=${1#*:}; hex=${hex//[[:space:]]/}
local m=$((16#$hex)) out="" s n
for s in INT QUIT USR1 PIPE TSTP TTIN TTOU; do
n=$(kill -l "$s"); out+="$s=$(( (m >> (n - 1)) & 1 )) "
done
echo "$2: $out"
}
grep SigIgn /proc/self/status > bg.txt & wait; show "$(<bg.txt)" async
show "$(grep SigIgn /proc/self/status)" cmdsubst
show "$(cat <(grep SigIgn /proc/self/status))" procsubst
#
# Default actions, interactive
#
- name: "SIGINT breaks out of a loop to the prompt"
known_failure: true # TODO(signals): signals aren't delivered to the shell
pty: true
ignore_stdout: true # pty transcripts differ in echo and escape-sequence details
ignore_whitespace: true
# N.B. The markers are computed, so that the terminal's echo of the line can't match.
stdin: |
#expect-prompt
n=0; while :; do n=$((n + 1)); [ "$n" = 3 ] && kill -INT $$; done; echo "unreached"
#send:Enter
#expect-prompt
echo "N[$n] S[$?]"
#send:Enter
#expect:N[3] S[130]
#expect-prompt
#send:Ctrl+D
- name: "INT trap runs and the loop goes on"
known_failure: true # TODO(signals): signals aren't delivered to the shell
pty: true
ignore_stdout: true
ignore_whitespace: true
stdin: |
#expect-prompt
trap 'echo "T[$((2 + 2))]"' INT; n=0; while [ "$n" -lt 2 ]; do n=$((n + 1)); kill -INT $$; done; echo "D[$n]"
#send:Enter
#expect:T[4]
#expect:T[4]
#expect:D[2]
#expect-prompt
#send:Ctrl+D
- name: "Ctrl+C ends a read from the terminal"
skip: true # TODO(signals): hangs: read isn't interrupted
pty: true
ignore_stdout: true
ignore_whitespace: true
stdin: |
#expect-prompt
read -p "Q[$((1 + 1))] " x; echo "unreached"
#send:Enter
#expect:Q[2]
#send:Ctrl+C
#expect-prompt
echo "S[$?]"
#send:Enter
#expect:S[130]
#expect-prompt
#send:Ctrl+D
# The stage reads a FIFO it holds open for writing too, so that the read blocks for good.
- name: "Ctrl+C ends a pipeline stage blocked in a builtin"
skip: true # TODO(signals): hangs: the stage blocked in a builtin isn't ended by Ctrl+C
pty: true
ignore_stdout: true
ignore_whitespace: true
stdin: |
#expect-prompt
mkfifo p; { exec 3<>p; echo "R[$((1 + 1))]"; mapfile -t a <&3; } | cat; echo "unreached"
#send:Enter
#expect:R[2]
#send:Ctrl+C
#expect-prompt
echo "S[$?]"
#send:Enter
#expect:S[130]
#expect-prompt
#send:Ctrl+D
- name: "an interactive shell ignores SIGTERM and SIGQUIT"
known_failure: true # TODO(signals): signals aren't delivered to the shell
pty: true
ignore_stdout: true
ignore_whitespace: true
stdin: |
#expect-prompt
kill -TERM $$; kill -QUIT $$; echo "A[$((1 + 1))]"
#send:Enter
#expect:A[2]
#expect-prompt
#send:Ctrl+D
# With job control, Ctrl+C reaches only the program; the shell acts on its dying of it.
# The loop is bounded, and the shell ends after it, so that a shell that goes on with the
# loop fails at once rather than when the harness gives up waiting.
- name: "Ctrl+C ends a loop running a program"
known_failure: true # TODO(signals): signals aren't delivered to the shell
pty: true
ignore_stdout: true
ignore_whitespace: true
stdin: |
#expect-prompt
echo "L[$((1 + 1))]"; for i in 1 2 3 4 5 6 7 8 9 10; do sleep 0.3; done; exit 7
#send:Enter
#expect:L[2]
#send:Ctrl+C
#expect-prompt
echo "S[$?]"
#send:Enter
#expect:S[130]
#expect-prompt
#send:Ctrl+D