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 | |
| 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.
28 files changed, 897 insertions, 1170 deletions
| @@ -3,4 +3,5 @@ | |||
| 3 | /esp/ | 3 | /esp/ |
| 4 | *.img | 4 | *.img |
| 5 | NvVars | 5 | NvVars |
| 6 | __pycache__/ | ||
| 6 | /.pi-subagents/ | 7 | /.pi-subagents/ |
| @@ -135,7 +135,10 @@ The single dd-able FAT filesystem image the build produces: bootloader, | |||
| 135 | kernel, /bin, /etc and seed content on one volume. Imaging it onto a disk | 135 | kernel, /bin, /etc and seed content on one volume. Imaging it onto a disk |
| 136 | yields a complete installed system. Binaries live on the system image | 136 | yields a complete installed system. Binaries live on the system image |
| 137 | under /bin/cli and /bin/gui; the shell finds them by scanning mounted | 137 | under /bin/cli and /bin/gui; the shell finds them by scanning mounted |
| 138 | volumes — no volume is privileged. | 138 | volumes — no volume is privileged. For user-facing paths, /bin is a |
| 139 | transparent VFS alias: `ls /bin/cli` resolves to the first mounted | ||
| 140 | volume whose root has \bin, so nobody needs /disk/<UID>/bin/... (ADR | ||
| 141 | 0006). | ||
| 139 | 142 | ||
| 140 | ## Loader | 143 | ## Loader |
| 141 | 144 | ||
| @@ -194,7 +197,18 @@ reaped) without taking the kernel down. | |||
| 194 | 197 | ||
| 195 | Standalone project — no integration with the RNG initramfs build system. | 198 | Standalone project — no integration with the RNG initramfs build system. |
| 196 | Testing via QEMU with OVMF. The kernel PE binary is placed on a FAT | 199 | Testing via QEMU with OVMF. The kernel PE binary is placed on a FAT |
| 197 | image as `\bzImage` (matching rsbl's expected path). | 200 | image as `\bzImage` (matching rsbl's expected path) and also as |
| 201 | `\EFI\BOOT\BOOTX64.EFI` (what the firmware boots directly). | ||
| 202 | |||
| 203 | Two commands, compile + boot: | ||
| 204 | |||
| 205 | ```sh | ||
| 206 | ./scripts/build.sh # cargo build --workspace --target x86_64-unknown-uefi --release | ||
| 207 | ./scripts/run.sh # stage /tmp/system.img if stale, then boot QEMU | ||
| 208 | ``` | ||
| 209 | |||
| 210 | (`./scripts/run.sh` alone does both; `--headless`, `--debug`, `--monitor`, | ||
| 211 | `--fresh` as per its `--help`. `make` / `make run` are thin aliases.) | ||
| 198 | 212 | ||
| 199 | ## Versioning | 213 | ## Versioning |
| 200 | 214 | ||
| @@ -397,20 +411,35 @@ Workspace of four crates, all built for the `x86_64-unknown-uefi` target: | |||
| 397 | 411 | ||
| 398 | The whole disk workflow is scripted (no manual mkfs/mcopy): | 412 | The whole disk workflow is scripted (no manual mkfs/mcopy): |
| 399 | 413 | ||
| 400 | - `make disk` / `./scripts/mkdisk.sh` — create the FAT32 image with | 414 | - `./scripts/build.sh` — the single cargo invocation: kernel + |
| 401 | `mkfs.fat -C` (the flag that actually creates the file; plain | 415 | every `/bin` binary (`bins/*.efi`), release profile by default |
| 402 | `mkfs.fat` needs an existing file), label `RSDOS`, seed from | 416 | - `./scripts/mksystem.sh` / `make system` — assemble the dd-able |
| 403 | `seed/` via mtools; `--fresh` re-formats, `--size 128M` sizes it | 417 | FAT32 image (`mkfs.fat -C`, label `RSDOS`, seed from `seed/` via |
| 418 | mtools, stage `/bin/cli` + `/bin/gui`); rebuilt only when a | ||
| 419 | kernel/binary/seed file is newer than it, `--force` wipes runtime | ||
| 420 | data, `--gpt` wraps it for USB sticks | ||
| 421 | - `./scripts/run.sh` / `make run` — build + stage + boot QEMU | ||
| 422 | (`--headless`, `--debug` GDB stub, `--monitor` HMP socket, | ||
| 423 | `--fresh`, `--mem/--smp/--bios/--image`); the two-command flow is | ||
| 424 | `./scripts/build.sh && ./scripts/run.sh` | ||
| 404 | - `make inspect` — `mdir -i` listing of the image (mtools) | 425 | - `make inspect` — `mdir -i` listing of the image (mtools) |
| 405 | - `make test-disk` / `./scripts/test-disk.sh` — full automated round | 426 | - `make test` — all automated round trips; each boots QEMU headless, |
| 406 | trip: build → deploy → ensure image → boot QEMU headless → drive | 427 | drives the shell via `sendkeys.py` through the monitor and asserts |
| 428 | on the serial log | ||
| 429 | - `./scripts/tests/test-disk.sh` / `make test-disk` — full automated | ||
| 430 | round trip: build → stage → boot QEMU headless → drive | ||
| 407 | the shell via `sendkey` through the monitor (`ls /disk`, `mkdir`, | 431 | the shell via `sendkey` through the monitor (`ls /disk`, `mkdir`, |
| 408 | `echo WRITING`, `cat`, `poweroff`) → fsck-clean check → `mtype` verifies | 432 | `echo WRITING`, `cat`, `poweroff`) → fsck-clean check → `mtype` verifies |
| 409 | the written file survived poweroff | 433 | the written file survived poweroff |
| 410 | - `./scripts/sendkeys.py` — types lines into the guest via the QEMU | 434 | - `./scripts/tests/sendkeys.py` — types lines into the guest via the QEMU |
| 411 | monitor (`--sock`, US layout, named keys for punctuation; bare | 435 | monitor (`--sock`, US layout, named keys for punctuation; bare |
| 412 | `-`/`.`/`/` are not valid sendkey names) | 436 | `-`/`.`/`/` are not valid sendkey names) |
| 413 | 437 | ||
| 438 | Shared config and helpers (image path, OVMF, QEMU flags, `build_all`, | ||
| 439 | `stage_image`, `test_boot`, `wait_log`, `fail`) live in | ||
| 440 | `scripts/lib.sh` — every script sources it; there is exactly one | ||
| 441 | `cargo build` and one QEMU invocation shape in the tree. | ||
| 442 | |||
| 414 | ✅ **`klog` kernel log ring** — a RAM ring of the last 128 serial log | 443 | ✅ **`klog` kernel log ring** — a RAM ring of the last 128 serial log |
| 415 | lines (128 chars each, ~16 KB), fed by `serial_println!` (one choke | 444 | lines (128 chars each, ~16 KB), fed by `serial_println!` (one choke |
| 416 | point — every boot/low-level message captured for free). Sequence | 445 | point — every boot/low-level message captured for free). Sequence |
| @@ -806,7 +835,7 @@ Resolved this session: | |||
| 806 | see intermediate steps of the filtering. | 835 | see intermediate steps of the filtering. |
| 807 | - **`> >> < <<` are gone.** Keywords are the *only* redirection syntax; | 836 | - **`> >> < <<` are gone.** Keywords are the *only* redirection syntax; |
| 808 | `>` is now ordinary text (`echo a > b` prints `a > b`). Collateral: | 837 | `>` is now ordinary text (`echo a > b` prints `a > b`). Collateral: |
| 809 | `scripts/test-disk.sh` and the `help` text must be rewritten to the | 838 | `scripts/tests/test-disk.sh` and the `help` text must be rewritten to the |
| 810 | keyword syntax; the milestone bullets above describing `echo >`/`>>` | 839 | keyword syntax; the milestone bullets above describing `echo >`/`>>` |
| 811 | record the old behavior (superseded by these notes). | 840 | record the old behavior (superseded by these notes). |
| 812 | - **Quoting (C): `"..."` groups + literalizes; `\x` escapes one char.** | 841 | - **Quoting (C): `"..."` groups + literalizes; `\x` escapes one char.** |
| @@ -888,7 +917,7 @@ Everything above is now implemented in `shell/src/lib.rs`: | |||
| 888 | - **^D** — EOF everywhere, at the prompt it is a no-op (fresh | 917 | - **^D** — EOF everywhere, at the prompt it is a no-op (fresh |
| 889 | prompt); ^C keeps line-cancel / stream-abort; `poweroff`/`exit` | 918 | prompt); ^C keeps line-cancel / stream-abort; `poweroff`/`exit` |
| 890 | are the only shutdown verbs. | 919 | are the only shutdown verbs. |
| 891 | - **Collateral** — `help` text rewritten; `scripts/test-disk.sh` and | 920 | - **Collateral** — `help` text rewritten; `scripts/tests/test-disk.sh` and |
| 892 | the `sendkeys.py` docstring updated to keyword syntax. | 921 | the `sendkeys.py` docstring updated to keyword syntax. |
| 893 | 922 | ||
| 894 | Bugs found by QEMU verification and fixed: | 923 | Bugs found by QEMU verification and fixed: |
| @@ -1,176 +1,93 @@ | |||
| 1 | # shellcheck shell=sh disable=SC2046,SC2091,SC2086,SC2155 | 1 | # Thin wrapper over scripts/ — all real logic lives in scripts/*.sh, with |
| 2 | # shared config and helpers in scripts/lib.sh. `make help` lists targets. | ||
| 3 | # | ||
| 2 | # NOTE: $(VAR) in this file is MAKE syntax, not shell substitution. | 4 | # NOTE: $(VAR) in this file is MAKE syntax, not shell substitution. |
| 3 | export TARGET=x86_64-unknown-uefi | 5 | |
| 4 | export PROFILE=release | 6 | SYSTEM_IMG ?= /tmp/system.img |
| 5 | export OUT_DIR=target/$(TARGET)/$(PROFILE) | 7 | |
| 6 | export ESP_DIR=esp | 8 | .PHONY: all build run run-headless debug system system-fresh inspect \ |
| 7 | export KERNEL=$(OUT_DIR)/rsdos.efi | 9 | test test-disk test-fault test-gfx test-mouse test-msg fmt clean help |
| 8 | export BZIMAGE=$(ESP_DIR)/bzImage | 10 | |
| 9 | export INITRD=$(ESP_DIR)/initramfs.cpio.gz | 11 | # ---- build ---------------------------------------------------------------- |
| 10 | export BOOTLOADER=rsboot/rsboot.efi | 12 | |
| 11 | export OVMF=/usr/share/edk2/x64/OVMF.4m.fd | 13 | all: build |
| 12 | export BOOT_EFI=$(ESP_DIR)/EFI/BOOT/BOOTX64.EFI | 14 | |
| 13 | # Attach the single dd-able system image as the SATA drive (ide.0). | 15 | build: |
| 14 | # Create/re-create with: make system-fresh (or: ./scripts/mksystem.sh --force) | 16 | ./scripts/build.sh |
| 15 | export SYSTEM_IMG ?= /tmp/system.img | 17 | |
| 16 | 18 | # ---- boot ----------------------------------------------------------------- | |
| 17 | 19 | ||
| 18 | # ---- Build --------------------------------------------------------------- | 20 | # Build + stage the image + boot QEMU (window + serial on stdio). |
| 19 | 21 | run: | |
| 20 | .PHONY: all run run-gui clean image debug release fmt bootloader boot-image system system-fresh inspect test-disk test-mouse test-gfx test-fault test-msg | 22 | ./scripts/run.sh |
| 21 | 23 | ||
| 22 | release: PROFILE=release | 24 | # Serial console only, no window. |
| 23 | release: export PROFILE=release | 25 | run-headless: |
| 24 | release: export OUT_DIR=target/$(TARGET)/release | 26 | ./scripts/run.sh --headless |
| 25 | release: CARGO_FLAGS=--release | 27 | |
| 26 | release: all | 28 | # GDB stub on :1234, CPU paused (attach: gdb -x scripts/debug-kernel.py). |
| 27 | 29 | debug: | |
| 28 | # CARGO_FLAGS set by PROFILE above | 30 | ./scripts/run.sh --debug |
| 29 | CARGO_FLAGS=--release | 31 | |
| 30 | 32 | # ---- system image --------------------------------------------------------- | |
| 31 | $(OUT_DIR)/rsdos.efi: $(shell find rsnix shell rsdos -name '*.rs') Cargo.toml rsnix/Cargo.toml shell/Cargo.toml rsdos/Cargo.toml | 33 | |
| 32 | cargo build --package rsdos --target $(TARGET) $(CARGO_FLAGS) | 34 | # Stage the image if it is stale (runtime files survive). |
| 33 | |||
| 34 | $(BZIMAGE): $(KERNEL) | $(ESP_DIR) | ||
| 35 | cp "$<" "$@" | ||
| 36 | |||
| 37 | $(ESP_DIR): | ||
| 38 | mkdir -p $(ESP_DIR) | ||
| 39 | |||
| 40 | $(INITRD): | $(ESP_DIR) | ||
| 41 | printf '' | gzip > "$@" | ||
| 42 | |||
| 43 | # ---- QEMU runners -------------------------------------------------------- | ||
| 44 | |||
| 45 | run: all system | ||
| 46 | qemu-system-x86_64 \ | ||
| 47 | -bios $(OVMF) \ | ||
| 48 | -drive file=$(SYSTEM_IMG),format=raw,if=none,id=disk \ | ||
| 49 | -device ide-hd,drive=disk,bus=ide.0 \ | ||
| 50 | -serial stdio \ | ||
| 51 | -m 256M \ | ||
| 52 | -machine q35 \ | ||
| 53 | -cpu qemu64 \ | ||
| 54 | -no-reboot \ | ||
| 55 | -display none | ||
| 56 | |||
| 57 | run-gui: all system | ||
| 58 | qemu-system-x86_64 \ | ||
| 59 | -bios $(OVMF) \ | ||
| 60 | -drive file=$(SYSTEM_IMG),format=raw,if=none,id=disk \ | ||
| 61 | -device ide-hd,drive=disk,bus=ide.0 \ | ||
| 62 | -serial stdio \ | ||
| 63 | -m 256M \ | ||
| 64 | -machine q35 \ | ||
| 65 | -cpu qemu64 \ | ||
| 66 | -no-reboot \ | ||
| 67 | -vga std | ||
| 68 | |||
| 69 | # GDB stub on :1234 | ||
| 70 | debug: all system | ||
| 71 | qemu-system-x86_64 \ | ||
| 72 | -bios $(OVMF) \ | ||
| 73 | -drive file=$(SYSTEM_IMG),format=raw,if=none,id=disk \ | ||
| 74 | -device ide-hd,drive=disk,bus=ide.0 \ | ||
| 75 | -serial stdio \ | ||
| 76 | -m 256M \ | ||
| 77 | -machine q35 \ | ||
| 78 | -cpu qemu64 \ | ||
| 79 | -no-reboot \ | ||
| 80 | -display none \ | ||
| 81 | -s -S | ||
| 82 | |||
| 83 | # Boot from a raw disk image (delegates to boot-image.sh which builds the | ||
| 84 | # full chain if the image doesn't exist yet) | ||
| 85 | run-image: all | ||
| 86 | |||
| 87 | # ---- System image -------------------------------------------------------- | ||
| 88 | |||
| 89 | # One dd-able FAT32 image: kernel + ESP + /etc + /bin + seed data. | ||
| 90 | # Stamp rule: rebuilt only when the kernel or this script changes — an | ||
| 91 | # up-to-date image is NEVER touched, so files written at runtime survive. | ||
| 92 | system: | 35 | system: |
| 93 | @if [ ! -f "$(SYSTEM_IMG)" ] || [ "$$(stat -c %Y $(BZIMAGE) 2>/dev/null || echo 0)" -gt "$$(stat -c %Y $(SYSTEM_IMG) 2>/dev/null || echo 0)" ]; then \ | 36 | ./scripts/mksystem.sh |
| 94 | ./scripts/mksystem.sh --force "$(SYSTEM_IMG)"; \ | ||
| 95 | else \ | ||
| 96 | echo "system image up to date: $(SYSTEM_IMG)"; \ | ||
| 97 | fi | ||
| 98 | 37 | ||
| 99 | # Re-create the system image from scratch (wipes runtime data). | 38 | # Re-create the image from scratch (wipes runtime data). |
| 100 | system-fresh: | 39 | system-fresh: |
| 101 | ./scripts/mksystem.sh --force "$(SYSTEM_IMG)" | 40 | ./scripts/mksystem.sh --force |
| 102 | 41 | ||
| 103 | # List the system image contents with mtools (requires mtools). | 42 | # List the image contents (needs mtools: sudo apt install mtools). |
| 104 | inspect: | 43 | inspect: |
| 105 | @command -v mdir >/dev/null 2>&1 || (echo "inspect needs mtools: sudo apt install mtools"; exit 1) | ||
| 106 | @mdir -i "$(SYSTEM_IMG)" :: | 44 | @mdir -i "$(SYSTEM_IMG)" :: |
| 107 | 45 | ||
| 108 | # Full automated round trip: build -> boot -> shell commands -> poweroff | 46 | # ---- tests ---------------------------------------------------------------- |
| 109 | # -> fsck + mtools verification. | ||
| 110 | test-disk: | ||
| 111 | ./scripts/test-disk.sh | ||
| 112 | 47 | ||
| 113 | # Automated PS/2 mouse round trip: boot -> inject mouse_move/mouse_button | 48 | test: test-disk test-gfx test-mouse test-msg test-fault |
| 114 | # via the monitor -> verify /dev/mouse + the `mouse` stream -> poweroff. | ||
| 115 | test-mouse: | ||
| 116 | ./scripts/test-mouse.sh | ||
| 117 | 49 | ||
| 118 | test-gfx: | 50 | test-disk: |
| 119 | ./scripts/test-gfx.sh | 51 | ./scripts/tests/test-disk.sh |
| 120 | 52 | ||
| 121 | # Fault-diagnostics round trip: boot -> `fault` (deliberate page fault) -> | ||
| 122 | # verify the full dump (vec/err/rip/cr2/task) hits serial + screen -> halt. | ||
| 123 | test-fault: | 53 | test-fault: |
| 124 | ./scripts/test-fault.sh | 54 | ./scripts/tests/test-fault.sh |
| 125 | |||
| 126 | # Automated scheduler message-queue round trip: msgrecv/send, real-input | ||
| 127 | # routing into a task's queue, queue-full boundary. | ||
| 128 | test-msg: | ||
| 129 | ./scripts/test-msg.sh | ||
| 130 | |||
| 131 | # ---- Full boot chain ----------------------------------------------------- | ||
| 132 | 55 | ||
| 133 | # Build bootloader only (rsboot submodule) | 56 | test-gfx: |
| 134 | bootloader: | 57 | ./scripts/tests/test-gfx.sh |
| 135 | ./scripts/build-bootloader.sh | ||
| 136 | 58 | ||
| 137 | # Build the full boot chain disk image (kernel + bootloader) | 59 | test-mouse: |
| 138 | image: all | 60 | ./scripts/tests/test-mouse.sh |
| 139 | ./scripts/mksystem.sh --chain rs-dos.img | ||
| 140 | 61 | ||
| 141 | # Boot the full chain disk image (builds it first if needed) | 62 | test-msg: |
| 142 | boot-image: all | 63 | ./scripts/tests/test-msg.sh |
| 143 | ./scripts/boot-image.sh rs-dos.img | ||
| 144 | 64 | ||
| 145 | # ---- Utilities ----------------------------------------------------------- | 65 | # ---- utilities ------------------------------------------------------------ |
| 146 | 66 | ||
| 147 | fmt: | 67 | fmt: |
| 148 | cargo fmt | 68 | cargo fmt |
| 149 | 69 | ||
| 150 | clean: | 70 | clean: |
| 151 | cargo clean | 71 | cargo clean |
| 152 | rm -rf $(ESP_DIR) | ||
| 153 | 72 | ||
| 154 | help: | 73 | help: |
| 155 | @echo "rs-dos — Make targets" | 74 | @echo "raisernics — make targets" |
| 156 | @echo "" | 75 | @echo "" |
| 157 | @echo " make build kernel + ESP (default)" | 76 | @echo " make build kernel + /bin binaries (release)" |
| 158 | @echo " make release build with --release (smaller binary)" | 77 | @echo " make run build + stage image + boot QEMU (window)" |
| 159 | @echo " make bootloader build rsboot from source" | 78 | @echo " make run-headless same, serial console only" |
| 160 | @echo " make image build full chain disk image (bootloader + kernel)" | 79 | @echo " make debug same, GDB stub on :1234 (CPU paused)" |
| 161 | @echo " make boot-image build + boot full chain via QEMU" | 80 | @echo " make system stage the system image if stale" |
| 162 | @echo " make run QEMU serial console (direct boot, no bootloader)" | 81 | @echo " make system-fresh re-create the system image (wipes data)" |
| 163 | @echo " make system create the system image if stale/missing (/tmp/system.img)" | 82 | @echo " make inspect list image contents (mtools)" |
| 164 | @echo " make system-fresh re-create the system image (wipes runtime data)" | 83 | @echo " make test run every automated test" |
| 165 | @echo " make inspect list system image contents (mtools)" | 84 | @echo " make test-disk disk round trip (build -> boot -> fsck)" |
| 166 | @echo " make test-disk automated disk round trip (build->boot->fsck)" | 85 | @echo " make test-gfx graphics self-test" |
| 167 | @echo " make clean remove build artifacts" | 86 | @echo " make test-mouse PS/2 mouse + cursor tracking" |
| 87 | @echo " make test-msg scheduler message queues" | ||
| 88 | @echo " make test-fault fault dump (screen + serial)" | ||
| 168 | @echo " make fmt format Rust source" | 89 | @echo " make fmt format Rust source" |
| 169 | @echo " ./scripts/mksystem.sh build the dd-able system image" | 90 | @echo " make clean remove build artifacts" |
| 170 | @echo " ./scripts/boot.sh quick direct boot (dev loop)" | 91 | @echo "" |
| 171 | @echo " ./scripts/test-disk.sh automated disk round trip" | 92 | @echo "Two-command flow: ./scripts/build.sh && ./scripts/run.sh" |
| 172 | @echo " ./scripts/sendkeys.py type into QEMU via the monitor" | 93 | @echo "Scripts: build.sh run.sh mksystem.sh lib.sh tests/" |
| 173 | @echo " ./scripts/boot-image.sh boot full chain (builds if needed)" | ||
| 174 | @echo " ./scripts/build-bootloader.sh build rsboot from source" | ||
| 175 | @echo " ./scripts/qemu.sh flexible QEMU launcher" | ||
| 176 | @echo " ./scripts/debug-kernel.py GDB plugin (attach after 'make debug')" | ||
diff --git a/docs/adr/0002-keyword-redirection-and-post-filters.md b/docs/adr/0002-keyword-redirection-and-post-filters.md index 1584077..227ce2c 100644 --- a/docs/adr/0002-keyword-redirection-and-post-filters.md +++ b/docs/adr/0002-keyword-redirection-and-post-filters.md | |||
| @@ -65,7 +65,7 @@ cmd args [USE src] [TIL marker] [filters…] [output kw dst] | |||
| 65 | -t 10`) is expressible without a pipe; ^C flushes the held-back | 65 | -t 10`) is expressible without a pipe; ^C flushes the held-back |
| 66 | window instead of losing it. | 66 | window instead of losing it. |
| 67 | - **Negative:** Breaks existing muscle memory and tooling: POSIX users | 67 | - **Negative:** Breaks existing muscle memory and tooling: POSIX users |
| 68 | must learn the keywords; `scripts/test-disk.sh` and the `help` text | 68 | must learn the keywords; `scripts/tests/test-disk.sh` and the `help` text |
| 69 | must be rewritten from `echo >` to the keyword syntax. | 69 | must be rewritten from `echo >` to the keyword syntax. |
| 70 | - **Negative:** Keywords are reserved, so printing text like `WRITING` | 70 | - **Negative:** Keywords are reserved, so printing text like `WRITING` |
| 71 | requires quoting; unknown commands cannot be filtered with flags | 71 | requires quoting; unknown commands cannot be filtered with flags |
diff --git a/docs/plan-executable-binaries.md b/docs/plan-executable-binaries.md index cd6f1d0..138b8e0 100644 --- a/docs/plan-executable-binaries.md +++ b/docs/plan-executable-binaries.md | |||
| @@ -11,7 +11,7 @@ the glossary rules. | |||
| 11 | The build produces one dd-able FAT32 image instead of the current | 11 | The build produces one dd-able FAT32 image instead of the current |
| 12 | two-source setup (vvfat `esp/` directory + separate `/tmp/disk.img`). | 12 | two-source setup (vvfat `esp/` directory + separate `/tmp/disk.img`). |
| 13 | 13 | ||
| 14 | - [ ] Extend `scripts/mkdisk.sh` (or add `scripts/mksystem.sh`) to create | 14 | - [x] `scripts/mksystem.sh` creates |
| 15 | `system.img`: `mkfs.fat -C`, then `mcopy` in | 15 | `system.img`: `mkfs.fat -C`, then `mcopy` in |
| 16 | `\bzImage`, `\EFI\` (as rsbl requires), `\etc\*` (motd moves out of | 16 | `\bzImage`, `\EFI\` (as rsbl requires), `\etc\*` (motd moves out of |
| 17 | the `fs.rs` code literal? — optional here), seed content. | 17 | the `fs.rs` code literal? — optional here), seed content. |
| @@ -97,7 +97,7 @@ is absent; **version bump to 0.6.0**. | |||
| 97 | - [ ] Migrate `cat` last (input consumer of every corner: heredoc, USE, | 97 | - [ ] Migrate `cat` last (input consumer of every corner: heredoc, USE, |
| 98 | TIL sugar, multi-path). | 98 | TIL sugar, multi-path). |
| 99 | 99 | ||
| 100 | **Gate:** full `scripts/test-disk.sh` grammar suite passes unchanged with | 100 | **Gate:** full `scripts/tests/test-disk.sh` grammar suite passes unchanged with |
| 101 | external binaries; `-h/-t/-g/-p` chains work across the task boundary; | 101 | external binaries; `-h/-t/-g/-p` chains work across the task boundary; |
| 102 | ^C flushes a tail window on an infinite stream. | 102 | ^C flushes a tail window on an infinite stream. |
| 103 | 103 | ||
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" | ||
