danos/docs/device-manager.md

9.2 KiB
Raw Blame History

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: spawn/supervise/kill/exit-notification; driver-model.md: the device table as a capability system; 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'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, 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). The three invariants in 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'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 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 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 14 are the lifecycle prerequisites and live in process-lifecycle.md (claim cleanup on death, exit reasons, published exit events, signals + runtime.process). On top of those:

  1. device-manager-protocol: hello, supervised spawn with restart policy; usb-xhci-bus becomes the first conforming driver.
  2. Tree reports: child_added/child_removed; the manager mirrors; xHCI reports the mouse and keyboard QEMU already hangs off it.
  3. App surface: enumerate/subscribe over IPC; device_enumerate retreats to a manager-internal seam.
  4. 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).