12 KiB
M17–M18 execution plan: process lifecycle + device manager
Archived — completed 2026-07-13 (every item checked; suite ended 54/54). Kept as the record of how M17–M18 landed; the successor is m19-m20-plan.md.
The operational plan for building process-lifecycle.md (M17) and device-manager.md increments 5–7 (M18). Design is settled in those documents; this file is the build order — one phase at a time, each phase green before the next starts. Delete or archive this file when M18 lands.
Definition of green, every phase: zig build clean, zig build test clean,
python3 test/qemu_test.py passes (existing scenarios plus the phase's new one),
and the relevant design doc's "known gaps" / status lines updated. Commit per
green phase (no co-author trailers).
Workflow (settled 2026-07-12): work happens in a dedicated git worktree, on
feature branches cut from main — feat/process-lifecycle (M17.1–17.4),
feat/device-manager (M18.1), feat/usb-xhci-bus (M18.2–18.3). When a branch's
phases are all green it is auto-merged into main; branches are kept after
merge, not deleted. Merges and branches are pushed to origin. Phase 0 (once):
commit the design docs, merge the outstanding feat/usb work into main, and
run the existing QEMU suite green before any new work starts.
Numbering note: continues the milestone sequence (driver track ended at M16).
Status
The loop marks a phase [x] in the same commit that lands it. A phase is marked
only when its definition of green holds.
- Phase 0 — baseline: docs committed, feat/usb merged to main, pushed;
usb-xhci-libary.zigrenamed tousb-xhci-library.zig; existing QEMU suite green from the worktree (48/48, 2026-07-12). - M17.1 — kernel releases claims/MSI on death (claims:
releaseAllOwnedByin the reap; MSI was already swept byirq.releaseOwner;claim-releasetest; suite 49/49) - M17.2 — exit reasons (
ExitReasonrecorded at exit/fault/kill before the notification;process_exit_reasonsupervisor-gated;runtime.process.exitReason; kernel + ring-3 assertions; suite 49/49) - M17.3 — published exit events + VFS subscriber (
process_subscribe, bounded ref-counted table, publish on every death;runtime.process.subscribeExits; VFS handles carry owners and are swept on the owner's death;vfs-client-deathtest; suite 50/50) - M17.4 — signals, timer notifications,
runtime.process, the service harness (signal_bind/process_signal + coalescing pending mask; timer_bind on the tick; bindSignals/signalsFrom/sendSignal/stop + timerOnce; runtime.service.run with the zero-length ping; VFS converted;signalsscenario; suite 51/51) - merge
feat/process-lifecycle→ main, push (merged 2026-07-13) - M18.1 — device-manager protocol: hello + restart policy
(device-manager-protocol module; the manager as a harness service:
supervised spawns, hello deadline via timer sweep, restart with
300/600/1200ms backoff, exit reasons deciding restart-vs-stopped,
crash-loop cap; usb-xhci-bus first conforming driver; crash-test fixture
re-proving claim release each respawn;
driver-restartscenario; maximum_tasks 16→32 — the sweep was overflowing the pool; suite 52/52) - merge
feat/device-manager→ main, push (merged 2026-07-13) - M18.2 — xHCI port scan + tree reports (child_added/child_removed in
the protocol; the manager's child mirror with death-pruning; xHCI maps the
register BAR — resource 0 is ECAM — reads CAPLENGTH/HCSPARAMS1, scans
PORTSC, reports connected ports with speed-class identity;
usb-reportscenario proves report → prune → respawn → re-report; suite 53/53) - M18.3 — app surface: enumerate/subscribe over IPC (subscriber
endpoint rides as the call's capability; events are the same structs the
buses send); device-list first client; protocol capped at the kernel's
IPC MESSAGE_MAXIMUM (256); the startUserTask debug print removed — it
sheared concurrent serial lines and was the scenario-flake root cause;
device-listscenario; suite 54/54) - merge
feat/usb-xhci-bus→ main, push (merged 2026-07-13) — plan complete
M17.1 — the kernel releases a dead process's claims
The cleanup half of iron rule 1; the prerequisite for every restart story.
system/kernel/devices-broker.zig:releaseAllOwnedBy(owner: u32)— clear everyclaimed[]slot holding this task id.system/kernel/process.zig: call it from the reap path, alongside the existing IRQ-binding release (the ordering comment there says why IRQs go first — claims slot in after them, before the exit notification).- MSI vectors: find where
msi_bindrecords per-device vectors (interrupts module) and release those by owner in the same pass. - Docs: remove the claims bullet from process-management.md "Known gaps".
Test: new QEMU scenario claim-release — a test child claims an unclaimed
device, is killed, is respawned, and claims the same device again successfully;
assert both claims in the serial log. Kernel-side unit coverage in
system/kernel/tests.zig for releaseAllOwnedBy (claim two devices as two owners,
release one owner, verify exactly its claims freed).
M17.2 — exit reasons
system/abi.zig:ExitReason(exited, aborted, segmentation_fault, illegal_instruction, arithmetic_fault, killed).- Kernel: record the reason at every death site — clean exit path, each fault
class in
onException, the kill path. Bounded recent-exits table (ids are never reused, so a small ring keyed by id is enough). - New system call
process_exit_reason(id)— supervisor-gated, like kill; returns the recorded reason or-ESRCHonce evicted. library/runtime/process.zig:ExitReason+exitReason(id: u32).- Docs: remove the no-exit-status bullet from process-management.md.
Test: extend the supervision scenario — three children: one exits cleanly,
one faults (the fault-recovery pattern), one is killed; the supervisor asserts all
three reasons.
M17.3 — published exit events
- Kernel: bounded subscriber table (endpoints); new system call
process_subscribe(endpoint)(ungated, likeprocess_enumerate); every death postsnotify_exit_bit | idto each subscriber — the same post the supervisor path already uses. library/runtime/process.zig:subscribeExits(endpoint).- VFS becomes the first subscriber: on an exit event, release every handle keyed by that task id (badges already are task ids). Log the release.
- Docs: note the convention in ipc.md (exit events reuse the exit-notification badge encoding).
Test: new QEMU scenario vfs-client-death — a client opens a file and is
killed without closing; assert the VFS logs the handle release and its open-handle
count returns to baseline.
M17.4 — signals and the service harness
- Kernel: per-task pending mask + bound endpoint; system calls
signal_bind(endpoint)andprocess_signal(id, signal)(supervisor-or-self gated); delivery postsnotify_signal_bit | pending mask, coalescing; pending signals with no bound endpoint pend silently. library/runtime/process.zig:Signal,SignalSet,bindSignals,signalsFrom,sendSignal,stop(id, deadline_ms)(terminate → wait for exit notification → kill). Implementterminate,reload,user_1,user_2;interrupt/quitare enum members with no sender yet;alarmstays unbuilt.- Kernel: one-shot timer notifications —
timer_bind(endpoint, ms)posts a notification badge when the deadline lands (IRQ-as-IPC again, on the timer wheelsleepalready uses). This is the missing timed-wait primitive:replyWaitblocks forever andsleepblocks the whole process, butstop()'s escalation, the device manager'shellodeadline (M18.1), and restart backoff all need a deadline while staying responsive. It is also the mechanismalarmgets for free later. - New
library/runtime/service.zig: the harness —run(callbacks)owning the replyWait loop, folding protocol messages, signals, and child-exit notifications intoinit/on_message/on_reload/on_terminate; answers the commonpingautomatically. Define the reservedpingrequest encoding here and document it in ipc.md (one obvious encoding; smallest that cannot collide with existing protocols). - Convert one existing service (input-source or hpet) to the harness as proof it subtracts code rather than adding it.
Test: extend supervision — a harness-built child: sendSignal(reload)
observed in its log, ping answered, stop() produces a clean exit with reason
exited; a second child that ignores signals (no bind) is killed by stop()'s
deadline with reason killed.
M18.1 — device-manager protocol: hello + restart policy
- New
system/services/device-manager/device-manager-protocol.zigmodule (vfs-protocol pattern):hello { version, role, device_id }; version constant; reserved fields. - Device manager: register the
.device_managerendpoint; spawn drivers with its exit endpoint; enforce the hello deadline; restart policy — backoff, crash-loop cap (three fast deaths → mark failed, log, stop), reasons from M17.2 deciding restart vs not. - usb-xhci-bus: adopt the harness + send hello. hpet/ps2-bus follow only if the conversion is mechanical; otherwise they keep working unconverted (the manager only enforces hello on drivers spawned with an assignment).
- build.zig: test-loop entry for the protocol module if it grows pure logic.
Test: new QEMU scenario driver-restart — the xHCI driver takes a test-only
argv flag to fault after hello on its first run; assert: fault, exit reason
recorded, manager respawns with backoff, second run claims the controller
(M17.1) and hellos clean. Assert the crash-loop cap by a driver that always
faults (a tiny test driver, not xhci).
M18.2 — bus tree reports
- Protocol:
child_added { parent, identity, resources }/child_removed { id }. - usb-xhci-bus: bring-up to port scan only — map the MMIO window (claimed in
M16-era work), controller reset/start per xHCI spec, walk the port registers,
report one
child_addedper connected port with speed + port number as identity. No transfer rings, no descriptors — reading device/interface descriptors (and therefore USB class triples for matching) is the follow-on USB track, not this plan. - Device manager: mirror reports into its tree; prune the subtree (emitting
child_removed) when a bus driver dies; assert re-report on restart.
Test: QEMU already attaches usb-kbd + usb-mouse on xhci.0 — assert two
child_added events reach the manager and appear in its tree dump; kill the
driver, assert two child_removed then two fresh child_added after respawn.
M18.3 — the application surface
- Protocol:
enumerate(tree snapshot) +subscribe(published add/remove events, input-service pattern). - A small client (
device-list, thepsanalog) exercising both; the manager becomes the one answer to "what devices exist" for user space.device_enumeratestays for drivers/kernel seeding — its retreat is tied to the discovery migration, out of this plan.
Test: QEMU scenario — device-list shows the tree including USB children;
during a driver restart the subscribing client logs remove + add events.
Explicitly out of scope (own tracks, after M18): discovery migration (pci-bus
driver, acpi service, retiring the kernel scan), USB control transfers +
descriptors + class-driver matching, the musl layer, interrupt/quit senders
(needs a console), job control.