From 1d4898496fcd58ba71096bf82bfb2b704159e33f Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 7 Nov 2026 22:49:03 +0000 Subject: [PATCH 1/3] acceptance: a failed check prints the serial log where it looked (#136) Run 197's riscv64 failure (#131) could only be read from the run's logs artifact, which Gitea's API does not list. qemu_session.py's check() now prints, on a FAIL, the serial lines after the first match of an anchor the check names, or the log's last lines; control characters as escapes, so a stray byte or terminal sequence in a line shows instead of only failing a match. The boot-disk checks anchor on 'Boot successful!'. A passing run prints as before (riscv64 here, logs/ committed). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GE3SDvQn5EoxdGufk84zz2 --- .../runs/doe-riscv64-20261107-224732.csv | 0 .../riscv64/qemu-riscv64-20261107-224732.log | 3 ++ scripts/qemu_session.py | 32 ++++++++++++++++--- 3 files changed, 31 insertions(+), 4 deletions(-) create mode 100644 experiments/bare_metal/runs/doe-riscv64-20261107-224732.csv create mode 100644 logs/20261107-224732/riscv64/qemu-riscv64-20261107-224732.log diff --git a/experiments/bare_metal/runs/doe-riscv64-20261107-224732.csv b/experiments/bare_metal/runs/doe-riscv64-20261107-224732.csv new file mode 100644 index 00000000..e69de29b diff --git a/logs/20261107-224732/riscv64/qemu-riscv64-20261107-224732.log b/logs/20261107-224732/riscv64/qemu-riscv64-20261107-224732.log new file mode 100644 index 00000000..659595e9 --- /dev/null +++ b/logs/20261107-224732/riscv64/qemu-riscv64-20261107-224732.log @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:08448453d06f8ce28ede962fda83eaff8a0fe3a24535d856f62ce2fcd09b7f4a +size 7993 diff --git a/scripts/qemu_session.py b/scripts/qemu_session.py index ec667e96..31867d9d 100755 --- a/scripts/qemu_session.py +++ b/scripts/qemu_session.py @@ -213,9 +213,33 @@ def wait_for(pat, secs, after=0): return False -def check(name, cond): +def excerpt(near=None, after=20, tail=40): + """The serial log where a failed check looked (#136): the lines after the first match of + `near`, else the log's last lines -- control characters shown as escapes, so a terminal's + sequences cannot garble the job's output.""" + lines = text().splitlines() + start = None + if near is not None: + start = next((i for i, l in enumerate(lines) if re.search(near, l)), None) + if start is None: + start, what = max(0, len(lines) - tail), f"the log's last {tail} lines" + stop = len(lines) + else: + stop, what = min(len(lines), start + 1 + after), f"after /{near}/, line {start + 1}" + print(f" --- serial log, {what} ({os.path.relpath(log, ROOT)}):", flush=True) + for i in range(start, stop): + shown = "".join(c if c.isprintable() else repr(c)[1:-1] for c in lines[i]) + print(f" {i + 1:6} | {shown}", flush=True) + print(" ---", flush=True) + + +def check(name, cond, near=None): + """PASS or FAIL; a FAIL also prints the serial log where the check looked: after the first + line matching `near` (a regex), when it is given, else the log's end.""" global ok print(f"{'PASS' if cond else 'FAIL'}: {name}", flush=True) + if not cond: + excerpt(near) ok = ok and cond @@ -328,7 +352,7 @@ if m == "headless": elif m == "zuse": check("Boot disk: not a StarshipOS stick (an ISO or ESP boot)", - re.search(r"Boot disk: not a StarshipOS stick", text()) is not None) + re.search(r"Boot disk: not a StarshipOS stick", text()) is not None, near=r"Boot successful!") check("Zuse: identity confirmed", wait_for(r"Zuse: identity confirmed from attached thumbdrive", 120)) check("[zuse@Hera] ok>", wait_for(PROMPT, 60)) n = count(PROMPT) @@ -343,7 +367,7 @@ elif m in ("stick_boot", "two_sticks"): if m == "stick_boot": # the PC stick: Artemis is the rest of 2 GiB (well over 1 million user blocks), its blank # identity 500 MiB (about 380000) - check("Boot disk: a StarshipOS stick", re.search(r"Boot disk: a StarshipOS stick", text()) is not None) + check("Boot disk: a StarshipOS stick", re.search(r"Boot disk: a StarshipOS stick", text()) is not None, near=r"Boot successful!") check("System: StarForth v4", re.search(r"System: StarForth v4", text()) is not None) check("Artemis: the stick's partition attached", wait_for(r"Artemis: the stick's partition attached", 120)) @@ -370,7 +394,7 @@ elif m in ("stick_boot", "two_sticks"): elif m in ("stick_artemis", "stick_zuse"): if m == "stick_zuse": - check("Boot disk: a StarshipOS stick", re.search(r"Boot disk: a StarshipOS stick", text()) is not None) + check("Boot disk: a StarshipOS stick", re.search(r"Boot disk: a StarshipOS stick", text()) is not None, near=r"Boot successful!") check("Artemis: the stick's partition attached", wait_for(r"Artemis: the stick's partition attached", 120)) check("Zuse: identity confirmed", wait_for(r"Zuse: identity confirmed from attached thumbdrive", 120)) check("[zuse@Hera] ok>", wait_for(PROMPT, 60)) From 171b3947dd9110bd1363ff45bc8c22fbb59c3179 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 7 Nov 2026 23:18:40 +0000 Subject: [PATCH 2/3] demo: two guests of 384 MiB, and a busy message for the rest (#116) Captain Bob, 2026-11-07: 'Limit guests and have a Busy, try back in a few minutes.' start-session.sh takes one of DEMO_MAX_GUESTS slots (default 2), a lock it holds for the session's life (flock -n: two visitors cannot both take the last one; QEMU and the bridge are started without its descriptor); with every slot taken, the visitor's terminal says every machine is busy and to try back in a few minutes, and the session ends without a machine. The service: DEMO_MAX_GUESTS=2, RuntimeDirectory for the locks, ttyd's --max-clients 6 so a visitor over the limit reaches the message. The guest: 384 MiB (v4 boots from 256 MiB, refuses at 128; measured, #116). Tested here: every slot held -> the busy message; slot 1 held -> slot 2 taken. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GE3SDvQn5EoxdGufk84zz2 --- demo/qemu-args.sh | 8 +++++--- demo/starshipos-demo.service | 9 +++++++-- demo/start-session.sh | 27 ++++++++++++++++++++++++--- 3 files changed, 36 insertions(+), 8 deletions(-) diff --git a/demo/qemu-args.sh b/demo/qemu-args.sh index cedd1c0b..19474994 100644 --- a/demo/qemu-args.sh +++ b/demo/qemu-args.sh @@ -3,8 +3,10 @@ # A session resumes a machine saved by make-snapshot.sh, and QEMU can resume a saved machine only # into the same machine: the same board, CPU, memory, firmware and devices. Both scripts build the # command line here, so they cannot drift apart. It is the machine `make -f kernel/Makefile -# ARCH=amd64 qemu` boots (q35, TCG, 1 GiB, OVMF, the ISO as a CD), less what a visitor must not -# have: no network device, no Artemis disk, no display, and QEMU's seccomp sandbox on. +# ARCH=amd64 qemu` boots (q35, TCG, OVMF, the ISO as a CD) -- in 384 MiB, not its 1 GiB: v4 boots +# to Zuse's prompt from 256 MiB, refuses at 128 ("no memory for the block chain"), measured +# 2026-11-07 (#116) -- less what a visitor must not have: no network device, no Artemis disk, no +# display, and QEMU's seccomp sandbox on. # # demo_qemu_args -> fills the array QEMU_ARGS # @@ -18,7 +20,7 @@ demo_qemu_args() { QEMU_ARGS=( -machine q35,accel=tcg -cpu qemu64,+rdrand - -m 1024 + -m 384 # v4 boots from 256 MiB; 384 leaves room (#116) -smp 1 -drive "if=pflash,format=raw,readonly=on,file=$OVMF_CODE" -drive "if=pflash,format=raw,file=$vars" diff --git a/demo/starshipos-demo.service b/demo/starshipos-demo.service index 2285a281..08ca318a 100644 --- a/demo/starshipos-demo.service +++ b/demo/starshipos-demo.service @@ -13,8 +13,13 @@ User=starshipos-demo Group=starshipos-demo Environment=DEMO_STATE=/var/lib/starshipos-demo Environment=DEMO_SESSION_LIMIT=1800 -# -m: sessions at once -- each a QEMU of about 1 GiB and most of a core (issue #116 measures it) -ExecStart=/usr/bin/ttyd --port 7681 --interface lo --writable --max-clients 3 \ +Environment=DEMO_MAX_GUESTS=2 +# the guests' slot locks (start-session.sh): /run/starshipos-demo, made for this user, gone at stop +RuntimeDirectory=starshipos-demo +# --max-clients: the guests and a few more, so a visitor over the limit reaches the busy message +# instead of a refused connection; still a cap on open connections +# each guest a QEMU of 384 MiB and most of a core (demo/qemu-args.sh; #116) +ExecStart=/usr/bin/ttyd --port 7681 --interface lo --writable --max-clients 6 \ --client-option titleFixed=StarshipOS --client-option disableLeaveAlert=true \ /opt/starshipos-demo/demo/start-session.sh Restart=on-failure diff --git a/demo/start-session.sh b/demo/start-session.sh index 09d4286f..b42340d8 100755 --- a/demo/start-session.sh +++ b/demo/start-session.sh @@ -12,8 +12,29 @@ here=$(cd "$(dirname "$0")" && pwd) . "$here/qemu-args.sh" dir=${1:-${DEMO_STATE:-/var/lib/starshipos-demo}} limit=${DEMO_SESSION_LIMIT:-1800} # seconds a session may last +guests=${DEMO_MAX_GUESTS:-2} # machines at once: 384 MiB each (qemu-args.sh, #116) +slots=${DEMO_SLOTS:-/run/starshipos-demo} # a lock file per machine [ -f "$dir/state" ] || { echo "The demo is not set up yet: no saved machine in $dir." >&2; exit 1; } + +# One of $guests slots, or a busy message (Captain Bob, 2026-11-07: "Limit guests and have a Busy, +# try back in a few minutes"). A slot is a lock this script holds for the session's life (flock -n: +# two visitors cannot both take the last one); it goes when the script does, however it ends. The +# machine's processes are started without the lock's descriptor ({slot}>&-), so it is this +# script's alone. +mkdir -p "$slots" +slot= +for i in $(seq 1 "$guests"); do + exec {fd}>"$slots/slot-$i.lock" + if flock -n "$fd"; then slot=$fd; break; fi + exec {fd}>&- +done +if [ -z "$slot" ]; then + printf '\r\nEvery StarshipOS machine is busy right now (%d at once on this small server).\r\n' "$guests" + printf 'Try back in a few minutes.\r\n' + sleep 20 + exit 0 +fi tmp=$(mktemp -d "${TMPDIR:-/tmp}/starshipos-session.XXXXXX") trap 'rm -rf "$tmp"' EXIT cp "$dir/vars.fd" "$tmp/vars.fd" @@ -28,13 +49,13 @@ printf 'Try: 3 4 + . : SQUARE DUP * ; 7 SQUARE .\r\n\r\n' qemu-system-x86_64 "${QEMU_ARGS[@]}" \ -chardev "socket,id=con,path=$tmp/con.sock,server=on,wait=off" -serial chardev:con \ -qmp "unix:$tmp/qmp.sock,server=on,wait=off" -monitor none \ - -incoming "file:$dir/state" &- & qemu=$! -( sleep "$limit"; kill "$qemu" 2>/dev/null ) & +( sleep "$limit"; kill "$qemu" 2>/dev/null ) {slot}>&- & timer=$! # The bridge is a tracked child too: bash runs a trap only once its foreground command returns, # so a hang-up would otherwise wait on the bridge. `wait` returns to the trap at once. -python3 -I "$here/console.py" "$tmp/qmp.sock" "$tmp/con.sock" 0<&0 & +python3 -I "$here/console.py" "$tmp/qmp.sock" "$tmp/con.sock" 0<&0 {slot}>&- & bridge=$! trap 'kill "$bridge" "$qemu" "$timer" 2>/dev/null; rm -rf "$tmp"' EXIT trap 'exit 0' HUP TERM INT From 1e3f211daa4f827f2dfd62da64dbeaa9d2443dfb Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 7 Nov 2026 23:19:28 +0000 Subject: [PATCH 3/3] FABRIC-3 XLVIII: the review, the protocol, the SDK, the demo's limits Today's rulings and work since XLVII.6, kept at the pace of the work (Captain Bob, 2026-11-07: 'the book of fabric needs to keep pace'): the alpha.4 list (#147) and what is in it, the developer protocol (#135), the Book of Fabric (#138), the SDK milestone with the capsule repository (#149) and the latency DoE (#150), #136 and the demo's guests and busy message, with what was measured. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GE3SDvQn5EoxdGufk84zz2 --- docs/fabric/FABRIC-3.md | 52 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/docs/fabric/FABRIC-3.md b/docs/fabric/FABRIC-3.md index e85a916d..debd4581 100644 --- a/docs/fabric/FABRIC-3.md +++ b/docs/fabric/FABRIC-3.md @@ -6191,3 +6191,55 @@ heaps linked to the user's -- the session passes in 47 s at ~1.8 GiB, through `scripts/nightly.sh proofs` itself (`logs/20261107-195341`). Nothing was published. Ruled (Captain Bob, 2026-11-07): the tag stays; `LITHOS_VERSION` `4.0.0-alpha.3`, the first published alpha; the review list begun meanwhile goes to the milestone v4.0.0-alpha.4. + +## XLVIII. After the first published alpha: the review, the protocol, the SDK, the demo's limits (2026-11-07) + +### 1. Ruled + +- **A ranked list for the next release** (Captain Bob: "a list of problems, deficiencies, smells and + so on placed in issues by rank priority"): milestone **v4.0.0-alpha.4**, its index #147. Must: + Gitea's plain-HTTP logins (#133), rotating what was exposed (#134), the riscv64 flake (#131), the + developer protocol (#135), CI's litter (#129). Should: a failed check prints its serial log + (#136), capsule trust against the root and unsigned capsules (#139, a ruling), the queue's + cancelling of waiting runs (#140, a ruling), CLAUDE.md's drift (#137), the Book of Fabric (#138), + downloads (#126). Nice: the empty DoE CSVs (#141 -- no DoE has run since 2026-09-16: noise, not + breakage), DoE columns always 0 (#142), mkcapsule's warnings (#143), a stale header (#144), + tracked files a build rewrites (#145), the sites' small things (#146). +- **The developer protocol** (#135): "We can simply run nightly as developers to further make sure + nothing sneaks in. Just no Nexus push ... a protocol for programmers. Then the protocol evidence + can be submitted in the PR." -- `scripts/nightly.sh protocol`, a `PROTOCOL.md` per run, checked + by CI against the PR's head. +- **The Book of Fabric** (#138): the FABRIC record as one book, published with every release -- + and this record kept at the pace of the work, so the book is (Captain Bob: "the book of fabric + needs to keep pace"). +- **v4.0.0-alpha.5: the SDK**, for FORTH and capsule developers; a systems SDK later, FPGA when we + get there (milestone 11). In it: + - **A capsule repository in blocks**, like `~/.m2/repository` (#149): on Artemis, indexed by + coordinates and capsule hash, a TTL by default as the rest of the system has, pinning one-way, + verified in and on use, composed in FORTH; filled by hand now, from Nexus once there is + networking. To rule: its block range and size, the default TTL, what is pinned. + - **K under network latency** (#150): "Networking latency will be a super good K test." -- the + repository pulling from Nexus with delay, jitter, loss and bandwidth dialled in (`tc netem`), + K and the DoE's measures per architecture. After networking, #141 and #142. +- **The demo's limits** (#116): "Limit guests and have a Busy, try back in a few minutes." + +### 2. Built + +- **#136:** `scripts/qemu_session.py`'s `check()` prints, on a FAIL, the serial lines after an anchor + the check names (the boot-disk checks: `Boot successful!`), else the log's last lines, control + characters as escapes -- so #131's next occurrence explains itself in the job's output. +- **The demo:** guests of **384 MiB** (`demo/qemu-args.sh`); `start-session.sh` takes one of + `DEMO_MAX_GUESTS` slots (2), a lock held for the session's life, and with every slot taken tells + the visitor every machine is busy and to try back in a few minutes; the service makes the locks' + directory and lets ttyd take 6 connections so the extra visitors reach the message. + +### 3. Verified + +- The demo machine, booted here with only `-m` changed: POST 538/538 and Zuse's prompt at 1024, 512, + 384 and 256 MiB (82-84 s); at 128 MiB POST passes, then "StarForth v4: no memory for the block + chain". The slots: every slot held -- the busy message; slot 1 held -- slot 2 taken. +- `excerpt()` against a real riscv64 serial log, with an anchor and without; a riscv64 acceptance + with the new script, passing as before. +- With both wikis up again (Shenandoah, the .com's heap at 512 MiB, the .org's given 256 more): the + server at 2.6 of 3.8 GiB used, 1.2 GiB available, 2 GiB of swap used; Build 204 (three images, the + v4 tests) passed alongside them.