wren-linux / tools / test-mockvps-proc.sh
#!/bin/bash
# Checks how vendor/mockvps-kvm/mockvps-kvm.sh knows QEMU is running (process_is_qemu, qemu_running):
# by /proc, and, where /proc doesn't show it, by the process being there and QEMU's monitor
# socket answering. Under hidepid (parts/hidepid.sh) /proc hides a process that isn't dumpable,
# which QEMU is, even from its own user: the script then thought a running VM was off, tried to
# start it again and stopped at "Host port 2242 is already in use" (the owner's laptop). Pretend
# processes only; nothing of this computer changes. Run by test.sh, or:
# bash tools/test-mockvps-proc.sh
# shellcheck disable=SC2016 # the inner shells expand their own variables
HERE=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
SCRIPT=$HERE/vendor/mockvps-kvm/mockvps-kvm.sh
T=$(mktemp -d) || { echo " FAIL couldn't make a temp folder (is /tmp full?)"; exit 1; }
trap 'jobs -p | xargs -r kill 2>/dev/null; rm -rf "$T"' EXIT
fails=0
check() { # DESCRIPTION CONDITION-COMMAND...
local what=$1
shift
if "$@" >/dev/null 2>&1; then printf ' ok %s\n' "$what"; else printf ' FAIL %s\n' "$what"; fails=$((fails + 1)); fi
}
not() { ! "$@"; }
# the functions, on their own (the script runs its work when it is sourced)
sed -n '/^monitor_answers() {/,/^}/p; /^process_is_qemu() {/,/^}/p; /^qemu_running() {/,/^}/p; /^drop_stale_files() {/,/^}/p' "$SCRIPT" >"$T/fn.sh"
check "the functions are found" grep -q '^process_is_qemu() {' "$T/fn.sh"
check "...and the monitor check is one of them" grep -q '^monitor_answers() {' "$T/fn.sh"
mkdir -p "$T/bin" "$T/emptyproc"
ln -s "$(command -v sleep)" "$T/bin/qemu-system-x86_64"
"$T/bin/qemu-system-x86_64" 60 &
QEMU=$!
sleep 60 &
OTHER=$!
# a monitor socket with something listening, as QEMU's is
cat >"$T/listen.py" <<'EOF'
import os, socket, sys, time
s = socket.socket(socket.AF_UNIX); s.bind(sys.argv[1]); s.listen(5)
print("up", flush=True)
time.sleep(60)
EOF
python3 -I "$T/listen.py" "$T/mon.sock" >"$T/listen.out" &
for _ in $(seq 40); do [ -s "$T/listen.out" ] && break; /bin/sleep 0.1; done
# a socket nobody listens on any more (QEMU killed hard leaves one)
python3 -I -c 'import socket,sys; socket.socket(socket.AF_UNIX).bind(sys.argv[1])' "$T/stale.sock"
# is PID MONITOR [PROC]: process_is_qemu, with /proc, or a pretend /proc that shows nothing
is() {
env PROC_DIR="${3:-/proc}" bash -c '. "$1"; PIDFILE=x; MONITOR_SOCK=$3; process_is_qemu "$2"' _ "$T/fn.sh" "$1" "$2"
}
check "/proc shows it, and it is QEMU: running" is "$QEMU" "$T/mon.sock"
check "...a process that is not QEMU: not" not is "$OTHER" "$T/mon.sock"
check "...not a number: not" not is "1; true" "$T/mon.sock"
check "/proc shows nothing (hidepid), the process is there and QEMU's monitor answers: running" is "$QEMU" "$T/mon.sock" "$T/emptyproc"
check "...the process is there but nothing answers on the monitor: not running" not is "$QEMU" "$T/stale.sock" "$T/emptyproc"
check "...no monitor socket at all: not running" not is "$QEMU" "$T/none.sock" "$T/emptyproc"
dead=$(bash -c 'echo $$')
check "...the process is gone (a stale pid file), even with a socket that answers: not running" not is "$dead" "$T/mon.sock" "$T/emptyproc"
# stale files: only those of a VM that is gone (a live process that isn't seen as QEMU keeps them)
drop() { # PID: the files of a VM with that pid in its pid file; prints kept or gone
echo "$1" >"$T/pidfile"; : >"$T/monfile"
bash -c '. "$1"; PIDFILE=$2; MONITOR_SOCK=$3; drop_stale_files; [ -e "$2" ] && echo kept || echo gone' _ "$T/fn.sh" "$T/pidfile" "$T/monfile"
}
check "a pid file whose process is alive (not seen as QEMU, say hidden): kept, with its socket" test "$(drop "$OTHER")" = kept
check "...whose process is gone: removed" test "$(drop "$dead")" = gone
check "...with no number in it: removed" test "$(drop nonsense)" = gone
# for real: hidden by hidepid where it is on, visible elsewhere; either way it is seen as running
cat >"$T/nd.py" <<'EOF'
import ctypes, os, socket, sys, time
ctypes.CDLL(None).prctl(4, 0, 0, 0, 0) # PR_SET_DUMPABLE = 4: not dumpable, as QEMU is
s = socket.socket(socket.AF_UNIX); s.bind(sys.argv[1]); s.listen(5)
print(os.getpid(), flush=True)
time.sleep(60)
EOF
ln -s "$(command -v python3)" "$T/bin/qemu-system-x86_64-py"
python3 -I "$T/nd.py" "$T/nd.sock" >"$T/nd.pid" &
for _ in $(seq 40); do [ -s "$T/nd.pid" ] && break; /bin/sleep 0.1; done
check "a process that is not dumpable (as QEMU is), by the real /proc: its own user sees it running, hidden or not" bash -c '
pid=$(head -1 "$2"); . "$1"; PIDFILE=x; MONITOR_SOCK=$3
# (it is not called qemu: the pretend process stands for the hidden case, so only the fallback can say yes)
if [ -r "/proc/$pid/comm" ]; then exit 0; fi
kill -0 "$pid" && monitor_answers' _ "$T/fn.sh" "$T/nd.pid" "$T/nd.sock"
check "the fallback is in the script, after the /proc way" bash -c 'grep -q "monitor_answers" "$1" && grep -q "PROC_DIR" "$1"' _ "$SCRIPT"
check "it is passed by shellcheck" bash -c 'command -v shellcheck >/dev/null || exit 0; shellcheck -x "$1"' _ "$SCRIPT"
[ "$fails" -eq 0 ]