cc-libs/.plans/archived/boot-lifecycle-refactor.md

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, with reboot and relaunch as opt-in policy values.
  • Move motd into programs/ 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 by trapos.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.luastartup/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 a repeat shell.run("shell") until action ~= "relaunch" loop inside shellFn, re-reading the setting each iteration (so set ... shutdown then exit actually powers off).
  • After parallel.waitForAny: if shellExited, dispatch os.reboot() for reboot, else os.shutdown() (covers shutdown; relaunch never 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.luaprograms/motd.lua (drop its TODO comment, lines 3-4). No code change otherwise — it already reads /trapos/manifest.json and returns early if absent.
  • In boot.lua, after init() (which appends :/programs to the path), call shell.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.upgradeapi.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"]; bump version 0.3.30.4.0 (file set + behavior changed).
  • packages/index.json:5: bump trapos-boot 0.3.30.4.0 so ccpm update advertises it and ccpm upgrade detects the new version on existing machines.

6. Tests

  • tests/startup-servers.lua → rename tests/boot.lua (or keep name; rename preferred for clarity). Update loadfile('/startup/servers.lua', ...)/startup/boot.lua. Keep the two existing cases retargeted to trapos.on_session_end semantics:
    • default → shutdown (existing "shuts down by default").
    • add reboot case → asserts calls.rebooted == true, calls.shutdown == false.
    • add relaunch case → make the settings.get stub return "relaunch" once then "shutdown" so the repeat...until loop terminates; assert shellRuns ran twice and ended in shutdown. The harness already stubs os.reboot/os.shutdown/parallel.waitForAny (lines 21-39).
  • tests/install-trapos.lua: the env stub (lines 75-101) does not stub os, so the new os.reboot() would really reboot during the test. Add an os stub that records the reboot (e.g. push a { program = '__reboot__' } marker into calls.executes or set calls.rebooted). Rewrite the "postinstall before startup servers" test (lines 164-174): drop the /startup/servers.lua lookup; assert reboot occurs after trapos-postinstall. In the --cpm-only test (176-184), assert reboot was not called.
  • tools/trapos-server/test-integration/lua/full-boot.lua:1: update the comment reference startup/servers.luastartup/boot.lua (no functional change — it doesn't load the file).

7. Docs

  • New docs/adrs/adr-0019-boot-lifecycle.md: orchestrator renamed boot.lua and framed as init; installer reboots instead of boot-in-place (single boot path); trapos.on_session_end policy (shutdown default / reboot / relaunch) replacing the shutdown_on_shell_exit boolean; motd moved to programs/; boot.lua self-heals stale startup files. Note ccpm-prune-on-upgrade as future work. Add it to the Records list in docs/adrs/README.md (markdown link — lychee validates it).
  • Update living-doc prose mentions startup/servers.luastartup/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,86 unchanged (historical; the mentions are backticked prose, not links, so just check/lychee is unaffected).

Files to modify

  • startup/servers.luarename startup/boot.lua (policy + prune + motd call)
  • startup/motd.luamove programs/motd.lua
  • install-trapos.lua (reboot instead of boot-in-place)
  • packages/trapos-boot/ccpm.json, packages/index.json (file list + version bumps)
  • tests/startup-servers.luarename tests/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.md
  • README.md, AGENTS.md, docs/periphemu.md

Verification

  • Unit tests: run the suite (just test / programs/runtest.lua). Confirm the renamed tests/boot.lua passes all three session-end cases and tests/install-trapos.lua passes with the new reboot assertions. Confirm the test runner discovers the renamed file.
  • Markdown lint: just check (lychee lint-markdown) — confirms the new ADR-0019 link in docs/adrs/README.md resolves and no doc links broke.
  • Boot path (CraftOS-PC harness, per ADR-0005): launch a clean computer, confirm boot.lua prints the motd banner, starts autostart servers, and gives an interactive shell with the eventloop live (e.g. a net.call-based program responds). Type exit → 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.lua present, drop in the new boot.lua and reboot; confirm both stale files are deleted and only boot.lua remains in /startup/.
  • Installer: run install-trapos.lua end-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.