diff options
| author | salmonik <salmonik@tuta.io> | 2026-11-01 15:53:29 +0100 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-11-01 15:53:29 +0100 |
| commit | 7958276c9f0d230053520ba20f2a00f5ecda18e4 (patch) | |
| tree | 596f5f71ad597ed951bae98de818cb37f171a18d /scripts | |
| parent | fs: /bin alias for the system volume (diff) | |
| download | rs-dos-7958276c9f0d230053520ba20f2a00f5ecda18e4.tar.gz rs-dos-7958276c9f0d230053520ba20f2a00f5ecda18e4.zip | |
scripts: rebuild the dev loop around build.sh/run.sh
`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.
Diffstat (limited to '')
| -rwxr-xr-x | scripts/boot-image.sh | 65 | ||||
| -rwxr-xr-x | scripts/boot.sh | 90 | ||||
| -rwxr-xr-x | scripts/build-bootloader.sh | 35 | ||||
| -rwxr-xr-x | scripts/build-image-release.sh | 30 | ||||
| -rwxr-xr-x | scripts/build.sh | 49 | ||||
| -rwxr-xr-x | scripts/debug-kernel.py | 16 | ||||
| -rw-r--r-- | scripts/lib-boot-args.sh | 38 | ||||
| -rw-r--r-- | scripts/lib.sh | 168 | ||||
| -rwxr-xr-x | scripts/mksystem.sh | 144 | ||||
| -rwxr-xr-x | scripts/qemu.sh | 99 | ||||
| -rwxr-xr-x | scripts/run.sh | 142 | ||||
| -rwxr-xr-x | scripts/test-disk.sh | 131 | ||||
| -rwxr-xr-x | scripts/test-fault.sh | 85 | ||||
| -rwxr-xr-x | scripts/test-gfx.sh | 84 | ||||
| -rwxr-xr-x | scripts/test-mouse.sh | 125 | ||||
| -rwxr-xr-x | scripts/test-msg.sh | 140 | ||||
| -rwxr-xr-x | scripts/tests/sendkeys.py (renamed from scripts/sendkeys.py) | 12 | ||||
| -rwxr-xr-x | scripts/tests/test-disk.sh | 67 | ||||
| -rwxr-xr-x | scripts/tests/test-fault.sh | 47 | ||||
| -rwxr-xr-x | scripts/tests/test-focus.py (renamed from scripts/test-focus.py) | 7 | ||||
| -rwxr-xr-x | scripts/tests/test-gfx.sh | 37 | ||||
| -rwxr-xr-x | scripts/tests/test-mouse.sh | 83 | ||||
| -rwxr-xr-x | scripts/tests/test-msg.sh | 98 |
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 | # | ||
| 20 | set -euo pipefail | ||
| 21 | |||
| 22 | SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" | ||
| 23 | PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" | ||
| 24 | cd "$PROJECT_DIR" | ||
| 25 | |||
| 26 | IMAGE="${1:-rs-dos.img}" | ||
| 27 | |||
| 28 | # ---- config ---- | ||
| 29 | OVMF="${OVMF:-/usr/share/edk2/x64/OVMF.4m.fd}" | ||
| 30 | MEM="${MEM:-256M}" | ||
| 31 | SMP="${SMP:-1}" | ||
| 32 | GUI="" | ||
| 33 | DEBUG="" | ||
| 34 | |||
| 35 | # ---- args ---- | ||
| 36 | source "$SCRIPT_DIR/lib-boot-args.sh" | ||
| 37 | parse_boot_args "$@" | ||
| 38 | |||
| 39 | # ---- build image if missing ---- | ||
| 40 | if [ ! -f "$IMAGE" ]; then | ||
| 41 | echo "Image not found, building..." | ||
| 42 | "$SCRIPT_DIR/mksystem.sh" --chain --gpt "$IMAGE" | ||
| 43 | fi | ||
| 44 | |||
| 45 | # ---- boot ---- | ||
| 46 | echo "" | ||
| 47 | echo "=== Booting (full chain: firmware → rsboot → rsnix) ===" | ||
| 48 | echo " image: $IMAGE" | ||
| 49 | echo " ovmf: $OVMF" | ||
| 50 | echo " mem: $MEM smp: $SMP" | ||
| 51 | echo " gui: $([ -n "$GUI" ] && echo yes || echo no)" | ||
| 52 | echo " gdb: $([ -n "$DEBUG" ] && echo ':1234' || echo off)" | ||
| 53 | echo "" | ||
| 54 | |||
| 55 | exec 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 | # | ||
| 24 | set -euo pipefail | ||
| 25 | |||
| 26 | SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" | ||
| 27 | PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" | ||
| 28 | cd "$PROJECT_DIR" | ||
| 29 | |||
| 30 | # ---- config ---- | ||
| 31 | OVMF="${OVMF:-/usr/share/edk2/x64/OVMF.4m.fd}" | ||
| 32 | MEM="${MEM:-256M}" | ||
| 33 | SMP="${SMP:-1}" | ||
| 34 | SYSTEM_IMG="${SYSTEM_IMG:-/tmp/system.img}" | ||
| 35 | PROFILE="release" | ||
| 36 | |||
| 37 | GUI="" | ||
| 38 | DEBUG="" | ||
| 39 | |||
| 40 | # ---- args ---- | ||
| 41 | source "$SCRIPT_DIR/lib-boot-args.sh" | ||
| 42 | handle_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 | } | ||
| 54 | parse_boot_args "$@" | ||
| 55 | |||
| 56 | OUT_DIR="target/x86_64-unknown-uefi/$PROFILE" | ||
| 57 | |||
| 58 | # ---- build ---- | ||
| 59 | echo "=== Building kernel ($PROFILE) ===" | ||
| 60 | if [ "$PROFILE" = "release" ]; then | ||
| 61 | cargo build --release --package rsdos --target x86_64-unknown-uefi | ||
| 62 | else | ||
| 63 | cargo build --package rsdos --target x86_64-unknown-uefi | ||
| 64 | fi | ||
| 65 | |||
| 66 | # ---- stage system image ---- | ||
| 67 | echo "=== Staging system image ===" | ||
| 68 | "$SCRIPT_DIR/mksystem.sh" --force "$SYSTEM_IMG" | ||
| 69 | echo " system image: $SYSTEM_IMG" | ||
| 70 | |||
| 71 | # ---- boot ---- | ||
| 72 | echo "" | ||
| 73 | echo "=== Booting (direct: firmware → rsnix) ===" | ||
| 74 | echo " ovmf: $OVMF" | ||
| 75 | echo " mem: $MEM smp: $SMP" | ||
| 76 | echo " gui: $([ -n "$GUI" ] && echo yes || echo no)" | ||
| 77 | echo " gdb: $([ -n "$DEBUG" ] && echo ':1234' || echo off)" | ||
| 78 | echo "" | ||
| 79 | |||
| 80 | exec 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 | # | ||
| 14 | set -euo pipefail | ||
| 15 | |||
| 16 | SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" | ||
| 17 | PROJECT_DIR="$(dirname "$SCRIPT_DIR")" | ||
| 18 | BUILD_DIR="$PROJECT_DIR/build" | ||
| 19 | BOOTLOADER_DIR="$BUILD_DIR/bootloader" | ||
| 20 | |||
| 21 | echo "=== 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). | ||
| 26 | cd "$PROJECT_DIR" | ||
| 27 | git submodule update --init --remote rsboot | ||
| 28 | |||
| 29 | cd "$PROJECT_DIR/rsboot" | ||
| 30 | make clean 2>/dev/null || true | ||
| 31 | make -j"$(nproc)" 2>&1 | sed 's/^/ /' | ||
| 32 | |||
| 33 | echo "" | ||
| 34 | echo "=== rsboot build complete ===" | ||
| 35 | 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 @@ | |||
| 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 | # | ||
| 11 | set -euo pipefail | ||
| 12 | |||
| 13 | SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" | ||
| 14 | PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" | ||
| 15 | cd "$PROJECT_DIR" | ||
| 16 | |||
| 17 | IMAGE="${1:-rs-dos.img}" | ||
| 18 | |||
| 19 | echo "=== Building release kernel ===" | ||
| 20 | cargo build --release --package rsdos --target x86_64-unknown-uefi | ||
| 21 | |||
| 22 | echo "=== Building system image (chain + GPT) ===" | ||
| 23 | "$SCRIPT_DIR/mksystem.sh" --chain --gpt "$IMAGE" | ||
| 24 | |||
| 25 | echo "" | ||
| 26 | echo "=== Done: $IMAGE ===" | ||
| 27 | ls -lh "$IMAGE" | ||
| 28 | echo "" | ||
| 29 | echo "Boot with: ./scripts/boot-image.sh --gui" | ||
| 30 | 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 @@ | |||
| 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 | # | ||
| 21 | set -euo pipefail | ||
| 22 | |||
| 23 | usage() { | ||
| 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. | ||
| 29 | while [ $# -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 | ||
| 43 | done | ||
| 44 | |||
| 45 | # shellcheck source=lib.sh | ||
| 46 | source "$(dirname "$0")/lib.sh" | ||
| 47 | |||
| 48 | build_all | ||
| 49 | 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 @@ | |||
| 2 | GDB Python script for debugging raisernics under QEMU. | 2 | GDB Python script for debugging raisernics under QEMU. |
| 3 | 3 | ||
| 4 | Usage: | 4 | Usage: |
| 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 | ||
| 9 | import gdb | 10 | import gdb |
| 10 | import os | 11 | import os |
| 11 | 12 | ||
| 12 | # Find the kernel binary | 13 | # Find the kernel binary (debug profile preferred, release as fallback) |
| 13 | script_dir = os.path.dirname(os.path.abspath(__file__)) | 14 | script_dir = os.path.dirname(os.path.abspath(__file__)) |
| 14 | project_root = os.path.dirname(script_dir) | 15 | project_root = os.path.dirname(script_dir) |
| 15 | binary = os.path.join( | 16 | candidates = [ |
| 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 | ] |
| 20 | binary = 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 |
| 21 | gdb.execute("target remote :1234") | 23 | 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 @@ | |||
| 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). | ||
| 8 | set -euo pipefail | ||
| 9 | |||
| 10 | parse_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. | ||
| 35 | handle_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. | ||
| 11 | set -euo pipefail | ||
| 12 | |||
| 13 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | ||
| 14 | PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" | ||
| 15 | TESTS_DIR="$SCRIPT_DIR/tests" | ||
| 16 | |||
| 17 | # ---- config --------------------------------------------------------------- | ||
| 18 | TARGET="${TARGET:-x86_64-unknown-uefi}" | ||
| 19 | PROFILE="${PROFILE:-release}" | ||
| 20 | OUT_DIR="${OUT_DIR:-$PROJECT_DIR/target/$TARGET/$PROFILE}" | ||
| 21 | KERNEL="$OUT_DIR/rsdos.efi" | ||
| 22 | SYSTEM_IMG="${SYSTEM_IMG:-/tmp/system.img}" | ||
| 23 | OVMF="${OVMF:-/usr/share/edk2/x64/OVMF.4m.fd}" | ||
| 24 | QEMU="${QEMU:-qemu-system-x86_64}" | ||
| 25 | MEM="${MEM:-256M}" | ||
| 26 | SMP="${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. | ||
| 30 | GUI_BINS="paint" | ||
| 31 | |||
| 32 | cd "$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 | |||
| 39 | cargo_flags() { | ||
| 40 | [ "$PROFILE" = "release" ] && printf '%s' "--release" | ||
| 41 | } | ||
| 42 | |||
| 43 | build_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. | ||
| 54 | stage_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. | ||
| 63 | qemu_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 | |||
| 83 | TNAME="${TNAME:-rsdos}" | ||
| 84 | QPID="/tmp/rsdos-$TNAME-qpid" | ||
| 85 | SOCK="/tmp/rsdos-$TNAME-qmon.sock" | ||
| 86 | LOG="/tmp/rsdos-$TNAME-serial.log" | ||
| 87 | QLOG="/tmp/rsdos-$TNAME-qemu.log" | ||
| 88 | |||
| 89 | fail() { | ||
| 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. | ||
| 96 | wait_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. | ||
| 107 | wait_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. | ||
| 118 | qemu_hmp() { | ||
| 119 | python3 - "$SOCK" "$1" <<'EOF' | ||
| 120 | import socket, sys, time | ||
| 121 | s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) | ||
| 122 | s.connect(sys.argv[1]) | ||
| 123 | s.sendall((sys.argv[2] + "\n").encode()) | ||
| 124 | time.sleep(0.4) | ||
| 125 | EOF | ||
| 126 | } | ||
| 127 | |||
| 128 | # sendkeys "line" ... — type shell commands into the guest via sendkeys.py. | ||
| 129 | sendkeys() { | ||
| 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. | ||
| 135 | test_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). | ||
| 142 | test_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). | ||
| 149 | test_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 | # |
| 33 | set -euo pipefail | 35 | set -euo pipefail |
| 34 | 36 | ||
| 35 | SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" | 37 | SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" |
| 36 | PROJECT_DIR="$(dirname "$SCRIPT_DIR")" | 38 | # Absolute self path: lib.sh cd's to the project root, so a relative $0 |
| 37 | cd "$PROJECT_DIR" | 39 | # would no longer resolve by the time --help prints the header. |
| 40 | SELF="$SCRIPT_DIR/$(basename "$0")" | ||
| 41 | |||
| 42 | # shellcheck source=lib.sh | ||
| 43 | source "$SCRIPT_DIR/lib.sh" | ||
| 38 | 44 | ||
| 39 | IMG="/tmp/system.img" | 45 | IMG="$SYSTEM_IMG" |
| 40 | SIZE_MB=64 | 46 | SIZE_MB=64 |
| 41 | FORCE=0 | 47 | FORCE=0 |
| 42 | CHAIN=0 | ||
| 43 | GPT=0 | 48 | GPT=0 |
| 44 | LABEL="RSDOS" | 49 | LABEL="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 |
| 78 | done | 80 | done |
| 79 | 81 | ||
| 80 | TARGET=x86_64-unknown-uefi | 82 | SEED_DIR="$PROJECT_DIR/seed" |
| 81 | PROFILE=release | ||
| 82 | OUT_DIR="target/$TARGET/$PROFILE" | ||
| 83 | KERNEL="$OUT_DIR/rsdos.efi" | ||
| 84 | BZIMAGE="esp/bzImage" | ||
| 85 | INITRD="esp/initramfs.cpio.gz" | ||
| 86 | BOOTLOADER="build/bootloader/BOOTX64.EFI" | ||
| 87 | 83 | ||
| 88 | # ---- checks ---- | 84 | # ---- checks --------------------------------------------------------------- |
| 89 | for cmd in mkfs.fat mcopy mmd; do | 85 | for 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 |
| 102 | fi | 98 | fi |
| 103 | 99 | ||
| 104 | if [ -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 | 102 | if [ ! -f "$KERNEL" ] || ! ls "$OUT_DIR"/*.efi >/dev/null 2>&1; then |
| 103 | "$SCRIPT_DIR/build.sh" | ||
| 107 | fi | 104 | fi |
| 108 | 105 | ||
| 109 | # ---- build kernel ---- | 106 | # ---- freshness stamp ------------------------------------------------------ |
| 110 | if [ ! -f "$KERNEL" ]; then | 107 | newest="" |
| 111 | echo "mksystem: building kernel..." | 108 | for f in "$OUT_DIR"/*.efi "$SEED_DIR"/*; do |
| 112 | cargo build --release --package rsdos --target "$TARGET" | 109 | [ -f "$f" ] || continue |
| 113 | fi | 110 | if [ -z "$newest" ] || [ "$f" -nt "$newest" ]; then |
| 114 | 111 | newest="$f" | |
| 115 | # ---- build bootloader (chain mode only) ---- | 112 | fi |
| 116 | if [ "$CHAIN" -eq 1 ] && [ ! -f "$BOOTLOADER" ]; then | 113 | done |
| 117 | echo "mksystem: building bootloader..." | 114 | if [ "$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 | ||
| 119 | fi | 117 | fi |
| 120 | 118 | ||
| 121 | # ---- deploy kernel artifacts to esp/ (shared with the old layout) ---- | 119 | # ---- create the FAT volume ------------------------------------------------ |
| 122 | mkdir -p esp/EFI/BOOT | ||
| 123 | cp "$KERNEL" "$BZIMAGE" | ||
| 124 | [ -f "$INITRD" ] || printf '' | gzip >"$INITRD" | ||
| 125 | |||
| 126 | # ---- create the FAT volume ---- | ||
| 127 | rm -f "$IMG" | 120 | rm -f "$IMG" |
| 128 | SIZE_BLOCKS=$((SIZE_MB * 1024)) # mkfs.fat counts 1024-byte blocks | 121 | SIZE_BLOCKS=$((SIZE_MB * 1024)) # mkfs.fat counts 1024-byte blocks |
| 129 | echo "mksystem: creating $IMG (${SIZE_MB}M FAT32, label $LABEL)" | 122 | 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 | |||
| 131 | 124 | ||
| 132 | echo "mksystem: staging system tree" | 125 | echo "mksystem: staging system tree" |
| 133 | mmd -i "$IMG" ::/EFI ::/EFI/BOOT ::/etc ::/bin ::/bin/cli ::/bin/gui | 126 | mmd -i "$IMG" ::/EFI ::/EFI/BOOT ::/etc ::/bin ::/bin/cli ::/bin/gui |
| 134 | mcopy -i "$IMG" "$BZIMAGE" ::/bzImage | 127 | |
| 128 | # The firmware entry point and rsbl's kernel path are the same file. | ||
| 129 | mcopy -i "$IMG" "$KERNEL" ::/EFI/BOOT/BOOTX64.EFI | ||
| 130 | mcopy -i "$IMG" "$KERNEL" ::/bzImage | ||
| 131 | INITRD="$(mktemp -t rsdos-initrd.XXXXXX)" | ||
| 132 | trap 'rm -f "$INITRD"' EXIT | ||
| 133 | printf '' | gzip >"$INITRD" | ||
| 135 | mcopy -i "$IMG" "$INITRD" ::/initramfs.cpio.gz | 134 | mcopy -i "$IMG" "$INITRD" ::/initramfs.cpio.gz |
| 136 | if [ "$CHAIN" -eq 1 ]; then | 135 | echo " \\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. |
| 139 | else | 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)" | 140 | for src in "$OUT_DIR"/*.efi; do |
| 142 | fi | 141 | [ -f "$src" ] || continue |
| 143 | echo " \\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" | ||
| 150 | done | ||
| 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. |
| 146 | SEED_DIR="$PROJECT_DIR/seed" | ||
| 147 | if [ -d "$SEED_DIR" ]; then | 153 | if [ -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 |
| 159 | fi | 165 | fi |
| 160 | 166 | ||
| 161 | # ---- optional GPT wrapper (real-hardware dd target) ---- | 167 | # ---- optional GPT wrapper (real-hardware dd target) ----------------------- |
| 162 | if [ "$GPT" -eq 1 ]; then | 168 | if [ "$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 | |||
| 179 | echo "" | 185 | echo "" |
| 180 | echo "=== Done: $IMG ===" | 186 | echo "=== Done: $IMG ===" |
| 181 | ls -lh "$IMG" | 187 | ls -lh "$IMG" |
| 182 | echo "Boot with: make run (or: make boot-image)" | 188 | echo "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" |
| 184 | exit 0 | 190 | 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 @@ | |||
| 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 | # ============================================================================= | ||
| 16 | set -euo pipefail | ||
| 17 | |||
| 18 | SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" | ||
| 19 | PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" | ||
| 20 | cd "$PROJECT_DIR" | ||
| 21 | |||
| 22 | # Defaults | ||
| 23 | GUI_FLAG="" | ||
| 24 | DEBUG_FLAG="" | ||
| 25 | CHAIN_FLAG="" | ||
| 26 | OVMF="${OVMF:-/usr/share/edk2/x64/OVMF.4m.fd}" | ||
| 27 | MEM="${MEM:-256M}" | ||
| 28 | SMP="${SMP:-1}" | ||
| 29 | SYSTEM_IMG="${SYSTEM_IMG:-/tmp/system.img}" | ||
| 30 | |||
| 31 | # Pre-build | ||
| 32 | make all | ||
| 33 | |||
| 34 | # Parse args | ||
| 35 | while [ $# -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 | ||
| 71 | done | ||
| 72 | |||
| 73 | # Stage system image if needed | ||
| 74 | if [ -n "$CHAIN_FLAG" ]; then | ||
| 75 | ./scripts/mksystem.sh --chain --gpt "$SYSTEM_IMG" | ||
| 76 | else | ||
| 77 | ./scripts/mksystem.sh "$SYSTEM_IMG" | ||
| 78 | fi | ||
| 79 | |||
| 80 | echo "" | ||
| 81 | echo "=== Booting ===" | ||
| 82 | echo " image: $SYSTEM_IMG" | ||
| 83 | echo " ovmf: $OVMF" | ||
| 84 | echo " mem: $MEM smp: $SMP" | ||
| 85 | echo " gui: $([ -n "$GUI_FLAG" ] && echo yes || echo no)" | ||
| 86 | echo " gdb: $([ -n "$DEBUG_FLAG" ] && echo ':1234' || echo off)" | ||
| 87 | echo "" | ||
| 88 | |||
| 89 | exec 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 | # | ||
| 35 | set -euo pipefail | ||
| 36 | |||
| 37 | SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" | ||
| 38 | |||
| 39 | usage() { | ||
| 40 | awk 'NR > 1 && /^#/ { sub(/^# ?/, ""); print; next } NR > 1 { exit }' "$0" | ||
| 41 | } | ||
| 42 | |||
| 43 | HEADLESS=0 | ||
| 44 | DEBUG=0 | ||
| 45 | FRESH=0 | ||
| 46 | MON=0 | ||
| 47 | MON_SOCK="/tmp/rsdos-monitor.sock" | ||
| 48 | GRAB="" # "" = auto: grab when a Wayland session has XWayland | ||
| 49 | |||
| 50 | while [ $# -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 | ||
| 75 | done | ||
| 76 | |||
| 77 | # shellcheck source=lib.sh | ||
| 78 | source "$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. | ||
| 83 | export 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. | ||
| 91 | if [ -z "$GRAB" ]; then | ||
| 92 | if [ "${XDG_SESSION_TYPE:-}" = "wayland" ] && [ -n "${DISPLAY:-}" ]; then | ||
| 93 | GRAB=1 | ||
| 94 | else | ||
| 95 | GRAB=0 | ||
| 96 | fi | ||
| 97 | fi | ||
| 98 | [ "$HEADLESS" -eq 1 ] && GRAB=0 # no window, nothing to grab | ||
| 99 | if [ "$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 | ||
| 106 | fi | ||
| 107 | |||
| 108 | # ---- build + stage -------------------------------------------------------- | ||
| 109 | "$SCRIPT_DIR/build.sh" | ||
| 110 | if [ "$FRESH" -eq 1 ]; then | ||
| 111 | stage_image --force | ||
| 112 | else | ||
| 113 | stage_image | ||
| 114 | fi | ||
| 115 | |||
| 116 | # ---- boot ----------------------------------------------------------------- | ||
| 117 | mapfile -t QARGS < <(qemu_args) | ||
| 118 | if [ "$HEADLESS" -eq 1 ]; then | ||
| 119 | QARGS+=(-display none) | ||
| 120 | else | ||
| 121 | QARGS+=(-vga std) | ||
| 122 | if [ "$GRAB" -eq 1 ]; then | ||
| 123 | QARGS+=(-display gtk,grab-on-hover=on) | ||
| 124 | fi | ||
| 125 | fi | ||
| 126 | [ "$MON" -eq 1 ] && QARGS+=(-monitor "unix:$MON_SOCK,server,nowait") | ||
| 127 | [ "$DEBUG" -eq 1 ] && QARGS+=(-s -S) | ||
| 128 | QARGS+=(-serial stdio) | ||
| 129 | |||
| 130 | echo "" | ||
| 131 | echo "=== boot ===" | ||
| 132 | echo " image: $SYSTEM_IMG" | ||
| 133 | echo " ovmf: $OVMF" | ||
| 134 | echo " mem: $MEM smp: $SMP" | ||
| 135 | echo " mode: $([ "$HEADLESS" -eq 1 ] && echo headless || echo window) gdb: $([ "$DEBUG" -eq 1 ] && echo ':1234 (paused)' || echo off)" | ||
| 136 | if [ "$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)')" | ||
| 138 | fi | ||
| 139 | [ "$MON" -eq 1 ] && echo " monitor: unix:$MON_SOCK" | ||
| 140 | echo "" | ||
| 141 | |||
| 142 | exec "$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 | # | ||
| 18 | set -euo pipefail | ||
| 19 | |||
| 20 | SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" | ||
| 21 | PROJECT_DIR="$(dirname "$SCRIPT_DIR")" | ||
| 22 | cd "$PROJECT_DIR" | ||
| 23 | |||
| 24 | FRESH=0 | ||
| 25 | [ "${1:-}" = "--fresh" ] && FRESH=1 | ||
| 26 | |||
| 27 | OVMF="${OVMF:-/usr/share/edk2/x64/OVMF.4m.fd}" | ||
| 28 | QEMU="${QEMU:-qemu-system-x86_64}" | ||
| 29 | IMG="${SYSTEM_IMG:-/tmp/system.img}" | ||
| 30 | QPID=/tmp/rsdos-test-qpid | ||
| 31 | SOCK=/tmp/rsdos-test-qmon.sock | ||
| 32 | LOG=/tmp/rsdos-test-serial.log | ||
| 33 | QLOG=/tmp/rsdos-test-qemu.log | ||
| 34 | STAMP="$(date +%s)" | ||
| 35 | |||
| 36 | # Never leave a QEMU behind (an orphan holding our stdout would hang any | ||
| 37 | # caller that pipes our output). | ||
| 38 | trap 'kill "$(cat "$QPID" 2>/dev/null)" 2>/dev/null || true' EXIT | ||
| 39 | |||
| 40 | fail() { | ||
| 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. | ||
| 47 | wait_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. | ||
| 58 | wait_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 | |||
| 68 | echo "==> 1/6 build kernel (debug)" | ||
| 69 | cargo build --release --package rsdos --target x86_64-unknown-uefi | ||
| 70 | |||
| 71 | echo "==> 2/6 ensure system image" | ||
| 72 | if [ "$FRESH" -eq 1 ]; then | ||
| 73 | "$SCRIPT_DIR/mksystem.sh" --force "$IMG" | ||
| 74 | else | ||
| 75 | "$SCRIPT_DIR/mksystem.sh" "$IMG" | ||
| 76 | fi | ||
| 77 | |||
| 78 | echo "==> 3/6 boot QEMU (headless)" | ||
| 79 | kill "$(cat "$QPID" 2>/dev/null)" 2>/dev/null || true | ||
| 80 | sleep 1 | ||
| 81 | rm -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. | ||
| 84 | NV="/tmp/rsdos-test-ovmf-vars.fd" | ||
| 85 | rm -f "$NV" | ||
| 86 | cp /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 | |||
| 95 | wait_log "$LOG" "boot: entering shell" 90 || fail "kernel never reached the shell" | ||
| 96 | sleep 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. | ||
| 101 | VOL_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)" | ||
| 103 | echo " mounted volume UID: $VOL_UID" | ||
| 104 | |||
| 105 | echo "==> 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). | ||
| 108 | TESTFILE="disk-test-$STAMP.txt" | ||
| 109 | python3 "$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 | |||
| 116 | echo "==> 5/6 wait for shutdown" | ||
| 117 | wait_qemu_exit 30 || fail "QEMU did not exit after poweroff" | ||
| 118 | |||
| 119 | echo "==> 6/6 verify" | ||
| 120 | grep -a "disk-test-$STAMP" "$LOG" >/dev/null || fail "cat output missing from serial log" | ||
| 121 | if ! fsck.fat -n "$IMG" >/tmp/rsdos-fsck.out 2>&1; then | ||
| 122 | fail "fsck.fat reports problems: $(cat /tmp/rsdos-fsck.out | tail -3)" | ||
| 123 | fi | ||
| 124 | MT="$(mtype -i "$IMG" "::/sub/$TESTFILE" 2>/dev/null || true)" | ||
| 125 | [ "$MT" = "disk-test-$STAMP" ] || fail "mtype mismatch: got '$MT'" | ||
| 126 | |||
| 127 | echo "" | ||
| 128 | echo "PASS — disk round trip OK (fsck clean, file survived poweroff)" | ||
| 129 | echo " image: $IMG" | ||
| 130 | echo " volume: /disk/$VOL_UID" | ||
| 131 | 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 @@ | |||
| 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 | # | ||
| 11 | set -euo pipefail | ||
| 12 | |||
| 13 | SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" | ||
| 14 | PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" | ||
| 15 | cd "$PROJECT_DIR" | ||
| 16 | |||
| 17 | OVMF="${OVMF:-/usr/share/edk2/x64/OVMF.4m.fd}" | ||
| 18 | QEMU="${QEMU:-qemu-system-x86_64}" | ||
| 19 | SYSTEM_IMG="${SYSTEM_IMG:-/tmp/system.img}" | ||
| 20 | QPID=/tmp/rsdos-fault-qpid | ||
| 21 | SOCK=/tmp/rsdos-fault-qmon.sock | ||
| 22 | LOG=/tmp/rsdos-fault-serial.log | ||
| 23 | QLOG=/tmp/rsdos-fault-qemu.log | ||
| 24 | |||
| 25 | trap 'kill "$(cat "$QPID" 2>/dev/null)" 2>/dev/null || true' EXIT | ||
| 26 | |||
| 27 | fail() { | ||
| 28 | echo "FAIL: $1" | ||
| 29 | [ -f "$LOG" ] && tail -40 "$LOG" | ||
| 30 | exit 1 | ||
| 31 | } | ||
| 32 | |||
| 33 | wait_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 | |||
| 43 | echo "==> 1/5 build kernel (debug)" | ||
| 44 | cargo build --release --package rsdos --target x86_64-unknown-uefi | ||
| 45 | |||
| 46 | echo "==> 2/5 stage system image" | ||
| 47 | "$SCRIPT_DIR/mksystem.sh" --force "$SYSTEM_IMG" | ||
| 48 | |||
| 49 | echo "==> 3/5 boot QEMU (headless, + dedicated CRASHLOG stick)" | ||
| 50 | kill "$(cat "$QPID" 2>/dev/null)" 2>/dev/null || true | ||
| 51 | sleep 1 | ||
| 52 | rm -f "$SOCK" "$LOG" | ||
| 53 | CRASH_IMG=/tmp/rsdos-crash.img | ||
| 54 | mkfs.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 & | ||
| 64 | echo $! >"$QPID" | ||
| 65 | |||
| 66 | wait_log "$LOG" "boot: entering shell" 90 || fail "kernel never reached the shell" | ||
| 67 | |||
| 68 | echo "==> 4/5 boot diagnostics present" | ||
| 69 | grep -aq "gop: fb base=" "$LOG" || fail "fb geometry diagnostic missing" | ||
| 70 | grep -aq "gop: mode .* selected" "$LOG" || fail "mode-selection diagnostic missing" | ||
| 71 | grep -aq "CRASHLOG" "$LOG" || fail "CRASHLOG stick not mounted" | ||
| 72 | |||
| 73 | echo "==> 5/5 inject a page fault (fault command)" | ||
| 74 | python3 "$SCRIPT_DIR/sendkeys.py" --sock "$SOCK" \ | ||
| 75 | "clear" \ | ||
| 76 | "fault" | ||
| 77 | sleep 2 | ||
| 78 | |||
| 79 | grep -aq "!!! page fault vec=14 .*rip=" "$LOG" || fail "page-fault dump line missing" | ||
| 80 | grep -aq "cr2=0x400000000000" "$LOG" || fail "CR2 not reported (faulting address)" | ||
| 81 | grep -aq "task=0 \"shell\"" "$LOG" || fail "faulting task not identified" | ||
| 82 | grep -aq "!!! halting" "$LOG" || fail "no halt message" | ||
| 83 | |||
| 84 | echo "" | ||
| 85 | echo "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 | # | ||
| 10 | set -euo pipefail | ||
| 11 | |||
| 12 | SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" | ||
| 13 | PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" | ||
| 14 | cd "$PROJECT_DIR" | ||
| 15 | |||
| 16 | OVMF="${OVMF:-/usr/share/edk2/x64/OVMF.4m.fd}" | ||
| 17 | QEMU="${QEMU:-qemu-system-x86_64}" | ||
| 18 | SYSTEM_IMG="${SYSTEM_IMG:-/tmp/system.img}" | ||
| 19 | QPID=/tmp/rsdos-gfx-qpid | ||
| 20 | SOCK=/tmp/rsdos-gfx-qmon.sock | ||
| 21 | LOG=/tmp/rsdos-gfx-serial.log | ||
| 22 | QLOG=/tmp/rsdos-gfx-qemu.log | ||
| 23 | |||
| 24 | trap 'kill "$(cat "$QPID" 2>/dev/null)" 2>/dev/null || true' EXIT | ||
| 25 | |||
| 26 | fail() { | ||
| 27 | echo "FAIL: $1" | ||
| 28 | [ -f "$LOG" ] && tail -40 "$LOG" | ||
| 29 | exit 1 | ||
| 30 | } | ||
| 31 | |||
| 32 | wait_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 | |||
| 42 | echo "==> 1/5 build kernel (debug)" | ||
| 43 | cargo build --release --package rsdos --target x86_64-unknown-uefi | ||
| 44 | |||
| 45 | echo "==> 2/5 stage system image" | ||
| 46 | "$SCRIPT_DIR/mksystem.sh" --force "$SYSTEM_IMG" | ||
| 47 | |||
| 48 | echo "==> 3/5 boot QEMU (headless)" | ||
| 49 | kill "$(cat "$QPID" 2>/dev/null)" 2>/dev/null || true | ||
| 50 | sleep 1 | ||
| 51 | rm -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 & | ||
| 59 | echo $! >"$QPID" | ||
| 60 | |||
| 61 | wait_log "$LOG" "boot: entering shell" 90 || fail "kernel never reached the shell" | ||
| 62 | wait_log "$LOG" "cursor: task " 10 || fail "cursor task did not spawn" | ||
| 63 | |||
| 64 | echo "==> 4/5 in-kernel pixel self-test (gfx check)" | ||
| 65 | python3 "$SCRIPT_DIR/sendkeys.py" --sock "$SOCK" "gfx check" | ||
| 66 | wait_log "$LOG" "gfx: check" 30 || fail "gfx check did not run" | ||
| 67 | |||
| 68 | echo "==> 5/5 gfx demo (draw scene)" | ||
| 69 | python3 "$SCRIPT_DIR/sendkeys.py" --sock "$SOCK" "gfx" "poweroff" | ||
| 70 | wait_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 | } | ||
| 79 | wait_qemu_exit 30 || fail "QEMU did not exit after poweroff" | ||
| 80 | |||
| 81 | grep -aq "gfx: drew status bar" "$LOG" || fail "gfx demo did not draw" | ||
| 82 | |||
| 83 | echo "" | ||
| 84 | 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 @@ | |||
| 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 | # | ||
| 11 | set -euo pipefail | ||
| 12 | |||
| 13 | SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" | ||
| 14 | PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" | ||
| 15 | cd "$PROJECT_DIR" | ||
| 16 | |||
| 17 | OVMF="${OVMF:-/usr/share/edk2/x64/OVMF.4m.fd}" | ||
| 18 | QEMU="${QEMU:-qemu-system-x86_64}" | ||
| 19 | SYSTEM_IMG="${SYSTEM_IMG:-/tmp/system.img}" | ||
| 20 | QPID=/tmp/rsdos-mouse-qpid | ||
| 21 | SOCK=/tmp/rsdos-mouse-qmon.sock | ||
| 22 | LOG=/tmp/rsdos-mouse-serial.log | ||
| 23 | QLOG=/tmp/rsdos-mouse-qemu.log | ||
| 24 | |||
| 25 | trap 'kill "$(cat "$QPID" 2>/dev/null)" 2>/dev/null || true' EXIT | ||
| 26 | |||
| 27 | fail() { | ||
| 28 | echo "FAIL: $1" | ||
| 29 | [ -f "$LOG" ] && tail -40 "$LOG" | ||
| 30 | exit 1 | ||
| 31 | } | ||
| 32 | |||
| 33 | wait_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 | |||
| 43 | qemu_hmp() { | ||
| 44 | python3 - "$SOCK" "$1" <<'EOF' | ||
| 45 | import socket, sys, time | ||
| 46 | s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) | ||
| 47 | s.connect(sys.argv[1]) | ||
| 48 | s.sendall((sys.argv[2] + "\n").encode()) | ||
| 49 | time.sleep(0.4) | ||
| 50 | EOF | ||
| 51 | } | ||
| 52 | |||
| 53 | cursor_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 | |||
| 59 | echo "==> 1/6 build kernel (debug)" | ||
| 60 | cargo build --release --package rsdos --target x86_64-unknown-uefi | ||
| 61 | |||
| 62 | echo "==> 2/6 stage system image" | ||
| 63 | "$SCRIPT_DIR/mksystem.sh" --force "$SYSTEM_IMG" | ||
| 64 | |||
| 65 | echo "==> 3/6 boot QEMU (headless)" | ||
| 66 | kill "$(cat "$QPID" 2>/dev/null)" 2>/dev/null || true | ||
| 67 | sleep 1 | ||
| 68 | rm -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 & | ||
| 76 | echo $! >"$QPID" | ||
| 77 | |||
| 78 | wait_log "$LOG" "boot: entering shell" 90 || fail "kernel never reached the shell" | ||
| 79 | grep -aq "ps2: mouse ready" "$LOG" || fail "mouse driver did not come up at boot" | ||
| 80 | wait_log "$LOG" "cursor: task " 10 || fail "cursor task did not spawn" | ||
| 81 | |||
| 82 | echo "==> 4/6 cursor position tracks injected movement" | ||
| 83 | INIT=$(cursor_state) | ||
| 84 | [ -n "$INIT" ] || fail "no initial cursor position" | ||
| 85 | IX=$(echo "$INIT" | awk '{print $1}') | ||
| 86 | IY=$(echo "$INIT" | awk '{print $2}') | ||
| 87 | IB=$(echo "$INIT" | awk '{print $3}') | ||
| 88 | echo " initial: x=$IX y=$IY buttons=$IB" | ||
| 89 | [ "$IB" = "---" ] || fail "expected no buttons at boot, got $IB" | ||
| 90 | |||
| 91 | qemu_hmp "mouse_move 20 7" | ||
| 92 | S1=$(cursor_state) | ||
| 93 | X1=$(echo "$S1" | awk '{print $1}') | ||
| 94 | Y1=$(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)" | ||
| 97 | echo " moved: x=$X1 y=$Y1 (delta +20,+7)" | ||
| 98 | |||
| 99 | echo "==> 5/6 left button edge detection" | ||
| 100 | qemu_hmp "mouse_button 1" | ||
| 101 | S2=$(cursor_state) | ||
| 102 | IB2=$(echo "$S2" | awk '{print $3}') | ||
| 103 | [ "$IB2" = "L--" ] || [ "$IB2" = "L--" ] || fail "expected L--, got $IB2" | ||
| 104 | |||
| 105 | qemu_hmp "mouse_button 0" | ||
| 106 | S3=$(cursor_state) | ||
| 107 | IB3=$(echo "$S3" | awk '{print $3}') | ||
| 108 | [ "$IB3" = "---" ] || fail "expected --- after release, got $IB3" | ||
| 109 | echo " button: $IB2 → $IB3" | ||
| 110 | |||
| 111 | echo "==> 6/6 mouse stream routing to task 3" | ||
| 112 | python3 "$SCRIPT_DIR/sendkeys.py" --sock "$SOCK" "msgrecv" | ||
| 113 | sleep 1 | ||
| 114 | python3 "$SCRIPT_DIR/sendkeys.py" --sock "$SOCK" "mouse 3" | ||
| 115 | qemu_hmp "mouse_move 20 7" | ||
| 116 | qemu_hmp "mouse_button 1" | ||
| 117 | sleep 0.5 | ||
| 118 | qemu_hmp "mouse_button 0" | ||
| 119 | sleep 0.5 | ||
| 120 | python3 "$SCRIPT_DIR/sendkeys.py" --sock "$SOCK" "ctrl-c" | ||
| 121 | sleep 0.5 | ||
| 122 | grep -aq "Mouse(+20,+7" "$LOG" || fail "routed mouse move not logged" | ||
| 123 | |||
| 124 | echo "" | ||
| 125 | 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 @@ | |||
| 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 | # | ||
| 10 | set -euo pipefail | ||
| 11 | |||
| 12 | SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" | ||
| 13 | PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" | ||
| 14 | cd "$PROJECT_DIR" | ||
| 15 | |||
| 16 | OVMF="${OVMF:-/usr/share/edk2/x64/OVMF.4m.fd}" | ||
| 17 | QEMU="${QEMU:-qemu-system-x86_64}" | ||
| 18 | SYSTEM_IMG="${SYSTEM_IMG:-/tmp/system.img}" | ||
| 19 | QPID=/tmp/rsdos-msg-qpid | ||
| 20 | SOCK=/tmp/rsdos-msg-qmon.sock | ||
| 21 | LOG=/tmp/rsdos-msg-serial.log | ||
| 22 | QLOG=/tmp/rsdos-msg-qemu.log | ||
| 23 | |||
| 24 | trap 'kill "$(cat "$QPID" 2>/dev/null)" 2>/dev/null || true' EXIT | ||
| 25 | |||
| 26 | fail() { | ||
| 27 | echo "FAIL: $1" | ||
| 28 | [ -f "$LOG" ] && tail -40 "$LOG" | ||
| 29 | exit 1 | ||
| 30 | } | ||
| 31 | |||
| 32 | wait_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 | |||
| 42 | qemu_hmp() { | ||
| 43 | python3 - "$SOCK" "$1" <<'EOF' | ||
| 44 | import socket, sys, time | ||
| 45 | s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) | ||
| 46 | s.connect(sys.argv[1]) | ||
| 47 | s.sendall((sys.argv[2] + "\n").encode()) | ||
| 48 | time.sleep(0.4) | ||
| 49 | EOF | ||
| 50 | } | ||
| 51 | |||
| 52 | wait_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 | |||
| 62 | echo "==> 1/5 build kernel (debug)" | ||
| 63 | cargo build --release --package rsdos --target x86_64-unknown-uefi | ||
| 64 | |||
| 65 | echo "==> 2/5 stage system image" | ||
| 66 | "$SCRIPT_DIR/mksystem.sh" --force "$SYSTEM_IMG" | ||
| 67 | |||
| 68 | echo "==> 3/5 boot QEMU (headless)" | ||
| 69 | kill "$(cat "$QPID" 2>/dev/null)" 2>/dev/null || true | ||
| 70 | sleep 1 | ||
| 71 | rm -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 & | ||
| 79 | echo $! >"$QPID" | ||
| 80 | |||
| 81 | wait_log "$LOG" "boot: entering shell" 90 || fail "kernel never reached the shell" | ||
| 82 | |||
| 83 | echo "==> 4/5 drive the shell (msgrecv / send / mouse routing / queue-full)" | ||
| 84 | python3 "$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 | |||
| 95 | python3 "$SCRIPT_DIR/sendkeys.py" --sock "$SOCK" "mouse 3" | ||
| 96 | qemu_hmp "mouse_move 20 7" | ||
| 97 | qemu_hmp "mouse_button 1" | ||
| 98 | qemu_hmp "mouse_move -5 -3" | ||
| 99 | qemu_hmp "mouse_button 0" | ||
| 100 | qemu_hmp "sendkey ctrl-c" | ||
| 101 | |||
| 102 | python3 "$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 | |||
| 124 | echo "==> 5/5 wait for shutdown + verify" | ||
| 125 | wait_qemu_exit 30 || fail "QEMU did not exit after poweroff" | ||
| 126 | |||
| 127 | grep -aq "msgrecv: task 3 listening" "$LOG" || fail "msgrecv did not spawn" | ||
| 128 | grep -aq "msgrecv\[3\]: User(42)" "$LOG" || fail "user message missing" | ||
| 129 | grep -aq "msgrecv\[3\]: Timer" "$LOG" || fail "timer message missing" | ||
| 130 | grep -aq "msgrecv\[3\]: Key(Enter)" "$LOG" || fail "key enter missing" | ||
| 131 | grep -aq "msgrecv\[3\]: Key(Ctrl-c)" "$LOG" || fail "key ctrl-c missing" | ||
| 132 | grep -aq "msgrecv\[3\]: Key(F3)" "$LOG" || fail "key f3 missing" | ||
| 133 | grep -aq "msgrecv\[3\]: Mouse(+5,-3 L R)" "$LOG" || fail "synthesized mouse message missing" | ||
| 134 | grep -aq "msgrecv\[3\]: Mouse(+20,+7" "$LOG" || fail "real mouse move 20,7 not routed to queue" | ||
| 135 | grep -aq "msgrecv\[3\]: Mouse(-5,-3 L" "$LOG" || fail "held-button routed move missing" | ||
| 136 | grep -aq "queue full" "$LOG" || fail "17th send did not report queue full" | ||
| 137 | grep -aq "send: unknown type" "$LOG" && fail "a bad send slipped through (expected none)" | ||
| 138 | |||
| 139 | echo "" | ||
| 140 | echo "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 |
| 6 | test script without a display. | 6 | test script without a display. |
| 7 | 7 | ||
| 8 | Usage: | 8 | Used by scripts/tests/test-*.sh, and manually against an interactive |
| 9 | sendkeys.py "ls /disk" "echo hi WRITING /disk/a.txt" "poweroff" | 9 | session started with `./scripts/run.sh --monitor`: |
| 10 | sendkeys.py --sock /tmp/qmon.sock "date" | ||
| 11 | 10 | ||
| 12 | Environment: 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 | |||
| 14 | Environment: QMON_SOCK (default: /tmp/rsdos-monitor.sock) | ||
| 13 | """ | 15 | """ |
| 14 | import os | 16 | import os |
| 15 | import socket | 17 | import socket |
| 16 | import sys | 18 | import sys |
| 17 | import time | 19 | import time |
| 18 | 20 | ||
| 19 | SOCK = os.environ.get("QMON_SOCK", "/tmp/qmon.sock") | 21 | SOCK = 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 | # | ||
| 15 | set -euo pipefail | ||
| 16 | TNAME="disk" | ||
| 17 | # shellcheck source=../lib.sh | ||
| 18 | source "$(dirname "$0")/../lib.sh" | ||
| 19 | |||
| 20 | FRESH=0 | ||
| 21 | [ "${1:-}" = "--fresh" ] && FRESH=1 | ||
| 22 | STAMP="$(date +%s)" | ||
| 23 | |||
| 24 | echo "==> 1/6 build (kernel + bins)" | ||
| 25 | build_all | ||
| 26 | |||
| 27 | echo "==> 2/6 stage system image" | ||
| 28 | if [ "$FRESH" -eq 1 ]; then stage_image --force; else stage_image; fi | ||
| 29 | |||
| 30 | echo "==> 3/6 boot QEMU (headless)" | ||
| 31 | test_boot | ||
| 32 | sleep 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. | ||
| 37 | VOL_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)" | ||
| 39 | echo " mounted volume UID: $VOL_UID" | ||
| 40 | |||
| 41 | echo "==> 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). | ||
| 44 | TESTFILE="disk-test-$STAMP.txt" | ||
| 45 | sendkeys \ | ||
| 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 | |||
| 52 | echo "==> 5/6 wait for shutdown" | ||
| 53 | wait_qemu_exit 30 || fail "QEMU did not exit after poweroff" | ||
| 54 | |||
| 55 | echo "==> 6/6 verify" | ||
| 56 | grep -a "disk-test-$STAMP" "$LOG" >/dev/null || fail "cat output missing from serial log" | ||
| 57 | if ! fsck.fat -n "$SYSTEM_IMG" >/tmp/rsdos-fsck.out 2>&1; then | ||
| 58 | fail "fsck.fat reports problems: $(tail -3 /tmp/rsdos-fsck.out)" | ||
| 59 | fi | ||
| 60 | MT="$(mtype -i "$SYSTEM_IMG" "::/sub/$TESTFILE" 2>/dev/null || true)" | ||
| 61 | [ "$MT" = "disk-test-$STAMP" ] || fail "mtype mismatch: got '$MT'" | ||
| 62 | |||
| 63 | echo "" | ||
| 64 | echo "PASS — disk round trip OK (fsck clean, file survived poweroff)" | ||
| 65 | echo " image: $SYSTEM_IMG" | ||
| 66 | echo " volume: /disk/$VOL_UID" | ||
| 67 | 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 @@ | |||
| 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 | # | ||
| 13 | set -euo pipefail | ||
| 14 | TNAME="fault" | ||
| 15 | # shellcheck source=../lib.sh | ||
| 16 | source "$(dirname "$0")/../lib.sh" | ||
| 17 | |||
| 18 | echo "==> 1/5 build (kernel + bins)" | ||
| 19 | build_all | ||
| 20 | |||
| 21 | echo "==> 2/5 stage system image" | ||
| 22 | stage_image --force | ||
| 23 | |||
| 24 | echo "==> 3/5 boot QEMU (headless, + dedicated CRASHLOG stick)" | ||
| 25 | CRASH_IMG=/tmp/rsdos-crash.img | ||
| 26 | rm -f "$CRASH_IMG" | ||
| 27 | mkfs.fat -C -F 32 -n CRASHLOG "$CRASH_IMG" 65536 >/dev/null | ||
| 28 | test_boot \ | ||
| 29 | -drive "file=$CRASH_IMG,format=raw,if=none,id=crash" \ | ||
| 30 | -device ide-hd,drive=crash,bus=ide.1 | ||
| 31 | |||
| 32 | echo "==> 4/5 boot diagnostics present" | ||
| 33 | grep -aq "gop: fb base=" "$LOG" || fail "fb geometry diagnostic missing" | ||
| 34 | grep -aq "gop: mode .* selected" "$LOG" || fail "mode-selection diagnostic missing" | ||
| 35 | grep -aq "CRASHLOG" "$LOG" || fail "CRASHLOG stick not mounted" | ||
| 36 | |||
| 37 | echo "==> 5/5 inject a page fault (fault command)" | ||
| 38 | sendkeys "clear" "fault" | ||
| 39 | sleep 2 | ||
| 40 | |||
| 41 | grep -aq "!!! page fault vec=14 .*rip=" "$LOG" || fail "page-fault dump line missing" | ||
| 42 | grep -aq "cr2=0x400000000000" "$LOG" || fail "CR2 not reported (faulting address)" | ||
| 43 | grep -aq "task=0 \"shell\"" "$LOG" || fail "faulting task not identified" | ||
| 44 | grep -aq "!!! halting" "$LOG" || fail "no halt message" | ||
| 45 | |||
| 46 | echo "" | ||
| 47 | echo "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 | ||
| 4 | Connects to the QEMU monitor once and keeps the socket open (the per-call | 4 | Connects to the QEMU monitor once and keeps the socket open (the per-call |
| 5 | scripts/sendkeys.py pattern loses the socket when the monitor resets). | 5 | sendkeys.py pattern loses the socket when the monitor resets). |
| 6 | 6 | ||
| 7 | Usage: python3 scripts/test-focus.py /tmp/qmon.sock | 7 | Usage: start the guest with `./scripts/run.sh --monitor`, then |
| 8 | python3 scripts/tests/test-focus.py [/tmp/rsdos-monitor.sock] | ||
| 8 | """ | 9 | """ |
| 9 | import socket | 10 | import socket |
| 10 | import sys | 11 | import sys |
| 11 | import time | 12 | import time |
| 12 | 13 | ||
| 13 | sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) | 14 | sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) |
| 14 | sock.connect(sys.argv[1] if len(sys.argv) > 1 else "/tmp/sysboot-mon.sock") | 15 | sock.connect(sys.argv[1] if len(sys.argv) > 1 else "/tmp/rsdos-monitor.sock") |
| 15 | 16 | ||
| 16 | 17 | ||
| 17 | def sk(k, d=0.35): | 18 | def 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 | # | ||
| 11 | set -euo pipefail | ||
| 12 | TNAME="gfx" | ||
| 13 | # shellcheck source=../lib.sh | ||
| 14 | source "$(dirname "$0")/../lib.sh" | ||
| 15 | |||
| 16 | echo "==> 1/5 build (kernel + bins)" | ||
| 17 | build_all | ||
| 18 | |||
| 19 | echo "==> 2/5 stage system image" | ||
| 20 | stage_image --force | ||
| 21 | |||
| 22 | echo "==> 3/5 boot QEMU (headless)" | ||
| 23 | test_boot | ||
| 24 | wait_log "$LOG" "cursor: task " 10 || fail "cursor task did not spawn" | ||
| 25 | |||
| 26 | echo "==> 4/5 in-kernel pixel self-test (gfx check)" | ||
| 27 | sendkeys "gfx check" | ||
| 28 | wait_log "$LOG" "gfx check: PASS" 30 || fail "gfx check did not run" | ||
| 29 | |||
| 30 | echo "==> 5/5 gfx demo (draw scene)" | ||
| 31 | sendkeys "gfx" "poweroff" | ||
| 32 | wait_qemu_exit 30 || fail "QEMU did not exit after poweroff" | ||
| 33 | |||
| 34 | grep -aq "gfx: drew status bar" "$LOG" || fail "gfx demo did not draw" | ||
| 35 | |||
| 36 | echo "" | ||
| 37 | 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 @@ | |||
| 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 | # | ||
| 12 | set -euo pipefail | ||
| 13 | TNAME="mouse" | ||
| 14 | # shellcheck source=../lib.sh | ||
| 15 | source "$(dirname "$0")/../lib.sh" | ||
| 16 | |||
| 17 | # cursor_state — one-shot `cursor` command, parsed to "x y buttons". | ||
| 18 | cursor_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 | |||
| 24 | echo "==> 1/6 build (kernel + bins)" | ||
| 25 | build_all | ||
| 26 | |||
| 27 | echo "==> 2/6 stage system image" | ||
| 28 | stage_image --force | ||
| 29 | |||
| 30 | echo "==> 3/6 boot QEMU (headless)" | ||
| 31 | test_boot | ||
| 32 | grep -aq "ps2: mouse ready" "$LOG" || fail "mouse driver did not come up at boot" | ||
| 33 | wait_log "$LOG" "cursor: task " 10 || fail "cursor task did not spawn" | ||
| 34 | |||
| 35 | echo "==> 4/6 cursor position tracks injected movement" | ||
| 36 | INIT=$(cursor_state) | ||
| 37 | [ -n "$INIT" ] || fail "no initial cursor position" | ||
| 38 | IX=$(echo "$INIT" | awk '{print $1}') | ||
| 39 | IY=$(echo "$INIT" | awk '{print $2}') | ||
| 40 | IB=$(echo "$INIT" | awk '{print $3}') | ||
| 41 | echo " initial: x=$IX y=$IY buttons=$IB" | ||
| 42 | [ "$IB" = "---" ] || fail "expected no buttons at boot, got $IB" | ||
| 43 | |||
| 44 | qemu_hmp "mouse_move 20 7" | ||
| 45 | S1=$(cursor_state) | ||
| 46 | X1=$(echo "$S1" | awk '{print $1}') | ||
| 47 | Y1=$(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)" | ||
| 50 | echo " moved: x=$X1 y=$Y1 (delta +20,+7)" | ||
| 51 | |||
| 52 | echo "==> 5/6 left button edge detection" | ||
| 53 | qemu_hmp "mouse_button 1" | ||
| 54 | S2=$(cursor_state) | ||
| 55 | IB2=$(echo "$S2" | awk '{print $3}') | ||
| 56 | [ "$IB2" = "L--" ] || fail "expected L--, got $IB2" | ||
| 57 | |||
| 58 | qemu_hmp "mouse_button 0" | ||
| 59 | S3=$(cursor_state) | ||
| 60 | IB3=$(echo "$S3" | awk '{print $3}') | ||
| 61 | [ "$IB3" = "---" ] || fail "expected --- after release, got $IB3" | ||
| 62 | echo " button: $IB2 → $IB3" | ||
| 63 | |||
| 64 | echo "==> 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. | ||
| 67 | sendkeys "msgrecv" | ||
| 68 | sleep 1 | ||
| 69 | MID="$(sed -n 's/.*msgrecv: task \([0-9][0-9]*\) listening.*/\1/p' "$LOG" | tail -1)" | ||
| 70 | [ -n "$MID" ] || fail "msgrecv did not spawn" | ||
| 71 | echo " msgrecv task id: $MID" | ||
| 72 | sendkeys "mouse $MID" | ||
| 73 | qemu_hmp "mouse_move 20 7" | ||
| 74 | qemu_hmp "mouse_button 1" | ||
| 75 | sleep 0.5 | ||
| 76 | qemu_hmp "mouse_button 0" | ||
| 77 | sleep 0.5 | ||
| 78 | sendkeys "ctrl-c" | ||
| 79 | sleep 0.5 | ||
| 80 | grep -aq "Mouse(+20,+7" "$LOG" || fail "routed mouse move not logged" | ||
| 81 | |||
| 82 | echo "" | ||
| 83 | 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 @@ | |||
| 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 | # | ||
| 11 | set -euo pipefail | ||
| 12 | TNAME="msg" | ||
| 13 | # shellcheck source=../lib.sh | ||
| 14 | source "$(dirname "$0")/../lib.sh" | ||
| 15 | |||
| 16 | echo "==> 1/5 build (kernel + bins)" | ||
| 17 | build_all | ||
| 18 | |||
| 19 | echo "==> 2/5 stage system image" | ||
| 20 | stage_image --force | ||
| 21 | |||
| 22 | echo "==> 3/5 boot QEMU (headless)" | ||
| 23 | test_boot | ||
| 24 | |||
| 25 | echo "==> 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. | ||
| 29 | sendkeys "msgrecv" | ||
| 30 | sleep 1 | ||
| 31 | MID="$(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)" | ||
| 33 | echo " msgrecv task id: $MID" | ||
| 34 | |||
| 35 | sendkeys \ | ||
| 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 | |||
| 45 | sendkeys "mouse $MID" | ||
| 46 | qemu_hmp "mouse_move 20 7" | ||
| 47 | qemu_hmp "mouse_button 1" | ||
| 48 | qemu_hmp "mouse_move -5 -3" | ||
| 49 | qemu_hmp "mouse_button 0" | ||
| 50 | qemu_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. | ||
| 56 | CID="$(awk '$2 == "cursor" { print $1 }' "$LOG" | tail -1)" | ||
| 57 | [ -n "$CID" ] || fail "no cursor task in jobs output (cannot probe queue-full)" | ||
| 58 | echo " queue-full target: task $CID (cursor, never recv'd)" | ||
| 59 | |||
| 60 | sendkeys \ | ||
| 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 | |||
| 82 | echo "==> 5/5 wait for shutdown + verify" | ||
| 83 | wait_qemu_exit 30 || fail "QEMU did not exit after poweroff" | ||
| 84 | |||
| 85 | grep -aq "msgrecv: task $MID listening" "$LOG" || fail "msgrecv did not spawn" | ||
| 86 | grep -aq "msgrecv\[$MID\]: User(42)" "$LOG" || fail "user message missing" | ||
| 87 | grep -aq "msgrecv\[$MID\]: Timer" "$LOG" || fail "timer message missing" | ||
| 88 | grep -aq "msgrecv\[$MID\]: Key(Enter)" "$LOG" || fail "key enter missing" | ||
| 89 | grep -aq "msgrecv\[$MID\]: Key(Ctrl-c)" "$LOG" || fail "key ctrl-c missing" | ||
| 90 | grep -aq "msgrecv\[$MID\]: Key(F3)" "$LOG" || fail "key f3 missing" | ||
| 91 | grep -aq "msgrecv\[$MID\]: Mouse(+5,-3 L R)" "$LOG" || fail "synthesized mouse message missing" | ||
| 92 | grep -aq "msgrecv\[$MID\]: Mouse(+20,+7" "$LOG" || fail "real mouse move 20,7 not routed to queue" | ||
| 93 | grep -aq "msgrecv\[$MID\]: Mouse(-5,-3 L" "$LOG" || fail "held-button routed move missing" | ||
| 94 | grep -aq "queue full" "$LOG" || fail "17th send did not report queue full" | ||
| 95 | grep -aq "send: unknown type" "$LOG" && fail "a bad send slipped through (expected none)" | ||
| 96 | |||
| 97 | echo "" | ||
| 98 | echo "PASS — message queues OK" | ||
