From 7958276c9f0d230053520ba20f2a00f5ecda18e4 Mon Sep 17 00:00:00 2001 From: salmonik Date: Sun, 1 Nov 2026 15:53:29 +0100 Subject: scripts: rebuild the dev loop around build.sh/run.sh MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `make` did not parse at all (missing separator at line 139), so every make target was dead. The folder also carried three overlapping launchers (boot.sh, qemu.sh, boot-image.sh) plus a fourth partial one in the Makefile, `cargo build --package rsdos` copy-pasted eight times, the QEMU invocation copy-pasted per script, and the test helpers (fail/wait_log/wait_qemu_exit/qemu_hmp) duplicated in five tests. New layout: - scripts/lib.sh: single config (image, OVMF, mem/smp), the one cargo invocation (build_all), the one QEMU shape (qemu_args), and the test harness (test_boot, wait_log, wait_qemu_exit, qemu_hmp, sendkeys) - scripts/build.sh: kernel + every bins/* in one pass - scripts/mksystem.sh: freshness-aware image build that now stages every bins/*.efi into /bin/cli|gui (they were never copied before) - scripts/run.sh: build + stage + boot in one command; --headless, --debug (GDB stub), --monitor (HMP socket), --fresh, --mem/--smp/ --bios/--image; on Wayland it auto-selects XWayland plus grab-on-hover so the mouse can be captured (Ctrl+Alt+G toggles) - scripts/tests/: the five automated tests on the shared harness - deleted: boot.sh, qemu.sh, boot-image.sh, lib-boot-args.sh, build-image-release.sh, build-bootloader.sh (rsboot chain dropped) The Makefile is now a thin wrapper: make run, make test, make system, make inspect, make help. Two-command flow: ./scripts/build.sh then ./scripts/run.sh (run.sh alone also works). Stale test assumptions fixed so the suite is green again: test-gfx waited for "gfx: check" while the shell prints "gfx check: PASS"; test-mouse/test-msg hardcoded msgrecv = task 3 (wm-input took slot 3 — ids are parsed from the log now); the queue-full probe targeted task 0, whose exec pump drains its own queue as completion codes (moved to the cursor task, which never recvs). Verified in QEMU: make test -> test-disk, test-gfx, test-mouse, test-msg, test-fault all PASS. --- scripts/boot-image.sh | 65 ---------------- scripts/boot.sh | 90 ---------------------- scripts/build-bootloader.sh | 35 --------- scripts/build-image-release.sh | 30 -------- scripts/build.sh | 49 ++++++++++++ scripts/debug-kernel.py | 16 ++-- scripts/lib-boot-args.sh | 38 ---------- scripts/lib.sh | 168 +++++++++++++++++++++++++++++++++++++++++ scripts/mksystem.sh | 144 ++++++++++++++++++----------------- scripts/qemu.sh | 99 ------------------------ scripts/run.sh | 142 ++++++++++++++++++++++++++++++++++ scripts/sendkeys.py | 103 ------------------------- scripts/test-disk.sh | 131 -------------------------------- scripts/test-fault.sh | 85 --------------------- scripts/test-focus.py | 56 -------------- scripts/test-gfx.sh | 84 --------------------- scripts/test-mouse.sh | 125 ------------------------------ scripts/test-msg.sh | 140 ---------------------------------- scripts/tests/sendkeys.py | 105 ++++++++++++++++++++++++++ scripts/tests/test-disk.sh | 67 ++++++++++++++++ scripts/tests/test-fault.sh | 47 ++++++++++++ scripts/tests/test-focus.py | 57 ++++++++++++++ scripts/tests/test-gfx.sh | 37 +++++++++ scripts/tests/test-mouse.sh | 83 ++++++++++++++++++++ scripts/tests/test-msg.sh | 98 ++++++++++++++++++++++++ 25 files changed, 937 insertions(+), 1157 deletions(-) delete mode 100755 scripts/boot-image.sh delete mode 100755 scripts/boot.sh delete mode 100755 scripts/build-bootloader.sh delete mode 100755 scripts/build-image-release.sh create mode 100755 scripts/build.sh delete mode 100644 scripts/lib-boot-args.sh create mode 100644 scripts/lib.sh delete mode 100755 scripts/qemu.sh create mode 100755 scripts/run.sh delete mode 100755 scripts/sendkeys.py delete mode 100755 scripts/test-disk.sh delete mode 100755 scripts/test-fault.sh delete mode 100755 scripts/test-focus.py delete mode 100755 scripts/test-gfx.sh delete mode 100755 scripts/test-mouse.sh delete mode 100755 scripts/test-msg.sh create mode 100755 scripts/tests/sendkeys.py create mode 100755 scripts/tests/test-disk.sh create mode 100755 scripts/tests/test-fault.sh create mode 100755 scripts/tests/test-focus.py create mode 100755 scripts/tests/test-gfx.sh create mode 100755 scripts/tests/test-mouse.sh create mode 100755 scripts/tests/test-msg.sh (limited to 'scripts') 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 @@ -#!/usr/bin/env bash -# -# scripts/boot-image.sh — boot rs-dos full chain disk image in QEMU -# -# Builds the image first if needed, then boots with UEFI firmware through -# the real boot chain: firmware → rsboot → rsnix kernel. -# -# Usage: -# ./scripts/boot-image.sh [--gui] [--debug] [--mem 512M] [--smp 2] -# -# Keybindings: -# Ctrl-A X — quit -# Ctrl-A C — QEMU monitor -# -# Environment: -# OVMF path to OVMF firmware (default: /usr/share/edk2/x64/OVMF.4m.fd) -# MEM memory size (default: 256M) -# SMP CPU count (default: 1) -# -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" -cd "$PROJECT_DIR" - -IMAGE="${1:-rs-dos.img}" - -# ---- config ---- -OVMF="${OVMF:-/usr/share/edk2/x64/OVMF.4m.fd}" -MEM="${MEM:-256M}" -SMP="${SMP:-1}" -GUI="" -DEBUG="" - -# ---- args ---- -source "$SCRIPT_DIR/lib-boot-args.sh" -parse_boot_args "$@" - -# ---- build image if missing ---- -if [ ! -f "$IMAGE" ]; then - echo "Image not found, building..." - "$SCRIPT_DIR/mksystem.sh" --chain --gpt "$IMAGE" -fi - -# ---- boot ---- -echo "" -echo "=== Booting (full chain: firmware → rsboot → rsnix) ===" -echo " image: $IMAGE" -echo " ovmf: $OVMF" -echo " mem: $MEM smp: $SMP" -echo " gui: $([ -n "$GUI" ] && echo yes || echo no)" -echo " gdb: $([ -n "$DEBUG" ] && echo ':1234' || echo off)" -echo "" - -exec qemu-system-x86_64 \ - -bios "$OVMF" \ - -drive file="$IMAGE",format=raw,if=none,id=hd \ - -device ide-hd,drive=hd \ - -serial stdio \ - -m "$MEM" \ - -machine q35 \ - -cpu qemu64 \ - -no-reboot \ - -smp "$SMP" \ - $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 @@ -#!/usr/bin/env bash -# -# scripts/boot.sh — quick direct boot in QEMU (dev loop) -# -# Builds the kernel, stages the system image, and boots directly as -# BOOTX64.EFI via QEMU's FAT driver. One image = one drive. -# -# Usage: -# ./scripts/boot.sh [--gui] [--debug] [--release] [--mem 512M] [--smp 2] -# -# Boot chain: firmware → rsnix (direct) -# Full chain: use ./scripts/boot-image.sh (firmware → rsboot → rsnix) -# -# Keybindings: -# Ctrl-A X — quit -# Ctrl-A C — QEMU monitor -# -# Environment: -# OVMF path to OVMF firmware (default: /usr/share/edk2/x64/OVMF.4m.fd) -# MEM memory size (default: 256M) -# SMP CPU count (default: 1) -# SYSTEM_IMG system image path (default: /tmp/system.img) -# -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" -cd "$PROJECT_DIR" - -# ---- config ---- -OVMF="${OVMF:-/usr/share/edk2/x64/OVMF.4m.fd}" -MEM="${MEM:-256M}" -SMP="${SMP:-1}" -SYSTEM_IMG="${SYSTEM_IMG:-/tmp/system.img}" -PROFILE="release" - -GUI="" -DEBUG="" - -# ---- args ---- -source "$SCRIPT_DIR/lib-boot-args.sh" -handle_arg() { - case "$1" in - --debug) - PROFILE="debug" - CARGO_FLAGS="" - ;; - *) - echo "unknown: $1" - exit 1 - ;; - esac -} -parse_boot_args "$@" - -OUT_DIR="target/x86_64-unknown-uefi/$PROFILE" - -# ---- build ---- -echo "=== Building kernel ($PROFILE) ===" -if [ "$PROFILE" = "release" ]; then - cargo build --release --package rsdos --target x86_64-unknown-uefi -else - cargo build --package rsdos --target x86_64-unknown-uefi -fi - -# ---- stage system image ---- -echo "=== Staging system image ===" -"$SCRIPT_DIR/mksystem.sh" --force "$SYSTEM_IMG" -echo " system image: $SYSTEM_IMG" - -# ---- boot ---- -echo "" -echo "=== Booting (direct: firmware → rsnix) ===" -echo " ovmf: $OVMF" -echo " mem: $MEM smp: $SMP" -echo " gui: $([ -n "$GUI" ] && echo yes || echo no)" -echo " gdb: $([ -n "$DEBUG" ] && echo ':1234' || echo off)" -echo "" - -exec qemu-system-x86_64 \ - -bios "$OVMF" \ - -drive "file=$SYSTEM_IMG,format=raw,if=none,id=disk" \ - -device ide-hd,drive=disk,bus=ide.0 \ - -serial stdio \ - -m "$MEM" \ - -machine q35 \ - -cpu qemu64 \ - -no-reboot \ - -smp "$SMP" \ - $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 @@ -#!/usr/bin/env bash -# -# scripts/build-bootloader.sh — build rsboot (UEFI bootloader) -# -# rsboot is a git submodule at rsboot/. This builds it and places -# BOOTX64.EFI into build/bootloader/. -# -# Usage: -# ./scripts/build-bootloader.sh -# -# Output: -# build/bootloader/BOOTX64.EFI -# -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -PROJECT_DIR="$(dirname "$SCRIPT_DIR")" -BUILD_DIR="$PROJECT_DIR/build" -BOOTLOADER_DIR="$BUILD_DIR/bootloader" - -echo "=== Building rsboot (UEFI bootloader) ===" - -# Ensure submodule is checked out, tracking the latest upstream HEAD -# (--remote fetches and checks out the newest main instead of reverting -# to the commit recorded in the superproject's gitlink). -cd "$PROJECT_DIR" -git submodule update --init --remote rsboot - -cd "$PROJECT_DIR/rsboot" -make clean 2>/dev/null || true -make -j"$(nproc)" 2>&1 | sed 's/^/ /' - -echo "" -echo "=== rsboot build complete ===" -ls -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 @@ -#!/usr/bin/env bash -# -# scripts/build-image-release.sh — build the release GPT disk image -# -# Produces a single dd-able system image with the release build of -# the kernel, rsboot bootloader, and all binaries in /bin/. -# -# Usage: -# ./scripts/build-image-release.sh [output.img] -# -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" -cd "$PROJECT_DIR" - -IMAGE="${1:-rs-dos.img}" - -echo "=== Building release kernel ===" -cargo build --release --package rsdos --target x86_64-unknown-uefi - -echo "=== Building system image (chain + GPT) ===" -"$SCRIPT_DIR/mksystem.sh" --chain --gpt "$IMAGE" - -echo "" -echo "=== Done: $IMAGE ===" -ls -lh "$IMAGE" -echo "" -echo "Boot with: ./scripts/boot-image.sh --gui" -echo "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 @@ +#!/usr/bin/env bash +# +# scripts/build.sh — compile the kernel and every external binary. +# +# The only cargo invocation in the project. One pass builds the kernel +# (rsdos.efi) and all /bin binaries (bins/*.efi), which is exactly what +# the system image stages. +# +# Usage: +# ./scripts/build.sh release (default — what the image uses) +# ./scripts/build.sh --debug debug profile (slower guest, GDB symbols) +# ./scripts/build.sh --help +# +# Output: +# target/x86_64-unknown-uefi//rsdos.efi kernel +# target/x86_64-unknown-uefi//.efi /bin binaries +# +# Typical flow: +# ./scripts/build.sh && ./scripts/run.sh +# +set -euo pipefail + +usage() { + awk 'NR > 1 && /^#/ { sub(/^# ?/, ""); print; next } NR > 1 { exit }' "$0" +} + +# PROFILE is read by lib.sh (it derives OUT_DIR/KERNEL from it), so parse +# before sourcing. +while [ $# -gt 0 ]; do + case "$1" in + --debug) PROFILE="debug" ;; + --release) PROFILE="release" ;; + --help | -h) + usage + exit 0 + ;; + *) + echo "build.sh: unknown argument: $1" >&2 + exit 1 + ;; + esac + shift +done + +# shellcheck source=lib.sh +source "$(dirname "$0")/lib.sh" + +build_all +echo "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 @@ GDB Python script for debugging raisernics under QEMU. Usage: - 1. Terminal 1: ./tools/qemu.sh --debug - 2. Terminal 2: gdb -x tools/debug-kernel.py target/x86_64-unknown-uefi/debug/raisernics.efi + 1. Terminal 1: ./scripts/run.sh --debug (GDB stub on :1234) + Add PROFILE=debug for line-level symbols (slower guest). + 2. Terminal 2: gdb -x scripts/debug-kernel.py """ import gdb import os -# Find the kernel binary +# Find the kernel binary (debug profile preferred, release as fallback) script_dir = os.path.dirname(os.path.abspath(__file__)) project_root = os.path.dirname(script_dir) -binary = os.path.join( - project_root, - "target/x86_64-unknown-uefi/debug/raisernics.efi" -) +candidates = [ + os.path.join(project_root, "target/x86_64-unknown-uefi/debug/rsdos.efi"), + os.path.join(project_root, "target/x86_64-unknown-uefi/release/rsdos.efi"), +] +binary = next((c for c in candidates if os.path.exists(c)), candidates[0]) # Connect to QEMU's GDB stub gdb.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 @@ -#!/usr/bin/env bash -# -# scripts/lib-boot-args.sh — shared CLI parsing for the QEMU boot scripts. -# Sourced, not executed. The caller sets default values for GUI/DEBUG/ -# MEM/SMP first, then calls `parse_boot_args "$@"`. A caller with extra -# flags defines its own `handle_arg()` to intercept them before the -# shared cases run (unknown args still fail loudly). -set -euo pipefail - -parse_boot_args() { - while [ $# -gt 0 ]; do - case "$1" in - --gui | -g) GUI="-vga std" ;; - --debug | -d) DEBUG="-s -S" ;; - --mem) - shift - MEM="$1" - ;; - --smp) - shift - SMP="$1" - ;; - --help | -h) - head -20 "$0" | grep "^#" | sed 's/^# //' - exit 0 - ;; - *) handle_arg "$1" ;; - esac - shift - done -} - -# Default: no extra flags. A script with more defines its own before -# calling parse_boot_args. -handle_arg() { - echo "unknown: $1" - exit 1 -} 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 @@ +#!/usr/bin/env bash +# +# scripts/lib.sh — shared config and helpers for the rs-dos scripts. +# +# Sourced, never executed. Everything here is the single definition of +# "how we build and how we boot": the build script, the image assembler, +# the QEMU runner and the tests all read their settings from this file. +# +# Config is environment-overridable (TARGET, PROFILE, SYSTEM_IMG, OVMF, +# QEMU, MEM, SMP) so callers and CI can tune it without editing scripts. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +TESTS_DIR="$SCRIPT_DIR/tests" + +# ---- config --------------------------------------------------------------- +TARGET="${TARGET:-x86_64-unknown-uefi}" +PROFILE="${PROFILE:-release}" +OUT_DIR="${OUT_DIR:-$PROJECT_DIR/target/$TARGET/$PROFILE}" +KERNEL="$OUT_DIR/rsdos.efi" +SYSTEM_IMG="${SYSTEM_IMG:-/tmp/system.img}" +OVMF="${OVMF:-/usr/share/edk2/x64/OVMF.4m.fd}" +QEMU="${QEMU:-qemu-system-x86_64}" +MEM="${MEM:-256M}" +SMP="${SMP:-1}" + +# Binaries whose launch surface is a window rather than a terminal +# (ADR 0006): staged into /bin/gui, everything else into /bin/cli. +GUI_BINS="paint" + +cd "$PROJECT_DIR" + +# ---- build ---------------------------------------------------------------- +# +# The one and only cargo invocation in the repo. Builds the kernel and +# every external binary in a single pass so the image can stage both. + +cargo_flags() { + [ "$PROFILE" = "release" ] && printf '%s' "--release" +} + +build_all() { + echo "=== build (kernel + bins, $PROFILE) ===" + # shellcheck disable=SC2046 # cargo_flags is a flag word, intentionally split + cargo build --workspace --target "$TARGET" $(cargo_flags) +} + +# ---- system image --------------------------------------------------------- + +# stage_image [mksystem args...] — assemble /tmp/system.img if it is stale. +# Freshness is decided inside mksystem.sh; an up-to-date image is never +# touched, so files written at runtime survive. +stage_image() { + "$SCRIPT_DIR/mksystem.sh" "$@" +} + +# ---- QEMU ----------------------------------------------------------------- + +# qemu_args — the common machine every boot shares: OVMF, the system +# image as the single SATA drive, 256M q35. Callers append display, +# monitor and serial choices. +qemu_args() { + printf '%s\n' \ + -bios "$OVMF" \ + -drive "file=$SYSTEM_IMG,format=raw,if=none,id=disk" \ + -device ide-hd,drive=disk,bus=ide.0 \ + -m "$MEM" \ + -machine q35 \ + -cpu qemu64 \ + -no-reboot \ + -smp "$SMP" +} + +# ---- test harness --------------------------------------------------------- +# +# Every test-*.sh does: +# TNAME=disk; source lib.sh; test_boot # waits for the shell +# sendkeys "ls /disk" "poweroff" +# wait_qemu_exit +# Runtime files are namespaced per test so parallel runs cannot collide. + +TNAME="${TNAME:-rsdos}" +QPID="/tmp/rsdos-$TNAME-qpid" +SOCK="/tmp/rsdos-$TNAME-qmon.sock" +LOG="/tmp/rsdos-$TNAME-serial.log" +QLOG="/tmp/rsdos-$TNAME-qemu.log" + +fail() { + echo "FAIL: $1" + [ -f "$LOG" ] && tail -40 "$LOG" + exit 1 +} + +# wait_log FILE PATTERN TIMEOUT-SECONDS — poll a file for a regex. +wait_log() { + local i=0 + while [ "$i" -lt $((${3:-60} * 10)) ]; do + grep -q "$2" "$1" 2>/dev/null && return 0 + sleep 0.1 + i=$((i + 1)) + done + return 1 +} + +# wait_qemu_exit TIMEOUT-SECONDS — poll until the pid is gone. +wait_qemu_exit() { + local i=0 + while [ "$i" -lt $((${1:-30} * 10)) ]; do + kill -0 "$(cat "$QPID" 2>/dev/null)" 2>/dev/null || return 0 + sleep 0.1 + i=$((i + 1)) + done + return 1 +} + +# qemu_hmp CMD — run one HMP command on the monitor socket. +qemu_hmp() { + python3 - "$SOCK" "$1" <<'EOF' +import socket, sys, time +s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) +s.connect(sys.argv[1]) +s.sendall((sys.argv[2] + "\n").encode()) +time.sleep(0.4) +EOF +} + +# sendkeys "line" ... — type shell commands into the guest via sendkeys.py. +sendkeys() { + python3 "$TESTS_DIR/sendkeys.py" --sock "$SOCK" "$@" +} + +# test_cleanup — reap a leftover QEMU from a previous run of this test +# and clear this test's runtime files. Called before test_boot. +test_cleanup() { + kill "$(cat "$QPID" 2>/dev/null)" 2>/dev/null || true + rm -f "$SOCK" "$LOG" +} + +# test_on_exit — EXIT trap: never leave a QEMU behind, but keep the logs +# (they are the post-mortem for a failed test). +test_on_exit() { + kill "$(cat "$QPID" 2>/dev/null)" 2>/dev/null || true +} + +# test_boot [extra-qemu-args...] — boot the system image headless with the +# serial log at $LOG and a monitor socket at $SOCK, then wait for the +# shell. Extra args attach more drives (e.g. a CRASHLOG stick). +test_boot() { + test_cleanup + trap test_on_exit EXIT + # A stale/poisoned OVMF varstore makes the firmware skip the + # \EFI\BOOT fallback entirely — every test boots from a fresh copy. + # Namespaced per test so parallel runs cannot race on it. + local nv="/tmp/rsdos-ovmf-vars-$TNAME.fd" + rm -f "$nv" + cp "$OVMF" "$nv" + local -a qargs + mapfile -t qargs < <(qemu_args) + "$QEMU" -bios "$nv" \ + "${qargs[@]}" \ + -serial "file:$LOG" \ + -monitor "unix:$SOCK,server,nowait" \ + -display none \ + "$@" >"$QLOG" 2>&1 & + echo $! >"$QPID" + wait_log "$LOG" "boot: entering shell" 90 || fail "kernel never reached the shell" +} 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 @@ # # scripts/mksystem.sh — assemble the single dd-able rs-dos system image # -# Replaces the old two-source setup (vvfat `esp/` directory for boot + -# separate /tmp/disk.img for data). One FAT32 image holds EVERYTHING: +# One FAT32 image holds EVERYTHING (boot medium and data medium are the +# same disk, mounted by the kernel at /disk//): # -# \bzImage kernel (rsbl requires this exact path) -# \initramfs.cpio.gz empty initramfs (rsbl requires it) -# \EFI\BOOT\BOOTX64.EFI boot entry: the kernel (default) or rsboot +# \EFI\BOOT\BOOTX64.EFI the kernel — the firmware boots it directly +# \bzImage kernel copy (rsbl's expected path, real hw) +# \initramfs.cpio.gz empty initramfs (rsbl requires the path) +# \bin\cli\*.efi terminal binaries (invoked by the shell) +# \bin\gui\*.efi windowed binaries (launched with no shell) # \etc\motd.txt seed config -# \bin\cli\ \bin\gui\ binary tree (empty until Phase 2/4 migrate tools) # \*.txt seed files (seed/) # -# The firmware boots from this image and the kernel mounts the same -# volume at /disk// — boot medium and data medium are one disk. +# Freshness: an existing image is only rebuilt when a kernel/binary/seed +# file is newer than it, so files written at runtime survive across +# boots. --force always re-creates (wipes runtime data). # # Usage: -# ./scripts/mksystem.sh [options] [output.img] # default /tmp/system.img +# ./scripts/mksystem.sh [options] [output.img] default: $SYSTEM_IMG # # Options: -# --force rebuild even if the image exists (Makefile stamps handle -# normal freshness; without --force an existing image is kept, -# so user files survive across reboots) +# --force rebuild even if the image is up to date # --size NM image size in MB (default 64) -# --chain \EFI\BOOT\BOOTX64.EFI = rsboot bootloader (full chain: -# firmware → rsboot → rsnix). Default is the kernel itself -# (direct: firmware → rsnix), matching the old dev loop. -# --gpt wrap the FAT volume in a GPT partition table (for dd-ing -# to real USB sticks whose firmware refuses superfloppies) # --label NAME FAT volume label (default RSDOS) +# --gpt wrap the FAT volume in a GPT partition table (for +# dd-ing to real USB sticks whose firmware refuses +# superfloppies) +# --help show this help +# +# Needs: dosfstools (mkfs.fat), mtools (mcopy/mmd), cargo (kernel+bins +# built via scripts/build.sh when missing). # set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -PROJECT_DIR="$(dirname "$SCRIPT_DIR")" -cd "$PROJECT_DIR" +# Absolute self path: lib.sh cd's to the project root, so a relative $0 +# would no longer resolve by the time --help prints the header. +SELF="$SCRIPT_DIR/$(basename "$0")" + +# shellcheck source=lib.sh +source "$SCRIPT_DIR/lib.sh" -IMG="/tmp/system.img" +IMG="$SYSTEM_IMG" SIZE_MB=64 FORCE=0 -CHAIN=0 GPT=0 LABEL="RSDOS" @@ -52,18 +57,15 @@ while [ $# -gt 0 ]; do shift SIZE_MB="$1" ;; - --chain) - CHAIN=1 - ;; - --gpt) - GPT=1 - ;; --label) shift LABEL="$1" ;; + --gpt) + GPT=1 + ;; --help | -h) - head -30 "$0" | grep "^#" | sed 's/^# \?//' + awk 'NR > 1 && /^#/ { sub(/^# ?/, ""); print; next } NR > 1 { exit }' "$SELF" exit 0 ;; -*) @@ -77,15 +79,9 @@ while [ $# -gt 0 ]; do shift done -TARGET=x86_64-unknown-uefi -PROFILE=release -OUT_DIR="target/$TARGET/$PROFILE" -KERNEL="$OUT_DIR/rsdos.efi" -BZIMAGE="esp/bzImage" -INITRD="esp/initramfs.cpio.gz" -BOOTLOADER="build/bootloader/BOOTX64.EFI" +SEED_DIR="$PROJECT_DIR/seed" -# ---- checks ---- +# ---- checks --------------------------------------------------------------- for cmd in mkfs.fat mcopy mmd; do if ! command -v "$cmd" >/dev/null 2>&1; then echo "mksystem: $cmd not found (install dosfstools + mtools)" >&2 @@ -101,29 +97,26 @@ if [ "$GPT" -eq 1 ]; then done fi -if [ -f "$IMG" ] && [ "$FORCE" -eq 0 ]; then - echo "mksystem: $IMG already exists (use --force to re-create)" - exit 0 +# ---- build (only when artifacts are missing; build.sh is the only cargo +# invocation, so an already-compiled tree costs nothing) -------------------- +if [ ! -f "$KERNEL" ] || ! ls "$OUT_DIR"/*.efi >/dev/null 2>&1; then + "$SCRIPT_DIR/build.sh" fi -# ---- build kernel ---- -if [ ! -f "$KERNEL" ]; then - echo "mksystem: building kernel..." - cargo build --release --package rsdos --target "$TARGET" -fi - -# ---- build bootloader (chain mode only) ---- -if [ "$CHAIN" -eq 1 ] && [ ! -f "$BOOTLOADER" ]; then - echo "mksystem: building bootloader..." - "$SCRIPT_DIR/build-bootloader.sh" +# ---- freshness stamp ------------------------------------------------------ +newest="" +for f in "$OUT_DIR"/*.efi "$SEED_DIR"/*; do + [ -f "$f" ] || continue + if [ -z "$newest" ] || [ "$f" -nt "$newest" ]; then + newest="$f" + fi +done +if [ "$FORCE" -eq 0 ] && [ -f "$IMG" ] && { [ -z "$newest" ] || [ ! "$newest" -nt "$IMG" ]; }; then + echo "mksystem: up to date: $IMG (use --force to re-create)" + exit 0 fi -# ---- deploy kernel artifacts to esp/ (shared with the old layout) ---- -mkdir -p esp/EFI/BOOT -cp "$KERNEL" "$BZIMAGE" -[ -f "$INITRD" ] || printf '' | gzip >"$INITRD" - -# ---- create the FAT volume ---- +# ---- create the FAT volume ------------------------------------------------ rm -f "$IMG" SIZE_BLOCKS=$((SIZE_MB * 1024)) # mkfs.fat counts 1024-byte blocks echo "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 echo "mksystem: staging system tree" mmd -i "$IMG" ::/EFI ::/EFI/BOOT ::/etc ::/bin ::/bin/cli ::/bin/gui -mcopy -i "$IMG" "$BZIMAGE" ::/bzImage + +# The firmware entry point and rsbl's kernel path are the same file. +mcopy -i "$IMG" "$KERNEL" ::/EFI/BOOT/BOOTX64.EFI +mcopy -i "$IMG" "$KERNEL" ::/bzImage +INITRD="$(mktemp -t rsdos-initrd.XXXXXX)" +trap 'rm -f "$INITRD"' EXIT +printf '' | gzip >"$INITRD" mcopy -i "$IMG" "$INITRD" ::/initramfs.cpio.gz -if [ "$CHAIN" -eq 1 ]; then - mcopy -i "$IMG" "$BOOTLOADER" ::/EFI/BOOT/BOOTX64.EFI - echo " \\EFI\\BOOT\\BOOTX64.EFI <- rsboot (full chain)" -else - mcopy -i "$IMG" "$KERNEL" ::/EFI/BOOT/BOOTX64.EFI - echo " \\EFI\\BOOT\\BOOTX64.EFI <- kernel (direct)" -fi -echo " \\bzImage \\initramfs.cpio.gz \\bin{\\cli,\\gui} \\etc" +echo " \\EFI\\BOOT\\BOOTX64.EFI \\bzImage \\initramfs.cpio.gz" + +# External binaries: everything under bins/ was compiled into $OUT_DIR. +# Launch surface decides the directory (ADR 0006): GUI_BINS -> /bin/gui, +# everything else -> /bin/cli. +for src in "$OUT_DIR"/*.efi; do + [ -f "$src" ] || continue + name="$(basename "$src" .efi)" + [ "$name" = "rsdos" ] && continue # the kernel is not a /bin binary + case " $GUI_BINS " in + *" $name "*) kind="gui" ;; + *) kind="cli" ;; + esac + mcopy -i "$IMG" "$src" "::/bin/$kind/$name.efi" + printf ' \\bin\\%s\\%s.efi\n' "$kind" "$name" +done # Seed files: motd lives in \etc, everything else at the volume root. -SEED_DIR="$PROJECT_DIR/seed" if [ -d "$SEED_DIR" ]; then for f in "$SEED_DIR"/*; do [ -f "$f" ] || continue name="$(basename "$f")" if [ "$name" = "motd.txt" ]; then mcopy -i "$IMG" "$f" ::/etc/"$name" - echo " \\etc\\$name" + printf ' \\etc\\%s\n' "$name" else mcopy -i "$IMG" "$f" ::/"$name" - echo " \\$name" + printf ' \\%s\n' "$name" fi done fi -# ---- optional GPT wrapper (real-hardware dd target) ---- +# ---- optional GPT wrapper (real-hardware dd target) ----------------------- if [ "$GPT" -eq 1 ]; then GPT_START=2048 # 1 MiB offset (standard alignment) GPT_IMG="$(mktemp)" @@ -179,6 +185,6 @@ fi echo "" echo "=== Done: $IMG ===" ls -lh "$IMG" -echo "Boot with: make run (or: make boot-image)" +echo "Boot with: ./scripts/run.sh (or: make run)" [ "$GPT" -eq 0 ] && echo "USB dd tip: add --gpt for firmwares that refuse superfloppies" exit 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 @@ -#!/usr/bin/env bash -# ============================================================================= -# scripts/qemu.sh — flexible QEMU launcher for rs-dos -# -# Usage: -# ./scripts/qemu.sh # serial console (no graphics) -# ./scripts/qemu.sh --gui # graphical window -# ./scripts/qemu.sh --debug # GDB stub on :1234 -# ./scripts/qemu.sh --chain # full boot chain (firmware → rsboot → rsnix) -# ./scripts/qemu.sh --bios /path/to.fd # custom OVMF -# ./scripts/qemu.sh --mem 512M # memory size -# ./scripts/qemu.sh --smp 2 # SMP cores -# -# One image, one drive — the system image contains kernel + ESP + data. -# ============================================================================= -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" -cd "$PROJECT_DIR" - -# Defaults -GUI_FLAG="" -DEBUG_FLAG="" -CHAIN_FLAG="" -OVMF="${OVMF:-/usr/share/edk2/x64/OVMF.4m.fd}" -MEM="${MEM:-256M}" -SMP="${SMP:-1}" -SYSTEM_IMG="${SYSTEM_IMG:-/tmp/system.img}" - -# Pre-build -make all - -# Parse args -while [ $# -gt 0 ]; do - case "$1" in - --gui | -g) - GUI_FLAG="-vga std" - shift - ;; - --debug | -d) - DEBUG_FLAG="-s -S" - shift - ;; - --chain | -c) - CHAIN_FLAG=1 - SYSTEM_IMG="${SYSTEM_IMG:-rs-dos.img}" - shift - ;; - --image | -i) - SYSTEM_IMG="$2" - shift 2 - ;; - --bios | -b) - OVMF="$2" - shift 2 - ;; - --mem) - MEM="$2" - shift 2 - ;; - --smp) - SMP="$2" - shift 2 - ;; - *) - echo "unknown: $1" - exit 1 - ;; - esac -done - -# Stage system image if needed -if [ -n "$CHAIN_FLAG" ]; then - ./scripts/mksystem.sh --chain --gpt "$SYSTEM_IMG" -else - ./scripts/mksystem.sh "$SYSTEM_IMG" -fi - -echo "" -echo "=== Booting ===" -echo " image: $SYSTEM_IMG" -echo " ovmf: $OVMF" -echo " mem: $MEM smp: $SMP" -echo " gui: $([ -n "$GUI_FLAG" ] && echo yes || echo no)" -echo " gdb: $([ -n "$DEBUG_FLAG" ] && echo ':1234' || echo off)" -echo "" - -exec qemu-system-x86_64 \ - -bios "$OVMF" \ - -drive "file=$SYSTEM_IMG,format=raw,if=none,id=hd" \ - -device ide-hd,drive=hd \ - -serial stdio \ - -m "$MEM" \ - -machine q35 \ - -cpu qemu64 \ - -no-reboot \ - -smp "$SMP" \ - $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 @@ +#!/usr/bin/env bash +# +# scripts/run.sh — build, stage and boot the system image in QEMU. +# +# Usage: +# ./scripts/run.sh build + stage + boot (window + serial) +# ./scripts/run.sh --headless serial console only, no window +# ./scripts/run.sh --debug GDB stub on :1234, CPU starts paused +# (attach: gdb -x scripts/debug-kernel.py) +# ./scripts/run.sh --monitor HMP monitor socket for sendkeys.py / +# test-focus.py (default path below) +# ./scripts/run.sh --fresh re-create the system image first +# (wipes runtime data on the image) +# ./scripts/run.sh --mem 512M guest memory (default 256M) +# ./scripts/run.sh --smp 2 guest CPUs (default 1) +# ./scripts/run.sh --bios FILE OVMF firmware (default: system path) +# ./scripts/run.sh --image FILE system image (default: /tmp/system.img) +# ./scripts/run.sh --grab confine the pointer: GTK runs as an X11 +# (XWayland) client and grabs on hover +# ./scripts/run.sh --no-grab Wayland-native window, no pointer grab +# +# Mouse: Wayland forbids global pointer grabs, so a Wayland-native window +# cannot confine the cursor (QEMU's grab silently does nothing). +# In a Wayland session with XWayland, run.sh defaults to --grab; +# Ctrl+Alt+G toggles the grab in either mode. +# +# Environment (same names the other scripts honour): TARGET, PROFILE, +# SYSTEM_IMG, OVMF, QEMU, MEM, SMP. +# +# Two-command flow: ./scripts/build.sh && ./scripts/run.sh +# One-command flow: ./scripts/run.sh (builds what is missing) +# +# Monitor socket: /tmp/rsdos-monitor.sock +# +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" + +usage() { + awk 'NR > 1 && /^#/ { sub(/^# ?/, ""); print; next } NR > 1 { exit }' "$0" +} + +HEADLESS=0 +DEBUG=0 +FRESH=0 +MON=0 +MON_SOCK="/tmp/rsdos-monitor.sock" +GRAB="" # "" = auto: grab when a Wayland session has XWayland + +while [ $# -gt 0 ]; do + case "$1" in + --headless | -H) HEADLESS=1 ;; + # GDB stub + keep whatever profile the env asked for (release is the + # default; PROFILE=debug ./scripts/run.sh --debug adds line symbols). + --debug | -d) DEBUG=1 ;; + --fresh) FRESH=1 ;; + --monitor) MON=1 ;; + --mem) shift; MEM="$1" ;; + --smp) shift; SMP="$1" ;; + --bios) shift; OVMF="$1" ;; + --image | -i) shift; SYSTEM_IMG="$1" ;; + --grab) GRAB=1 ;; + --no-grab) GRAB=0 ;; + --help | -h) + usage + exit 0 + ;; + *) + echo "run.sh: unknown argument: $1" >&2 + usage >&2 + exit 1 + ;; + esac + shift +done + +# shellcheck source=lib.sh +source "$SCRIPT_DIR/lib.sh" # reads MEM/SMP/OVMF/SYSTEM_IMG from the env + +# build.sh is a separate process: it re-derives OUT_DIR/KERNEL from these, +# so whatever we resolved above (including a --mem/--bios override) wins +# there too. +export TARGET PROFILE QEMU MEM SMP OVMF SYSTEM_IMG + +# ---- mouse grab (Wayland) -------------------------------------------------- +# Wayland forbids global pointer grabs, so a Wayland-native GTK window cannot +# confine the cursor — QEMU's grab silently does nothing there. XWayland can, +# so a Wayland session that has XWayland (DISPLAY set) defaults to GTK-on-X11 +# plus grab-on-hover. Ctrl+Alt+G toggles the grab; --no-grab keeps the +# Wayland-native window. +if [ -z "$GRAB" ]; then + if [ "${XDG_SESSION_TYPE:-}" = "wayland" ] && [ -n "${DISPLAY:-}" ]; then + GRAB=1 + else + GRAB=0 + fi +fi +[ "$HEADLESS" -eq 1 ] && GRAB=0 # no window, nothing to grab +if [ "$GRAB" -eq 1 ]; then + if [ -n "${DISPLAY:-}" ]; then + export GDK_BACKEND=x11 + else + echo "run.sh: --grab needs XWayland (DISPLAY is unset); not grabbing" >&2 + GRAB=0 + fi +fi + +# ---- build + stage -------------------------------------------------------- +"$SCRIPT_DIR/build.sh" +if [ "$FRESH" -eq 1 ]; then + stage_image --force +else + stage_image +fi + +# ---- boot ----------------------------------------------------------------- +mapfile -t QARGS < <(qemu_args) +if [ "$HEADLESS" -eq 1 ]; then + QARGS+=(-display none) +else + QARGS+=(-vga std) + if [ "$GRAB" -eq 1 ]; then + QARGS+=(-display gtk,grab-on-hover=on) + fi +fi +[ "$MON" -eq 1 ] && QARGS+=(-monitor "unix:$MON_SOCK,server,nowait") +[ "$DEBUG" -eq 1 ] && QARGS+=(-s -S) +QARGS+=(-serial stdio) + +echo "" +echo "=== boot ===" +echo " image: $SYSTEM_IMG" +echo " ovmf: $OVMF" +echo " mem: $MEM smp: $SMP" +echo " mode: $([ "$HEADLESS" -eq 1 ] && echo headless || echo window) gdb: $([ "$DEBUG" -eq 1 ] && echo ':1234 (paused)' || echo off)" +if [ "$HEADLESS" -eq 0 ]; then + echo " mouse: $([ "$GRAB" -eq 1 ] && echo 'grab on hover (XWayland; Ctrl+Alt+G toggles)' || echo 'not grabbed (Wayland cannot confine the pointer)')" +fi +[ "$MON" -eq 1 ] && echo " monitor: unix:$MON_SOCK" +echo "" + +exec "$QEMU" "${QARGS[@]}" diff --git a/scripts/sendkeys.py b/scripts/sendkeys.py deleted file mode 100755 index a67903d..0000000 --- a/scripts/sendkeys.py +++ /dev/null @@ -1,103 +0,0 @@ -#!/usr/bin/env python3 -"""Type lines into a QEMU guest via the monitor's `sendkey` command. - -Each positional argument is one line: it is typed character by character -(US layout) followed by Enter. Use it to drive the rs-dos shell from a -test script without a display. - -Usage: - sendkeys.py "ls /disk" "echo hi WRITING /disk/a.txt" "poweroff" - sendkeys.py --sock /tmp/qmon.sock "date" - -Environment: QMON_SOCK (default: /tmp/qmon.sock) -""" -import os -import socket -import sys -import time - -SOCK = os.environ.get("QMON_SOCK", "/tmp/qmon.sock") - -# Unshifted punctuation needs QEMU's *named* keys — bare '-'/'.'/'/' are -# not valid sendkey names (and '-' is the modifier separator). -BASE = { - "-": "minus", "=": "equal", "[": "bracketleft", "]": "bracketright", - "\\": "backslash", ";": "semicolon", "'": "apostrophe", - ",": "comma", ".": "dot", "/": "slash", "`": "grave", -} - -# Characters that need a shift key on US layout, and the key QEMU's -# `sendkey` understands for them. -SHIFT = { - "~": "grave", "!": "1", "@": "2", "#": "3", "$": "4", "%": "5", - "^": "6", "&": "7", "*": "8", "(": "9", ")": "0", - "_": "minus", "+": "equal", "{": "bracketleft", "}": "bracketright", - "|": "backslash", ":": "semicolon", '"': "apostrophe", - "<": "comma", ">": "dot", "?": "slash", -} - -ALNUM = set("abcdefghijklmnopqrstuvwxyz0123456789") - - -def connect(path, timeout=60): - """Connect to the monitor socket, retrying until QEMU is up.""" - deadline = time.time() + timeout - while True: - try: - s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) - s.connect(path) - return s - except OSError: - if time.time() > deadline: - raise - time.sleep(0.2) - - -def sendkey(sock, keys): - sock.sendall(("sendkey %s\n" % keys).encode()) - - -def type_line(sock, line): - for ch in line: - if ch == " ": - sendkey(sock, "spc") - elif ch in BASE: - sendkey(sock, BASE[ch]) - elif ch in SHIFT: - sendkey(sock, "shift-" + SHIFT[ch]) - elif ch.isupper(): - sendkey(sock, "shift-" + ch.lower()) - elif ch in ALNUM: - sendkey(sock, ch) - else: - sys.stderr.write("warning: no sendkey mapping for %r\n" % ch) - continue - time.sleep(0.25) - sendkey(sock, "ret") - time.sleep(1.5) - - -def main(): - args = [a for a in sys.argv[1:] if a != "--sock"] - sock_path = SOCK - if "--sock" in sys.argv: - i = sys.argv.index("--sock") - sock_path = sys.argv[i + 1] - args = sys.argv[i + 2:] - if not args: - sys.stderr.write(__doc__ or "") - return 1 - sock = connect(sock_path) - # Let the monitor drain any startup banner before typing. - time.sleep(0.5) - for line in args: - try: - type_line(sock, line) - except OSError: - # Guest powered off (last line was usually "poweroff"). - break - return 0 - - -if __name__ == "__main__": - sys.exit(main()) 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 @@ -#!/usr/bin/env bash -# -# scripts/test-disk.sh — full automated data-disk round trip. -# -# Builds the kernel, ensures the FAT32 image exists (with --fresh it is -# re-formatted and re-seeded), boots QEMU headless, drives the shell via -# the monitor, then verifies the results with mtools and fsck. -# -# Usage: -# ./scripts/test-disk.sh # reuse existing image -# ./scripts/test-disk.sh --fresh # re-format the image first -# -# Environment: -# DISK_IMG data disk path (default: /tmp/disk.img) -# OVMF firmware (default: /usr/share/edk2/x64/OVMF.4m.fd) -# QEMU qemu binary (default: qemu-system-x86_64) -# -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -PROJECT_DIR="$(dirname "$SCRIPT_DIR")" -cd "$PROJECT_DIR" - -FRESH=0 -[ "${1:-}" = "--fresh" ] && FRESH=1 - -OVMF="${OVMF:-/usr/share/edk2/x64/OVMF.4m.fd}" -QEMU="${QEMU:-qemu-system-x86_64}" -IMG="${SYSTEM_IMG:-/tmp/system.img}" -QPID=/tmp/rsdos-test-qpid -SOCK=/tmp/rsdos-test-qmon.sock -LOG=/tmp/rsdos-test-serial.log -QLOG=/tmp/rsdos-test-qemu.log -STAMP="$(date +%s)" - -# Never leave a QEMU behind (an orphan holding our stdout would hang any -# caller that pipes our output). -trap 'kill "$(cat "$QPID" 2>/dev/null)" 2>/dev/null || true' EXIT - -fail() { - echo "FAIL: $1" - [ -f "$LOG" ] && tail -20 "$LOG" - exit 1 -} - -# wait_log FILE PATTERN TIMEOUT-SECONDS — poll a file for a regex. -wait_log() { - local i=0 - while [ "$i" -lt $((${3:-60} * 10)) ]; do - grep -q "$2" "$1" 2>/dev/null && return 0 - sleep 0.1 - i=$((i + 1)) - done - return 1 -} - -# wait_qemu_exit TIMEOUT-SECONDS — poll until the pid is gone. -wait_qemu_exit() { - local i=0 - while [ "$i" -lt $((${1:-30} * 10)) ]; do - kill -0 "$(cat "$QPID" 2>/dev/null)" 2>/dev/null || return 0 - sleep 0.1 - i=$((i + 1)) - done - return 1 -} - -echo "==> 1/6 build kernel (debug)" -cargo build --release --package rsdos --target x86_64-unknown-uefi - -echo "==> 2/6 ensure system image" -if [ "$FRESH" -eq 1 ]; then - "$SCRIPT_DIR/mksystem.sh" --force "$IMG" -else - "$SCRIPT_DIR/mksystem.sh" "$IMG" -fi - -echo "==> 3/6 boot QEMU (headless)" -kill "$(cat "$QPID" 2>/dev/null)" 2>/dev/null || true -sleep 1 -rm -f "$SOCK" "$LOG" -# Throwaway OVMF varstore: a stale/poisoned NvVars (e.g. left by the old -# vvfat setup) makes the firmware skip the \EFI\BOOT fallback entirely. -NV="/tmp/rsdos-test-ovmf-vars.fd" -rm -f "$NV" -cp /usr/share/edk2/x64/OVMF.4m.fd "$NV" -"$QEMU" -bios "$NV" \ - -drive file="$IMG",format=raw,if=none,id=disk \ - -device ide-hd,drive=disk,bus=ide.0 \ - -serial file:"$LOG" \ - -monitor unix:"$SOCK",server,nowait \ - -m 256M -machine q35 -cpu qemu64 -no-reboot -display none \ - >"$QLOG" 2>&1 & - -wait_log "$LOG" "boot: entering shell" 90 || fail "kernel never reached the shell" -sleep 1 - -# Volumes mount under /disk// where UID = "LABEL-SERIAL" (e.g. -# RSDOS-5BD1E55F). Discover it from the boot log so the test works on -# any image without knowing the serial in advance. -VOL_UID="$(grep -a 'fat: mounted' "$LOG" | sed -n 's/.*at \/disk\/\([^ )]*\).*/\1/p' | head -1)" -[ -n "$VOL_UID" ] || fail "no FAT32 volume was mounted (look for 'fat: mounted' in the log)" -echo " mounted volume UID: $VOL_UID" - -echo "==> 4/6 drive the shell (ls / mkdir / echo WRITING / cat / poweroff)" -# The filename is deliberately >13 chars so the FAT LFN path gets -# multi-entry entries (they're stored last-first on disk). -TESTFILE="disk-test-$STAMP.txt" -python3 "$SCRIPT_DIR/sendkeys.py" --sock "$SOCK" \ - "ls /disk" \ - "mkdir /disk/$VOL_UID/sub" \ - "echo disk-test-$STAMP WRITING /disk/$VOL_UID/sub/$TESTFILE" \ - "cat /disk/$VOL_UID/sub/$TESTFILE" \ - "poweroff" - -echo "==> 5/6 wait for shutdown" -wait_qemu_exit 30 || fail "QEMU did not exit after poweroff" - -echo "==> 6/6 verify" -grep -a "disk-test-$STAMP" "$LOG" >/dev/null || fail "cat output missing from serial log" -if ! fsck.fat -n "$IMG" >/tmp/rsdos-fsck.out 2>&1; then - fail "fsck.fat reports problems: $(cat /tmp/rsdos-fsck.out | tail -3)" -fi -MT="$(mtype -i "$IMG" "::/sub/$TESTFILE" 2>/dev/null || true)" -[ "$MT" = "disk-test-$STAMP" ] || fail "mtype mismatch: got '$MT'" - -echo "" -echo "PASS — disk round trip OK (fsck clean, file survived poweroff)" -echo " image: $IMG" -echo " volume: /disk/$VOL_UID" -echo " 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 @@ -#!/usr/bin/env bash -# -# scripts/test-fault.sh — fault-diagnostics verification. -# -# Proves the fault-dump path: every CPU exception prints a white-on-red -# band to the framebuffer and a parseable serial line before halting. -# -# Usage: ./scripts/test-fault.sh -# Env: OVMF, QEMU, SYSTEM_IMG -# -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" -cd "$PROJECT_DIR" - -OVMF="${OVMF:-/usr/share/edk2/x64/OVMF.4m.fd}" -QEMU="${QEMU:-qemu-system-x86_64}" -SYSTEM_IMG="${SYSTEM_IMG:-/tmp/system.img}" -QPID=/tmp/rsdos-fault-qpid -SOCK=/tmp/rsdos-fault-qmon.sock -LOG=/tmp/rsdos-fault-serial.log -QLOG=/tmp/rsdos-fault-qemu.log - -trap 'kill "$(cat "$QPID" 2>/dev/null)" 2>/dev/null || true' EXIT - -fail() { - echo "FAIL: $1" - [ -f "$LOG" ] && tail -40 "$LOG" - exit 1 -} - -wait_log() { - local i=0 - while [ "$i" -lt $((${3:-60} * 10)) ]; do - grep -q "$2" "$1" 2>/dev/null && return 0 - sleep 0.1 - i=$((i + 1)) - done - return 1 -} - -echo "==> 1/5 build kernel (debug)" -cargo build --release --package rsdos --target x86_64-unknown-uefi - -echo "==> 2/5 stage system image" -"$SCRIPT_DIR/mksystem.sh" --force "$SYSTEM_IMG" - -echo "==> 3/5 boot QEMU (headless, + dedicated CRASHLOG stick)" -kill "$(cat "$QPID" 2>/dev/null)" 2>/dev/null || true -sleep 1 -rm -f "$SOCK" "$LOG" -CRASH_IMG=/tmp/rsdos-crash.img -mkfs.fat -C -F 32 -n CRASHLOG "$CRASH_IMG" >/dev/null -"$QEMU" -bios "$OVMF" \ - -drive file="$SYSTEM_IMG",format=raw,if=none,id=disk \ - -device ide-hd,drive=disk,bus=ide.0 \ - -drive file="$CRASH_IMG",format=raw,if=none,id=crash \ - -device ide-hd,drive=crash,bus=ide.1 \ - -serial file:"$LOG" \ - -monitor unix:"$SOCK",server,nowait \ - -m 256M -machine q35 -cpu qemu64 -no-reboot -display none \ - >"$QLOG" 2>&1 & -echo $! >"$QPID" - -wait_log "$LOG" "boot: entering shell" 90 || fail "kernel never reached the shell" - -echo "==> 4/5 boot diagnostics present" -grep -aq "gop: fb base=" "$LOG" || fail "fb geometry diagnostic missing" -grep -aq "gop: mode .* selected" "$LOG" || fail "mode-selection diagnostic missing" -grep -aq "CRASHLOG" "$LOG" || fail "CRASHLOG stick not mounted" - -echo "==> 5/5 inject a page fault (fault command)" -python3 "$SCRIPT_DIR/sendkeys.py" --sock "$SOCK" \ - "clear" \ - "fault" -sleep 2 - -grep -aq "!!! page fault vec=14 .*rip=" "$LOG" || fail "page-fault dump line missing" -grep -aq "cr2=0x400000000000" "$LOG" || fail "CR2 not reported (faulting address)" -grep -aq "task=0 \"shell\"" "$LOG" || fail "faulting task not identified" -grep -aq "!!! halting" "$LOG" || fail "no halt message" - -echo "" -echo "PASS — fault dump verified" diff --git a/scripts/test-focus.py b/scripts/test-focus.py deleted file mode 100755 index e1bbe2d..0000000 --- a/scripts/test-focus.py +++ /dev/null @@ -1,56 +0,0 @@ -#!/usr/bin/env python3 -"""Drive the focus-routing gate: 3 windows, type markers, verify routing. - -Connects to the QEMU monitor once and keeps the socket open (the per-call -scripts/sendkeys.py pattern loses the socket when the monitor resets). - -Usage: python3 scripts/test-focus.py /tmp/qmon.sock -""" -import socket -import sys -import time - -sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) -sock.connect(sys.argv[1] if len(sys.argv) > 1 else "/tmp/sysboot-mon.sock") - - -def sk(k, d=0.35): - sock.sendall(f"sendkey {k}\n".encode()) - time.sleep(d) - - -def line(txt, d=0.8): - for ch in txt: - if ch == " ": - sk("spc", 0.1) - elif ch.isupper(): - sk("shift-" + ch.lower(), 0.12) - else: - sk(ch, 0.12) - sk("ret", d) - - -time.sleep(0.5) - -# Home is focused at boot. -line("windows") # expect: home + nothing (or existing windows) -time.sleep(1.5) - -# F1 -> term1 (focused on creation) -sk("f1", 1.5) -line("echo TERM1MARK") -time.sleep(1.0) - -# F1 -> term2 (focused on creation) -sk("f1", 1.5) -line("echo TERM2MARK") -time.sleep(1.0) - -# F2 cycles z-order: term2 -> term1 -sk("f2", 0.8) -line("echo CYCLED") -time.sleep(1.0) - -# F2 again: term1 -> home (window 0, oldest) -sk("f2", 0.8) -line("windows") 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 @@ -#!/usr/bin/env bash -# -# scripts/test-gfx.sh — automated graphics round-trip test. -# -# Builds kernel, boots headless, runs the in-kernel gfx self-test and -# the draw/paint demo. -# -# Env: OVMF, QEMU, SYSTEM_IMG -# -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" -cd "$PROJECT_DIR" - -OVMF="${OVMF:-/usr/share/edk2/x64/OVMF.4m.fd}" -QEMU="${QEMU:-qemu-system-x86_64}" -SYSTEM_IMG="${SYSTEM_IMG:-/tmp/system.img}" -QPID=/tmp/rsdos-gfx-qpid -SOCK=/tmp/rsdos-gfx-qmon.sock -LOG=/tmp/rsdos-gfx-serial.log -QLOG=/tmp/rsdos-gfx-qemu.log - -trap 'kill "$(cat "$QPID" 2>/dev/null)" 2>/dev/null || true' EXIT - -fail() { - echo "FAIL: $1" - [ -f "$LOG" ] && tail -40 "$LOG" - exit 1 -} - -wait_log() { - local i=0 - while [ "$i" -lt $((${3:-60} * 10)) ]; do - grep -q "$2" "$1" 2>/dev/null && return 0 - sleep 0.1 - i=$((i + 1)) - done - return 1 -} - -echo "==> 1/5 build kernel (debug)" -cargo build --release --package rsdos --target x86_64-unknown-uefi - -echo "==> 2/5 stage system image" -"$SCRIPT_DIR/mksystem.sh" --force "$SYSTEM_IMG" - -echo "==> 3/5 boot QEMU (headless)" -kill "$(cat "$QPID" 2>/dev/null)" 2>/dev/null || true -sleep 1 -rm -f "$SOCK" "$LOG" -"$QEMU" -bios "$OVMF" \ - -drive file="$SYSTEM_IMG",format=raw,if=none,id=disk \ - -device ide-hd,drive=disk,bus=ide.0 \ - -serial file:"$LOG" \ - -monitor unix:"$SOCK",server,nowait \ - -m 256M -machine q35 -cpu qemu64 -no-reboot -display none \ - >"$QLOG" 2>&1 & -echo $! >"$QPID" - -wait_log "$LOG" "boot: entering shell" 90 || fail "kernel never reached the shell" -wait_log "$LOG" "cursor: task " 10 || fail "cursor task did not spawn" - -echo "==> 4/5 in-kernel pixel self-test (gfx check)" -python3 "$SCRIPT_DIR/sendkeys.py" --sock "$SOCK" "gfx check" -wait_log "$LOG" "gfx: check" 30 || fail "gfx check did not run" - -echo "==> 5/5 gfx demo (draw scene)" -python3 "$SCRIPT_DIR/sendkeys.py" --sock "$SOCK" "gfx" "poweroff" -wait_qemu_exit() { - local i=0 - while [ "$i" -lt $((${1:-30} * 10)) ]; do - kill -0 "$(cat "$QPID" 2>/dev/null)" 2>/dev/null || return 0 - sleep 0.1 - i=$((i + 1)) - done - return 1 -} -wait_qemu_exit 30 || fail "QEMU did not exit after poweroff" - -grep -aq "gfx: drew status bar" "$LOG" || fail "gfx demo did not draw" - -echo "" -echo "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 @@ -#!/usr/bin/env bash -# -# scripts/test-mouse.sh — automated PS/2 mouse round-trip test. -# -# Boots QEMU headless, injects mouse movement via QEMU monitor, -# verifies the on-screen cursor tracks it, and tests the mouse -# stream routing command. -# -# Env: OVMF, QEMU, SYSTEM_IMG -# -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" -cd "$PROJECT_DIR" - -OVMF="${OVMF:-/usr/share/edk2/x64/OVMF.4m.fd}" -QEMU="${QEMU:-qemu-system-x86_64}" -SYSTEM_IMG="${SYSTEM_IMG:-/tmp/system.img}" -QPID=/tmp/rsdos-mouse-qpid -SOCK=/tmp/rsdos-mouse-qmon.sock -LOG=/tmp/rsdos-mouse-serial.log -QLOG=/tmp/rsdos-mouse-qemu.log - -trap 'kill "$(cat "$QPID" 2>/dev/null)" 2>/dev/null || true' EXIT - -fail() { - echo "FAIL: $1" - [ -f "$LOG" ] && tail -40 "$LOG" - exit 1 -} - -wait_log() { - local i=0 - while [ "$i" -lt $((${3:-60} * 10)) ]; do - grep -q "$2" "$1" 2>/dev/null && return 0 - sleep 0.1 - i=$((i + 1)) - done - return 1 -} - -qemu_hmp() { - python3 - "$SOCK" "$1" <<'EOF' -import socket, sys, time -s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) -s.connect(sys.argv[1]) -s.sendall((sys.argv[2] + "\n").encode()) -time.sleep(0.4) -EOF -} - -cursor_state() { - python3 "$SCRIPT_DIR/sendkeys.py" --sock "$SOCK" "cursor" - sleep 0.5 - sed -n 's/.*cursor: x=\([0-9]*\) y=\([0-9]*\) buttons=\([A-Za-z-]*\).*/\1 \2 \3/p' "$LOG" | tail -1 -} - -echo "==> 1/6 build kernel (debug)" -cargo build --release --package rsdos --target x86_64-unknown-uefi - -echo "==> 2/6 stage system image" -"$SCRIPT_DIR/mksystem.sh" --force "$SYSTEM_IMG" - -echo "==> 3/6 boot QEMU (headless)" -kill "$(cat "$QPID" 2>/dev/null)" 2>/dev/null || true -sleep 1 -rm -f "$SOCK" "$LOG" -"$QEMU" -bios "$OVMF" \ - -drive file="$SYSTEM_IMG",format=raw,if=none,id=disk \ - -device ide-hd,drive=disk,bus=ide.0 \ - -serial file:"$LOG" \ - -monitor unix:"$SOCK",server,nowait \ - -m 256M -machine q35 -cpu qemu64 -no-reboot -display none \ - >"$QLOG" 2>&1 & -echo $! >"$QPID" - -wait_log "$LOG" "boot: entering shell" 90 || fail "kernel never reached the shell" -grep -aq "ps2: mouse ready" "$LOG" || fail "mouse driver did not come up at boot" -wait_log "$LOG" "cursor: task " 10 || fail "cursor task did not spawn" - -echo "==> 4/6 cursor position tracks injected movement" -INIT=$(cursor_state) -[ -n "$INIT" ] || fail "no initial cursor position" -IX=$(echo "$INIT" | awk '{print $1}') -IY=$(echo "$INIT" | awk '{print $2}') -IB=$(echo "$INIT" | awk '{print $3}') -echo " initial: x=$IX y=$IY buttons=$IB" -[ "$IB" = "---" ] || fail "expected no buttons at boot, got $IB" - -qemu_hmp "mouse_move 20 7" -S1=$(cursor_state) -X1=$(echo "$S1" | awk '{print $1}') -Y1=$(echo "$S1" | awk '{print $2}') -[ "$X1" -eq $((IX + 20)) ] || fail "x did not move +20 (was $IX, now $X1)" -[ "$Y1" -eq $((IY + 7)) ] || fail "y did not move +7 (was $IY, now $Y1)" -echo " moved: x=$X1 y=$Y1 (delta +20,+7)" - -echo "==> 5/6 left button edge detection" -qemu_hmp "mouse_button 1" -S2=$(cursor_state) -IB2=$(echo "$S2" | awk '{print $3}') -[ "$IB2" = "L--" ] || [ "$IB2" = "L--" ] || fail "expected L--, got $IB2" - -qemu_hmp "mouse_button 0" -S3=$(cursor_state) -IB3=$(echo "$S3" | awk '{print $3}') -[ "$IB3" = "---" ] || fail "expected --- after release, got $IB3" -echo " button: $IB2 → $IB3" - -echo "==> 6/6 mouse stream routing to task 3" -python3 "$SCRIPT_DIR/sendkeys.py" --sock "$SOCK" "msgrecv" -sleep 1 -python3 "$SCRIPT_DIR/sendkeys.py" --sock "$SOCK" "mouse 3" -qemu_hmp "mouse_move 20 7" -qemu_hmp "mouse_button 1" -sleep 0.5 -qemu_hmp "mouse_button 0" -sleep 0.5 -python3 "$SCRIPT_DIR/sendkeys.py" --sock "$SOCK" "ctrl-c" -sleep 0.5 -grep -aq "Mouse(+20,+7" "$LOG" || fail "routed mouse move not logged" - -echo "" -echo "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 @@ -#!/usr/bin/env bash -# -# scripts/test-msg.sh — automated scheduler message-queue round trip. -# -# Tests all message types (user, timer, key, mouse), real-input routing, -# and queue-full boundary. -# -# Env: OVMF, QEMU, SYSTEM_IMG -# -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" -cd "$PROJECT_DIR" - -OVMF="${OVMF:-/usr/share/edk2/x64/OVMF.4m.fd}" -QEMU="${QEMU:-qemu-system-x86_64}" -SYSTEM_IMG="${SYSTEM_IMG:-/tmp/system.img}" -QPID=/tmp/rsdos-msg-qpid -SOCK=/tmp/rsdos-msg-qmon.sock -LOG=/tmp/rsdos-msg-serial.log -QLOG=/tmp/rsdos-msg-qemu.log - -trap 'kill "$(cat "$QPID" 2>/dev/null)" 2>/dev/null || true' EXIT - -fail() { - echo "FAIL: $1" - [ -f "$LOG" ] && tail -40 "$LOG" - exit 1 -} - -wait_log() { - local i=0 - while [ "$i" -lt $((${3:-60} * 10)) ]; do - grep -q "$2" "$1" 2>/dev/null && return 0 - sleep 0.1 - i=$((i + 1)) - done - return 1 -} - -qemu_hmp() { - python3 - "$SOCK" "$1" <<'EOF' -import socket, sys, time -s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) -s.connect(sys.argv[1]) -s.sendall((sys.argv[2] + "\n").encode()) -time.sleep(0.4) -EOF -} - -wait_qemu_exit() { - local i=0 - while [ "$i" -lt $((${1:-30} * 10)) ]; do - kill -0 "$(cat "$QPID" 2>/dev/null)" 2>/dev/null || return 0 - sleep 0.1 - i=$((i + 1)) - done - return 1 -} - -echo "==> 1/5 build kernel (debug)" -cargo build --release --package rsdos --target x86_64-unknown-uefi - -echo "==> 2/5 stage system image" -"$SCRIPT_DIR/mksystem.sh" --force "$SYSTEM_IMG" - -echo "==> 3/5 boot QEMU (headless)" -kill "$(cat "$QPID" 2>/dev/null)" 2>/dev/null || true -sleep 1 -rm -f "$SOCK" "$LOG" -"$QEMU" -bios "$OVMF" \ - -drive file="$SYSTEM_IMG",format=raw,if=none,id=disk \ - -device ide-hd,drive=disk,bus=ide.0 \ - -serial file:"$LOG" \ - -monitor unix:"$SOCK",server,nowait \ - -m 256M -machine q35 -cpu qemu64 -no-reboot -display none \ - >"$QLOG" 2>&1 & -echo $! >"$QPID" - -wait_log "$LOG" "boot: entering shell" 90 || fail "kernel never reached the shell" - -echo "==> 4/5 drive the shell (msgrecv / send / mouse routing / queue-full)" -python3 "$SCRIPT_DIR/sendkeys.py" --sock "$SOCK" \ - "msgrecv" \ - "send 3 user 42" \ - "send 3 timer" \ - "send 3 key enter" \ - "send 3 key ctrl-c" \ - "send 3 key f3" \ - "send 3 mouse 5 -3 3" \ - "sleep 800" \ - "jobs" - -python3 "$SCRIPT_DIR/sendkeys.py" --sock "$SOCK" "mouse 3" -qemu_hmp "mouse_move 20 7" -qemu_hmp "mouse_button 1" -qemu_hmp "mouse_move -5 -3" -qemu_hmp "mouse_button 0" -qemu_hmp "sendkey ctrl-c" - -python3 "$SCRIPT_DIR/sendkeys.py" --sock "$SOCK" \ - "send 0 user 1" \ - "send 0 user 2" \ - "send 0 user 3" \ - "send 0 user 4" \ - "send 0 user 5" \ - "send 0 user 6" \ - "send 0 user 7" \ - "send 0 user 8" \ - "send 0 user 9" \ - "send 0 user 10" \ - "send 0 user 11" \ - "send 0 user 12" \ - "send 0 user 13" \ - "send 0 user 14" \ - "send 0 user 15" \ - "send 0 user 16" \ - "send 0 user 17" \ - "sleep 800" \ - "jobs" \ - "poweroff" - -echo "==> 5/5 wait for shutdown + verify" -wait_qemu_exit 30 || fail "QEMU did not exit after poweroff" - -grep -aq "msgrecv: task 3 listening" "$LOG" || fail "msgrecv did not spawn" -grep -aq "msgrecv\[3\]: User(42)" "$LOG" || fail "user message missing" -grep -aq "msgrecv\[3\]: Timer" "$LOG" || fail "timer message missing" -grep -aq "msgrecv\[3\]: Key(Enter)" "$LOG" || fail "key enter missing" -grep -aq "msgrecv\[3\]: Key(Ctrl-c)" "$LOG" || fail "key ctrl-c missing" -grep -aq "msgrecv\[3\]: Key(F3)" "$LOG" || fail "key f3 missing" -grep -aq "msgrecv\[3\]: Mouse(+5,-3 L R)" "$LOG" || fail "synthesized mouse message missing" -grep -aq "msgrecv\[3\]: Mouse(+20,+7" "$LOG" || fail "real mouse move 20,7 not routed to queue" -grep -aq "msgrecv\[3\]: Mouse(-5,-3 L" "$LOG" || fail "held-button routed move missing" -grep -aq "queue full" "$LOG" || fail "17th send did not report queue full" -grep -aq "send: unknown type" "$LOG" && fail "a bad send slipped through (expected none)" - -echo "" -echo "PASS — message queues OK" diff --git a/scripts/tests/sendkeys.py b/scripts/tests/sendkeys.py new file mode 100755 index 0000000..9300242 --- /dev/null +++ b/scripts/tests/sendkeys.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 +"""Type lines into a QEMU guest via the monitor's `sendkey` command. + +Each positional argument is one line: it is typed character by character +(US layout) followed by Enter. Use it to drive the rs-dos shell from a +test script without a display. + +Used by scripts/tests/test-*.sh, and manually against an interactive +session started with `./scripts/run.sh --monitor`: + + ./scripts/tests/sendkeys.py "ls /disk" "date" "poweroff" + ./scripts/tests/sendkeys.py --sock /tmp/rsdos-monitor.sock "date" + +Environment: QMON_SOCK (default: /tmp/rsdos-monitor.sock) +""" +import os +import socket +import sys +import time + +SOCK = os.environ.get("QMON_SOCK", "/tmp/rsdos-monitor.sock") + +# Unshifted punctuation needs QEMU's *named* keys — bare '-'/'.'/'/' are +# not valid sendkey names (and '-' is the modifier separator). +BASE = { + "-": "minus", "=": "equal", "[": "bracketleft", "]": "bracketright", + "\\": "backslash", ";": "semicolon", "'": "apostrophe", + ",": "comma", ".": "dot", "/": "slash", "`": "grave", +} + +# Characters that need a shift key on US layout, and the key QEMU's +# `sendkey` understands for them. +SHIFT = { + "~": "grave", "!": "1", "@": "2", "#": "3", "$": "4", "%": "5", + "^": "6", "&": "7", "*": "8", "(": "9", ")": "0", + "_": "minus", "+": "equal", "{": "bracketleft", "}": "bracketright", + "|": "backslash", ":": "semicolon", '"': "apostrophe", + "<": "comma", ">": "dot", "?": "slash", +} + +ALNUM = set("abcdefghijklmnopqrstuvwxyz0123456789") + + +def connect(path, timeout=60): + """Connect to the monitor socket, retrying until QEMU is up.""" + deadline = time.time() + timeout + while True: + try: + s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + s.connect(path) + return s + except OSError: + if time.time() > deadline: + raise + time.sleep(0.2) + + +def sendkey(sock, keys): + sock.sendall(("sendkey %s\n" % keys).encode()) + + +def type_line(sock, line): + for ch in line: + if ch == " ": + sendkey(sock, "spc") + elif ch in BASE: + sendkey(sock, BASE[ch]) + elif ch in SHIFT: + sendkey(sock, "shift-" + SHIFT[ch]) + elif ch.isupper(): + sendkey(sock, "shift-" + ch.lower()) + elif ch in ALNUM: + sendkey(sock, ch) + else: + sys.stderr.write("warning: no sendkey mapping for %r\n" % ch) + continue + time.sleep(0.25) + sendkey(sock, "ret") + time.sleep(1.5) + + +def main(): + args = [a for a in sys.argv[1:] if a != "--sock"] + sock_path = SOCK + if "--sock" in sys.argv: + i = sys.argv.index("--sock") + sock_path = sys.argv[i + 1] + args = sys.argv[i + 2:] + if not args: + sys.stderr.write(__doc__ or "") + return 1 + sock = connect(sock_path) + # Let the monitor drain any startup banner before typing. + time.sleep(0.5) + for line in args: + try: + type_line(sock, line) + except OSError: + # Guest powered off (last line was usually "poweroff"). + break + return 0 + + +if __name__ == "__main__": + sys.exit(main()) 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 @@ +#!/usr/bin/env bash +# +# scripts/tests/test-disk.sh — full automated data-disk round trip. +# +# Builds, stages the system image, boots QEMU headless, drives the shell +# via the monitor (ls / mkdir / echo WRITING / cat / poweroff), then +# verifies the results with mtools and fsck. +# +# Usage: +# ./scripts/tests/test-disk.sh reuse the existing image +# ./scripts/tests/test-disk.sh --fresh reformat + reseed first +# +# Env: SYSTEM_IMG, OVMF, QEMU (see scripts/lib.sh) +# +set -euo pipefail +TNAME="disk" +# shellcheck source=../lib.sh +source "$(dirname "$0")/../lib.sh" + +FRESH=0 +[ "${1:-}" = "--fresh" ] && FRESH=1 +STAMP="$(date +%s)" + +echo "==> 1/6 build (kernel + bins)" +build_all + +echo "==> 2/6 stage system image" +if [ "$FRESH" -eq 1 ]; then stage_image --force; else stage_image; fi + +echo "==> 3/6 boot QEMU (headless)" +test_boot +sleep 1 + +# Volumes mount under /disk// where UID = "LABEL-SERIAL" (e.g. +# RSDOS-5BD1E55F). Discover it from the boot log so the test works on +# any image without knowing the serial in advance. +VOL_UID="$(grep -a 'fat: mounted' "$LOG" | sed -n 's/.*at \/disk\/\([^ )]*\).*/\1/p' | head -1)" +[ -n "$VOL_UID" ] || fail "no FAT32 volume was mounted (look for 'fat: mounted' in the log)" +echo " mounted volume UID: $VOL_UID" + +echo "==> 4/6 drive the shell (ls / mkdir / echo WRITING / cat / poweroff)" +# The filename is deliberately >13 chars so the FAT LFN path gets +# multi-entry entries (they're stored last-first on disk). +TESTFILE="disk-test-$STAMP.txt" +sendkeys \ + "ls /disk" \ + "mkdir /disk/$VOL_UID/sub" \ + "echo disk-test-$STAMP WRITING /disk/$VOL_UID/sub/$TESTFILE" \ + "cat /disk/$VOL_UID/sub/$TESTFILE" \ + "poweroff" + +echo "==> 5/6 wait for shutdown" +wait_qemu_exit 30 || fail "QEMU did not exit after poweroff" + +echo "==> 6/6 verify" +grep -a "disk-test-$STAMP" "$LOG" >/dev/null || fail "cat output missing from serial log" +if ! fsck.fat -n "$SYSTEM_IMG" >/tmp/rsdos-fsck.out 2>&1; then + fail "fsck.fat reports problems: $(tail -3 /tmp/rsdos-fsck.out)" +fi +MT="$(mtype -i "$SYSTEM_IMG" "::/sub/$TESTFILE" 2>/dev/null || true)" +[ "$MT" = "disk-test-$STAMP" ] || fail "mtype mismatch: got '$MT'" + +echo "" +echo "PASS — disk round trip OK (fsck clean, file survived poweroff)" +echo " image: $SYSTEM_IMG" +echo " volume: /disk/$VOL_UID" +echo " 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 @@ +#!/usr/bin/env bash +# +# scripts/tests/test-fault.sh — fault-diagnostics verification. +# +# Proves the fault-dump path: every CPU exception prints a white-on-red +# band to the framebuffer and a parseable serial line before halting. +# Boots with a dedicated CRASHLOG FAT stick on ide.1 so the crash band +# has somewhere to write. +# +# Usage: ./scripts/tests/test-fault.sh +# Env: SYSTEM_IMG, OVMF, QEMU (see scripts/lib.sh) +# +set -euo pipefail +TNAME="fault" +# shellcheck source=../lib.sh +source "$(dirname "$0")/../lib.sh" + +echo "==> 1/5 build (kernel + bins)" +build_all + +echo "==> 2/5 stage system image" +stage_image --force + +echo "==> 3/5 boot QEMU (headless, + dedicated CRASHLOG stick)" +CRASH_IMG=/tmp/rsdos-crash.img +rm -f "$CRASH_IMG" +mkfs.fat -C -F 32 -n CRASHLOG "$CRASH_IMG" 65536 >/dev/null +test_boot \ + -drive "file=$CRASH_IMG,format=raw,if=none,id=crash" \ + -device ide-hd,drive=crash,bus=ide.1 + +echo "==> 4/5 boot diagnostics present" +grep -aq "gop: fb base=" "$LOG" || fail "fb geometry diagnostic missing" +grep -aq "gop: mode .* selected" "$LOG" || fail "mode-selection diagnostic missing" +grep -aq "CRASHLOG" "$LOG" || fail "CRASHLOG stick not mounted" + +echo "==> 5/5 inject a page fault (fault command)" +sendkeys "clear" "fault" +sleep 2 + +grep -aq "!!! page fault vec=14 .*rip=" "$LOG" || fail "page-fault dump line missing" +grep -aq "cr2=0x400000000000" "$LOG" || fail "CR2 not reported (faulting address)" +grep -aq "task=0 \"shell\"" "$LOG" || fail "faulting task not identified" +grep -aq "!!! halting" "$LOG" || fail "no halt message" + +echo "" +echo "PASS — fault dump verified" diff --git a/scripts/tests/test-focus.py b/scripts/tests/test-focus.py new file mode 100755 index 0000000..cfa317a --- /dev/null +++ b/scripts/tests/test-focus.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +"""Drive the focus-routing gate: 3 windows, type markers, verify routing. + +Connects to the QEMU monitor once and keeps the socket open (the per-call +sendkeys.py pattern loses the socket when the monitor resets). + +Usage: start the guest with `./scripts/run.sh --monitor`, then + python3 scripts/tests/test-focus.py [/tmp/rsdos-monitor.sock] +""" +import socket +import sys +import time + +sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) +sock.connect(sys.argv[1] if len(sys.argv) > 1 else "/tmp/rsdos-monitor.sock") + + +def sk(k, d=0.35): + sock.sendall(f"sendkey {k}\n".encode()) + time.sleep(d) + + +def line(txt, d=0.8): + for ch in txt: + if ch == " ": + sk("spc", 0.1) + elif ch.isupper(): + sk("shift-" + ch.lower(), 0.12) + else: + sk(ch, 0.12) + sk("ret", d) + + +time.sleep(0.5) + +# Home is focused at boot. +line("windows") # expect: home + nothing (or existing windows) +time.sleep(1.5) + +# F1 -> term1 (focused on creation) +sk("f1", 1.5) +line("echo TERM1MARK") +time.sleep(1.0) + +# F1 -> term2 (focused on creation) +sk("f1", 1.5) +line("echo TERM2MARK") +time.sleep(1.0) + +# F2 cycles z-order: term2 -> term1 +sk("f2", 0.8) +line("echo CYCLED") +time.sleep(1.0) + +# F2 again: term1 -> home (window 0, oldest) +sk("f2", 0.8) +line("windows") 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 @@ +#!/usr/bin/env bash +# +# scripts/tests/test-gfx.sh — automated graphics round-trip test. +# +# Builds, boots headless, runs the in-kernel gfx self-test and the +# status-bar demo, then powers off. +# +# Usage: ./scripts/tests/test-gfx.sh +# Env: SYSTEM_IMG, OVMF, QEMU (see scripts/lib.sh) +# +set -euo pipefail +TNAME="gfx" +# shellcheck source=../lib.sh +source "$(dirname "$0")/../lib.sh" + +echo "==> 1/5 build (kernel + bins)" +build_all + +echo "==> 2/5 stage system image" +stage_image --force + +echo "==> 3/5 boot QEMU (headless)" +test_boot +wait_log "$LOG" "cursor: task " 10 || fail "cursor task did not spawn" + +echo "==> 4/5 in-kernel pixel self-test (gfx check)" +sendkeys "gfx check" +wait_log "$LOG" "gfx check: PASS" 30 || fail "gfx check did not run" + +echo "==> 5/5 gfx demo (draw scene)" +sendkeys "gfx" "poweroff" +wait_qemu_exit 30 || fail "QEMU did not exit after poweroff" + +grep -aq "gfx: drew status bar" "$LOG" || fail "gfx demo did not draw" + +echo "" +echo "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 @@ +#!/usr/bin/env bash +# +# scripts/tests/test-mouse.sh — automated PS/2 mouse round-trip test. +# +# Boots QEMU headless, injects mouse movement via the QEMU monitor, +# verifies the on-screen cursor tracks it, and tests the mouse stream +# routing command. +# +# Usage: ./scripts/tests/test-mouse.sh +# Env: SYSTEM_IMG, OVMF, QEMU (see scripts/lib.sh) +# +set -euo pipefail +TNAME="mouse" +# shellcheck source=../lib.sh +source "$(dirname "$0")/../lib.sh" + +# cursor_state — one-shot `cursor` command, parsed to "x y buttons". +cursor_state() { + sendkeys "cursor" + sleep 0.5 + sed -n 's/.*cursor: x=\([0-9]*\) y=\([0-9]*\) buttons=\([A-Za-z-]*\).*/\1 \2 \3/p' "$LOG" | tail -1 +} + +echo "==> 1/6 build (kernel + bins)" +build_all + +echo "==> 2/6 stage system image" +stage_image --force + +echo "==> 3/6 boot QEMU (headless)" +test_boot +grep -aq "ps2: mouse ready" "$LOG" || fail "mouse driver did not come up at boot" +wait_log "$LOG" "cursor: task " 10 || fail "cursor task did not spawn" + +echo "==> 4/6 cursor position tracks injected movement" +INIT=$(cursor_state) +[ -n "$INIT" ] || fail "no initial cursor position" +IX=$(echo "$INIT" | awk '{print $1}') +IY=$(echo "$INIT" | awk '{print $2}') +IB=$(echo "$INIT" | awk '{print $3}') +echo " initial: x=$IX y=$IY buttons=$IB" +[ "$IB" = "---" ] || fail "expected no buttons at boot, got $IB" + +qemu_hmp "mouse_move 20 7" +S1=$(cursor_state) +X1=$(echo "$S1" | awk '{print $1}') +Y1=$(echo "$S1" | awk '{print $2}') +[ "$X1" -eq $((IX + 20)) ] || fail "x did not move +20 (was $IX, now $X1)" +[ "$Y1" -eq $((IY + 7)) ] || fail "y did not move +7 (was $IY, now $Y1)" +echo " moved: x=$X1 y=$Y1 (delta +20,+7)" + +echo "==> 5/6 left button edge detection" +qemu_hmp "mouse_button 1" +S2=$(cursor_state) +IB2=$(echo "$S2" | awk '{print $3}') +[ "$IB2" = "L--" ] || fail "expected L--, got $IB2" + +qemu_hmp "mouse_button 0" +S3=$(cursor_state) +IB3=$(echo "$S3" | awk '{print $3}') +[ "$IB3" = "---" ] || fail "expected --- after release, got $IB3" +echo " button: $IB2 → $IB3" + +echo "==> 6/6 mouse stream routing to msgrecv's task" +# The spawned task id shifts with the task table (wm-input took slot 3) — +# parse it rather than hardcoding. +sendkeys "msgrecv" +sleep 1 +MID="$(sed -n 's/.*msgrecv: task \([0-9][0-9]*\) listening.*/\1/p' "$LOG" | tail -1)" +[ -n "$MID" ] || fail "msgrecv did not spawn" +echo " msgrecv task id: $MID" +sendkeys "mouse $MID" +qemu_hmp "mouse_move 20 7" +qemu_hmp "mouse_button 1" +sleep 0.5 +qemu_hmp "mouse_button 0" +sleep 0.5 +sendkeys "ctrl-c" +sleep 0.5 +grep -aq "Mouse(+20,+7" "$LOG" || fail "routed mouse move not logged" + +echo "" +echo "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 @@ +#!/usr/bin/env bash +# +# scripts/tests/test-msg.sh — automated scheduler message-queue round trip. +# +# Tests all message types (user, timer, key, mouse), real-input routing, +# and the queue-full boundary. +# +# Usage: ./scripts/tests/test-msg.sh +# Env: SYSTEM_IMG, OVMF, QEMU (see scripts/lib.sh) +# +set -euo pipefail +TNAME="msg" +# shellcheck source=../lib.sh +source "$(dirname "$0")/../lib.sh" + +echo "==> 1/5 build (kernel + bins)" +build_all + +echo "==> 2/5 stage system image" +stage_image --force + +echo "==> 3/5 boot QEMU (headless)" +test_boot + +echo "==> 4/5 drive the shell (msgrecv / send / mouse routing / queue-full)" +# msgrecv's task id is assigned at spawn time and shifts when the task +# table grows (the Windowed GUI added wm-input at slot 3) — read it from +# the log instead of hardcoding. +sendkeys "msgrecv" +sleep 1 +MID="$(sed -n 's/.*msgrecv: task \([0-9][0-9]*\) listening.*/\1/p' "$LOG" | tail -1)" +[ -n "$MID" ] || fail "msgrecv did not spawn (no 'msgrecv: task N listening' in the log)" +echo " msgrecv task id: $MID" + +sendkeys \ + "send $MID user 42" \ + "send $MID timer" \ + "send $MID key enter" \ + "send $MID key ctrl-c" \ + "send $MID key f3" \ + "send $MID mouse 5 -3 3" \ + "sleep 800" \ + "jobs" + +sendkeys "mouse $MID" +qemu_hmp "mouse_move 20 7" +qemu_hmp "mouse_button 1" +qemu_hmp "mouse_move -5 -3" +qemu_hmp "mouse_button 0" +qemu_hmp "sendkey ctrl-c" + +# Queue-full probe: overflow a queue that nobody drains. The shell used +# to be that task, but its exec pump now consumes its own User messages +# as completion codes (q=0 at the prompt), and msgrecv consumes on wake — +# the cursor task never calls recv, so its 16-slot ring fills for real. +CID="$(awk '$2 == "cursor" { print $1 }' "$LOG" | tail -1)" +[ -n "$CID" ] || fail "no cursor task in jobs output (cannot probe queue-full)" +echo " queue-full target: task $CID (cursor, never recv'd)" + +sendkeys \ + "send $CID user 1" \ + "send $CID user 2" \ + "send $CID user 3" \ + "send $CID user 4" \ + "send $CID user 5" \ + "send $CID user 6" \ + "send $CID user 7" \ + "send $CID user 8" \ + "send $CID user 9" \ + "send $CID user 10" \ + "send $CID user 11" \ + "send $CID user 12" \ + "send $CID user 13" \ + "send $CID user 14" \ + "send $CID user 15" \ + "send $CID user 16" \ + "send $CID user 17" \ + "sleep 800" \ + "jobs" \ + "poweroff" + +echo "==> 5/5 wait for shutdown + verify" +wait_qemu_exit 30 || fail "QEMU did not exit after poweroff" + +grep -aq "msgrecv: task $MID listening" "$LOG" || fail "msgrecv did not spawn" +grep -aq "msgrecv\[$MID\]: User(42)" "$LOG" || fail "user message missing" +grep -aq "msgrecv\[$MID\]: Timer" "$LOG" || fail "timer message missing" +grep -aq "msgrecv\[$MID\]: Key(Enter)" "$LOG" || fail "key enter missing" +grep -aq "msgrecv\[$MID\]: Key(Ctrl-c)" "$LOG" || fail "key ctrl-c missing" +grep -aq "msgrecv\[$MID\]: Key(F3)" "$LOG" || fail "key f3 missing" +grep -aq "msgrecv\[$MID\]: Mouse(+5,-3 L R)" "$LOG" || fail "synthesized mouse message missing" +grep -aq "msgrecv\[$MID\]: Mouse(+20,+7" "$LOG" || fail "real mouse move 20,7 not routed to queue" +grep -aq "msgrecv\[$MID\]: Mouse(-5,-3 L" "$LOG" || fail "held-button routed move missing" +grep -aq "queue full" "$LOG" || fail "17th send did not report queue full" +grep -aq "send: unknown type" "$LOG" && fail "a bad send slipped through (expected none)" + +echo "" +echo "PASS — message queues OK" -- cgit v1.2.3