#!/usr/bin/env bash # Boot the Foundry OS image headless on the pinned Rustbelt kernel, let the # kernel run the shell self-test (B:/scripts/selftest.sh from rootfs/), and # check the output. Moved from the kernel's scripts/run-headless.sh; the # expected lines are the same, so a pass here means "same behaviour as the # kernel's own image". # # Usage: scripts/run-headless.sh IMAGE KERNEL_BOOTIMAGE KERNEL_TARGET OUT_DIR # IMAGE our disk image (drive B:, /mnt/disk0) # KERNEL_BOOTIMAGE out/kernel/bootimage-headless.bin # KERNEL_TARGET out/kernel-target (has the kernel-owned test disks # ext2.img, exfat.img, btrfs.img: drives C:, D:, E:) # OUT_DIR where logs go (out/) # Env (the Makefile sets these from config.mk; they change with the kernel pin): # KERNEL_BANNER the kernel's first line, e.g. "Rustbelt 0.20.0 (stage 20: ...)" # KERNEL_VERSION e.g. 0.20.0 # BIN_COUNT how many programs the image has in /bin (`dir bin | wc -l`) # EXTRA_EXPECT optional file: more lines the log must contain, one per # line (Foundry OS's own checks; # starts a comment) # QEMU_CPU (default max), QEMU_TIMEOUT (default 120 s). # # Only the QEMU this script starts is ever killed: QEMU writes its PID to # OUT_DIR/qemu-headless.pid and the exit trap kills exactly that PID. set -euo pipefail cd "$(dirname "$0")/.." # repo root: QEMU's guestfwd runs scripts/http-reply.sh IMAGE=$1 IMG=$2 KT=$3 OUT=$4 : "${KERNEL_BANNER:?set KERNEL_BANNER (see config.mk)}" "${KERNEL_VERSION:?}" "${BIN_COUNT:?}" STAGE=${KERNEL_BANNER#*(}; STAGE=${STAGE%%:*} # "stage 20" mkdir -p "$OUT" for f in "$IMAGE" "$IMG" "$KT/ext2.img" "$KT/exfat.img" "$KT/btrfs.img"; do [ -f "$f" ] || { echo "[run-headless] missing $f (run make image / make kernel)"; exit 2; } done PIDFILE=$OUT/qemu-headless.pid rm -f "$PIDFILE" cleanup() { if [ -s "$PIDFILE" ] && kill -0 "$(cat "$PIDFILE")" 2>/dev/null; then kill "$(cat "$PIDFILE")" 2>/dev/null || true fi [ -n "${CURL_PID:-}" ] && kill "$CURL_PID" 2>/dev/null || true } trap cleanup EXIT # Network: QEMU's user-mode NAT. guestfwd runs a tiny test web server for # 10.0.2.100:80 (and `cat` as an echo server on :7); 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=$OUT/headless-curl.txt rm -f "$CURL_OUT" ( for _ in $(seq 1 80); do 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=$! # virtio-gpu, virtio-rng, and a virtio console (hvc0) fed from # scripts/hvc0-session.txt; its output goes to OUT/headless-hvc0.txt. HVC_OUT=$OUT/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=$OUT/headless-serial.log echo "[run-headless] kernel $IMG" echo "[run-headless] disk 0 (B:) = $IMAGE" set +e timeout "${QEMU_TIMEOUT:-120}" qemu-system-x86_64 -smp 4 -cpu "${QEMU_CPU:-max}" \ -pidfile "$PIDFILE" \ -drive format=raw,file="$IMG",snapshot=on \ -drive file="$IMAGE",format=raw,if=virtio,snapshot=on \ -drive file="$KT/ext2.img",format=raw,if=virtio,snapshot=on \ -drive file="$KT/exfat.img",format=raw,if=virtio,snapshot=on \ -drive file="$KT/btrfs.img",format=raw,if=virtio,snapshot=on \ -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 (same list as the kernel repo at the pinned commit). if [ "$code" = 33 ]; then expect=( "$KERNEL_BANNER x86_64" # ver "[smp] 4 CPUs online" # smp.rs started 3 more cores "[fpu] " # XSAVE (-cpu max) or FXSAVE (qemu64) "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---- fb0" # ls -l /dev (Linux modes since stage 19) "crw------- 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 "[threads] all checks passed" # stage 15: libc plumbing "[echoserver] all checks passed" "[ptytest] all checks passed" "[nbtest] all checks passed" "[fputest] all checks passed" # stage 15.1: musl prerequisites "[pathtest] all checks passed" "rawabi: uname: Rustbelt $KERNEL_VERSION" # a no-libc gcc binary "rawabi: all checks passed" "sh: nosuchprogram: command not found" "cd: there is no drive Q:" "A:/tmp $ pwd" ) if [ -n "${EXTRA_EXPECT:-}" ]; then while IFS= read -r line; do case $line in ''|'#'*) continue ;; esac line=${line//@KERNEL_VERSION@/$KERNEL_VERSION} line=${line//@BIN_COUNT@/$BIN_COUNT} expect+=("$line") done < "$EXTRA_EXPECT" fi 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` -> $BIN_COUNT (the programs), # `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 "$BIN_COUNT" 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 $KERNEL_VERSION" "$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; } # hvc0 is a tty now: its output turns "\n" into "\r\n" (ONLCR), so # drop the '\r's before comparing whole lines. tr -d '\r' < "$HVC_OUT" > "$HVC_OUT.txt" 2>/dev/null && mv "$HVC_OUT.txt" "$HVC_OUT" # The shell on hvc0 ran the host's commands and answered over hvc0. for line in "Rustbelt getty: shell on this terminal" "Rustbelt $KERNEL_VERSION ($STAGE" \ "$ 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