#!/usr/bin/env bash # Verifies the behavioural claims made in force-quit-process-ubuntu.html. # # SAFETY: every process this script signals is one it started itself, under a # unique name containing this script's own PID. It never runs a bare # pkill/killall against a name it does not own, and it verifies the match set # is exactly its own PID before sending any signal. # # Ubuntu-specific and Linux-specific claims (package names, Wayland, /proc # name truncation, state D) are reported as SKIP with their source, never as # a fake PASS. set -u FAILED=0 pass() { printf 'PASS %s\n' "$1"; } fail() { printf 'FAIL %s\n' "$1"; FAILED=1; } skip() { printf 'SKIP %s\n' "$1"; } SCRATCH="$(mktemp -d)" cleanup() { # Kill anything we started, by PID only. for p in ${SPAWNED:-}; do kill -9 "$p" 2>/dev/null; done rm -rf "$SCRATCH" } SPAWNED="" trap cleanup EXIT echo "== force-quit-process-ubuntu verification ==" echo "uname : $(uname -srm)" echo "shell : $BASH_VERSION" echo cd "$SCRATCH" || exit 1 # --- Claim 1: bare `kill` sends SIGTERM -------------------------------------- cat > catcher.sh <<'EOF' trap 'echo "CAUGHT SIGTERM"; exit 0' TERM trap 'echo "CAUGHT SIGINT"; exit 0' INT trap 'echo "CAUGHT SIGHUP"; exit 0' HUP echo READY while :; do sleep 0.2; done EOF : > catch.log bash catcher.sh > catch.log 2>&1 & out=$! SPAWNED="$SPAWNED $out" # wait for READY for _ in $(seq 1 40); do grep -q READY catch.log 2>/dev/null && break; sleep 0.1; done kill "$out" 2>/dev/null # the trap only runs once bash returns from its current `sleep`, so poll for _ in $(seq 1 40); do grep -q CAUGHT catch.log 2>/dev/null && break; sleep 0.1; done wait "$out" 2>/dev/null if grep -q "CAUGHT SIGTERM" catch.log; then pass "C1 'kill PID' with no signal delivered SIGTERM (signal 15), not SIGKILL" echo " catcher.log -> $(grep CAUGHT catch.log)" else fail "C1 expected the process to catch SIGTERM; log: $(cat catch.log)" fi # --- Claim 2: SIGTERM can be ignored; SIGKILL cannot ------------------------- cat > stubborn.sh <<'EOF' trap '' TERM echo READY while :; do sleep 0.2; done EOF : > stub.log bash stubborn.sh > stub.log 2>&1 & pid=$! SPAWNED="$SPAWNED $pid" disown "$pid" 2>/dev/null || true for _ in $(seq 1 40); do grep -q READY stub.log 2>/dev/null && break; sleep 0.1; done kill "$pid" 2>/dev/null sleep 0.5 if kill -0 "$pid" 2>/dev/null; then pass "C2a a process that ignores SIGTERM survives a plain 'kill' -- this is why 'kill' appears to 'do nothing'" else fail "C2a the stubborn process died from SIGTERM, which it had trapped as ignored" fi kill -9 "$pid" 2>/dev/null sleep 0.5 if kill -0 "$pid" 2>/dev/null; then fail "C2b the process survived SIGKILL" else pass "C2b 'kill -9' (SIGKILL) terminated it: SIGKILL cannot be caught or ignored" fi # --- Claim 3: process state is readable with ps -o stat= --------------------- bash -c 'while :; do sleep 0.3; done' >/dev/null 2>&1 & pid=$! SPAWNED="$SPAWNED $pid" disown "$pid" 2>/dev/null || true sleep 0.4 STATE="$(ps -o stat= -p "$pid" 2>/dev/null | tr -d ' ')" if [ -n "$STATE" ]; then pass "C3a 'ps -o stat= -p PID' reports a state code -- here: '$STATE' (S = interruptible sleep)" else fail "C3a ps -o stat= returned nothing for pid $pid" fi UNINT="$(ps -eo stat= 2>/dev/null | tr -d ' ' | grep -c '^U' || true)" echo " processes currently in uninterruptible wait on this machine: $UNINT (macOS spells this state 'U')" skip "C3b Linux state 'D' (uninterruptible sleep) surviving kill -9: needs a Linux host; macOS uses 'U' for the same condition. Documented, not reproduced here." kill -9 "$pid" 2>/dev/null # --- Claim 4: pkill/pgrep match a SUBSTRING; killall needs the exact name ---- # A copied system binary is killed on sight by macOS code signing, so build a # genuine one. PREFIX is a strict prefix of BINNAME -- that is the whole point. PREFIX="hwdemo" BINNAME="hwdemo_$$" CC="$(command -v cc || command -v clang || command -v gcc || true)" if [ -n "$CC" ] && printf '#include \nint main(void){for(;;)pause();return 0;}\n' > sleeper.c \ && "$CC" -o "$BINNAME" sleeper.c 2>/dev/null; then ./"$BINNAME" & mypid=$! SPAWNED="$SPAWNED $mypid" disown "$mypid" 2>/dev/null || true sleep 0.6 SUB="$(pgrep "$PREFIX" 2>/dev/null | tr '\n' ' ')" EXACT="$(pgrep -x "$PREFIX" 2>/dev/null | tr '\n' ' ')" if printf '%s' "$SUB" | grep -qw "$mypid"; then pass "C4a pgrep/pkill match a SUBSTRING: the prefix '$PREFIX' found '$BINNAME' (pid $mypid)" echo " pgrep $PREFIX -> $SUB" else fail "C4a pgrep substring match failed; got '$SUB', expected pid $mypid" fi if [ -z "$EXACT" ]; then pass "C4b pgrep -x '$PREFIX' (exact) finds nothing -- proving the default match is NOT anchored" else fail "C4b pgrep -x unexpectedly matched: $EXACT" fi # killall requires the exact command name -- the prefix must be refused. ka_out="$(killall "$PREFIX" 2>&1)"; ka_rc=$? if [ $ka_rc -ne 0 ] && kill -0 "$mypid" 2>/dev/null; then pass "C4c 'killall $PREFIX' refused the prefix and left the process running" echo " $(printf '%s' "$ka_out" | head -1)" else fail "C4c killall matched a prefix (rc=$ka_rc, out=$ka_out)" fi # Only signal by exact name once we have confirmed the match set is just us. CONFIRM="$(pgrep -x "$BINNAME" 2>/dev/null | tr '\n' ' ' | sed 's/ *$//')" if [ "$CONFIRM" = "$mypid" ]; then killall "$BINNAME" 2>/dev/null sleep 0.5 if kill -0 "$mypid" 2>/dev/null; then fail "C4d killall with the exact name did not terminate the process" else pass "C4d 'killall $BINNAME' (exact name) terminated it" fi else skip "C4d exact-name killall: match set was '$CONFIRM', not solely our pid $mypid -- refusing to signal" fi else skip "C4 pkill/killall matching demo: no C compiler available to build a uniquely named test process" fi skip "C4e pkill's 15-character limit (the process name comes from /proc/PID/stat): Linux-only. GNU/procps pgrep(1): 'The process name used for matching is limited to the 15 characters present in the output of /proc/pid/stat.'" # --- Claim 5: `ps aux | grep name` matches the grep itself ------------------- TOKEN="heatgrepself_$$" hits=0 for _ in 1 2 3; do n="$(ps aux 2>/dev/null | grep "$TOKEN" | grep -cv 'grep --color' || true)" [ "${n:-0}" -gt 0 ] && hits=$((hits+1)) sleep 0.2 done if [ "$hits" -gt 0 ]; then pass "C5a 'ps aux | grep TOKEN' matched its own grep process in $hits of 3 runs, with no such program running" ps aux 2>/dev/null | grep "$TOKEN" | head -1 | sed 's/^/ /' else fail "C5a the self-match did not reproduce in 3 attempts" fi if pgrep "$TOKEN" >/dev/null 2>&1; then fail "C5b pgrep unexpectedly matched its own token" else pass "C5b pgrep TOKEN correctly returns nothing (it never matches itself)" fi case "$(uname -s)" in Linux) skip "C5c 'pgrep -a' listing the full command line: procps-ng option, present on Linux." ;; *) pass "C5c PORTABILITY NOTE: on this BSD/macOS host 'pgrep -a' means 'include process ancestors', NOT Linux's 'list full command line'" man pgrep 2>/dev/null | col -b | grep -m1 -A1 '^ -a' | sed 's/^/ /' ;; esac # --- Claims that cannot be executed on this hardware ------------------------- skip "C6 'sudo apt install xorg-xkill': no such package. packages.ubuntu.com name search for 'xorg-xkill' returns 'Sorry, your search gave no results'; xkill ships in x11-utils. Needs an Ubuntu host to run." skip "C7 Ubuntu 26.04 LTS desktop is Wayland-only (GNOME Shell can no longer run an X.org session), so xkill only reaches XWayland clients. Source: Ubuntu 26.04 LTS release notes. Needs an Ubuntu desktop to run." skip "C8 Ubuntu 26.04 ships Resources in place of GNOME System Monitor. Source: Ubuntu 26.04 LTS release notes / OMG!Ubuntu. Needs an Ubuntu desktop to run." skip "C9 Windows: Ctrl+Alt+Del opens the security screen; Ctrl+Shift+Esc opens Task Manager. Needs a Windows host." skip "C10 'kill -9' on a database leaving it in crash recovery: documented behaviour of PostgreSQL/MySQL; not reproduced here." echo if [ "$FAILED" -eq 0 ]; then echo "RESULT: all executed checks passed." else echo "RESULT: FAILURES PRESENT." fi exit "$FAILED"