//! /system/services/device-manager — the ring-3 process that turns the device //! tree into a running system: **the matcher and the supervisor** //! (docs/device-manager.md). The kernel enumerates the hardware and enforces the //! claim capability (mechanism); this decides which driver serves which device, //! spawns it, and keeps it alive (policy). Keeping that split in user space is //! the whole point of the microkernel: the manager is an ordinary, restartable //! process with no special privilege. //! //! M18.1 (this increment): the manager is a harness service on the well-known //! `.device_manager` endpoint. Every driver is spawned **supervised** — exit //! notifications land in the same loop as protocol messages. Drivers with an //! assignment must `hello` within a deadline or be stopped; a driver that dies //! is restarted with backoff, and a crash loop (three fast deaths) marks it //! failed instead of respawning forever. Exit reasons (M17.2) drive the //! decision: a clean exit meant to stop; only faults and missed deadlines //! restart. Tree reports (`child_added`) land in M18.2. const std = @import("std"); const device = @import("driver"); const ipc = @import("ipc"); const process = @import("process"); const service = @import("service"); const time = @import("time"); const memory = @import("memory"); const logging = @import("logging"); const device_manager_protocol = @import("device-manager-protocol"); const envelope = @import("envelope"); const registry = @import("device-registry"); /// The generated device-manager dispatch. One manager per system, so the handler /// context is empty and the tables stay in this file's globals. const Serve = device_manager_protocol.Protocol.Provider(void); const Invocation = envelope.Invocation; const Answer = envelope.Answer; const fs = @import("file-system"); // --- the device registry ------------------------------------------------------ // Driver matching is data-driven and authoritative: /system/configuration/devices.csv (parsed by // the device-registry module) names, per bus, which driver binds a reported // device, the most-specific match winning. There is no compiled-in fallback — a // device no row matches goes unbound and is logged. This retired the hand-kept // pciDriverForIdentity / hidDriverFor / usbDriverForIdentity switch tables // (docs/device-manager.md: "matching stays code until the third bus"). /// The CSV bytes, held for the life of the process because the parsed rules' /// string fields (hid, driver) slice into this buffer. var registry_source: [8192]u8 = undefined; var registry_rules: [64]registry.Rule = undefined; var registry_count: usize = 0; /// Read and parse /system/configuration/devices.csv once at boot. The file lives in the initial /// ramdisk, which the kernel serves directly — no filesystem service need be up /// (fat is spawned after the manager), so this is a plain fs.open + read. fn loadRegistry() void { var file = fs.open("/system/configuration/devices.csv", .{}) orelse { _ = logging.write("/system/services/device-manager: /system/configuration/devices.csv missing — nothing will match\n"); return; }; defer file.close(); var used: usize = 0; while (used < registry_source.len) { const n = file.read(registry_source[used..]) orelse break; if (n == 0) break; used += n; } const result = registry.parse(registry_source[0..used], ®istry_rules); registry_count = result.count; if (result.malformed != 0) std.log.info("/system/configuration/devices.csv: {d} malformed line(s) skipped", .{result.malformed}); if (result.truncated) _ = logging.write("/system/services/device-manager: /system/configuration/devices.csv has more rules than the table holds\n"); std.log.info("/system/configuration/devices.csv: {d} rule(s) loaded", .{registry_count}); } /// Build a registry Identity from a bus driver's report: the bus it named, the /// class triple unpacked from `identity` (0xCCSSPP — the same packing for a PCI /// class code and a USB class triple), the widened numeric ids, and the ACPI hid. fn identityFromReport(report: device_manager_protocol.ChildAdded) registry.Identity { const bus: registry.Bus = switch (report.bus) { @intFromEnum(device_manager_protocol.BusKind.pci) => .pci, @intFromEnum(device_manager_protocol.BusKind.usb) => .usb, @intFromEnum(device_manager_protocol.BusKind.acpi) => .acpi, else => .unknown, }; const hid_len = std.mem.indexOfScalar(u8, &report.hid, 0) orelse report.hid.len; return .{ .bus = bus, .base = @truncate(report.identity >> 16), .subclass = @truncate(report.identity >> 8), .prog_if = @truncate(report.identity), .vendor = report.vendor, .device = report.device, .subsystem = report.subsystem, .hid = report.hid[0..hid_len], }; } /// Whether some driver entry already serves registered device `device_id` — /// a re-report after a bus restart must not spawn a second instance. fn driverForDevice(device_id: u64) bool { for (&drivers) |*driver| { if (driver.used and driver.device_id == device_id) return true; } return false; } // --- supervision ------------------------------------------------------------- /// How long a protocol driver has to hello after its spawn. const hello_deadline_ms: u64 = 3000; /// Deaths faster than this count toward the crash loop; slower ones reset it. const fast_death_ns: u64 = 2_000_000_000; /// Consecutive fast deaths before the manager gives up on a driver. const crash_loop_cap: u32 = 3; /// Restart backoff: base << (restarts - 1), so 300 ms, 600 ms, 1200 ms. const backoff_base_ms: u64 = 300; const DriverState = enum { awaiting_hello, // spawned; the deadline is armed (protocol drivers only) running, restarting, // dead; respawn due at restart_due_ns stopped, // exited cleanly — it meant to; not restarted failed, // crash loop, or unspawnable; the manager gave up }; const Driver = struct { used: bool = false, name_buffer: [64]u8 = undefined, // fits a full binary path (abi.maximum_process_name) name_len: usize = 0, // The assigned device id (becomes argv[1]), or device_manager_protocol.no_device. device_id: u64 = device_manager_protocol.no_device, // Whether this driver speaks the protocol (hello expected, deadline // enforced). Legacy drivers (e.g. ps2-bus) are supervised and restarted // but not yet required to hello. speaks_protocol: bool = false, process_id: u32 = 0, state: DriverState = .running, restarts: u32 = 0, spawn_ns: u64 = 0, hello_deadline_ns: u64 = 0, restart_due_ns: u64 = 0, fn name(driver: *const Driver) []const u8 { return driver.name_buffer[0..driver.name_len]; } }; const maximum_drivers = 16; var drivers: [maximum_drivers]Driver = .{Driver{}} ** maximum_drivers; var manager_endpoint: ipc.Handle = 0; var test_restart_mode = false; var test_usb_restart_mode = false; var test_usb_killed = false; var test_pci_restart_mode = false; var test_scanout_restart_mode = false; var test_scanout_killed = false; var test_kill_pid: u32 = 0; var test_kill_due_ns: u64 = 0; /// The application subscribers (M18.3, the input-service pattern): endpoints /// handed over as capabilities, each receiving every child add/remove as a /// buffered message. A subscriber whose endpoint stops accepting (it died) is /// dropped on the failed send. const maximum_subscribers = 8; var subscribers: [maximum_subscribers]?ipc.Handle = .{null} ** maximum_subscribers; /// Push one event to every subscriber: the same struct a bus driver *called* /// with, framed as an event instead — one encoding, both directions, told apart /// by the packet's verb rather than by anything inside it. A subscriber whose /// endpoint stops accepting (it died) is dropped on the failed send. fn publish( comptime event: device_manager_protocol.Event, target: u64, payload: device_manager_protocol.Protocol.PayloadOf(event), ) void { var packet: [envelope.post_maximum]u8 = undefined; const framed = device_manager_protocol.Protocol.encodeEvent(event, target, payload, &packet) orelse return; for (&subscribers) |*slot| { if (slot.*) |handle| { if (!ipc.send(handle, framed)) slot.* = null; // dead subscriber } } } /// The manager's mirror of what bus drivers report (docs/device-manager.md "the /// tree"): the children, keyed by (parent, bus address), each remembering which /// driver instance reported it — that is what death-pruning sweeps by. const Child = struct { used: bool = false, parent: u64 = 0, bus_address: u64 = 0, identity: u64 = 0, // The kernel device id (registered by the reporter), or device_manager_protocol.no_device. device_id: u64 = 0, reporter: u32 = 0, // the reporting driver instance's process id }; const maximum_children = 64; // ACPI adds ~34 device nodes (M20.2), plus PCI + USB var children: [maximum_children]Child = .{Child{}} ** maximum_children; /// Record (or refresh) a reported child. Refreshing matters: a restarted bus /// driver re-reports what it rediscovers, and the same (parent, port) must not /// duplicate. fn addChild(parent: u64, bus_address: u64, identity: u64, device_id: u64, reporter: u32) bool { var free: ?*Child = null; for (&children) |*child| { if (child.used and child.parent == parent and child.bus_address == bus_address) { child.identity = identity; child.device_id = device_id; child.reporter = reporter; return true; } if (!child.used and free == null) free = child; } const slot = free orelse return false; slot.* = .{ .used = true, .parent = parent, .bus_address = bus_address, .identity = identity, .device_id = device_id, .reporter = reporter }; return true; } /// Prune every child a dead driver instance reported: the children describe /// protocol state (slots, rings) that died with the process — keeping the nodes /// would be keeping a lie. The restarted instance rediscovers and re-reports. /// Watchers hear the honest story: removed now, added again on rediscovery. fn pruneChildrenOf(reporter: u32) void { for (&children) |*child| { if (child.used and child.reporter == reporter) { std.log.info("child removed (device {d} port {d})", .{ child.parent, child.bus_address }); child.used = false; publish(.child_removed, 0, .{ .parent = child.parent, .bus_address = child.bus_address }); } } } /// How many children a driver instance has reported (the test-usb-restart /// trigger counts these). fn childCountOf(reporter: u32) u32 { var n: u32 = 0; for (&children) |*child| { if (child.used and child.reporter == reporter) n += 1; } return n; } fn driverByProcess(process_id: u32) ?*Driver { for (&drivers) |*driver| { if (driver.used and driver.process_id == process_id) return driver; } return null; } /// Whether a singleton driver is already in the table (two ACPI nodes can both /// map to ps2-bus; one instance serves both). fn alreadySupervised(name: []const u8) bool { for (&drivers) |*driver| { if (driver.used and std.mem.eql(u8, driver.name(), name)) return true; } return false; } /// Record a driver in the table and spawn its first instance. fn addDriver(name: []const u8, device_id: u64, speaks_protocol: bool) void { for (&drivers) |*driver| { if (driver.used) continue; const n = @min(name.len, driver.name_buffer.len); @memcpy(driver.name_buffer[0..n], name[0..n]); driver.name_len = n; driver.device_id = device_id; driver.speaks_protocol = speaks_protocol; driver.used = true; spawnDriver(driver); return; } std.log.info("driver table full; cannot supervise {s}", .{name}); } /// (Re)spawn a driver instance: supervised on the manager's own endpoint, the /// device id as argv[1] when it has one, the hello deadline armed when it /// speaks the protocol. fn spawnDriver(driver: *Driver) void { var id_text: [20]u8 = undefined; var arguments: [1][]const u8 = undefined; var argument_count: usize = 0; if (driver.device_id != device_manager_protocol.no_device) { arguments[0] = std.fmt.bufPrint(&id_text, "{d}", .{driver.device_id}) catch return; argument_count = 1; } const child = process.spawnSupervised(driver.name(), arguments[0..argument_count], manager_endpoint) orelse { std.log.info("failed to spawn {s}", .{driver.name()}); driver.state = .failed; return; }; driver.process_id = child; driver.spawn_ns = time.clock(); if (driver.speaks_protocol) { driver.state = .awaiting_hello; driver.hello_deadline_ns = driver.spawn_ns + hello_deadline_ms * 1_000_000; _ = time.timerOnce(manager_endpoint, hello_deadline_ms + 100); } else { driver.state = .running; } if (driver.device_id != device_manager_protocol.no_device) { std.log.info("spawned {s} for device {d}", .{ driver.name(), driver.device_id }); } else { std.log.info("spawned {s}", .{driver.name()}); } } /// A driver died. Prune what it reported first — then the exit reason (M17.2) /// is the whole restart decision: a clean exit meant to stop; anything else /// restarts with backoff until the crash-loop cap. fn onDriverExit(driver: *Driver) void { pruneChildrenOf(driver.process_id); const reason = process.exitReason(driver.process_id) orelse .fault; if (reason == .exited) { driver.state = .stopped; std.log.info("{s} exited cleanly; not restarting", .{driver.name()}); return; } const now = time.clock(); const alive_ns = now - driver.spawn_ns; driver.restarts = if (alive_ns < fast_death_ns) driver.restarts + 1 else 1; if (driver.restarts >= crash_loop_cap) { driver.state = .failed; std.log.info("{s} is failing repeatedly (crash loop); giving up", .{driver.name()}); return; } const delay_ms = backoff_base_ms << @intCast(driver.restarts - 1); driver.state = .restarting; driver.restart_due_ns = now + delay_ms * 1_000_000; std.log.info("restarting {s} in {d} ms (died: {s})", .{ driver.name(), delay_ms, @tagName(reason) }); _ = time.timerOnce(manager_endpoint, delay_ms + 50); } /// A timer landed: sweep every deadline. Overdue hellos are killed (the exit /// notification then routes through the normal restart policy); due restarts /// respawn. Timers carry no id on purpose — the table is the state, and one /// sweep serves every armed deadline. fn sweepDeadlines() void { const now = time.clock(); if (test_kill_pid != 0 and now >= test_kill_due_ns) { std.log.info("test mode: killing the reporter", .{}); _ = process.kill(test_kill_pid); test_kill_pid = 0; } for (&drivers) |*driver| { if (!driver.used) continue; switch (driver.state) { .awaiting_hello => if (now >= driver.hello_deadline_ns) { std.log.info("{s} missed its hello deadline", .{driver.name()}); _ = process.kill(driver.process_id); // The exit notification finishes the job via onDriverExit. }, .restarting => if (now >= driver.restart_due_ns) spawnDriver(driver), else => {}, } } } // --- the harness callbacks ----------------------------------------------------- fn initialise(endpoint: ipc.Handle) bool { manager_endpoint = endpoint; // Load the authoritative driver-match registry before any bus driver can // report a device to match against it. loadRegistry(); // Enumerate into a heap buffer (too big for the one-page user stack). const buffer = memory.allocator().alloc(device.DeviceDescriptor, 64) catch { _ = logging.write("/system/services/device-manager: out of memory\n"); return false; }; const total = device.enumerate(buffer); const count = @min(total, buffer.len); var matched: usize = 0; for (buffer[0..count]) |descriptor| { if (descriptor.class == @intFromEnum(device.DeviceClass.pci_host_bridge)) { // The PCI bus driver: enumeration in ring 3 (M19), one instance // per bridge, the bridge id as its assignment. matched += 1; addDriver("pci-bus", descriptor.id, true); continue; } // Nothing else is matched from the boot snapshot today. The kernel-seeded // HPET timer node is served by the kernel's own clock (docs/timers.md), not // a user-space driver; PCI functions and PS/2 _HID devices arrive later as // pci-bus / acpi-service reports and match in onChildAdded (docs/discovery.md). // A fuller system's static class->driver manifest (docs/device-manager.md) // would slot in here. } // The discovery service (docs/discovery.md): one per firmware, packed // under the neutral name "discovery", spawned once at startup. It finds and // claims the acpi-tables (or devicetree-blob) node itself. Not a per-device // match — it is the discoverer, not a driver bound to one device. addDriver("discovery", device_manager_protocol.no_device, false); if (test_restart_mode) { // The driver-restart scenario's fixture: claims device 0 (the tree // root, otherwise unclaimed), hellos, then faults — driving backoff, // re-claim-after-death, and the crash-loop cap deterministically. addDriver("crash-test", 0, true); } if (matched == 0) { _ = logging.write("/system/services/device-manager: no matchable devices\n"); } else { _ = logging.write("/system/services/device-manager: ok\n"); } return true; } /// Set by `onSubscribe` when the subscriber table has taken ownership of the /// capability the call carried, and read by `onMessage`, where the turn's /// `Arrival` lives. The generated dispatch hands a handler the raw handle rather /// than the `Arrival` — deliberately, since a handler has no business closing the /// turn's property — so the *claim* travels back out this way. One turn, one /// handler, one thread: there is nothing here to race. var capability_claimed = false; fn onMessage(message: []const u8, reply: []u8, sender: u32, arrived: *ipc.Arrival) usize { capability_claimed = false; const written = Serve.dispatch({}, handlers, message, sender, arrived.peek(), reply); if (capability_claimed) _ = arrived.take(); return written; } const handlers = Serve.Handlers{ .hello = onHello, .child_added = onChildAdded, .child_removed = onChildRemoved, .enumerate = onEnumerate, .subscribe = onSubscribe, }; /// The handshake. The device this driver was assigned is the packet's target. fn onHello(_: void, invocation: Invocation(device_manager_protocol.Hello), _: Answer(void)) isize { if (invocation.request.version != device_manager_protocol.version) { std.log.info("refused hello (version {d}) from process {d}", .{ invocation.request.version, invocation.sender }); return -envelope.EPROTO; } const driver = driverByProcess(invocation.sender) orelse { std.log.info("hello from unknown process {d}", .{invocation.sender}); return -envelope.EPERM; }; driver.state = .running; std.log.info("hello from {s} (device {d})", .{ driver.name(), invocation.target }); // Resilience drill (V6): once, kill the virtio-gpu driver a moment after it hellos, so // the normal restart policy respawns it — the compositor must survive and re-attach. if (test_scanout_restart_mode and !test_scanout_killed and std.mem.eql(u8, driver.name(), "/system/drivers/virtio-gpu")) { test_scanout_killed = true; test_kill_pid = invocation.sender; test_kill_due_ns = time.clock() + 1_500_000_000; _ = time.timerOnce(manager_endpoint, 1600); } return 0; } /// A bus driver reported a discovered device: mirror it, publish it, match a /// driver for it — and in the restart drills kill the reporter once, the /// deterministic trigger for prune -> backoff -> respawn -> re-report. fn onChildAdded(_: void, invocation: Invocation(device_manager_protocol.ChildAdded), _: Answer(void)) isize { const report = invocation.request; const sender = invocation.sender; // The registered kernel device id is the packet's target, not a field: what // the manager hands a matched driver as its argv assignment. const device_id = invocation.target; var status: isize = 0; if (driverByProcess(sender)) |driver| { if (!addChild(report.parent, report.bus_address, report.identity, device_id, sender)) status = -envelope.ENOSPC; std.log.info("child added (device {d} port {d}, identity {d}) by {s}", .{ report.parent, report.bus_address, report.identity, driver.name() }); if (status == 0) publish(.child_added, device_id, report); // Matching from reports (M19.3), now data-driven via the /system/configuration/devices.csv // registry: a registered child gets the most-specific driver its identity // matches, once — re-reports after a bus restart dedupe on the registered // id, exactly like the registrations do. if (status == 0 and device_id != device_manager_protocol.no_device) { const id = identityFromReport(report); if (registry.matchDriver(registry_rules[0..registry_count], id)) |match| { if (match.ambiguous) std.log.info("/system/configuration/devices.csv: multiple equally-specific rules match the device {s} reported; binding {s}", .{ driver.name(), match.driver }); if (id.bus == .acpi) { // An hid-matched driver (ps2-bus) is a singleton that finds its // own devices once spawned — spawn it once, no device assignment. if (!alreadySupervised(match.driver)) addDriver(match.driver, device_manager_protocol.no_device, false); } else { // A per-device driver: one instance, the registered id as argv[1]. if (!driverForDevice(device_id)) addDriver(match.driver, device_id, true); } } } } else { status = -envelope.EPERM; } if (test_pci_restart_mode and !test_usb_killed) { if (driverByProcess(sender)) |driver| { if (std.mem.eql(u8, driver.name(), "pci-bus") and childCountOf(sender) >= 3) { // The pci restart drill: kill the enumerator after it has // reported; the respawn must re-register without duplicates // (M19.0 idempotence, proven end to end by pci-scan). test_usb_killed = true; test_kill_pid = sender; test_kill_due_ns = time.clock() + 1_000_000_000; _ = time.timerOnce(manager_endpoint, 1100); } } } if (test_usb_restart_mode and !test_usb_killed and childCountOf(sender) >= 2) { // Only the xHCI reporter is the drill's victim — pci-bus also reports // now, and whichever finishes second must not trigger the kill. if (driverByProcess(sender)) |driver| { if (std.mem.eql(u8, driver.name(), "/system/drivers/usb-xhci-bus")) { // Delayed, not immediate: the device-list scenario's subscriber // needs a window to enumerate and subscribe before the events. test_usb_killed = true; test_kill_pid = sender; test_kill_due_ns = time.clock() + 2_000_000_000; _ = time.timerOnce(manager_endpoint, 2100); } } } return status; } /// A bus driver reported a device gone (hot-unplug). Addressed by the composite /// (parent, bus address) the reporter knows, which is why that pair is the /// packet's body rather than its target. fn onChildRemoved(_: void, invocation: Invocation(device_manager_protocol.ChildRemoved), _: Answer(void)) isize { const report = invocation.request; var status: isize = -envelope.ENOENT; for (&children) |*child| { if (child.used and child.parent == report.parent and child.bus_address == report.bus_address and child.reporter == invocation.sender) { std.log.info("child removed (device {d} port {d})", .{ child.parent, child.bus_address }); child.used = false; status = 0; } } return status; } /// The reserved `enumerate` verb: the mirror, one `ChildEntry` per known child, /// packed into the reply's tail. How many arrived is the reply's own length — /// `Status.len` — so no count header is spent saying it twice. fn onEnumerate(_: void, _: Invocation(void), answer: Answer(void)) isize { const entry_size = @sizeOf(device_manager_protocol.ChildEntry); const tail = answer.tail(); var written: usize = 0; for (&children) |*child| { if (!child.used) continue; if (written + entry_size > tail.len) break; const entry = device_manager_protocol.ChildEntry{ .parent = child.parent, .bus_address = child.bus_address, .identity = child.identity }; @memcpy(tail[written..][0..entry_size], std.mem.asBytes(&entry)); written += entry_size; } return @intCast(written); } /// The reserved `subscribe` verb: an application's endpoint arrived as the call's /// capability. The table taking a slot is what claims it; a full table refuses /// and lets the turn close it, so a subscribe storm cannot spend the handle table. fn onSubscribe(_: void, invocation: Invocation(void), _: Answer(void)) isize { const endpoint = invocation.capability orelse return -envelope.EPROTO; for (&subscribers) |*slot| { if (slot.* == null) { slot.* = endpoint; capability_claimed = true; // the table holds it from here return 0; } } return -envelope.ENOSPC; } fn onNotification(badge: u64) void { if (badge & ipc.notify_exit_bit != 0) { const dead: u32 = @intCast(badge & ~(ipc.notify_badge_bit | ipc.notify_exit_bit)); if (driverByProcess(dead)) |driver| onDriverExit(driver); return; } if (badge & ipc.notify_timer_bit != 0) sweepDeadlines(); } pub fn main(init: process.Init) void { if (init.arguments.get(1)) |mode| { test_restart_mode = std.mem.eql(u8, mode, "test-restart"); test_usb_restart_mode = std.mem.eql(u8, mode, "test-usb-restart"); test_pci_restart_mode = std.mem.eql(u8, mode, "test-pci-restart"); test_scanout_restart_mode = std.mem.eql(u8, mode, "test-scanout-restart"); } service.run(device_manager_protocol.message_maximum, .{ .service = "device-manager", .init = initialise, .on_message = onMessage, .on_notification = onNotification, }); }