The xHCI driver scans its root-hub ports and reports the tree (M18.2)

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.
This commit is contained in:
Daniel Samson
2026-07-13 00:28:29 +01:00
parent 37fb09f75e
commit 79d859a111
7 changed files with 284 additions and 18 deletions
@@ -19,10 +19,11 @@ pub const Role = enum(u8) {
device = 2,
};
/// The message kinds. `child_added`/`child_removed` land in M18.2;
/// `enumerate`/`subscribe` in M18.3.
/// The message kinds. `enumerate`/`subscribe` land in M18.3.
pub const Operation = enum(u8) {
hello = 1,
child_added = 2,
child_removed = 3,
};
/// `Hello.device_id` for a driver that serves no enumerated device (a test
@@ -54,5 +55,47 @@ pub const HelloReply = extern struct {
pub const reply_size = @sizeOf(HelloReply);
/// A bus driver reporting one device it discovered behind its controller
/// (docs/device-manager.md "the tree"). Identity is the bus's native language —
/// for USB a port-speed class; the (class, subclass, protocol) triple joins it
/// once control transfers exist (the USB track). The manager mirrors the child
/// into its tree; when the reporting driver dies, the manager prunes everything
/// it reported (the children describe protocol state that died with it) and the
/// restarted instance rediscovers and re-reports.
pub const ChildAdded = extern struct {
operation: u8 = @intFromEnum(Operation.child_added),
reserved0: u8 = 0,
reserved1: u16 = 0,
reserved2: u32 = 0,
/// The reporting driver's own device (the controller) — the child's parent.
parent: u64,
/// Where on the bus (for USB: the root port number, 1-based).
bus_address: u64,
/// Bus-specific identity (for USB: the PORTSC port-speed class).
identity: u64,
};
pub const child_added_size = @sizeOf(ChildAdded);
/// A bus driver reporting a device gone (hot-unplug). Not yet sent by any
/// driver — the port scan has no unplug interrupt — but the manager handles it;
/// death-pruning covers removal until hotplug lands.
pub const ChildRemoved = extern struct {
operation: u8 = @intFromEnum(Operation.child_removed),
reserved0: u8 = 0,
reserved1: u16 = 0,
reserved2: u32 = 0,
parent: u64,
bus_address: u64,
};
pub const child_removed_size = @sizeOf(ChildRemoved);
/// The manager's answer to a tree report.
pub const ReportReply = extern struct {
status: i32,
reserved: u32 = 0,
};
/// Upper bound on any message in this protocol — sizes the endpoint buffers.
pub const message_maximum = 64;
@@ -106,6 +106,62 @@ 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| {
@@ -171,9 +227,11 @@ fn spawnDriver(driver: *Driver) void {
}
}
/// A driver died. The exit reason (M17.2) is the whole decision: a clean exit
/// meant to stop; anything else restarts with backoff until the crash-loop cap.
/// 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;
@@ -261,9 +319,15 @@ fn initialise(endpoint: runtime.ipc.Handle) bool {
}
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]);
if (hello.operation != @intFromEnum(protocol.Operation.hello)) return 0;
var status: i32 = 0;
if (hello.version != protocol.version) {
@@ -281,6 +345,47 @@ fn onMessage(message: []const u8, reply: []u8, sender: u32) usize {
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));
@@ -293,6 +398,7 @@ fn onNotification(badge: u64) void {
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,