#!/usr/bin/env bash
# Build the kernel with the `headless` feature and boot it in QEMU with no
# window. Serial output goes to this terminal; the kernel powers QEMU off via
# the isa-debug-exit device when done. A timeout guards against hangs.
#
# A normal boot runs the shell self-test (disk/scripts/selftest.sh); the
# shell's exit code decides success, and this script then also checks that
# the expected lines appear in the output.
#
# Usage: ./scripts/run-headless.sh # boot + shell self-test
# ./scripts/run-headless.sh stack-overflow # extra cargo features, comma separated
# EXT2_IMG=copy.img ./scripts/run-headless.sh
# use copy.img as the ext2 disk *without* snapshot=on, so the
# self-test's writes land in that file (scripts/ext2-check.sh
# then runs e2fsck on it)
# EXFAT_IMG=copy.img ./scripts/run-headless.sh
# the same for the exFAT disk (scripts/exfat-check.sh runs
# fsck.exfat on it)
# BTRFS_IMG=other.img ./scripts/run-headless.sh
# use other.img as drive E: (read-only, so always snapshot=on)
set -euo pipefail
cd "$(dirname "$0")/.."
cargo build --features "headless${1:+,$1}"
# Force bootimage to repack the disk image (see scripts/runner.sh for why).
touch target/x86_64-rust_kernel/debug/rustbelt
cargo bootimage --features "headless${1:+,$1}"
IMG=target/x86_64-rust_kernel/debug/bootimage-rustbelt.bin
if [ -n "${EXT2_IMG:-}" ]; then
EXT2_DRIVE="file=$EXT2_IMG,format=raw,if=virtio"
else
EXT2_DRIVE="file=target/ext2.img,format=raw,if=virtio,snapshot=on"
fi
if [ -n "${EXFAT_IMG:-}" ]; then
EXFAT_DRIVE="file=$EXFAT_IMG,format=raw,if=virtio"
else
EXFAT_DRIVE="file=target/exfat.img,format=raw,if=virtio,snapshot=on"
fi
BTRFS_DRIVE="file=${BTRFS_IMG:-target/btrfs.img},format=raw,if=virtio,snapshot=on"
# Network: QEMU's user-mode NAT (see Cargo.toml run-args). guestfwd runs a
# tiny test web server for 10.0.2.100:80; hostfwd forwards a free host port
# to the guest's port 80, where the self-test's `httpd -n 1` waits, and a
# background curl below fetches its page.
HTTP_PORT=$(python3 -c 'import socket; s=socket.socket(); s.bind(("127.0.0.1", 0)); print(s.getsockname()[1])')
NETDEV="user,id=net0,guestfwd=tcp:10.0.2.100:80-cmd:sh scripts/http-reply.sh,guestfwd=tcp:10.0.2.100:7-cmd:cat"
NETDEV="$NETDEV,hostfwd=tcp:127.0.0.1:$HTTP_PORT-:80"
CURL_OUT=target/headless-curl.txt
rm -f "$CURL_OUT"
( for _ in $(seq 1 80); do # retry until httpd is up (~1 s apart)
sleep 1
curl -s -m 5 -o "$CURL_OUT" "http://127.0.0.1:$HTTP_PORT/" 2>/dev/null && [ -s "$CURL_OUT" ] && break
done ) &
CURL_PID=$!
# Stage 13 virtual drivers: virtio-gpu (instead of the VGA card),
# virtio-rng, and virtio-console whose hvc0 port reads the commands in
# scripts/hvc0-session.txt and writes into target/headless-hvc0.txt (the
# self-test runs `getty -1 /dev/hvc0`, a shell on that port).
HVC_OUT=target/headless-hvc0.txt
rm -f "$HVC_OUT"
VIRTIO_DEVICES=(-vga none -device virtio-vga -device virtio-rng-pci
-device virtio-serial-pci -device virtconsole,chardev=hvc
-chardev "file,id=hvc,path=$HVC_OUT,input-path=scripts/hvc0-session.txt")
LOG=target/headless-serial.log
set +e
timeout 90 qemu-system-x86_64 -smp 4 \
-drive format=raw,file="$IMG" \
-drive file=target/disk.img,format=raw,if=virtio,snapshot=on \
-drive "$EXT2_DRIVE" \
-drive "$EXFAT_DRIVE" \
-drive "$BTRFS_DRIVE" \
-netdev "$NETDEV" -device virtio-net-pci,netdev=net0 \
"${VIRTIO_DEVICES[@]}" \
-device isa-debug-exit,iobase=0xf4,iosize=0x04 \
-serial stdio -display none -no-reboot | tee "$LOG"
code=${PIPESTATUS[0]}
kill $CURL_PID 2>/dev/null; wait $CURL_PID 2>/dev/null
set -e
# Lines the shell self-test must have printed (only for a plain boot).
if [ -z "${1:-}" ] && [ "$code" = 33 ]; then
expect=(
"Rustbelt 0.14.0 (stage 14: processes) x86_64" # ver
"[smp] 4 CPUs online" # smp.rs started 3 more cores
"B: /mnt/disk0 fat32" # drives
"C: /mnt/disk1 ext2"
"D: /mnt/disk2 exfat"
"E: /mnt/disk3 btrfs"
"B:/ $ ls" # cd B: changed the prompt
"readme.txt scripts/" # ls in columns
"In the Rustbelt shell this disk is drive B:" # type B:/readme.txt
"second line" # >> and <
" 125 todo.txt" # cp + ls -l
'[trace 18] open("/mnt/disk0/notes/todo.txt", 0o0) = 3' # run --trace
"sleep started in the background" # start
"[done] pid 19 (start sleep 1), exit code 0" # wait
"C:/ $ ls" # the ext2 disk
"Hello from the ext2 disk!" # cat through a symlink
"big.txt line 12345" # double-indirect block
"written by Rustbelt" # mkdir + create on ext2
"-rw-r--r-- 125 todo.txt" # cp B: -> C:, ls -l
"lrwxrwxrwx hello-link -> hello.txt"
"drwxr-xr-x
made-here/"
"D:/ $ ls" # the exFAT disk
"This file has a long name with spaces in it." # quoted name, other case
"MixedCase.TXT: exFAT keeps the case" # cp D: -> C:, cat
"hi from exFAT" # md + echo >, other case
" 125 todo.txt" # cp B: -> D:, ls -l D:/
" New Folder/"
" 340000 numbers.txt"
"E:/ $ ls -l" # the btrfs disk
"lrwxrwxrwx hello-link -> hello.txt"
"Hello from btrfs! (an inline extent" # inline, through a symlink
"btrfs never overwrites a block in place" # compressed inline extent
" 120000 360000 2160000 big.txt" # 17 zlib extents
"btrfs line 119999"
"Stage 10: a read-only btrfs driver" # inside subvolume projects
"sh: new.txt: Read-only file system" # writes refused
"rm: hello.txt: Read-only file system"
"mkdir: newdir: Read-only file system"
"[exit code 139]" # run evil
"(overflow) killed: stack overflow" # user stack guard zone
"4 CPUs online (this program ran on CPU" # sysinfo
" CPU3 ["
"[net] DHCP lease: 10.0.2.15/24, gateway 10.0.2.2, DNS 10.0.2.3" # net/
" inet 10.0.2.15/24 gateway 10.0.2.2 DNS 10.0.2.3 (DHCP)" # ifconfig
"64 bytes from 10.0.2.2: icmp_seq=3" # ping the slirp gateway
"3 packets transmitted, 3 received, 0% packet loss"
"Hello from the host, via QEMU guestfwd!" # fetch (TCP + HTTP)
"fetch: HTTP/1.0 200 OK, saved 108894 bytes to /mnt/disk2/seq.txt" # wget -o D:
"[httpd] served 1 request, bye" # httpd, reached via hostfwd
"[virtio] rng: virtio-rng at 00:04.0" # stage 13: virtio/
"[virtio] hvc0: virtio-console at 00:05.0"
"[virtio] gpu: virtio-gpu 2D at 00:03.0, display 1280x800, framebuffer 1024x768"
"[virtio] fbcon: text console on the framebuffer, 128x48 characters"
"crw-rw-rw- fb0" # ls -l /dev
"crw-rw-rw- hvc0"
"tile: 4 windows on 1024x768, console 61x46" # the tiling demo
"console: full screen, 128x48 characters" # tile -r
"[forktest] all checks passed" # stage 14: processes
"[forktest args] all checks passed" # exec'd with argv/envp/auxv
"[cowtest] all checks passed"
"[mmaptest] all checks passed"
"[sigtest] all checks passed"
"(start spin 1000), exit code 143" # kill %1 (SIGTERM)
"15) SIGTERM" # kill -l
"sh: nosuchprogram: command not found"
"cd: there is no drive Q:"
"A:/tmp $ pwd"
)
missing=0
for line in "${expect[@]}"; do
grep -qF -- "$line" "$LOG" || { echo "[run-headless] MISSING: $line"; missing=1; }
done
# Pipe results: `... | grep -i EXT | wc -l` -> 1, `dir bin | wc -l` -> 29,
# `cat C:/chunks.txt | wc -l` (copied from E:) -> 3,
# `type D:/COW.TXT | wc -l` (copied from E:) -> 4, and
# `cat D:/seq.txt | wc -l` (downloaded by wget) -> 20000.
for n in 1 29 3 4 20000; do
grep -qx -- "$n" "$LOG" || { echo "[run-headless] MISSING pipe result: $n"; missing=1; }
done
# No memory leaks: once the shell and all ~60 programs it ran have
# exited, exactly as many frames must be in use as before it started.
if grep -qE '^\[memory\] ([0-9]+) frames in use before the shell, \1 after' "$LOG"; then
echo "[run-headless] frames in use returned to the baseline"
else
echo "[run-headless] MISSING: frames back at the baseline after the shell"; missing=1
fi
# The other direction: the host fetched a page from httpd in the guest.
if grep -qF "Hello from Rustbelt! Rustbelt 0.14.0" "$CURL_OUT" 2>/dev/null; then
echo "[run-headless] host curl got httpd's page: $(head -1 "$CURL_OUT")"
else
echo "[run-headless] MISSING: host curl through hostfwd to httpd"; missing=1
fi
# random -n 8: 16 hex digits; random -d 6 5: five numbers 1-6.
grep -qxE -- "[0-9a-f]{16}" "$LOG" || { echo "[run-headless] MISSING: random -n 8 output"; missing=1; }
grep -qxE -- "[1-6]( [1-6]){4}" "$LOG" || { echo "[run-headless] MISSING: random -d 6 5 output"; missing=1; }
# The shell on hvc0 ran the host's commands and answered over hvc0.
for line in "Rustbelt getty: shell on this terminal" "Rustbelt 0.14.0 (stage 14" \
"$ echo hi over hvc0" "hi over hvc0" "fb0" "hvc0"; do
grep -qF -- "$line" "$HVC_OUT" 2>/dev/null || { echo "[run-headless] MISSING on hvc0: $line"; missing=1; }
done
grep -qxE -- "[0-9]+ [0-9]+ [0-9]+" "$HVC_OUT" || { echo "[run-headless] MISSING on hvc0: random -d 20 3"; missing=1; }
[ $missing = 0 ] && echo "[run-headless] all ${#expect[@]}+12 expected shell and hvc0 lines found" || code=35
fi
# isa-debug-exit: QEMU exit status = (value << 1) | 1, so Success(0x10) -> 33.
case $code in
33) echo "[run-headless] kernel exited OK"; exit 0 ;;
35) echo "[run-headless] kernel reported FAILURE"; exit 1 ;;
124) echo "[run-headless] timed out (kernel hung?)"; exit 124 ;;
*) echo "[run-headless] QEMU exited with $code"; exit "$code" ;;
esac