aboutsummaryrefslogtreecommitdiff
path: root/tests/console
diff options
context:
space:
mode:
authorLena <lena@omega>2026-08-01 00:00:00 +0000
committerLena <lena@omega>2026-08-01 00:00:00 +0000
commitc235cb62beefd03087e7cfc7f7f55f0c7c6601c9 (patch)
tree2776f86d9226d55c78e3d735764fcd9ecbe5e08e /tests/console
downloadvmm-master.tar.gz
Enter vmmHEADmaster
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.
Diffstat (limited to 'tests/console')
-rw-r--r--tests/console228
1 files changed, 228 insertions, 0 deletions
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 <<EOF
+MEM=256
+CPUS=1
+HWACCEL=no
+EOF
+
+if ! "$VMM" start 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" <<EOF
+#!/bin/sh
+trap ':' INT
+stty -g > "$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" <<EOF
+#!/bin/sh
+echo \$\$ > "$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 <<EOF
+ARCH=x86_64
+HWACCEL=no
+IMAGE_FORMAT=raw
+MEM=256
+CPUS=1
+EOF
+ if "$VMM" start 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