summaryrefslogtreecommitdiffstats
path: root/scripts
diff options
context:
space:
mode:
Diffstat (limited to 'scripts')
-rwxr-xr-xscripts/boot-image.sh65
-rwxr-xr-xscripts/boot.sh90
-rwxr-xr-xscripts/build-bootloader.sh35
-rwxr-xr-xscripts/build-image-release.sh30
-rwxr-xr-xscripts/build.sh49
-rwxr-xr-xscripts/debug-kernel.py16
-rw-r--r--scripts/lib-boot-args.sh38
-rw-r--r--scripts/lib.sh168
-rwxr-xr-xscripts/mksystem.sh144
-rwxr-xr-xscripts/qemu.sh99
-rwxr-xr-xscripts/run.sh142
-rwxr-xr-xscripts/test-disk.sh131
-rwxr-xr-xscripts/test-fault.sh85
-rwxr-xr-xscripts/test-gfx.sh84
-rwxr-xr-xscripts/test-mouse.sh125
-rwxr-xr-xscripts/test-msg.sh140
-rwxr-xr-xscripts/tests/sendkeys.py (renamed from scripts/sendkeys.py)12
-rwxr-xr-xscripts/tests/test-disk.sh67
-rwxr-xr-xscripts/tests/test-fault.sh47
-rwxr-xr-xscripts/tests/test-focus.py (renamed from scripts/test-focus.py)7
-rwxr-xr-xscripts/tests/test-gfx.sh37
-rwxr-xr-xscripts/tests/test-mouse.sh83
-rwxr-xr-xscripts/tests/test-msg.sh98
23 files changed, 786 insertions, 1006 deletions
diff --git a/scripts/boot-image.sh b/scripts/boot-image.sh
deleted file mode 100755
index f70dcd4..0000000
--- a/scripts/boot-image.sh
+++ /dev/null
@@ -1,65 +0,0 @@
1#!/usr/bin/env bash
2#
3# scripts/boot-image.sh — boot rs-dos full chain disk image in QEMU
4#
5# Builds the image first if needed, then boots with UEFI firmware through
6# the real boot chain: firmware → rsboot → rsnix kernel.
7#
8# Usage:
9# ./scripts/boot-image.sh [--gui] [--debug] [--mem 512M] [--smp 2]
10#
11# Keybindings:
12# Ctrl-A X — quit
13# Ctrl-A C — QEMU monitor
14#
15# Environment:
16# OVMF path to OVMF firmware (default: /usr/share/edk2/x64/OVMF.4m.fd)
17# MEM memory size (default: 256M)
18# SMP CPU count (default: 1)
19#
20set -euo pipefail
21
22SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
23PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
24cd "$PROJECT_DIR"
25
26IMAGE="${1:-rs-dos.img}"
27
28# ---- config ----
29OVMF="${OVMF:-/usr/share/edk2/x64/OVMF.4m.fd}"
30MEM="${MEM:-256M}"
31SMP="${SMP:-1}"
32GUI=""
33DEBUG=""
34
35# ---- args ----
36source "$SCRIPT_DIR/lib-boot-args.sh"
37parse_boot_args "$@"
38
39# ---- build image if missing ----
40if [ ! -f "$IMAGE" ]; then
41 echo "Image not found, building..."
42 "$SCRIPT_DIR/mksystem.sh" --chain --gpt "$IMAGE"
43fi
44
45# ---- boot ----
46echo ""
47echo "=== Booting (full chain: firmware → rsboot → rsnix) ==="
48echo " image: $IMAGE"
49echo " ovmf: $OVMF"
50echo " mem: $MEM smp: $SMP"
51echo " gui: $([ -n "$GUI" ] && echo yes || echo no)"
52echo " gdb: $([ -n "$DEBUG" ] && echo ':1234' || echo off)"
53echo ""
54
55exec qemu-system-x86_64 \
56 -bios "$OVMF" \
57 -drive file="$IMAGE",format=raw,if=none,id=hd \
58 -device ide-hd,drive=hd \
59 -serial stdio \
60 -m "$MEM" \
61 -machine q35 \
62 -cpu qemu64 \
63 -no-reboot \
64 -smp "$SMP" \
65 $GUI $DEBUG
diff --git a/scripts/boot.sh b/scripts/boot.sh
deleted file mode 100755
index cb017d8..0000000
--- a/scripts/boot.sh
+++ /dev/null
@@ -1,90 +0,0 @@
1#!/usr/bin/env bash
2#
3# scripts/boot.sh — quick direct boot in QEMU (dev loop)
4#
5# Builds the kernel, stages the system image, and boots directly as
6# BOOTX64.EFI via QEMU's FAT driver. One image = one drive.
7#
8# Usage:
9# ./scripts/boot.sh [--gui] [--debug] [--release] [--mem 512M] [--smp 2]
10#
11# Boot chain: firmware → rsnix (direct)
12# Full chain: use ./scripts/boot-image.sh (firmware → rsboot → rsnix)
13#
14# Keybindings:
15# Ctrl-A X — quit
16# Ctrl-A C — QEMU monitor
17#
18# Environment:
19# OVMF path to OVMF firmware (default: /usr/share/edk2/x64/OVMF.4m.fd)
20# MEM memory size (default: 256M)
21# SMP CPU count (default: 1)
22# SYSTEM_IMG system image path (default: /tmp/system.img)
23#
24set -euo pipefail
25
26SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
27PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
28cd "$PROJECT_DIR"
29
30# ---- config ----
31OVMF="${OVMF:-/usr/share/edk2/x64/OVMF.4m.fd}"
32MEM="${MEM:-256M}"
33SMP="${SMP:-1}"
34SYSTEM_IMG="${SYSTEM_IMG:-/tmp/system.img}"
35PROFILE="release"
36
37GUI=""
38DEBUG=""
39
40# ---- args ----
41source "$SCRIPT_DIR/lib-boot-args.sh"
42handle_arg() {
43 case "$1" in
44 --debug)
45 PROFILE="debug"
46 CARGO_FLAGS=""
47 ;;
48 *)
49 echo "unknown: $1"
50 exit 1
51 ;;
52 esac
53}
54parse_boot_args "$@"
55
56OUT_DIR="target/x86_64-unknown-uefi/$PROFILE"
57
58# ---- build ----
59echo "=== Building kernel ($PROFILE) ==="
60if [ "$PROFILE" = "release" ]; then
61 cargo build --release --package rsdos --target x86_64-unknown-uefi
62else
63 cargo build --package rsdos --target x86_64-unknown-uefi
64fi
65
66# ---- stage system image ----
67echo "=== Staging system image ==="
68"$SCRIPT_DIR/mksystem.sh" --force "$SYSTEM_IMG"
69echo " system image: $SYSTEM_IMG"
70
71# ---- boot ----
72echo ""
73echo "=== Booting (direct: firmware → rsnix) ==="
74echo " ovmf: $OVMF"
75echo " mem: $MEM smp: $SMP"
76echo " gui: $([ -n "$GUI" ] && echo yes || echo no)"
77echo " gdb: $([ -n "$DEBUG" ] && echo ':1234' || echo off)"
78echo ""
79
80exec qemu-system-x86_64 \
81 -bios "$OVMF" \
82 -drive "file=$SYSTEM_IMG,format=raw,if=none,id=disk" \
83 -device ide-hd,drive=disk,bus=ide.0 \
84 -serial stdio \
85 -m "$MEM" \
86 -machine q35 \
87 -cpu qemu64 \
88 -no-reboot \
89 -smp "$SMP" \
90 $GUI $DEBUG
diff --git a/scripts/build-bootloader.sh b/scripts/build-bootloader.sh
deleted file mode 100755
index f0f0ce7..0000000
--- a/scripts/build-bootloader.sh
+++ /dev/null
@@ -1,35 +0,0 @@
1#!/usr/bin/env bash
2#
3# scripts/build-bootloader.sh — build rsboot (UEFI bootloader)
4#
5# rsboot is a git submodule at rsboot/. This builds it and places
6# BOOTX64.EFI into build/bootloader/.
7#
8# Usage:
9# ./scripts/build-bootloader.sh
10#
11# Output:
12# build/bootloader/BOOTX64.EFI
13#
14set -euo pipefail
15
16SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
17PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
18BUILD_DIR="$PROJECT_DIR/build"
19BOOTLOADER_DIR="$BUILD_DIR/bootloader"
20
21echo "=== Building rsboot (UEFI bootloader) ==="
22
23# Ensure submodule is checked out, tracking the latest upstream HEAD
24# (--remote fetches and checks out the newest main instead of reverting
25# to the commit recorded in the superproject's gitlink).
26cd "$PROJECT_DIR"
27git submodule update --init --remote rsboot
28
29cd "$PROJECT_DIR/rsboot"
30make clean 2>/dev/null || true
31make -j"$(nproc)" 2>&1 | sed 's/^/ /'
32
33echo ""
34echo "=== rsboot build complete ==="
35ls -lh "$BOOTLOADER_DIR/BOOTX64.EFI"
diff --git a/scripts/build-image-release.sh b/scripts/build-image-release.sh
deleted file mode 100755
index e4c5250..0000000
--- a/scripts/build-image-release.sh
+++ /dev/null
@@ -1,30 +0,0 @@
1#!/usr/bin/env bash
2#
3# scripts/build-image-release.sh — build the release GPT disk image
4#
5# Produces a single dd-able system image with the release build of
6# the kernel, rsboot bootloader, and all binaries in /bin/.
7#
8# Usage:
9# ./scripts/build-image-release.sh [output.img]
10#
11set -euo pipefail
12
13SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
14PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
15cd "$PROJECT_DIR"
16
17IMAGE="${1:-rs-dos.img}"
18
19echo "=== Building release kernel ==="
20cargo build --release --package rsdos --target x86_64-unknown-uefi
21
22echo "=== Building system image (chain + GPT) ==="
23"$SCRIPT_DIR/mksystem.sh" --chain --gpt "$IMAGE"
24
25echo ""
26echo "=== Done: $IMAGE ==="
27ls -lh "$IMAGE"
28echo ""
29echo "Boot with: ./scripts/boot-image.sh --gui"
30echo "USB: sudo dd if=$IMAGE of=/dev/sdX bs=4M status=progress"
diff --git a/scripts/build.sh b/scripts/build.sh
new file mode 100755
index 0000000..71430a4
--- /dev/null
+++ b/scripts/build.sh
@@ -0,0 +1,49 @@
1#!/usr/bin/env bash
2#
3# scripts/build.sh — compile the kernel and every external binary.
4#
5# The only cargo invocation in the project. One pass builds the kernel
6# (rsdos.efi) and all /bin binaries (bins/*.efi), which is exactly what
7# the system image stages.
8#
9# Usage:
10# ./scripts/build.sh release (default — what the image uses)
11# ./scripts/build.sh --debug debug profile (slower guest, GDB symbols)
12# ./scripts/build.sh --help
13#
14# Output:
15# target/x86_64-unknown-uefi/<profile>/rsdos.efi kernel
16# target/x86_64-unknown-uefi/<profile>/<name>.efi /bin binaries
17#
18# Typical flow:
19# ./scripts/build.sh && ./scripts/run.sh
20#
21set -euo pipefail
22
23usage() {
24 awk 'NR > 1 && /^#/ { sub(/^# ?/, ""); print; next } NR > 1 { exit }' "$0"
25}
26
27# PROFILE is read by lib.sh (it derives OUT_DIR/KERNEL from it), so parse
28# before sourcing.
29while [ $# -gt 0 ]; do
30 case "$1" in
31 --debug) PROFILE="debug" ;;
32 --release) PROFILE="release" ;;
33 --help | -h)
34 usage
35 exit 0
36 ;;
37 *)
38 echo "build.sh: unknown argument: $1" >&2
39 exit 1
40 ;;
41 esac
42 shift
43done
44
45# shellcheck source=lib.sh
46source "$(dirname "$0")/lib.sh"
47
48build_all
49echo "kernel: $KERNEL"
diff --git a/scripts/debug-kernel.py b/scripts/debug-kernel.py
index c794ad6..c675045 100755
--- a/scripts/debug-kernel.py
+++ b/scripts/debug-kernel.py
@@ -2,20 +2,22 @@
2GDB Python script for debugging raisernics under QEMU. 2GDB Python script for debugging raisernics under QEMU.
3 3
4Usage: 4Usage:
5 1. Terminal 1: ./tools/qemu.sh --debug 5 1. Terminal 1: ./scripts/run.sh --debug (GDB stub on :1234)
6 2. Terminal 2: gdb -x tools/debug-kernel.py target/x86_64-unknown-uefi/debug/raisernics.efi 6 Add PROFILE=debug for line-level symbols (slower guest).
7 2. Terminal 2: gdb -x scripts/debug-kernel.py
7""" 8"""
8 9
9import gdb 10import gdb
10import os 11import os
11 12
12# Find the kernel binary 13# Find the kernel binary (debug profile preferred, release as fallback)
13script_dir = os.path.dirname(os.path.abspath(__file__)) 14script_dir = os.path.dirname(os.path.abspath(__file__))
14project_root = os.path.dirname(script_dir) 15project_root = os.path.dirname(script_dir)
15binary = os.path.join( 16candidates = [
16 project_root, 17 os.path.join(project_root, "target/x86_64-unknown-uefi/debug/rsdos.efi"),
17 "target/x86_64-unknown-uefi/debug/raisernics.efi" 18 os.path.join(project_root, "target/x86_64-unknown-uefi/release/rsdos.efi"),
18) 19]
20binary = next((c for c in candidates if os.path.exists(c)), candidates[0])
19 21
20# Connect to QEMU's GDB stub 22# Connect to QEMU's GDB stub
21gdb.execute("target remote :1234") 23gdb.execute("target remote :1234")
diff --git a/scripts/lib-boot-args.sh b/scripts/lib-boot-args.sh
deleted file mode 100644
index ec97500..0000000
--- a/scripts/lib-boot-args.sh
+++ /dev/null
@@ -1,38 +0,0 @@
1#!/usr/bin/env bash
2#
3# scripts/lib-boot-args.sh — shared CLI parsing for the QEMU boot scripts.
4# Sourced, not executed. The caller sets default values for GUI/DEBUG/
5# MEM/SMP first, then calls `parse_boot_args "$@"`. A caller with extra
6# flags defines its own `handle_arg()` to intercept them before the
7# shared cases run (unknown args still fail loudly).
8set -euo pipefail
9
10parse_boot_args() {
11 while [ $# -gt 0 ]; do
12 case "$1" in
13 --gui | -g) GUI="-vga std" ;;
14 --debug | -d) DEBUG="-s -S" ;;
15 --mem)
16 shift
17 MEM="$1"
18 ;;
19 --smp)
20 shift
21 SMP="$1"
22 ;;
23 --help | -h)
24 head -20 "$0" | grep "^#" | sed 's/^# //'
25 exit 0
26 ;;
27 *) handle_arg "$1" ;;
28 esac
29 shift
30 done
31}
32
33# Default: no extra flags. A script with more defines its own before
34# calling parse_boot_args.
35handle_arg() {
36 echo "unknown: $1"
37 exit 1
38}
diff --git a/scripts/lib.sh b/scripts/lib.sh
new file mode 100644
index 0000000..d5aecac
--- /dev/null
+++ b/scripts/lib.sh
@@ -0,0 +1,168 @@
1#!/usr/bin/env bash
2#
3# scripts/lib.sh — shared config and helpers for the rs-dos scripts.
4#
5# Sourced, never executed. Everything here is the single definition of
6# "how we build and how we boot": the build script, the image assembler,
7# the QEMU runner and the tests all read their settings from this file.
8#
9# Config is environment-overridable (TARGET, PROFILE, SYSTEM_IMG, OVMF,
10# QEMU, MEM, SMP) so callers and CI can tune it without editing scripts.
11set -euo pipefail
12
13SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
14PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
15TESTS_DIR="$SCRIPT_DIR/tests"
16
17# ---- config ---------------------------------------------------------------
18TARGET="${TARGET:-x86_64-unknown-uefi}"
19PROFILE="${PROFILE:-release}"
20OUT_DIR="${OUT_DIR:-$PROJECT_DIR/target/$TARGET/$PROFILE}"
21KERNEL="$OUT_DIR/rsdos.efi"
22SYSTEM_IMG="${SYSTEM_IMG:-/tmp/system.img}"
23OVMF="${OVMF:-/usr/share/edk2/x64/OVMF.4m.fd}"
24QEMU="${QEMU:-qemu-system-x86_64}"
25MEM="${MEM:-256M}"
26SMP="${SMP:-1}"
27
28# Binaries whose launch surface is a window rather than a terminal
29# (ADR 0006): staged into /bin/gui, everything else into /bin/cli.
30GUI_BINS="paint"
31
32cd "$PROJECT_DIR"
33
34# ---- build ----------------------------------------------------------------
35#
36# The one and only cargo invocation in the repo. Builds the kernel and
37# every external binary in a single pass so the image can stage both.
38
39cargo_flags() {
40 [ "$PROFILE" = "release" ] && printf '%s' "--release"
41}
42
43build_all() {
44 echo "=== build (kernel + bins, $PROFILE) ==="
45 # shellcheck disable=SC2046 # cargo_flags is a flag word, intentionally split
46 cargo build --workspace --target "$TARGET" $(cargo_flags)
47}
48
49# ---- system image ---------------------------------------------------------
50
51# stage_image [mksystem args...] — assemble /tmp/system.img if it is stale.
52# Freshness is decided inside mksystem.sh; an up-to-date image is never
53# touched, so files written at runtime survive.
54stage_image() {
55 "$SCRIPT_DIR/mksystem.sh" "$@"
56}
57
58# ---- QEMU -----------------------------------------------------------------
59
60# qemu_args — the common machine every boot shares: OVMF, the system
61# image as the single SATA drive, 256M q35. Callers append display,
62# monitor and serial choices.
63qemu_args() {
64 printf '%s\n' \
65 -bios "$OVMF" \
66 -drive "file=$SYSTEM_IMG,format=raw,if=none,id=disk" \
67 -device ide-hd,drive=disk,bus=ide.0 \
68 -m "$MEM" \
69 -machine q35 \
70 -cpu qemu64 \
71 -no-reboot \
72 -smp "$SMP"
73}
74
75# ---- test harness ---------------------------------------------------------
76#
77# Every test-*.sh does:
78# TNAME=disk; source lib.sh; test_boot # waits for the shell
79# sendkeys "ls /disk" "poweroff"
80# wait_qemu_exit
81# Runtime files are namespaced per test so parallel runs cannot collide.
82
83TNAME="${TNAME:-rsdos}"
84QPID="/tmp/rsdos-$TNAME-qpid"
85SOCK="/tmp/rsdos-$TNAME-qmon.sock"
86LOG="/tmp/rsdos-$TNAME-serial.log"
87QLOG="/tmp/rsdos-$TNAME-qemu.log"
88
89fail() {
90 echo "FAIL: $1"
91 [ -f "$LOG" ] && tail -40 "$LOG"
92 exit 1
93}
94
95# wait_log FILE PATTERN TIMEOUT-SECONDS — poll a file for a regex.
96wait_log() {
97 local i=0
98 while [ "$i" -lt $((${3:-60} * 10)) ]; do
99 grep -q "$2" "$1" 2>/dev/null && return 0
100 sleep 0.1
101 i=$((i + 1))
102 done
103 return 1
104}
105
106# wait_qemu_exit TIMEOUT-SECONDS — poll until the pid is gone.
107wait_qemu_exit() {
108 local i=0
109 while [ "$i" -lt $((${1:-30} * 10)) ]; do
110 kill -0 "$(cat "$QPID" 2>/dev/null)" 2>/dev/null || return 0
111 sleep 0.1
112 i=$((i + 1))
113 done
114 return 1
115}
116
117# qemu_hmp CMD — run one HMP command on the monitor socket.
118qemu_hmp() {
119 python3 - "$SOCK" "$1" <<'EOF'
120import socket, sys, time
121s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
122s.connect(sys.argv[1])
123s.sendall((sys.argv[2] + "\n").encode())
124time.sleep(0.4)
125EOF
126}
127
128# sendkeys "line" ... — type shell commands into the guest via sendkeys.py.
129sendkeys() {
130 python3 "$TESTS_DIR/sendkeys.py" --sock "$SOCK" "$@"
131}
132
133# test_cleanup — reap a leftover QEMU from a previous run of this test
134# and clear this test's runtime files. Called before test_boot.
135test_cleanup() {
136 kill "$(cat "$QPID" 2>/dev/null)" 2>/dev/null || true
137 rm -f "$SOCK" "$LOG"
138}
139
140# test_on_exit — EXIT trap: never leave a QEMU behind, but keep the logs
141# (they are the post-mortem for a failed test).
142test_on_exit() {
143 kill "$(cat "$QPID" 2>/dev/null)" 2>/dev/null || true
144}
145
146# test_boot [extra-qemu-args...] — boot the system image headless with the
147# serial log at $LOG and a monitor socket at $SOCK, then wait for the
148# shell. Extra args attach more drives (e.g. a CRASHLOG stick).
149test_boot() {
150 test_cleanup
151 trap test_on_exit EXIT
152 # A stale/poisoned OVMF varstore makes the firmware skip the
153 # \EFI\BOOT fallback entirely — every test boots from a fresh copy.
154 # Namespaced per test so parallel runs cannot race on it.
155 local nv="/tmp/rsdos-ovmf-vars-$TNAME.fd"
156 rm -f "$nv"
157 cp "$OVMF" "$nv"
158 local -a qargs
159 mapfile -t qargs < <(qemu_args)
160 "$QEMU" -bios "$nv" \
161 "${qargs[@]}" \
162 -serial "file:$LOG" \
163 -monitor "unix:$SOCK,server,nowait" \
164 -display none \
165 "$@" >"$QLOG" 2>&1 &
166 echo $! >"$QPID"
167 wait_log "$LOG" "boot: entering shell" 90 || fail "kernel never reached the shell"
168}
diff --git a/scripts/mksystem.sh b/scripts/mksystem.sh
index 044c86c..053fb3b 100755
--- a/scripts/mksystem.sh
+++ b/scripts/mksystem.sh
@@ -2,44 +2,49 @@
2# 2#
3# scripts/mksystem.sh — assemble the single dd-able rs-dos system image 3# scripts/mksystem.sh — assemble the single dd-able rs-dos system image
4# 4#
5# Replaces the old two-source setup (vvfat `esp/` directory for boot + 5# One FAT32 image holds EVERYTHING (boot medium and data medium are the
6# separate /tmp/disk.img for data). One FAT32 image holds EVERYTHING: 6# same disk, mounted by the kernel at /disk/<UID>/):
7# 7#
8# \bzImage kernel (rsbl requires this exact path) 8# \EFI\BOOT\BOOTX64.EFI the kernel — the firmware boots it directly
9# \initramfs.cpio.gz empty initramfs (rsbl requires it) 9# \bzImage kernel copy (rsbl's expected path, real hw)
10# \EFI\BOOT\BOOTX64.EFI boot entry: the kernel (default) or rsboot 10# \initramfs.cpio.gz empty initramfs (rsbl requires the path)
11# \bin\cli\*.efi terminal binaries (invoked by the shell)
12# \bin\gui\*.efi windowed binaries (launched with no shell)
11# \etc\motd.txt seed config 13# \etc\motd.txt seed config
12# \bin\cli\ \bin\gui\ binary tree (empty until Phase 2/4 migrate tools)
13# \*.txt seed files (seed/) 14# \*.txt seed files (seed/)
14# 15#
15# The firmware boots from this image and the kernel mounts the same 16# Freshness: an existing image is only rebuilt when a kernel/binary/seed
16# volume at /disk/<UID>/ — boot medium and data medium are one disk. 17# file is newer than it, so files written at runtime survive across
18# boots. --force always re-creates (wipes runtime data).
17# 19#
18# Usage: 20# Usage:
19# ./scripts/mksystem.sh [options] [output.img] # default /tmp/system.img 21# ./scripts/mksystem.sh [options] [output.img] default: $SYSTEM_IMG
20# 22#
21# Options: 23# Options:
22# --force rebuild even if the image exists (Makefile stamps handle 24# --force rebuild even if the image is up to date
23# normal freshness; without --force an existing image is kept,
24# so user files survive across reboots)
25# --size NM image size in MB (default 64) 25# --size NM image size in MB (default 64)
26# --chain \EFI\BOOT\BOOTX64.EFI = rsboot bootloader (full chain:
27# firmware → rsboot → rsnix). Default is the kernel itself
28# (direct: firmware → rsnix), matching the old dev loop.
29# --gpt wrap the FAT volume in a GPT partition table (for dd-ing
30# to real USB sticks whose firmware refuses superfloppies)
31# --label NAME FAT volume label (default RSDOS) 26# --label NAME FAT volume label (default RSDOS)
27# --gpt wrap the FAT volume in a GPT partition table (for
28# dd-ing to real USB sticks whose firmware refuses
29# superfloppies)
30# --help show this help
31#
32# Needs: dosfstools (mkfs.fat), mtools (mcopy/mmd), cargo (kernel+bins
33# built via scripts/build.sh when missing).
32# 34#
33set -euo pipefail 35set -euo pipefail
34 36
35SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" 37SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
36PROJECT_DIR="$(dirname "$SCRIPT_DIR")" 38# Absolute self path: lib.sh cd's to the project root, so a relative $0
37cd "$PROJECT_DIR" 39# would no longer resolve by the time --help prints the header.
40SELF="$SCRIPT_DIR/$(basename "$0")"
41
42# shellcheck source=lib.sh
43source "$SCRIPT_DIR/lib.sh"
38 44
39IMG="/tmp/system.img" 45IMG="$SYSTEM_IMG"
40SIZE_MB=64 46SIZE_MB=64
41FORCE=0 47FORCE=0
42CHAIN=0
43GPT=0 48GPT=0
44LABEL="RSDOS" 49LABEL="RSDOS"
45 50
@@ -52,18 +57,15 @@ while [ $# -gt 0 ]; do
52 shift 57 shift
53 SIZE_MB="$1" 58 SIZE_MB="$1"
54 ;; 59 ;;
55 --chain)
56 CHAIN=1
57 ;;
58 --gpt)
59 GPT=1
60 ;;
61 --label) 60 --label)
62 shift 61 shift
63 LABEL="$1" 62 LABEL="$1"
64 ;; 63 ;;
64 --gpt)
65 GPT=1
66 ;;
65 --help | -h) 67 --help | -h)
66 head -30 "$0" | grep "^#" | sed 's/^# \?//' 68 awk 'NR > 1 && /^#/ { sub(/^# ?/, ""); print; next } NR > 1 { exit }' "$SELF"
67 exit 0 69 exit 0
68 ;; 70 ;;
69 -*) 71 -*)
@@ -77,15 +79,9 @@ while [ $# -gt 0 ]; do
77 shift 79 shift
78done 80done
79 81
80TARGET=x86_64-unknown-uefi 82SEED_DIR="$PROJECT_DIR/seed"
81PROFILE=release
82OUT_DIR="target/$TARGET/$PROFILE"
83KERNEL="$OUT_DIR/rsdos.efi"
84BZIMAGE="esp/bzImage"
85INITRD="esp/initramfs.cpio.gz"
86BOOTLOADER="build/bootloader/BOOTX64.EFI"
87 83
88# ---- checks ---- 84# ---- checks ---------------------------------------------------------------
89for cmd in mkfs.fat mcopy mmd; do 85for cmd in mkfs.fat mcopy mmd; do
90 if ! command -v "$cmd" >/dev/null 2>&1; then 86 if ! command -v "$cmd" >/dev/null 2>&1; then
91 echo "mksystem: $cmd not found (install dosfstools + mtools)" >&2 87 echo "mksystem: $cmd not found (install dosfstools + mtools)" >&2
@@ -101,29 +97,26 @@ if [ "$GPT" -eq 1 ]; then
101 done 97 done
102fi 98fi
103 99
104if [ -f "$IMG" ] && [ "$FORCE" -eq 0 ]; then 100# ---- build (only when artifacts are missing; build.sh is the only cargo
105 echo "mksystem: $IMG already exists (use --force to re-create)" 101# invocation, so an already-compiled tree costs nothing) --------------------
106 exit 0 102if [ ! -f "$KERNEL" ] || ! ls "$OUT_DIR"/*.efi >/dev/null 2>&1; then
103 "$SCRIPT_DIR/build.sh"
107fi 104fi
108 105
109# ---- build kernel ---- 106# ---- freshness stamp ------------------------------------------------------
110if [ ! -f "$KERNEL" ]; then 107newest=""
111 echo "mksystem: building kernel..." 108for f in "$OUT_DIR"/*.efi "$SEED_DIR"/*; do
112 cargo build --release --package rsdos --target "$TARGET" 109 [ -f "$f" ] || continue
113fi 110 if [ -z "$newest" ] || [ "$f" -nt "$newest" ]; then
114 111 newest="$f"
115# ---- build bootloader (chain mode only) ---- 112 fi
116if [ "$CHAIN" -eq 1 ] && [ ! -f "$BOOTLOADER" ]; then 113done
117 echo "mksystem: building bootloader..." 114if [ "$FORCE" -eq 0 ] && [ -f "$IMG" ] && { [ -z "$newest" ] || [ ! "$newest" -nt "$IMG" ]; }; then
118 "$SCRIPT_DIR/build-bootloader.sh" 115 echo "mksystem: up to date: $IMG (use --force to re-create)"
116 exit 0
119fi 117fi
120 118
121# ---- deploy kernel artifacts to esp/ (shared with the old layout) ---- 119# ---- create the FAT volume ------------------------------------------------
122mkdir -p esp/EFI/BOOT
123cp "$KERNEL" "$BZIMAGE"
124[ -f "$INITRD" ] || printf '' | gzip >"$INITRD"
125
126# ---- create the FAT volume ----
127rm -f "$IMG" 120rm -f "$IMG"
128SIZE_BLOCKS=$((SIZE_MB * 1024)) # mkfs.fat counts 1024-byte blocks 121SIZE_BLOCKS=$((SIZE_MB * 1024)) # mkfs.fat counts 1024-byte blocks
129echo "mksystem: creating $IMG (${SIZE_MB}M FAT32, label $LABEL)" 122echo "mksystem: creating $IMG (${SIZE_MB}M FAT32, label $LABEL)"
@@ -131,34 +124,47 @@ mkfs.fat -C -F 32 -n "$LABEL" "$IMG" "$SIZE_BLOCKS" >/dev/null
131 124
132echo "mksystem: staging system tree" 125echo "mksystem: staging system tree"
133mmd -i "$IMG" ::/EFI ::/EFI/BOOT ::/etc ::/bin ::/bin/cli ::/bin/gui 126mmd -i "$IMG" ::/EFI ::/EFI/BOOT ::/etc ::/bin ::/bin/cli ::/bin/gui
134mcopy -i "$IMG" "$BZIMAGE" ::/bzImage 127
128# The firmware entry point and rsbl's kernel path are the same file.
129mcopy -i "$IMG" "$KERNEL" ::/EFI/BOOT/BOOTX64.EFI
130mcopy -i "$IMG" "$KERNEL" ::/bzImage
131INITRD="$(mktemp -t rsdos-initrd.XXXXXX)"
132trap 'rm -f "$INITRD"' EXIT
133printf '' | gzip >"$INITRD"
135mcopy -i "$IMG" "$INITRD" ::/initramfs.cpio.gz 134mcopy -i "$IMG" "$INITRD" ::/initramfs.cpio.gz
136if [ "$CHAIN" -eq 1 ]; then 135echo " \\EFI\\BOOT\\BOOTX64.EFI \\bzImage \\initramfs.cpio.gz"
137 mcopy -i "$IMG" "$BOOTLOADER" ::/EFI/BOOT/BOOTX64.EFI 136
138 echo " \\EFI\\BOOT\\BOOTX64.EFI <- rsboot (full chain)" 137# External binaries: everything under bins/ was compiled into $OUT_DIR.
139else 138# Launch surface decides the directory (ADR 0006): GUI_BINS -> /bin/gui,
140 mcopy -i "$IMG" "$KERNEL" ::/EFI/BOOT/BOOTX64.EFI 139# everything else -> /bin/cli.
141 echo " \\EFI\\BOOT\\BOOTX64.EFI <- kernel (direct)" 140for src in "$OUT_DIR"/*.efi; do
142fi 141 [ -f "$src" ] || continue
143echo " \\bzImage \\initramfs.cpio.gz \\bin{\\cli,\\gui} \\etc" 142 name="$(basename "$src" .efi)"
143 [ "$name" = "rsdos" ] && continue # the kernel is not a /bin binary
144 case " $GUI_BINS " in
145 *" $name "*) kind="gui" ;;
146 *) kind="cli" ;;
147 esac
148 mcopy -i "$IMG" "$src" "::/bin/$kind/$name.efi"
149 printf ' \\bin\\%s\\%s.efi\n' "$kind" "$name"
150done
144 151
145# Seed files: motd lives in \etc, everything else at the volume root. 152# Seed files: motd lives in \etc, everything else at the volume root.
146SEED_DIR="$PROJECT_DIR/seed"
147if [ -d "$SEED_DIR" ]; then 153if [ -d "$SEED_DIR" ]; then
148 for f in "$SEED_DIR"/*; do 154 for f in "$SEED_DIR"/*; do
149 [ -f "$f" ] || continue 155 [ -f "$f" ] || continue
150 name="$(basename "$f")" 156 name="$(basename "$f")"
151 if [ "$name" = "motd.txt" ]; then 157 if [ "$name" = "motd.txt" ]; then
152 mcopy -i "$IMG" "$f" ::/etc/"$name" 158 mcopy -i "$IMG" "$f" ::/etc/"$name"
153 echo " \\etc\\$name" 159 printf ' \\etc\\%s\n' "$name"
154 else 160 else
155 mcopy -i "$IMG" "$f" ::/"$name" 161 mcopy -i "$IMG" "$f" ::/"$name"
156 echo " \\$name" 162 printf ' \\%s\n' "$name"
157 fi 163 fi
158 done 164 done
159fi 165fi
160 166
161# ---- optional GPT wrapper (real-hardware dd target) ---- 167# ---- optional GPT wrapper (real-hardware dd target) -----------------------
162if [ "$GPT" -eq 1 ]; then 168if [ "$GPT" -eq 1 ]; then
163 GPT_START=2048 # 1 MiB offset (standard alignment) 169 GPT_START=2048 # 1 MiB offset (standard alignment)
164 GPT_IMG="$(mktemp)" 170 GPT_IMG="$(mktemp)"
@@ -179,6 +185,6 @@ fi
179echo "" 185echo ""
180echo "=== Done: $IMG ===" 186echo "=== Done: $IMG ==="
181ls -lh "$IMG" 187ls -lh "$IMG"
182echo "Boot with: make run (or: make boot-image)" 188echo "Boot with: ./scripts/run.sh (or: make run)"
183[ "$GPT" -eq 0 ] && echo "USB dd tip: add --gpt for firmwares that refuse superfloppies" 189[ "$GPT" -eq 0 ] && echo "USB dd tip: add --gpt for firmwares that refuse superfloppies"
184exit 0 190exit 0
diff --git a/scripts/qemu.sh b/scripts/qemu.sh
deleted file mode 100755
index eeaf4de..0000000
--- a/scripts/qemu.sh
+++ /dev/null
@@ -1,99 +0,0 @@
1#!/usr/bin/env bash
2# =============================================================================
3# scripts/qemu.sh — flexible QEMU launcher for rs-dos
4#
5# Usage:
6# ./scripts/qemu.sh # serial console (no graphics)
7# ./scripts/qemu.sh --gui # graphical window
8# ./scripts/qemu.sh --debug # GDB stub on :1234
9# ./scripts/qemu.sh --chain # full boot chain (firmware → rsboot → rsnix)
10# ./scripts/qemu.sh --bios /path/to.fd # custom OVMF
11# ./scripts/qemu.sh --mem 512M # memory size
12# ./scripts/qemu.sh --smp 2 # SMP cores
13#
14# One image, one drive — the system image contains kernel + ESP + data.
15# =============================================================================
16set -euo pipefail
17
18SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
19PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
20cd "$PROJECT_DIR"
21
22# Defaults
23GUI_FLAG=""
24DEBUG_FLAG=""
25CHAIN_FLAG=""
26OVMF="${OVMF:-/usr/share/edk2/x64/OVMF.4m.fd}"
27MEM="${MEM:-256M}"
28SMP="${SMP:-1}"
29SYSTEM_IMG="${SYSTEM_IMG:-/tmp/system.img}"
30
31# Pre-build
32make all
33
34# Parse args
35while [ $# -gt 0 ]; do
36 case "$1" in
37 --gui | -g)
38 GUI_FLAG="-vga std"
39 shift
40 ;;
41 --debug | -d)
42 DEBUG_FLAG="-s -S"
43 shift
44 ;;
45 --chain | -c)
46 CHAIN_FLAG=1
47 SYSTEM_IMG="${SYSTEM_IMG:-rs-dos.img}"
48 shift
49 ;;
50 --image | -i)
51 SYSTEM_IMG="$2"
52 shift 2
53 ;;
54 --bios | -b)
55 OVMF="$2"
56 shift 2
57 ;;
58 --mem)
59 MEM="$2"
60 shift 2
61 ;;
62 --smp)
63 SMP="$2"
64 shift 2
65 ;;
66 *)
67 echo "unknown: $1"
68 exit 1
69 ;;
70 esac
71done
72
73# Stage system image if needed
74if [ -n "$CHAIN_FLAG" ]; then
75 ./scripts/mksystem.sh --chain --gpt "$SYSTEM_IMG"
76else
77 ./scripts/mksystem.sh "$SYSTEM_IMG"
78fi
79
80echo ""
81echo "=== Booting ==="
82echo " image: $SYSTEM_IMG"
83echo " ovmf: $OVMF"
84echo " mem: $MEM smp: $SMP"
85echo " gui: $([ -n "$GUI_FLAG" ] && echo yes || echo no)"
86echo " gdb: $([ -n "$DEBUG_FLAG" ] && echo ':1234' || echo off)"
87echo ""
88
89exec qemu-system-x86_64 \
90 -bios "$OVMF" \
91 -drive "file=$SYSTEM_IMG,format=raw,if=none,id=hd" \
92 -device ide-hd,drive=hd \
93 -serial stdio \
94 -m "$MEM" \
95 -machine q35 \
96 -cpu qemu64 \
97 -no-reboot \
98 -smp "$SMP" \
99 $GUI_FLAG $DEBUG_FLAG
diff --git a/scripts/run.sh b/scripts/run.sh
new file mode 100755
index 0000000..4b6f481
--- /dev/null
+++ b/scripts/run.sh
@@ -0,0 +1,142 @@
1#!/usr/bin/env bash
2#
3# scripts/run.sh — build, stage and boot the system image in QEMU.
4#
5# Usage:
6# ./scripts/run.sh build + stage + boot (window + serial)
7# ./scripts/run.sh --headless serial console only, no window
8# ./scripts/run.sh --debug GDB stub on :1234, CPU starts paused
9# (attach: gdb -x scripts/debug-kernel.py)
10# ./scripts/run.sh --monitor HMP monitor socket for sendkeys.py /
11# test-focus.py (default path below)
12# ./scripts/run.sh --fresh re-create the system image first
13# (wipes runtime data on the image)
14# ./scripts/run.sh --mem 512M guest memory (default 256M)
15# ./scripts/run.sh --smp 2 guest CPUs (default 1)
16# ./scripts/run.sh --bios FILE OVMF firmware (default: system path)
17# ./scripts/run.sh --image FILE system image (default: /tmp/system.img)
18# ./scripts/run.sh --grab confine the pointer: GTK runs as an X11
19# (XWayland) client and grabs on hover
20# ./scripts/run.sh --no-grab Wayland-native window, no pointer grab
21#
22# Mouse: Wayland forbids global pointer grabs, so a Wayland-native window
23# cannot confine the cursor (QEMU's grab silently does nothing).
24# In a Wayland session with XWayland, run.sh defaults to --grab;
25# Ctrl+Alt+G toggles the grab in either mode.
26#
27# Environment (same names the other scripts honour): TARGET, PROFILE,
28# SYSTEM_IMG, OVMF, QEMU, MEM, SMP.
29#
30# Two-command flow: ./scripts/build.sh && ./scripts/run.sh
31# One-command flow: ./scripts/run.sh (builds what is missing)
32#
33# Monitor socket: /tmp/rsdos-monitor.sock
34#
35set -euo pipefail
36
37SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
38
39usage() {
40 awk 'NR > 1 && /^#/ { sub(/^# ?/, ""); print; next } NR > 1 { exit }' "$0"
41}
42
43HEADLESS=0
44DEBUG=0
45FRESH=0
46MON=0
47MON_SOCK="/tmp/rsdos-monitor.sock"
48GRAB="" # "" = auto: grab when a Wayland session has XWayland
49
50while [ $# -gt 0 ]; do
51 case "$1" in
52 --headless | -H) HEADLESS=1 ;;
53 # GDB stub + keep whatever profile the env asked for (release is the
54 # default; PROFILE=debug ./scripts/run.sh --debug adds line symbols).
55 --debug | -d) DEBUG=1 ;;
56 --fresh) FRESH=1 ;;
57 --monitor) MON=1 ;;
58 --mem) shift; MEM="$1" ;;
59 --smp) shift; SMP="$1" ;;
60 --bios) shift; OVMF="$1" ;;
61 --image | -i) shift; SYSTEM_IMG="$1" ;;
62 --grab) GRAB=1 ;;
63 --no-grab) GRAB=0 ;;
64 --help | -h)
65 usage
66 exit 0
67 ;;
68 *)
69 echo "run.sh: unknown argument: $1" >&2
70 usage >&2
71 exit 1
72 ;;
73 esac
74 shift
75done
76
77# shellcheck source=lib.sh
78source "$SCRIPT_DIR/lib.sh" # reads MEM/SMP/OVMF/SYSTEM_IMG from the env
79
80# build.sh is a separate process: it re-derives OUT_DIR/KERNEL from these,
81# so whatever we resolved above (including a --mem/--bios override) wins
82# there too.
83export TARGET PROFILE QEMU MEM SMP OVMF SYSTEM_IMG
84
85# ---- mouse grab (Wayland) --------------------------------------------------
86# Wayland forbids global pointer grabs, so a Wayland-native GTK window cannot
87# confine the cursor — QEMU's grab silently does nothing there. XWayland can,
88# so a Wayland session that has XWayland (DISPLAY set) defaults to GTK-on-X11
89# plus grab-on-hover. Ctrl+Alt+G toggles the grab; --no-grab keeps the
90# Wayland-native window.
91if [ -z "$GRAB" ]; then
92 if [ "${XDG_SESSION_TYPE:-}" = "wayland" ] && [ -n "${DISPLAY:-}" ]; then
93 GRAB=1
94 else
95 GRAB=0
96 fi
97fi
98[ "$HEADLESS" -eq 1 ] && GRAB=0 # no window, nothing to grab
99if [ "$GRAB" -eq 1 ]; then
100 if [ -n "${DISPLAY:-}" ]; then
101 export GDK_BACKEND=x11
102 else
103 echo "run.sh: --grab needs XWayland (DISPLAY is unset); not grabbing" >&2
104 GRAB=0
105 fi
106fi
107
108# ---- build + stage --------------------------------------------------------
109"$SCRIPT_DIR/build.sh"
110if [ "$FRESH" -eq 1 ]; then
111 stage_image --force
112else
113 stage_image
114fi
115
116# ---- boot -----------------------------------------------------------------
117mapfile -t QARGS < <(qemu_args)
118if [ "$HEADLESS" -eq 1 ]; then
119 QARGS+=(-display none)
120else
121 QARGS+=(-vga std)
122 if [ "$GRAB" -eq 1 ]; then
123 QARGS+=(-display gtk,grab-on-hover=on)
124 fi
125fi
126[ "$MON" -eq 1 ] && QARGS+=(-monitor "unix:$MON_SOCK,server,nowait")
127[ "$DEBUG" -eq 1 ] && QARGS+=(-s -S)
128QARGS+=(-serial stdio)
129
130echo ""
131echo "=== boot ==="
132echo " image: $SYSTEM_IMG"
133echo " ovmf: $OVMF"
134echo " mem: $MEM smp: $SMP"
135echo " mode: $([ "$HEADLESS" -eq 1 ] && echo headless || echo window) gdb: $([ "$DEBUG" -eq 1 ] && echo ':1234 (paused)' || echo off)"
136if [ "$HEADLESS" -eq 0 ]; then
137 echo " mouse: $([ "$GRAB" -eq 1 ] && echo 'grab on hover (XWayland; Ctrl+Alt+G toggles)' || echo 'not grabbed (Wayland cannot confine the pointer)')"
138fi
139[ "$MON" -eq 1 ] && echo " monitor: unix:$MON_SOCK"
140echo ""
141
142exec "$QEMU" "${QARGS[@]}"
diff --git a/scripts/test-disk.sh b/scripts/test-disk.sh
deleted file mode 100755
index 881b475..0000000
--- a/scripts/test-disk.sh
+++ /dev/null
@@ -1,131 +0,0 @@
1#!/usr/bin/env bash
2#
3# scripts/test-disk.sh — full automated data-disk round trip.
4#
5# Builds the kernel, ensures the FAT32 image exists (with --fresh it is
6# re-formatted and re-seeded), boots QEMU headless, drives the shell via
7# the monitor, then verifies the results with mtools and fsck.
8#
9# Usage:
10# ./scripts/test-disk.sh # reuse existing image
11# ./scripts/test-disk.sh --fresh # re-format the image first
12#
13# Environment:
14# DISK_IMG data disk path (default: /tmp/disk.img)
15# OVMF firmware (default: /usr/share/edk2/x64/OVMF.4m.fd)
16# QEMU qemu binary (default: qemu-system-x86_64)
17#
18set -euo pipefail
19
20SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
21PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
22cd "$PROJECT_DIR"
23
24FRESH=0
25[ "${1:-}" = "--fresh" ] && FRESH=1
26
27OVMF="${OVMF:-/usr/share/edk2/x64/OVMF.4m.fd}"
28QEMU="${QEMU:-qemu-system-x86_64}"
29IMG="${SYSTEM_IMG:-/tmp/system.img}"
30QPID=/tmp/rsdos-test-qpid
31SOCK=/tmp/rsdos-test-qmon.sock
32LOG=/tmp/rsdos-test-serial.log
33QLOG=/tmp/rsdos-test-qemu.log
34STAMP="$(date +%s)"
35
36# Never leave a QEMU behind (an orphan holding our stdout would hang any
37# caller that pipes our output).
38trap 'kill "$(cat "$QPID" 2>/dev/null)" 2>/dev/null || true' EXIT
39
40fail() {
41 echo "FAIL: $1"
42 [ -f "$LOG" ] && tail -20 "$LOG"
43 exit 1
44}
45
46# wait_log FILE PATTERN TIMEOUT-SECONDS — poll a file for a regex.
47wait_log() {
48 local i=0
49 while [ "$i" -lt $((${3:-60} * 10)) ]; do
50 grep -q "$2" "$1" 2>/dev/null && return 0
51 sleep 0.1
52 i=$((i + 1))
53 done
54 return 1
55}
56
57# wait_qemu_exit TIMEOUT-SECONDS — poll until the pid is gone.
58wait_qemu_exit() {
59 local i=0
60 while [ "$i" -lt $((${1:-30} * 10)) ]; do
61 kill -0 "$(cat "$QPID" 2>/dev/null)" 2>/dev/null || return 0
62 sleep 0.1
63 i=$((i + 1))
64 done
65 return 1
66}
67
68echo "==> 1/6 build kernel (debug)"
69cargo build --release --package rsdos --target x86_64-unknown-uefi
70
71echo "==> 2/6 ensure system image"
72if [ "$FRESH" -eq 1 ]; then
73 "$SCRIPT_DIR/mksystem.sh" --force "$IMG"
74else
75 "$SCRIPT_DIR/mksystem.sh" "$IMG"
76fi
77
78echo "==> 3/6 boot QEMU (headless)"
79kill "$(cat "$QPID" 2>/dev/null)" 2>/dev/null || true
80sleep 1
81rm -f "$SOCK" "$LOG"
82# Throwaway OVMF varstore: a stale/poisoned NvVars (e.g. left by the old
83# vvfat setup) makes the firmware skip the \EFI\BOOT fallback entirely.
84NV="/tmp/rsdos-test-ovmf-vars.fd"
85rm -f "$NV"
86cp /usr/share/edk2/x64/OVMF.4m.fd "$NV"
87"$QEMU" -bios "$NV" \
88 -drive file="$IMG",format=raw,if=none,id=disk \
89 -device ide-hd,drive=disk,bus=ide.0 \
90 -serial file:"$LOG" \
91 -monitor unix:"$SOCK",server,nowait \
92 -m 256M -machine q35 -cpu qemu64 -no-reboot -display none \
93 >"$QLOG" 2>&1 &
94
95wait_log "$LOG" "boot: entering shell" 90 || fail "kernel never reached the shell"
96sleep 1
97
98# Volumes mount under /disk/<UID>/ where UID = "LABEL-SERIAL" (e.g.
99# RSDOS-5BD1E55F). Discover it from the boot log so the test works on
100# any image without knowing the serial in advance.
101VOL_UID="$(grep -a 'fat: mounted' "$LOG" | sed -n 's/.*at \/disk\/\([^ )]*\).*/\1/p' | head -1)"
102[ -n "$VOL_UID" ] || fail "no FAT32 volume was mounted (look for 'fat: mounted' in the log)"
103echo " mounted volume UID: $VOL_UID"
104
105echo "==> 4/6 drive the shell (ls / mkdir / echo WRITING / cat / poweroff)"
106# The filename is deliberately >13 chars so the FAT LFN path gets
107# multi-entry entries (they're stored last-first on disk).
108TESTFILE="disk-test-$STAMP.txt"
109python3 "$SCRIPT_DIR/sendkeys.py" --sock "$SOCK" \
110 "ls /disk" \
111 "mkdir /disk/$VOL_UID/sub" \
112 "echo disk-test-$STAMP WRITING /disk/$VOL_UID/sub/$TESTFILE" \
113 "cat /disk/$VOL_UID/sub/$TESTFILE" \
114 "poweroff"
115
116echo "==> 5/6 wait for shutdown"
117wait_qemu_exit 30 || fail "QEMU did not exit after poweroff"
118
119echo "==> 6/6 verify"
120grep -a "disk-test-$STAMP" "$LOG" >/dev/null || fail "cat output missing from serial log"
121if ! fsck.fat -n "$IMG" >/tmp/rsdos-fsck.out 2>&1; then
122 fail "fsck.fat reports problems: $(cat /tmp/rsdos-fsck.out | tail -3)"
123fi
124MT="$(mtype -i "$IMG" "::/sub/$TESTFILE" 2>/dev/null || true)"
125[ "$MT" = "disk-test-$STAMP" ] || fail "mtype mismatch: got '$MT'"
126
127echo ""
128echo "PASS — disk round trip OK (fsck clean, file survived poweroff)"
129echo " image: $IMG"
130echo " volume: /disk/$VOL_UID"
131echo " written: /disk/$VOL_UID/sub/$TESTFILE = \"disk-test-$STAMP\""
diff --git a/scripts/test-fault.sh b/scripts/test-fault.sh
deleted file mode 100755
index be2d7cd..0000000
--- a/scripts/test-fault.sh
+++ /dev/null
@@ -1,85 +0,0 @@
1#!/usr/bin/env bash
2#
3# scripts/test-fault.sh — fault-diagnostics verification.
4#
5# Proves the fault-dump path: every CPU exception prints a white-on-red
6# band to the framebuffer and a parseable serial line before halting.
7#
8# Usage: ./scripts/test-fault.sh
9# Env: OVMF, QEMU, SYSTEM_IMG
10#
11set -euo pipefail
12
13SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
14PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
15cd "$PROJECT_DIR"
16
17OVMF="${OVMF:-/usr/share/edk2/x64/OVMF.4m.fd}"
18QEMU="${QEMU:-qemu-system-x86_64}"
19SYSTEM_IMG="${SYSTEM_IMG:-/tmp/system.img}"
20QPID=/tmp/rsdos-fault-qpid
21SOCK=/tmp/rsdos-fault-qmon.sock
22LOG=/tmp/rsdos-fault-serial.log
23QLOG=/tmp/rsdos-fault-qemu.log
24
25trap 'kill "$(cat "$QPID" 2>/dev/null)" 2>/dev/null || true' EXIT
26
27fail() {
28 echo "FAIL: $1"
29 [ -f "$LOG" ] && tail -40 "$LOG"
30 exit 1
31}
32
33wait_log() {
34 local i=0
35 while [ "$i" -lt $((${3:-60} * 10)) ]; do
36 grep -q "$2" "$1" 2>/dev/null && return 0
37 sleep 0.1
38 i=$((i + 1))
39 done
40 return 1
41}
42
43echo "==> 1/5 build kernel (debug)"
44cargo build --release --package rsdos --target x86_64-unknown-uefi
45
46echo "==> 2/5 stage system image"
47"$SCRIPT_DIR/mksystem.sh" --force "$SYSTEM_IMG"
48
49echo "==> 3/5 boot QEMU (headless, + dedicated CRASHLOG stick)"
50kill "$(cat "$QPID" 2>/dev/null)" 2>/dev/null || true
51sleep 1
52rm -f "$SOCK" "$LOG"
53CRASH_IMG=/tmp/rsdos-crash.img
54mkfs.fat -C -F 32 -n CRASHLOG "$CRASH_IMG" >/dev/null
55"$QEMU" -bios "$OVMF" \
56 -drive file="$SYSTEM_IMG",format=raw,if=none,id=disk \
57 -device ide-hd,drive=disk,bus=ide.0 \
58 -drive file="$CRASH_IMG",format=raw,if=none,id=crash \
59 -device ide-hd,drive=crash,bus=ide.1 \
60 -serial file:"$LOG" \
61 -monitor unix:"$SOCK",server,nowait \
62 -m 256M -machine q35 -cpu qemu64 -no-reboot -display none \
63 >"$QLOG" 2>&1 &
64echo $! >"$QPID"
65
66wait_log "$LOG" "boot: entering shell" 90 || fail "kernel never reached the shell"
67
68echo "==> 4/5 boot diagnostics present"
69grep -aq "gop: fb base=" "$LOG" || fail "fb geometry diagnostic missing"
70grep -aq "gop: mode .* selected" "$LOG" || fail "mode-selection diagnostic missing"
71grep -aq "CRASHLOG" "$LOG" || fail "CRASHLOG stick not mounted"
72
73echo "==> 5/5 inject a page fault (fault command)"
74python3 "$SCRIPT_DIR/sendkeys.py" --sock "$SOCK" \
75 "clear" \
76 "fault"
77sleep 2
78
79grep -aq "!!! page fault vec=14 .*rip=" "$LOG" || fail "page-fault dump line missing"
80grep -aq "cr2=0x400000000000" "$LOG" || fail "CR2 not reported (faulting address)"
81grep -aq "task=0 \"shell\"" "$LOG" || fail "faulting task not identified"
82grep -aq "!!! halting" "$LOG" || fail "no halt message"
83
84echo ""
85echo "PASS — fault dump verified"
diff --git a/scripts/test-gfx.sh b/scripts/test-gfx.sh
deleted file mode 100755
index 1c7f9ac..0000000
--- a/scripts/test-gfx.sh
+++ /dev/null
@@ -1,84 +0,0 @@
1#!/usr/bin/env bash
2#
3# scripts/test-gfx.sh — automated graphics round-trip test.
4#
5# Builds kernel, boots headless, runs the in-kernel gfx self-test and
6# the draw/paint demo.
7#
8# Env: OVMF, QEMU, SYSTEM_IMG
9#
10set -euo pipefail
11
12SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
13PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
14cd "$PROJECT_DIR"
15
16OVMF="${OVMF:-/usr/share/edk2/x64/OVMF.4m.fd}"
17QEMU="${QEMU:-qemu-system-x86_64}"
18SYSTEM_IMG="${SYSTEM_IMG:-/tmp/system.img}"
19QPID=/tmp/rsdos-gfx-qpid
20SOCK=/tmp/rsdos-gfx-qmon.sock
21LOG=/tmp/rsdos-gfx-serial.log
22QLOG=/tmp/rsdos-gfx-qemu.log
23
24trap 'kill "$(cat "$QPID" 2>/dev/null)" 2>/dev/null || true' EXIT
25
26fail() {
27 echo "FAIL: $1"
28 [ -f "$LOG" ] && tail -40 "$LOG"
29 exit 1
30}
31
32wait_log() {
33 local i=0
34 while [ "$i" -lt $((${3:-60} * 10)) ]; do
35 grep -q "$2" "$1" 2>/dev/null && return 0
36 sleep 0.1
37 i=$((i + 1))
38 done
39 return 1
40}
41
42echo "==> 1/5 build kernel (debug)"
43cargo build --release --package rsdos --target x86_64-unknown-uefi
44
45echo "==> 2/5 stage system image"
46"$SCRIPT_DIR/mksystem.sh" --force "$SYSTEM_IMG"
47
48echo "==> 3/5 boot QEMU (headless)"
49kill "$(cat "$QPID" 2>/dev/null)" 2>/dev/null || true
50sleep 1
51rm -f "$SOCK" "$LOG"
52"$QEMU" -bios "$OVMF" \
53 -drive file="$SYSTEM_IMG",format=raw,if=none,id=disk \
54 -device ide-hd,drive=disk,bus=ide.0 \
55 -serial file:"$LOG" \
56 -monitor unix:"$SOCK",server,nowait \
57 -m 256M -machine q35 -cpu qemu64 -no-reboot -display none \
58 >"$QLOG" 2>&1 &
59echo $! >"$QPID"
60
61wait_log "$LOG" "boot: entering shell" 90 || fail "kernel never reached the shell"
62wait_log "$LOG" "cursor: task " 10 || fail "cursor task did not spawn"
63
64echo "==> 4/5 in-kernel pixel self-test (gfx check)"
65python3 "$SCRIPT_DIR/sendkeys.py" --sock "$SOCK" "gfx check"
66wait_log "$LOG" "gfx: check" 30 || fail "gfx check did not run"
67
68echo "==> 5/5 gfx demo (draw scene)"
69python3 "$SCRIPT_DIR/sendkeys.py" --sock "$SOCK" "gfx" "poweroff"
70wait_qemu_exit() {
71 local i=0
72 while [ "$i" -lt $((${1:-30} * 10)) ]; do
73 kill -0 "$(cat "$QPID" 2>/dev/null)" 2>/dev/null || return 0
74 sleep 0.1
75 i=$((i + 1))
76 done
77 return 1
78}
79wait_qemu_exit 30 || fail "QEMU did not exit after poweroff"
80
81grep -aq "gfx: drew status bar" "$LOG" || fail "gfx demo did not draw"
82
83echo ""
84echo "PASS — gfx round trip"
diff --git a/scripts/test-mouse.sh b/scripts/test-mouse.sh
deleted file mode 100755
index 9f9f018..0000000
--- a/scripts/test-mouse.sh
+++ /dev/null
@@ -1,125 +0,0 @@
1#!/usr/bin/env bash
2#
3# scripts/test-mouse.sh — automated PS/2 mouse round-trip test.
4#
5# Boots QEMU headless, injects mouse movement via QEMU monitor,
6# verifies the on-screen cursor tracks it, and tests the mouse
7# stream routing command.
8#
9# Env: OVMF, QEMU, SYSTEM_IMG
10#
11set -euo pipefail
12
13SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
14PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
15cd "$PROJECT_DIR"
16
17OVMF="${OVMF:-/usr/share/edk2/x64/OVMF.4m.fd}"
18QEMU="${QEMU:-qemu-system-x86_64}"
19SYSTEM_IMG="${SYSTEM_IMG:-/tmp/system.img}"
20QPID=/tmp/rsdos-mouse-qpid
21SOCK=/tmp/rsdos-mouse-qmon.sock
22LOG=/tmp/rsdos-mouse-serial.log
23QLOG=/tmp/rsdos-mouse-qemu.log
24
25trap 'kill "$(cat "$QPID" 2>/dev/null)" 2>/dev/null || true' EXIT
26
27fail() {
28 echo "FAIL: $1"
29 [ -f "$LOG" ] && tail -40 "$LOG"
30 exit 1
31}
32
33wait_log() {
34 local i=0
35 while [ "$i" -lt $((${3:-60} * 10)) ]; do
36 grep -q "$2" "$1" 2>/dev/null && return 0
37 sleep 0.1
38 i=$((i + 1))
39 done
40 return 1
41}
42
43qemu_hmp() {
44 python3 - "$SOCK" "$1" <<'EOF'
45import socket, sys, time
46s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
47s.connect(sys.argv[1])
48s.sendall((sys.argv[2] + "\n").encode())
49time.sleep(0.4)
50EOF
51}
52
53cursor_state() {
54 python3 "$SCRIPT_DIR/sendkeys.py" --sock "$SOCK" "cursor"
55 sleep 0.5
56 sed -n 's/.*cursor: x=\([0-9]*\) y=\([0-9]*\) buttons=\([A-Za-z-]*\).*/\1 \2 \3/p' "$LOG" | tail -1
57}
58
59echo "==> 1/6 build kernel (debug)"
60cargo build --release --package rsdos --target x86_64-unknown-uefi
61
62echo "==> 2/6 stage system image"
63"$SCRIPT_DIR/mksystem.sh" --force "$SYSTEM_IMG"
64
65echo "==> 3/6 boot QEMU (headless)"
66kill "$(cat "$QPID" 2>/dev/null)" 2>/dev/null || true
67sleep 1
68rm -f "$SOCK" "$LOG"
69"$QEMU" -bios "$OVMF" \
70 -drive file="$SYSTEM_IMG",format=raw,if=none,id=disk \
71 -device ide-hd,drive=disk,bus=ide.0 \
72 -serial file:"$LOG" \
73 -monitor unix:"$SOCK",server,nowait \
74 -m 256M -machine q35 -cpu qemu64 -no-reboot -display none \
75 >"$QLOG" 2>&1 &
76echo $! >"$QPID"
77
78wait_log "$LOG" "boot: entering shell" 90 || fail "kernel never reached the shell"
79grep -aq "ps2: mouse ready" "$LOG" || fail "mouse driver did not come up at boot"
80wait_log "$LOG" "cursor: task " 10 || fail "cursor task did not spawn"
81
82echo "==> 4/6 cursor position tracks injected movement"
83INIT=$(cursor_state)
84[ -n "$INIT" ] || fail "no initial cursor position"
85IX=$(echo "$INIT" | awk '{print $1}')
86IY=$(echo "$INIT" | awk '{print $2}')
87IB=$(echo "$INIT" | awk '{print $3}')
88echo " initial: x=$IX y=$IY buttons=$IB"
89[ "$IB" = "---" ] || fail "expected no buttons at boot, got $IB"
90
91qemu_hmp "mouse_move 20 7"
92S1=$(cursor_state)
93X1=$(echo "$S1" | awk '{print $1}')
94Y1=$(echo "$S1" | awk '{print $2}')
95[ "$X1" -eq $((IX + 20)) ] || fail "x did not move +20 (was $IX, now $X1)"
96[ "$Y1" -eq $((IY + 7)) ] || fail "y did not move +7 (was $IY, now $Y1)"
97echo " moved: x=$X1 y=$Y1 (delta +20,+7)"
98
99echo "==> 5/6 left button edge detection"
100qemu_hmp "mouse_button 1"
101S2=$(cursor_state)
102IB2=$(echo "$S2" | awk '{print $3}')
103[ "$IB2" = "L--" ] || [ "$IB2" = "L--" ] || fail "expected L--, got $IB2"
104
105qemu_hmp "mouse_button 0"
106S3=$(cursor_state)
107IB3=$(echo "$S3" | awk '{print $3}')
108[ "$IB3" = "---" ] || fail "expected --- after release, got $IB3"
109echo " button: $IB2 → $IB3"
110
111echo "==> 6/6 mouse stream routing to task 3"
112python3 "$SCRIPT_DIR/sendkeys.py" --sock "$SOCK" "msgrecv"
113sleep 1
114python3 "$SCRIPT_DIR/sendkeys.py" --sock "$SOCK" "mouse 3"
115qemu_hmp "mouse_move 20 7"
116qemu_hmp "mouse_button 1"
117sleep 0.5
118qemu_hmp "mouse_button 0"
119sleep 0.5
120python3 "$SCRIPT_DIR/sendkeys.py" --sock "$SOCK" "ctrl-c"
121sleep 0.5
122grep -aq "Mouse(+20,+7" "$LOG" || fail "routed mouse move not logged"
123
124echo ""
125echo "PASS — mouse round trip"
diff --git a/scripts/test-msg.sh b/scripts/test-msg.sh
deleted file mode 100755
index ff7fd72..0000000
--- a/scripts/test-msg.sh
+++ /dev/null
@@ -1,140 +0,0 @@
1#!/usr/bin/env bash
2#
3# scripts/test-msg.sh — automated scheduler message-queue round trip.
4#
5# Tests all message types (user, timer, key, mouse), real-input routing,
6# and queue-full boundary.
7#
8# Env: OVMF, QEMU, SYSTEM_IMG
9#
10set -euo pipefail
11
12SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
13PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
14cd "$PROJECT_DIR"
15
16OVMF="${OVMF:-/usr/share/edk2/x64/OVMF.4m.fd}"
17QEMU="${QEMU:-qemu-system-x86_64}"
18SYSTEM_IMG="${SYSTEM_IMG:-/tmp/system.img}"
19QPID=/tmp/rsdos-msg-qpid
20SOCK=/tmp/rsdos-msg-qmon.sock
21LOG=/tmp/rsdos-msg-serial.log
22QLOG=/tmp/rsdos-msg-qemu.log
23
24trap 'kill "$(cat "$QPID" 2>/dev/null)" 2>/dev/null || true' EXIT
25
26fail() {
27 echo "FAIL: $1"
28 [ -f "$LOG" ] && tail -40 "$LOG"
29 exit 1
30}
31
32wait_log() {
33 local i=0
34 while [ "$i" -lt $((${3:-60} * 10)) ]; do
35 grep -q "$2" "$1" 2>/dev/null && return 0
36 sleep 0.1
37 i=$((i + 1))
38 done
39 return 1
40}
41
42qemu_hmp() {
43 python3 - "$SOCK" "$1" <<'EOF'
44import socket, sys, time
45s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
46s.connect(sys.argv[1])
47s.sendall((sys.argv[2] + "\n").encode())
48time.sleep(0.4)
49EOF
50}
51
52wait_qemu_exit() {
53 local i=0
54 while [ "$i" -lt $((${1:-30} * 10)) ]; do
55 kill -0 "$(cat "$QPID" 2>/dev/null)" 2>/dev/null || return 0
56 sleep 0.1
57 i=$((i + 1))
58 done
59 return 1
60}
61
62echo "==> 1/5 build kernel (debug)"
63cargo build --release --package rsdos --target x86_64-unknown-uefi
64
65echo "==> 2/5 stage system image"
66"$SCRIPT_DIR/mksystem.sh" --force "$SYSTEM_IMG"
67
68echo "==> 3/5 boot QEMU (headless)"
69kill "$(cat "$QPID" 2>/dev/null)" 2>/dev/null || true
70sleep 1
71rm -f "$SOCK" "$LOG"
72"$QEMU" -bios "$OVMF" \
73 -drive file="$SYSTEM_IMG",format=raw,if=none,id=disk \
74 -device ide-hd,drive=disk,bus=ide.0 \
75 -serial file:"$LOG" \
76 -monitor unix:"$SOCK",server,nowait \
77 -m 256M -machine q35 -cpu qemu64 -no-reboot -display none \
78 >"$QLOG" 2>&1 &
79echo $! >"$QPID"
80
81wait_log "$LOG" "boot: entering shell" 90 || fail "kernel never reached the shell"
82
83echo "==> 4/5 drive the shell (msgrecv / send / mouse routing / queue-full)"
84python3 "$SCRIPT_DIR/sendkeys.py" --sock "$SOCK" \
85 "msgrecv" \
86 "send 3 user 42" \
87 "send 3 timer" \
88 "send 3 key enter" \
89 "send 3 key ctrl-c" \
90 "send 3 key f3" \
91 "send 3 mouse 5 -3 3" \
92 "sleep 800" \
93 "jobs"
94
95python3 "$SCRIPT_DIR/sendkeys.py" --sock "$SOCK" "mouse 3"
96qemu_hmp "mouse_move 20 7"
97qemu_hmp "mouse_button 1"
98qemu_hmp "mouse_move -5 -3"
99qemu_hmp "mouse_button 0"
100qemu_hmp "sendkey ctrl-c"
101
102python3 "$SCRIPT_DIR/sendkeys.py" --sock "$SOCK" \
103 "send 0 user 1" \
104 "send 0 user 2" \
105 "send 0 user 3" \
106 "send 0 user 4" \
107 "send 0 user 5" \
108 "send 0 user 6" \
109 "send 0 user 7" \
110 "send 0 user 8" \
111 "send 0 user 9" \
112 "send 0 user 10" \
113 "send 0 user 11" \
114 "send 0 user 12" \
115 "send 0 user 13" \
116 "send 0 user 14" \
117 "send 0 user 15" \
118 "send 0 user 16" \
119 "send 0 user 17" \
120 "sleep 800" \
121 "jobs" \
122 "poweroff"
123
124echo "==> 5/5 wait for shutdown + verify"
125wait_qemu_exit 30 || fail "QEMU did not exit after poweroff"
126
127grep -aq "msgrecv: task 3 listening" "$LOG" || fail "msgrecv did not spawn"
128grep -aq "msgrecv\[3\]: User(42)" "$LOG" || fail "user message missing"
129grep -aq "msgrecv\[3\]: Timer" "$LOG" || fail "timer message missing"
130grep -aq "msgrecv\[3\]: Key(Enter)" "$LOG" || fail "key enter missing"
131grep -aq "msgrecv\[3\]: Key(Ctrl-c)" "$LOG" || fail "key ctrl-c missing"
132grep -aq "msgrecv\[3\]: Key(F3)" "$LOG" || fail "key f3 missing"
133grep -aq "msgrecv\[3\]: Mouse(+5,-3 L R)" "$LOG" || fail "synthesized mouse message missing"
134grep -aq "msgrecv\[3\]: Mouse(+20,+7" "$LOG" || fail "real mouse move 20,7 not routed to queue"
135grep -aq "msgrecv\[3\]: Mouse(-5,-3 L" "$LOG" || fail "held-button routed move missing"
136grep -aq "queue full" "$LOG" || fail "17th send did not report queue full"
137grep -aq "send: unknown type" "$LOG" && fail "a bad send slipped through (expected none)"
138
139echo ""
140echo "PASS — message queues OK"
diff --git a/scripts/sendkeys.py b/scripts/tests/sendkeys.py
index a67903d..9300242 100755
--- a/scripts/sendkeys.py
+++ b/scripts/tests/sendkeys.py
@@ -5,18 +5,20 @@ Each positional argument is one line: it is typed character by character
5(US layout) followed by Enter. Use it to drive the rs-dos shell from a 5(US layout) followed by Enter. Use it to drive the rs-dos shell from a
6test script without a display. 6test script without a display.
7 7
8Usage: 8Used by scripts/tests/test-*.sh, and manually against an interactive
9 sendkeys.py "ls /disk" "echo hi WRITING /disk/a.txt" "poweroff" 9session started with `./scripts/run.sh --monitor`:
10 sendkeys.py --sock /tmp/qmon.sock "date"
11 10
12Environment: QMON_SOCK (default: /tmp/qmon.sock) 11 ./scripts/tests/sendkeys.py "ls /disk" "date" "poweroff"
12 ./scripts/tests/sendkeys.py --sock /tmp/rsdos-monitor.sock "date"
13
14Environment: QMON_SOCK (default: /tmp/rsdos-monitor.sock)
13""" 15"""
14import os 16import os
15import socket 17import socket
16import sys 18import sys
17import time 19import time
18 20
19SOCK = os.environ.get("QMON_SOCK", "/tmp/qmon.sock") 21SOCK = os.environ.get("QMON_SOCK", "/tmp/rsdos-monitor.sock")
20 22
21# Unshifted punctuation needs QEMU's *named* keys — bare '-'/'.'/'/' are 23# Unshifted punctuation needs QEMU's *named* keys — bare '-'/'.'/'/' are
22# not valid sendkey names (and '-' is the modifier separator). 24# not valid sendkey names (and '-' is the modifier separator).
diff --git a/scripts/tests/test-disk.sh b/scripts/tests/test-disk.sh
new file mode 100755
index 0000000..3836b6d
--- /dev/null
+++ b/scripts/tests/test-disk.sh
@@ -0,0 +1,67 @@
1#!/usr/bin/env bash
2#
3# scripts/tests/test-disk.sh — full automated data-disk round trip.
4#
5# Builds, stages the system image, boots QEMU headless, drives the shell
6# via the monitor (ls / mkdir / echo WRITING / cat / poweroff), then
7# verifies the results with mtools and fsck.
8#
9# Usage:
10# ./scripts/tests/test-disk.sh reuse the existing image
11# ./scripts/tests/test-disk.sh --fresh reformat + reseed first
12#
13# Env: SYSTEM_IMG, OVMF, QEMU (see scripts/lib.sh)
14#
15set -euo pipefail
16TNAME="disk"
17# shellcheck source=../lib.sh
18source "$(dirname "$0")/../lib.sh"
19
20FRESH=0
21[ "${1:-}" = "--fresh" ] && FRESH=1
22STAMP="$(date +%s)"
23
24echo "==> 1/6 build (kernel + bins)"
25build_all
26
27echo "==> 2/6 stage system image"
28if [ "$FRESH" -eq 1 ]; then stage_image --force; else stage_image; fi
29
30echo "==> 3/6 boot QEMU (headless)"
31test_boot
32sleep 1
33
34# Volumes mount under /disk/<UID>/ where UID = "LABEL-SERIAL" (e.g.
35# RSDOS-5BD1E55F). Discover it from the boot log so the test works on
36# any image without knowing the serial in advance.
37VOL_UID="$(grep -a 'fat: mounted' "$LOG" | sed -n 's/.*at \/disk\/\([^ )]*\).*/\1/p' | head -1)"
38[ -n "$VOL_UID" ] || fail "no FAT32 volume was mounted (look for 'fat: mounted' in the log)"
39echo " mounted volume UID: $VOL_UID"
40
41echo "==> 4/6 drive the shell (ls / mkdir / echo WRITING / cat / poweroff)"
42# The filename is deliberately >13 chars so the FAT LFN path gets
43# multi-entry entries (they're stored last-first on disk).
44TESTFILE="disk-test-$STAMP.txt"
45sendkeys \
46 "ls /disk" \
47 "mkdir /disk/$VOL_UID/sub" \
48 "echo disk-test-$STAMP WRITING /disk/$VOL_UID/sub/$TESTFILE" \
49 "cat /disk/$VOL_UID/sub/$TESTFILE" \
50 "poweroff"
51
52echo "==> 5/6 wait for shutdown"
53wait_qemu_exit 30 || fail "QEMU did not exit after poweroff"
54
55echo "==> 6/6 verify"
56grep -a "disk-test-$STAMP" "$LOG" >/dev/null || fail "cat output missing from serial log"
57if ! fsck.fat -n "$SYSTEM_IMG" >/tmp/rsdos-fsck.out 2>&1; then
58 fail "fsck.fat reports problems: $(tail -3 /tmp/rsdos-fsck.out)"
59fi
60MT="$(mtype -i "$SYSTEM_IMG" "::/sub/$TESTFILE" 2>/dev/null || true)"
61[ "$MT" = "disk-test-$STAMP" ] || fail "mtype mismatch: got '$MT'"
62
63echo ""
64echo "PASS — disk round trip OK (fsck clean, file survived poweroff)"
65echo " image: $SYSTEM_IMG"
66echo " volume: /disk/$VOL_UID"
67echo " written: /disk/$VOL_UID/sub/$TESTFILE = \"disk-test-$STAMP\""
diff --git a/scripts/tests/test-fault.sh b/scripts/tests/test-fault.sh
new file mode 100755
index 0000000..0c39cde
--- /dev/null
+++ b/scripts/tests/test-fault.sh
@@ -0,0 +1,47 @@
1#!/usr/bin/env bash
2#
3# scripts/tests/test-fault.sh — fault-diagnostics verification.
4#
5# Proves the fault-dump path: every CPU exception prints a white-on-red
6# band to the framebuffer and a parseable serial line before halting.
7# Boots with a dedicated CRASHLOG FAT stick on ide.1 so the crash band
8# has somewhere to write.
9#
10# Usage: ./scripts/tests/test-fault.sh
11# Env: SYSTEM_IMG, OVMF, QEMU (see scripts/lib.sh)
12#
13set -euo pipefail
14TNAME="fault"
15# shellcheck source=../lib.sh
16source "$(dirname "$0")/../lib.sh"
17
18echo "==> 1/5 build (kernel + bins)"
19build_all
20
21echo "==> 2/5 stage system image"
22stage_image --force
23
24echo "==> 3/5 boot QEMU (headless, + dedicated CRASHLOG stick)"
25CRASH_IMG=/tmp/rsdos-crash.img
26rm -f "$CRASH_IMG"
27mkfs.fat -C -F 32 -n CRASHLOG "$CRASH_IMG" 65536 >/dev/null
28test_boot \
29 -drive "file=$CRASH_IMG,format=raw,if=none,id=crash" \
30 -device ide-hd,drive=crash,bus=ide.1
31
32echo "==> 4/5 boot diagnostics present"
33grep -aq "gop: fb base=" "$LOG" || fail "fb geometry diagnostic missing"
34grep -aq "gop: mode .* selected" "$LOG" || fail "mode-selection diagnostic missing"
35grep -aq "CRASHLOG" "$LOG" || fail "CRASHLOG stick not mounted"
36
37echo "==> 5/5 inject a page fault (fault command)"
38sendkeys "clear" "fault"
39sleep 2
40
41grep -aq "!!! page fault vec=14 .*rip=" "$LOG" || fail "page-fault dump line missing"
42grep -aq "cr2=0x400000000000" "$LOG" || fail "CR2 not reported (faulting address)"
43grep -aq "task=0 \"shell\"" "$LOG" || fail "faulting task not identified"
44grep -aq "!!! halting" "$LOG" || fail "no halt message"
45
46echo ""
47echo "PASS — fault dump verified"
diff --git a/scripts/test-focus.py b/scripts/tests/test-focus.py
index e1bbe2d..cfa317a 100755
--- a/scripts/test-focus.py
+++ b/scripts/tests/test-focus.py
@@ -2,16 +2,17 @@
2"""Drive the focus-routing gate: 3 windows, type markers, verify routing. 2"""Drive the focus-routing gate: 3 windows, type markers, verify routing.
3 3
4Connects to the QEMU monitor once and keeps the socket open (the per-call 4Connects to the QEMU monitor once and keeps the socket open (the per-call
5scripts/sendkeys.py pattern loses the socket when the monitor resets). 5sendkeys.py pattern loses the socket when the monitor resets).
6 6
7Usage: python3 scripts/test-focus.py /tmp/qmon.sock 7Usage: start the guest with `./scripts/run.sh --monitor`, then
8 python3 scripts/tests/test-focus.py [/tmp/rsdos-monitor.sock]
8""" 9"""
9import socket 10import socket
10import sys 11import sys
11import time 12import time
12 13
13sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) 14sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
14sock.connect(sys.argv[1] if len(sys.argv) > 1 else "/tmp/sysboot-mon.sock") 15sock.connect(sys.argv[1] if len(sys.argv) > 1 else "/tmp/rsdos-monitor.sock")
15 16
16 17
17def sk(k, d=0.35): 18def sk(k, d=0.35):
diff --git a/scripts/tests/test-gfx.sh b/scripts/tests/test-gfx.sh
new file mode 100755
index 0000000..613318f
--- /dev/null
+++ b/scripts/tests/test-gfx.sh
@@ -0,0 +1,37 @@
1#!/usr/bin/env bash
2#
3# scripts/tests/test-gfx.sh — automated graphics round-trip test.
4#
5# Builds, boots headless, runs the in-kernel gfx self-test and the
6# status-bar demo, then powers off.
7#
8# Usage: ./scripts/tests/test-gfx.sh
9# Env: SYSTEM_IMG, OVMF, QEMU (see scripts/lib.sh)
10#
11set -euo pipefail
12TNAME="gfx"
13# shellcheck source=../lib.sh
14source "$(dirname "$0")/../lib.sh"
15
16echo "==> 1/5 build (kernel + bins)"
17build_all
18
19echo "==> 2/5 stage system image"
20stage_image --force
21
22echo "==> 3/5 boot QEMU (headless)"
23test_boot
24wait_log "$LOG" "cursor: task " 10 || fail "cursor task did not spawn"
25
26echo "==> 4/5 in-kernel pixel self-test (gfx check)"
27sendkeys "gfx check"
28wait_log "$LOG" "gfx check: PASS" 30 || fail "gfx check did not run"
29
30echo "==> 5/5 gfx demo (draw scene)"
31sendkeys "gfx" "poweroff"
32wait_qemu_exit 30 || fail "QEMU did not exit after poweroff"
33
34grep -aq "gfx: drew status bar" "$LOG" || fail "gfx demo did not draw"
35
36echo ""
37echo "PASS — gfx round trip"
diff --git a/scripts/tests/test-mouse.sh b/scripts/tests/test-mouse.sh
new file mode 100755
index 0000000..bb835be
--- /dev/null
+++ b/scripts/tests/test-mouse.sh
@@ -0,0 +1,83 @@
1#!/usr/bin/env bash
2#
3# scripts/tests/test-mouse.sh — automated PS/2 mouse round-trip test.
4#
5# Boots QEMU headless, injects mouse movement via the QEMU monitor,
6# verifies the on-screen cursor tracks it, and tests the mouse stream
7# routing command.
8#
9# Usage: ./scripts/tests/test-mouse.sh
10# Env: SYSTEM_IMG, OVMF, QEMU (see scripts/lib.sh)
11#
12set -euo pipefail
13TNAME="mouse"
14# shellcheck source=../lib.sh
15source "$(dirname "$0")/../lib.sh"
16
17# cursor_state — one-shot `cursor` command, parsed to "x y buttons".
18cursor_state() {
19 sendkeys "cursor"
20 sleep 0.5
21 sed -n 's/.*cursor: x=\([0-9]*\) y=\([0-9]*\) buttons=\([A-Za-z-]*\).*/\1 \2 \3/p' "$LOG" | tail -1
22}
23
24echo "==> 1/6 build (kernel + bins)"
25build_all
26
27echo "==> 2/6 stage system image"
28stage_image --force
29
30echo "==> 3/6 boot QEMU (headless)"
31test_boot
32grep -aq "ps2: mouse ready" "$LOG" || fail "mouse driver did not come up at boot"
33wait_log "$LOG" "cursor: task " 10 || fail "cursor task did not spawn"
34
35echo "==> 4/6 cursor position tracks injected movement"
36INIT=$(cursor_state)
37[ -n "$INIT" ] || fail "no initial cursor position"
38IX=$(echo "$INIT" | awk '{print $1}')
39IY=$(echo "$INIT" | awk '{print $2}')
40IB=$(echo "$INIT" | awk '{print $3}')
41echo " initial: x=$IX y=$IY buttons=$IB"
42[ "$IB" = "---" ] || fail "expected no buttons at boot, got $IB"
43
44qemu_hmp "mouse_move 20 7"
45S1=$(cursor_state)
46X1=$(echo "$S1" | awk '{print $1}')
47Y1=$(echo "$S1" | awk '{print $2}')
48[ "$X1" -eq $((IX + 20)) ] || fail "x did not move +20 (was $IX, now $X1)"
49[ "$Y1" -eq $((IY + 7)) ] || fail "y did not move +7 (was $IY, now $Y1)"
50echo " moved: x=$X1 y=$Y1 (delta +20,+7)"
51
52echo "==> 5/6 left button edge detection"
53qemu_hmp "mouse_button 1"
54S2=$(cursor_state)
55IB2=$(echo "$S2" | awk '{print $3}')
56[ "$IB2" = "L--" ] || fail "expected L--, got $IB2"
57
58qemu_hmp "mouse_button 0"
59S3=$(cursor_state)
60IB3=$(echo "$S3" | awk '{print $3}')
61[ "$IB3" = "---" ] || fail "expected --- after release, got $IB3"
62echo " button: $IB2 → $IB3"
63
64echo "==> 6/6 mouse stream routing to msgrecv's task"
65# The spawned task id shifts with the task table (wm-input took slot 3) —
66# parse it rather than hardcoding.
67sendkeys "msgrecv"
68sleep 1
69MID="$(sed -n 's/.*msgrecv: task \([0-9][0-9]*\) listening.*/\1/p' "$LOG" | tail -1)"
70[ -n "$MID" ] || fail "msgrecv did not spawn"
71echo " msgrecv task id: $MID"
72sendkeys "mouse $MID"
73qemu_hmp "mouse_move 20 7"
74qemu_hmp "mouse_button 1"
75sleep 0.5
76qemu_hmp "mouse_button 0"
77sleep 0.5
78sendkeys "ctrl-c"
79sleep 0.5
80grep -aq "Mouse(+20,+7" "$LOG" || fail "routed mouse move not logged"
81
82echo ""
83echo "PASS — mouse round trip"
diff --git a/scripts/tests/test-msg.sh b/scripts/tests/test-msg.sh
new file mode 100755
index 0000000..7786beb
--- /dev/null
+++ b/scripts/tests/test-msg.sh
@@ -0,0 +1,98 @@
1#!/usr/bin/env bash
2#
3# scripts/tests/test-msg.sh — automated scheduler message-queue round trip.
4#
5# Tests all message types (user, timer, key, mouse), real-input routing,
6# and the queue-full boundary.
7#
8# Usage: ./scripts/tests/test-msg.sh
9# Env: SYSTEM_IMG, OVMF, QEMU (see scripts/lib.sh)
10#
11set -euo pipefail
12TNAME="msg"
13# shellcheck source=../lib.sh
14source "$(dirname "$0")/../lib.sh"
15
16echo "==> 1/5 build (kernel + bins)"
17build_all
18
19echo "==> 2/5 stage system image"
20stage_image --force
21
22echo "==> 3/5 boot QEMU (headless)"
23test_boot
24
25echo "==> 4/5 drive the shell (msgrecv / send / mouse routing / queue-full)"
26# msgrecv's task id is assigned at spawn time and shifts when the task
27# table grows (the Windowed GUI added wm-input at slot 3) — read it from
28# the log instead of hardcoding.
29sendkeys "msgrecv"
30sleep 1
31MID="$(sed -n 's/.*msgrecv: task \([0-9][0-9]*\) listening.*/\1/p' "$LOG" | tail -1)"
32[ -n "$MID" ] || fail "msgrecv did not spawn (no 'msgrecv: task N listening' in the log)"
33echo " msgrecv task id: $MID"
34
35sendkeys \
36 "send $MID user 42" \
37 "send $MID timer" \
38 "send $MID key enter" \
39 "send $MID key ctrl-c" \
40 "send $MID key f3" \
41 "send $MID mouse 5 -3 3" \
42 "sleep 800" \
43 "jobs"
44
45sendkeys "mouse $MID"
46qemu_hmp "mouse_move 20 7"
47qemu_hmp "mouse_button 1"
48qemu_hmp "mouse_move -5 -3"
49qemu_hmp "mouse_button 0"
50qemu_hmp "sendkey ctrl-c"
51
52# Queue-full probe: overflow a queue that nobody drains. The shell used
53# to be that task, but its exec pump now consumes its own User messages
54# as completion codes (q=0 at the prompt), and msgrecv consumes on wake —
55# the cursor task never calls recv, so its 16-slot ring fills for real.
56CID="$(awk '$2 == "cursor" { print $1 }' "$LOG" | tail -1)"
57[ -n "$CID" ] || fail "no cursor task in jobs output (cannot probe queue-full)"
58echo " queue-full target: task $CID (cursor, never recv'd)"
59
60sendkeys \
61 "send $CID user 1" \
62 "send $CID user 2" \
63 "send $CID user 3" \
64 "send $CID user 4" \
65 "send $CID user 5" \
66 "send $CID user 6" \
67 "send $CID user 7" \
68 "send $CID user 8" \
69 "send $CID user 9" \
70 "send $CID user 10" \
71 "send $CID user 11" \
72 "send $CID user 12" \
73 "send $CID user 13" \
74 "send $CID user 14" \
75 "send $CID user 15" \
76 "send $CID user 16" \
77 "send $CID user 17" \
78 "sleep 800" \
79 "jobs" \
80 "poweroff"
81
82echo "==> 5/5 wait for shutdown + verify"
83wait_qemu_exit 30 || fail "QEMU did not exit after poweroff"
84
85grep -aq "msgrecv: task $MID listening" "$LOG" || fail "msgrecv did not spawn"
86grep -aq "msgrecv\[$MID\]: User(42)" "$LOG" || fail "user message missing"
87grep -aq "msgrecv\[$MID\]: Timer" "$LOG" || fail "timer message missing"
88grep -aq "msgrecv\[$MID\]: Key(Enter)" "$LOG" || fail "key enter missing"
89grep -aq "msgrecv\[$MID\]: Key(Ctrl-c)" "$LOG" || fail "key ctrl-c missing"
90grep -aq "msgrecv\[$MID\]: Key(F3)" "$LOG" || fail "key f3 missing"
91grep -aq "msgrecv\[$MID\]: Mouse(+5,-3 L R)" "$LOG" || fail "synthesized mouse message missing"
92grep -aq "msgrecv\[$MID\]: Mouse(+20,+7" "$LOG" || fail "real mouse move 20,7 not routed to queue"
93grep -aq "msgrecv\[$MID\]: Mouse(-5,-3 L" "$LOG" || fail "held-button routed move missing"
94grep -aq "queue full" "$LOG" || fail "17th send did not report queue full"
95grep -aq "send: unknown type" "$LOG" && fail "a bad send slipped through (expected none)"
96
97echo ""
98echo "PASS — message queues OK"