child_added/child_removed join the device-manager protocol. The driver maps its register BAR (resource 0 is the ECAM config space; the walk starts at 1), reads CAPLENGTH and HCSPARAMS1, and reads one PORTSC per port: the connect bit and speed class come straight from hardware, no rings needed to see the devices. The manager mirrors reported children keyed by (parent, port), remembers which instance reported each, and prunes a dead reporter's children before deciding the restart — the children describe protocol state that died with the process. The usb-report scenario drives the whole loop: two QEMU devices reported, reporter killed, children pruned, driver respawned with backoff, and the new instance re-claims, re-scans, and re-reports.
415 lines
17 KiB
Zig
415 lines
17 KiB
Zig
//! /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 runtime = @import("runtime");
|
|
const acpi_ids = @import("acpi-ids");
|
|
const protocol = runtime.device_manager_protocol;
|
|
const device = runtime.device;
|
|
const system = runtime.system;
|
|
|
|
/// Format one whole log line and emit it in a single `debug_write`, so output
|
|
/// from the drivers this manager starts (which run concurrently) can never land
|
|
/// in the middle of it.
|
|
fn writeLine(comptime fmt: []const u8, arguments: anytype) void {
|
|
var line: [128]u8 = undefined;
|
|
_ = runtime.system.write(std.fmt.bufPrint(&line, fmt, arguments) catch return);
|
|
}
|
|
|
|
/// The driver that serves each device — the policy table. In a fuller system
|
|
/// this comes from a manifest (docs/device-manager.md: the third bus type
|
|
/// triggers it); for now a static map. `null` = no driver for this class yet.
|
|
fn driverFor(d: device.DeviceDescriptor) ?[]const u8 {
|
|
// detect device via DeviceClass
|
|
if (d.class == @intFromEnum(device.DeviceClass.timer)) return "hpet";
|
|
// detect device via hid
|
|
const hid = d.hid[0..@intCast(d.hid_len)];
|
|
const id = acpi_ids.HardwareId.fromHid(hid) orelse return null;
|
|
return switch (id) {
|
|
.ps2_keyboard, .ps2_mouse => "ps2-bus",
|
|
else => null,
|
|
};
|
|
}
|
|
|
|
/// The PCI class/subclass/prog-IF triple of an xHCI (USB 3) host controller:
|
|
/// Serial Bus Controller (0x0C) / USB Controller (0x03) / XHCI (0x30) — the names
|
|
/// pci-class.zig decodes.
|
|
const xhci_pci_class: u64 = 0x0C_03_30;
|
|
|
|
/// The bus driver that serves a PCI function, or null. A machine can carry
|
|
/// several identical controllers — one driver instance per device, the id as
|
|
/// argv[1]. These drivers speak the protocol: a hello is expected.
|
|
fn pciDriverFor(d: device.DeviceDescriptor) ?[]const u8 {
|
|
if (d.class != @intFromEnum(device.DeviceClass.pci_device)) return null;
|
|
return switch (d.pci_class) {
|
|
xhci_pci_class => "usb-xhci-bus",
|
|
else => null,
|
|
};
|
|
}
|
|
|
|
// --- 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: [24]u8 = undefined,
|
|
name_len: usize = 0,
|
|
// The assigned device id (becomes argv[1]), or protocol.no_device.
|
|
device_id: u64 = protocol.no_device,
|
|
// Whether this driver speaks the protocol (hello expected, deadline
|
|
// enforced). Legacy drivers (hpet, 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: runtime.ipc.Handle = 0;
|
|
var test_restart_mode = false;
|
|
var test_usb_restart_mode = false;
|
|
var test_usb_killed = false;
|
|
|
|
/// 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,
|
|
reporter: u32 = 0, // the reporting driver instance's process id
|
|
};
|
|
|
|
const maximum_children = 32;
|
|
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, 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.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, .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.
|
|
fn pruneChildrenOf(reporter: u32) void {
|
|
for (&children) |*child| {
|
|
if (child.used and child.reporter == reporter) {
|
|
writeLine("device-manager: child removed (device {d} port {d})\n", .{ child.parent, child.bus_address });
|
|
child.used = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 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;
|
|
}
|
|
writeLine("device-manager: driver table full; cannot supervise {s}\n", .{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 != protocol.no_device) {
|
|
arguments[0] = std.fmt.bufPrint(&id_text, "{d}", .{driver.device_id}) catch return;
|
|
argument_count = 1;
|
|
}
|
|
const child = system.spawnSupervised(driver.name(), arguments[0..argument_count], manager_endpoint) orelse {
|
|
writeLine("device-manager: failed to spawn {s}\n", .{driver.name()});
|
|
driver.state = .failed;
|
|
return;
|
|
};
|
|
driver.process_id = child;
|
|
driver.spawn_ns = system.clock();
|
|
if (driver.speaks_protocol) {
|
|
driver.state = .awaiting_hello;
|
|
driver.hello_deadline_ns = driver.spawn_ns + hello_deadline_ms * 1_000_000;
|
|
_ = system.timerOnce(manager_endpoint, hello_deadline_ms + 100);
|
|
} else {
|
|
driver.state = .running;
|
|
}
|
|
if (driver.device_id != protocol.no_device) {
|
|
writeLine("device-manager: spawned {s} for device {d}\n", .{ driver.name(), driver.device_id });
|
|
} else {
|
|
writeLine("device-manager: spawned {s}\n", .{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 = runtime.process.exitReason(driver.process_id) orelse .fault;
|
|
if (reason == .exited) {
|
|
driver.state = .stopped;
|
|
writeLine("device-manager: {s} exited cleanly; not restarting\n", .{driver.name()});
|
|
return;
|
|
}
|
|
const now = system.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;
|
|
writeLine("device-manager: {s} is failing repeatedly (crash loop); giving up\n", .{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;
|
|
writeLine("device-manager: restarting {s} in {d} ms (died: {s})\n", .{ driver.name(), delay_ms, @tagName(reason) });
|
|
_ = system.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 = system.clock();
|
|
for (&drivers) |*driver| {
|
|
if (!driver.used) continue;
|
|
switch (driver.state) {
|
|
.awaiting_hello => if (now >= driver.hello_deadline_ns) {
|
|
writeLine("device-manager: {s} missed its hello deadline\n", .{driver.name()});
|
|
_ = system.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: runtime.ipc.Handle) bool {
|
|
manager_endpoint = endpoint;
|
|
|
|
// Enumerate into a heap buffer (too big for the one-page user stack).
|
|
const buffer = runtime.allocator().alloc(device.DeviceDescriptor, 64) catch {
|
|
_ = runtime.system.write("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 (pciDriverFor(descriptor)) |driver_name| {
|
|
matched += 1;
|
|
addDriver(driver_name, descriptor.id, true);
|
|
continue;
|
|
}
|
|
const driver_name = driverFor(descriptor) orelse continue;
|
|
matched += 1;
|
|
// Skip a singleton that is already alive (the initial-ramdisk sweep test
|
|
// starts every bundled binary bare, this manager included) — spawning a
|
|
// second instance would only lose the claim race and churn the log.
|
|
if (!alreadySupervised(driver_name) and !system.isProcessRunning(driver_name)) {
|
|
addDriver(driver_name, 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) {
|
|
_ = runtime.system.write("device-manager: no matchable devices\n");
|
|
} else {
|
|
_ = runtime.system.write("device-manager: ok\n");
|
|
}
|
|
return true;
|
|
}
|
|
|
|
fn onMessage(message: []const u8, reply: []u8, sender: u32) usize {
|
|
if (message.len < 1) return 0;
|
|
switch (message[0]) {
|
|
@intFromEnum(protocol.Operation.child_added) => return onChildAdded(message, reply, sender),
|
|
@intFromEnum(protocol.Operation.child_removed) => return onChildRemoved(message, reply, sender),
|
|
@intFromEnum(protocol.Operation.hello) => {},
|
|
else => return 0,
|
|
}
|
|
if (message.len < protocol.hello_size) return 0;
|
|
const hello = std.mem.bytesToValue(protocol.Hello, message[0..protocol.hello_size]);
|
|
|
|
var status: i32 = 0;
|
|
if (hello.version != protocol.version) {
|
|
status = -1;
|
|
writeLine("device-manager: refused hello (version {d}) from process {d}\n", .{ hello.version, sender });
|
|
} else if (driverByProcess(sender)) |driver| {
|
|
driver.state = .running;
|
|
writeLine("device-manager: hello from {s} (device {d})\n", .{ driver.name(), hello.device_id });
|
|
} else {
|
|
status = -1;
|
|
writeLine("device-manager: hello from unknown process {d}\n", .{sender});
|
|
}
|
|
const hello_reply = protocol.HelloReply{ .status = status };
|
|
@memcpy(reply[0..protocol.reply_size], std.mem.asBytes(&hello_reply));
|
|
return protocol.reply_size;
|
|
}
|
|
|
|
/// A bus driver reported a discovered device: mirror it, and in
|
|
/// test-usb-restart mode kill the reporter once after its second child — the
|
|
/// deterministic trigger for prune -> backoff -> respawn -> re-report.
|
|
fn onChildAdded(message: []const u8, reply: []u8, sender: u32) usize {
|
|
if (message.len < protocol.child_added_size) return 0;
|
|
const report = std.mem.bytesToValue(protocol.ChildAdded, message[0..protocol.child_added_size]);
|
|
var status: i32 = 0;
|
|
if (driverByProcess(sender)) |driver| {
|
|
if (!addChild(report.parent, report.bus_address, report.identity, sender)) status = -1;
|
|
writeLine("device-manager: child added (device {d} port {d}, identity {d}) by {s}\n", .{ report.parent, report.bus_address, report.identity, driver.name() });
|
|
} else {
|
|
status = -1;
|
|
}
|
|
const report_reply = protocol.ReportReply{ .status = status };
|
|
@memcpy(reply[0..@sizeOf(protocol.ReportReply)], std.mem.asBytes(&report_reply));
|
|
if (test_usb_restart_mode and !test_usb_killed and childCountOf(sender) >= 2) {
|
|
test_usb_killed = true;
|
|
writeLine("device-manager: test mode: killing the reporter\n", .{});
|
|
_ = system.kill(sender);
|
|
}
|
|
return @sizeOf(protocol.ReportReply);
|
|
}
|
|
|
|
/// A bus driver reported a device gone (hot-unplug; no sender exists yet, but
|
|
/// the handler is protocol-complete — death-pruning covers removal until then).
|
|
fn onChildRemoved(message: []const u8, reply: []u8, sender: u32) usize {
|
|
if (message.len < protocol.child_removed_size) return 0;
|
|
const report = std.mem.bytesToValue(protocol.ChildRemoved, message[0..protocol.child_removed_size]);
|
|
var status: i32 = -1;
|
|
for (&children) |*child| {
|
|
if (child.used and child.parent == report.parent and child.bus_address == report.bus_address and child.reporter == sender) {
|
|
writeLine("device-manager: child removed (device {d} port {d})\n", .{ child.parent, child.bus_address });
|
|
child.used = false;
|
|
status = 0;
|
|
}
|
|
}
|
|
const report_reply = protocol.ReportReply{ .status = status };
|
|
@memcpy(reply[0..@sizeOf(protocol.ReportReply)], std.mem.asBytes(&report_reply));
|
|
return @sizeOf(protocol.ReportReply);
|
|
}
|
|
|
|
fn onNotification(badge: u64) void {
|
|
if (badge & runtime.ipc.notify_exit_bit != 0) {
|
|
const dead: u32 = @intCast(badge & ~(runtime.ipc.notify_badge_bit | runtime.ipc.notify_exit_bit));
|
|
if (driverByProcess(dead)) |driver| onDriverExit(driver);
|
|
return;
|
|
}
|
|
if (badge & runtime.ipc.notify_timer_bit != 0) sweepDeadlines();
|
|
}
|
|
|
|
pub fn main(init: runtime.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");
|
|
}
|
|
runtime.service.run(protocol.message_maximum, .{
|
|
.service = .device_manager,
|
|
.init = initialise,
|
|
.on_message = onMessage,
|
|
.on_notification = onNotification,
|
|
});
|
|
}
|
|
|
|
pub const panic = runtime.panic;
|
|
comptime {
|
|
_ = &runtime.start._start; // pull the runtime entry shim into the image
|
|
}
|