From c235cb62beefd03087e7cfc7f7f55f0c7c6601c9 Mon Sep 17 00:00:00 2001 From: Lena Date: Sat, 1 Aug 2026 00:00:00 +0000 Subject: Enter vmm A QEMU/KVM virtual machine manager in one POSIX shell script. Each VM is a directory of plain files under $VMMDIR, configured by a hand-edited KEY=VALUE file that is parsed rather than sourced, so nothing in it can inject a raw QEMU argument. Control is a QMP FIFO pair inside that directory, guarded by its permissions alone; nothing binds a TCP port. The tests are TAP without a framework and boot real guests where the host allows. --- tests/console | 228 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 228 insertions(+) create mode 100644 tests/console (limited to 'tests/console') diff --git a/tests/console b/tests/console new file mode 100644 index 0000000..8ec2242 --- /dev/null +++ b/tests/console @@ -0,0 +1,228 @@ +# The serial console needs a controlling terminal, which script(1) makes. +# The guests are emulated, so no accelerator is needed here. + +if [ -t 0 ]; then + skip 1 "stdin is a terminal, a console refusal cannot be tested here" +else + "$VMM" create co0 64M >/dev/null 2>&1 + refuses 'needs a terminal' "console refuses without a terminal" console co0 +fi + +if ! command -v script >/dev/null 2>&1; then + skip 8 "no script(1) to allocate a pty" + return 0 +fi + +"$VMM" create co1 64M >/dev/null 2>&1 +cfg co1 </dev/null 2>&1; then + skip 8 "co1 did not start" + return 0 +fi + +# Ctrl-] raises SIGINT in the pty's whole foreground process group, this +# wrapper included. A no-op trap keeps the wrapper alive to report; a +# child gets default handling either way, so vmm's own trap fires. +cat > "$WORK/console-run" < "$WORK/tty.before" +"$VMM" console co1 +echo \$? > "$WORK/console.rc" +stty -g > "$WORK/tty.after" +EOF +chmod +x "$WORK/console-run" + +# One Ctrl-] per second until the console takes it and the pipe breaks, +# so a slow attach costs a retry. timeout is the backstop: a console +# that never detaches must fail, not hang. +( + i=0 + while [ "$i" -lt 15 ]; do + sleep 1 + printf '\035' + i=$((i + 1)) + done +) | timeout 60 script -q -c "$WORK/console-run" /dev/null \ + > "$WORK/console.out" 2>&1 || : + +if grep -q 'Ctrl-] detaches' "$WORK/console.out"; then + ok "console attaches and names the pty" +else + notok "console attaches and names the pty" "$(cat "$WORK/console.out")" +fi + +rc=$(cat "$WORK/console.rc" 2>/dev/null || echo missing) +if [ "$rc" = 130 ]; then + ok "Ctrl-] detaches, and says it was interrupted" +else + notok "Ctrl-] detaches, and says it was interrupted" "rc=$rc" +fi + +# A botched console leaves the terminal raw. +if cmp -s "$WORK/tty.before" "$WORK/tty.after"; then + ok "the terminal is handed back exactly as it was" +else + notok "the terminal is handed back exactly as it was" \ + "before: $(cat "$WORK/tty.before" 2>/dev/null)" \ + "after: $(cat "$WORK/tty.after" 2>/dev/null)" +fi + +exits 0 "detaching leaves the guest running" status co1 + +# SIGTERM reaches console_restore with the terminal still open, and the +# copy processes must all go with it. Ctrl-] cannot show this: it +# signals the whole foreground group and takes them down anyway. +rm -f "$WORK/term.wrapper" "$WORK/term.rc" +cat > "$WORK/console-term" < "$WORK/term.wrapper" +"$VMM" console co1 +echo \$? > "$WORK/term.rc" +sleep 20 +EOF +chmod +x "$WORK/console-term" +timeout 60 script -q -c "$WORK/console-term" /dev/null \ + > "$WORK/console-term.out" 2>&1 & +term_job=$! +i=0 +while ! grep -q 'Ctrl-] detaches' "$WORK/console-term.out" 2>/dev/null; do + [ "$i" -lt 15 ] || break + sleep 1 + i=$((i + 1)) +done +pts=$(tr -d '\r' < "$WORK/console-term.out" | + sed -n 's/^console co1 (\([^)]*\)).*/\1/p') +# vmm and the subshells it forks carry one argv between them, so the +# console is the match that is the wrapper's own child. Signalling any +# other one leaves vmm attached and proves nothing. +wrapper=$(cat "$WORK/term.wrapper" 2>/dev/null || echo 0) +termpid= +for cmdline in /proc/[0-9]*/cmdline; do + p=${cmdline#/proc/} + p=${p%/cmdline} + line= + IFS= read -r line 2>/dev/null < "$cmdline" || : + case $line in + *"$VMM"consoleco1) ;; + *) continue ;; + esac + if [ "$(awk '{print $4}' "/proc/$p/stat" 2>/dev/null)" = "$wrapper" ]; then + termpid=$p + fi +done +kill -TERM "$termpid" 2>/dev/null || : +i=0 +while [ ! -s "$WORK/term.rc" ] && [ "$i" -lt 10 ]; do + sleep 1 + i=$((i + 1)) +done +holding= +if [ -n "$pts" ]; then + for fd in /proc/[0-9]*/fd/0 /proc/[0-9]*/fd/1; do + if [ "$(readlink "$fd" 2>/dev/null)" = "$pts" ]; then + p=${fd#/proc/} + holding="$holding ${p%%/*}" + fi + done +fi +if [ -z "$termpid" ] || [ -z "$pts" ] || [ ! -s "$WORK/term.rc" ]; then + skip 1 "the console did not attach and exit, cannot check teardown" +elif [ -z "$holding" ]; then + ok "a terminated console leaves nothing holding the guest pty" +else + notok "a terminated console leaves nothing holding the guest pty" \ + "pty $pts still open by pid:$holding" +fi +kill "$term_job" 2>/dev/null || : +wait "$term_job" 2>/dev/null || : + +# Closing QEMU's pty makes both copies report EIO. That is an expected +# detach, not an error for the user's terminal. +rm -f "$WORK/console.rc" +( + while :; do + printf '\n' + sleep 1 + done +) | timeout 60 script -q -c "$WORK/console-run" /dev/null \ + > "$WORK/console-die.out" 2>&1 & +console_job=$! +i=0 +while ! grep -q 'Ctrl-] detaches' "$WORK/console-die.out" 2>/dev/null; do + [ "$i" -lt 15 ] || break + sleep 1 + i=$((i + 1)) +done +"$VMM" kill co1 >/dev/null 2>&1 || : +wait "$console_job" 2>/dev/null || : + +rc=$(cat "$WORK/console.rc" 2>/dev/null || echo missing) +case $rc in +0 | 130) + ok "guest death detaches the console" + ;; +*) + notok "guest death detaches the console" "rc=$rc" + ;; +esac +if grep -q 'cat:' "$WORK/console-die.out"; then + notok "guest death does not print cat errors" \ + "$(cat "$WORK/console-die.out")" +else + ok "guest death does not print cat errors" +fi + +# The writer half of the console. No OS is needed to answer, only 15 +# bytes of real mode code that poll the 16550 status register, read the +# byte and write it straight back: +# BA FD 03 mov dx,0x3fd | EC in al,dx | A8 01 test al,1 | 74 F8 jz -8 +# BA F8 03 mov dx,0x3f8 | EC in al,dx | EE out dx,al | EB F1 jmp -15 +# SeaBIOS will not boot a disk of one sector, so the image is padded. +if command -v qemu-system-x86_64 >/dev/null 2>&1; then + "$VMM" create co2 64M >/dev/null 2>&1 + dd if=/dev/zero of="$VMMDIR/co2/disk.qcow2" bs=1M count=1 2>/dev/null + printf '\272\375\003\354\250\001\164\370\272\370\003\354\356\353\361' | + dd of="$VMMDIR/co2/disk.qcow2" conv=notrunc 2>/dev/null + printf '\125\252' | + dd of="$VMMDIR/co2/disk.qcow2" bs=1 seek=510 conv=notrunc 2>/dev/null + cfg co2 </dev/null 2>&1; then + # One line per second until the console is attached to take + # one, as above. The console turns the terminal's own echo + # off, so whatever comes back came back from the guest. + ( + i=0 + while [ "$i" -lt 5 ]; do + sleep 1 + printf 'ECHO123' + i=$((i + 1)) + done + sleep 2 + printf '\035' + ) | timeout 60 script -q -c "$VMM console co2" /dev/null \ + > "$WORK/echo.out" 2>&1 || : + if grep -q ECHO123 "$WORK/echo.out"; then + ok "a keystroke reaches the guest and comes back" + else + notok "a keystroke reaches the guest and comes back" \ + "$(cat "$WORK/echo.out")" + fi + "$VMM" kill co2 >/dev/null 2>&1 || : + else + skip 1 "co2 did not start" + fi +else + skip 1 "no qemu-system-x86_64 for the echo guest" +fi -- cgit v1.2.3