acceptance prints the serial log of a failed check (#136); the demo limits its guests (#116) #148

Merged
rajames merged 3 commits from StarForth-v4.0.0 into master 2026-11-08 00:07:46 +00:00
7 changed files with 119 additions and 12 deletions
+5 -3
View File
@@ -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 <state-dir> <vars.fd> -> 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"
+7 -2
View File
@@ -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
+24 -3
View File
@@ -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" </dev/null &
-incoming "file:$dir/state" </dev/null {slot}>&- &
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
+52
View File
@@ -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.
+28 -4
View File
@@ -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))