155 lines
9.2 KiB
Markdown
155 lines
9.2 KiB
Markdown
# The device manager
|
||
|
||
**Status: the protocol and supervision are built** (M18.1, 2026-07-13): `hello`
|
||
with its deadline, supervised spawn, restart with backoff, and the crash-loop
|
||
cap are in — usb-xhci-bus is the first conforming driver, and the
|
||
`driver-restart` scenario proves fault → backoff → re-claim → cap end to end.
|
||
Tree reports are built too (M18.2, 2026-07-13): the xHCI driver scans its
|
||
root-hub ports and reports each connected device (`child_added`); the manager
|
||
mirrors them and prunes a dead reporter's children, and the `usb-report`
|
||
scenario proves report → prune → respawn → re-report. The application surface is built (M18.3, 2026-07-13):
|
||
`enumerate` and `subscribe` over IPC, with `device-list` as the first client —
|
||
the manager is now the one answer to "what devices exist" for applications.
|
||
The primitives underneath are real ([process-management.md](process-management.md):
|
||
spawn/supervise/kill/exit-notification; [driver-model.md](driver-model.md): the device
|
||
table as a capability system; [drivers.md](drivers.md): claim/map/IRQ), and the first
|
||
per-device driver spawn works (the device manager matches the xHCI controller by PCI
|
||
class and spawns `usb-xhci-bus` with the device id as argv[1]). This document designs
|
||
the rest: the device manager as **the tree, the matcher, and the supervisor** — the
|
||
policy process that turns [resilience.md](resilience.md)'s restart goal into practice
|
||
for drivers.
|
||
|
||
How processes stop, reload, and report their deaths is deliberately **not** in this
|
||
document: that is the universal lifecycle every danos process speaks —
|
||
[process-lifecycle.md](process-lifecycle.md), signals over IPC and the stable
|
||
`runtime.process` interface. The device manager is that design's first serious
|
||
customer, not its owner. Its own protocol contains nothing lifecycle-shaped; a
|
||
driver is stopped, health-checked, and buried exactly like any other process.
|
||
|
||
## The tree: structure in the manager, authority in the kernel
|
||
|
||
The device tree is two things fused: *information* (what exists, how it nests) and
|
||
*authority* (a descriptor is a licence to map physical memory). They separate:
|
||
|
||
- The **kernel keeps the capability system** — device, I/O-port, and interrupt
|
||
claims, resource containment on `device_register`, the
|
||
`mmio_map`/`irq_bind`/`msi_bind` gates — and **cleans all of it up when a process
|
||
dies** (settled; it is increment 1 of
|
||
[process-lifecycle.md](process-lifecycle.md)). The three invariants in
|
||
[driver-model.md](driver-model.md) stay exactly where they are. A device manager
|
||
that could mint MMIO mappings by its own say-so would be a second kernel, and a
|
||
buggy one would un-earn everything the microkernel bought.
|
||
- The **device manager owns the tree as data** — identity, topology, naming, driver
|
||
matching, hotplug events, and being the one process everything else asks about
|
||
devices. Firmware discovery seeds it (today via the kernel's snapshot); **bus
|
||
drivers grow it** by reporting what they see; applications query and watch it.
|
||
`device_enumerate` fades to a manager-internal (then deleted) seam.
|
||
|
||
Long-term, discovery itself leaves the kernel — but not *into* the manager. PCI
|
||
enumeration is a **pci-bus driver**: the manager spawns it against the host bridge
|
||
(already a device with the ECAM window as a resource), it scans, it reports functions
|
||
like any bus reports children. ACPI becomes an **acpi service** that interprets the
|
||
tables and reports the namespace. The manager only orchestrates and merges. Moving
|
||
AML interpretation out of ring 0 is its own project on its own track; nothing here
|
||
depends on when it lands.
|
||
|
||
## The protocol
|
||
|
||
A `device-manager-protocol` module (the vfs-protocol pattern): extern-struct
|
||
messages, a version in the handshake, reserved fields everywhere. The manager is a
|
||
well-known endpoint (`ipc.register(.device_manager)`); the badge tells it who is
|
||
talking; the same endpoint receives its children's exit notifications — one loop,
|
||
one world.
|
||
|
||
| Direction | Message | Purpose |
|
||
|---|---|---|
|
||
| driver → manager | `hello { version, role, device_id }` | confirms the argv assignment, starts the deadline clock |
|
||
| bus → manager | `child_added { parent, identity, resources }` | one node the bus discovered |
|
||
| bus → manager | `child_removed { id }` | unplug, or the bus lost it |
|
||
| app → manager | `enumerate` | snapshot of the tree (read-only) |
|
||
| app → manager | `subscribe` | receive published add/remove events |
|
||
|
||
`hello` is the one deadline the manager enforces itself: spawned and silent past the
|
||
deadline means wrong binary, wrong protocol version, or wedged before main — apply
|
||
the stop sequence and the restart policy. Everything else lifecycle-shaped
|
||
(terminate, the common `ping` liveness call, exit reasons) arrives through
|
||
[process-lifecycle.md](process-lifecycle.md)'s vocabulary, not this protocol.
|
||
|
||
Assignment stays argv (`usb-xhci-bus <device id>`) for now — simple, and it works.
|
||
The step after `hello` exists is delegation: the manager claims (or is granted) the
|
||
devices and passes the claim to the driver over IPC (the M13 capability-transfer
|
||
mechanism), replacing first-come-first-served `device_claim` with policy. Identity in
|
||
`child_added` is per-bus: PCI children carry the class triple (`pci_class`, as the
|
||
xHCI match already uses); USB children carry the (class, subclass, protocol) triple
|
||
from usb-ids.zig — each bus's native language, decoded by the shared ids modules.
|
||
|
||
## Supervision and restart
|
||
|
||
Every driver is spawned with the manager's exit endpoint (`spawnSupervised` — built).
|
||
On a death notification:
|
||
|
||
1. **Read the reason** ([process-lifecycle.md](process-lifecycle.md) increment 2).
|
||
Clean exit → it meant to; don't restart. Fault or missed `hello` deadline →
|
||
restart with **backoff**, and a crash-loop cap (three fast deaths → mark failed,
|
||
stop respawning, log loudly; a later `reload` to the manager can retry).
|
||
2. **Prune the subtree** the dead bus driver reported. Its children describe
|
||
protocol state (xHCI slot ids, transfer rings) that died with the process;
|
||
keeping the nodes would be keeping a lie. Watchers receive `child_removed` — the
|
||
input service losing, then regaining, a keyboard is the *honest* description of
|
||
what happened. The restarted instance rediscovers and re-reports.
|
||
3. **The claim is already free** because the kernel released it at death — the
|
||
restarted instance claims the same controller and comes up.
|
||
|
||
Who supervises the supervisor: **init** (PID 1), which already supervises the
|
||
services it starts. If the manager dies, drivers keep running (they hold their
|
||
claims; the kernel doesn't care who their supervisor was — though their exit
|
||
notifications now dangle harmlessly). The restarted manager re-learns the world:
|
||
kernel snapshot, then a re-`hello` round — drivers answer a broadcast or are stopped
|
||
and respawned. Full state handoff is deliberately not attempted.
|
||
|
||
## Thin drivers, class protocols
|
||
|
||
The [driver-model.md](driver-model.md) three-shape split, restated as processes:
|
||
|
||
- A **bus driver** (usb-xhci-bus) owns its controller — claim, MMIO, IRQ/MSI, DMA
|
||
rings — and offers a *transfer* protocol ("submit a control transfer to device N",
|
||
built from the usb-abi request constructors) plus tree reports to the manager.
|
||
- A **class driver** (usb-hid, usb-storage) owns nothing: it is matched to a reported
|
||
child by its identity triple, speaks the bus's transfer protocol downward and its
|
||
service's protocol upward — HID reports to the input service, blocks to the block
|
||
service. It works unchanged over any controller.
|
||
- **Services** (input, display, block) aggregate class drivers and face applications.
|
||
|
||
Each arrow is a protocol module. The manager routes none of the data plane — it
|
||
introduces the parties (matching), supervises them (lifecycle), and gets out of the
|
||
way.
|
||
|
||
## Increments
|
||
|
||
Increments 1–4 are the lifecycle prerequisites and live in
|
||
[process-lifecycle.md](process-lifecycle.md) (claim cleanup on death, exit reasons,
|
||
published exit events, signals + `runtime.process`). On top of those:
|
||
|
||
5. **device-manager-protocol**: `hello`, supervised spawn with restart policy;
|
||
usb-xhci-bus becomes the first conforming driver.
|
||
6. **Tree reports**: `child_added`/`child_removed`; the manager mirrors; xHCI reports
|
||
the mouse and keyboard QEMU already hangs off it.
|
||
7. **App surface**: `enumerate`/`subscribe` over IPC; `device_enumerate` retreats
|
||
to a manager-internal seam.
|
||
8. **Discovery migration**: pci-bus driver first, acpi service second, kernel scan
|
||
retired last. (AML-in-user-space is its own track.)
|
||
|
||
## Settled questions (2026-07-12)
|
||
|
||
- **Stateful buses**: pruning the subtree on bus-driver death is right for USB. A
|
||
future storage bus with in-flight writes wants drain-before-terminate — which is
|
||
exactly the `deadline_ms` parameter `stop()` already has; a per-driver deadline
|
||
is one value in the manager's policy table when such a bus arrives. No design
|
||
change.
|
||
- **Manager death**: drivers survive the manager; the restarted manager re-learns
|
||
the world (above). Checkpointing driver state with the manager is deferred until
|
||
something demonstrates the need.
|
||
- **Matching stays code until the third bus.** `driverFor`/`pciDriverFor` are
|
||
honest at two bus types; the third triggers the manifest (a driver declares what
|
||
it binds: a PCI class triple, a USB class triple, an ACPI `_HID`).
|