9.1 KiB
Plan: Fix the TrapOS boot lifecycle
Context
startup/servers.lua is filed as a CraftOS startup hook but behaves as an init
process. CraftOS runs /startup/* files inside its own shell and expects each to
return; servers.lua instead blocks forever (parallel.waitForAny(shellFn, eventLoopFn))
and then calls os.shutdown() (startup/servers.lua:69). That os.shutdown() is not a
feature — it's a guard against falling through to the host CraftOS shell with no eventloop
and no servers running (a degraded bare prompt).
The installer makes this worse: install-trapos.lua:229-231 "boots TrapOS in place" via
shell.execute('/startup/servers.lua'), three shells deep, where the os.shutdown() kills
the machine instead of letting shell.setDir(previousDir) (line 233) run.
Goal: keep the (correct, unavoidable) parallel(shell, eventLoop) shape, but stop running
the orchestrator as a startup-hook-pretending-to-be-init. Make the boot path single and
honest, decouple the installer, and turn the implicit shutdown into an explicit policy.
Decisions (confirmed with user)
- Session-end default =
shutdown, withrebootandrelaunchas opt-in policy values. - Move
motdintoprograms/and invoke it from the orchestrator (single/startup/file). - Docs: new ADR-0019 for the boot-lifecycle decision; update living docs (README/AGENTS/periphemu); leave accepted ADRs 0002/0005/0010 as historical record.
trapos.shutdown_on_shell_exit: clean break — replaced bytrapos.on_session_end. The old key is undocumented and never persisted (only read in the orchestrator + one test), so no migration shim. (Recorded in ADR-0019.)
Implementation
1. Rename orchestrator → startup/boot.lua + explicit session-end policy
Rename startup/servers.lua → startup/boot.lua. Replace the SHUTDOWN_ON_SHELL_EXIT_SETTING
boolean logic with a named policy:
- New setting
trapos.on_session_end, values"shutdown"(default) |"reboot"|"relaunch". Any unset/invalid value falls back to"shutdown". relaunch: when the shell exits, re-run it (never drop to bare CraftOS); shutdown/reboot only via explicit command. Implement as arepeat shell.run("shell") until action ~= "relaunch"loop insideshellFn, re-reading the setting each iteration (soset ... shutdownthen exit actually powers off).- After
parallel.waitForAny: ifshellExited, dispatchos.reboot()forreboot, elseos.shutdown()(coversshutdown;relaunchnever reaches here). - Drop the file's self-deprecating TODO comment (
servers.lua:6).
2. Move motd into programs/ and call it from boot.lua
- Move
startup/motd.lua→programs/motd.lua(drop its TODO comment, lines 3-4). No code change otherwise — it already reads/trapos/manifest.jsonand returns early if absent. - In
boot.lua, afterinit()(which appends:/programsto the path), callshell.run('/programs/motd.lua')early — before the "Starting servers..." block — to preserve the current banner-before-servers order. Use the absolute path so it doesn't depend on path-setup ordering.
3. Installer: replace "boot in place" with os.reboot()
In install-trapos.lua, replace lines 229-231 (the fs.exists('/startup/servers.lua') +
shell.execute(...) block) with a reboot at the end of the full-install path:
shell.execute('trapos-postinstall');
shell.setDir(previousDir);
print();
print('=> Rebooting...');
os.reboot();
This gives one boot path (fresh install == normal power-on), drops the deep nesting, and the
installer no longer needs to know the orchestrator's filename. (--cpm-only already returns
early at line 220, so it won't reboot.) Mirrors the existing programs/trapos-upgrade.lua:34-37
reboot pattern.
4. Upgrade migration: prune stale startup files (self-heal)
api.upgrade → api.install(force=true) (apis/libccpm.lua:412,432,372) writes the new file
list but never deletes files dropped from a package (only uninstall deletes,
libccpm.lua:463-466). So upgraded machines would keep the old startup/servers.lua +
startup/motd.lua alongside the new files → two orchestrators in /startup/.
Fix in boot.lua: at startup (in init()), prune known-stale paths, guarded by fs.exists:
for _, p in ipairs({ '/startup/servers.lua', '/startup/motd.lua' }) do
if fs.exists(p) then fs.delete(p) end
end
CraftOS runs /startup/* alphabetically; boot.lua sorts before servers.lua, so it deletes
the stale orchestrator before it would ever run. Idempotent; converges in one boot.
(The general "ccpm should prune removed files on force-install" gap is noted as future work in
ADR-0019, not fixed here to keep scope contained.)
5. Package metadata
packages/trapos-boot/ccpm.json:files→["programs/motd.lua", "startup/boot.lua"]; bumpversion0.3.3→0.4.0(file set + behavior changed).packages/index.json:5: bumptrapos-boot0.3.3→0.4.0soccpm updateadvertises it andccpm upgradedetects the new version on existing machines.
6. Tests
tests/startup-servers.lua→ renametests/boot.lua(or keep name; rename preferred for clarity). Updateloadfile('/startup/servers.lua', ...)→/startup/boot.lua. Keep the two existing cases retargeted totrapos.on_session_endsemantics:- default →
shutdown(existing "shuts down by default"). - add
rebootcase → assertscalls.rebooted == true,calls.shutdown == false. - add
relaunchcase → make thesettings.getstub return"relaunch"once then"shutdown"so therepeat...untilloop terminates; assertshellRunsran twice and ended in shutdown. The harness already stubsos.reboot/os.shutdown/parallel.waitForAny(lines 21-39).
- default →
tests/install-trapos.lua: the env stub (lines 75-101) does not stubos, so the newos.reboot()would really reboot during the test. Add anosstub that records the reboot (e.g. push a{ program = '__reboot__' }marker intocalls.executesor setcalls.rebooted). Rewrite the "postinstall before startup servers" test (lines 164-174): drop the/startup/servers.lualookup; assert reboot occurs aftertrapos-postinstall. In the--cpm-onlytest (176-184), assert reboot was not called.tools/trapos-server/test-integration/lua/full-boot.lua:1: update the comment referencestartup/servers.lua→startup/boot.lua(no functional change — it doesn't load the file).
7. Docs
- New
docs/adrs/adr-0019-boot-lifecycle.md: orchestrator renamedboot.luaand framed as init; installer reboots instead of boot-in-place (single boot path);trapos.on_session_endpolicy (shutdown default / reboot / relaunch) replacing theshutdown_on_shell_exitboolean; motd moved toprograms/; boot.lua self-heals stale startup files. Note ccpm-prune-on-upgrade as future work. Add it to the Records list indocs/adrs/README.md(markdown link — lychee validates it). - Update living-doc prose mentions
startup/servers.lua→startup/boot.lua:README.md:73,AGENTS.md:27,36,42,docs/periphemu.md:9,26. - Leave accepted ADRs
0002:36,0005:22,41,86,0010:57,86unchanged (historical; the mentions are backticked prose, not links, sojust check/lychee is unaffected).
Files to modify
startup/servers.lua→ renamestartup/boot.lua(policy + prune + motd call)startup/motd.lua→ moveprograms/motd.luainstall-trapos.lua(reboot instead of boot-in-place)packages/trapos-boot/ccpm.json,packages/index.json(file list + version bumps)tests/startup-servers.lua→ renametests/boot.lua(policy cases)tests/install-trapos.lua(os stub + reboot assertions)tools/trapos-server/test-integration/lua/full-boot.lua(comment)docs/adrs/adr-0019-boot-lifecycle.md(new),docs/adrs/README.mdREADME.md,AGENTS.md,docs/periphemu.md
Verification
- Unit tests: run the suite (
just test/programs/runtest.lua). Confirm the renamedtests/boot.luapasses all three session-end cases andtests/install-trapos.luapasses with the new reboot assertions. Confirm the test runner discovers the renamed file. - Markdown lint:
just check(lycheelint-markdown) — confirms the new ADR-0019 link indocs/adrs/README.mdresolves and no doc links broke. - Boot path (CraftOS-PC harness, per ADR-0005): launch a clean computer, confirm
boot.luaprints the motd banner, starts autostart servers, and gives an interactive shell with the eventloop live (e.g. anet.call-based program responds). Typeexit→ machine powers off (default).set trapos.on_session_end reboot→ exit re-enters TrapOS.set ... relaunch→ exit returns a fresh prompt without powering off. - Upgrade migration: on a computer with the old
startup/servers.lua+startup/motd.luapresent, drop in the newboot.luaand reboot; confirm both stale files are deleted and onlyboot.luaremains in/startup/. - Installer: run
install-trapos.luaend-to-end (or via the test harness) and confirm it finishes with=> Rebooting...and comes back up into a live TrapOS rather than a nested shell.