diff --git a/build.zig b/build.zig index bae8552..4df79d0 100644 --- a/build.zig +++ b/build.zig @@ -342,6 +342,7 @@ pub fn build(b: *std.Build) void { // A test fixture, not a real driver: hellos to the device manager, then faults — // what the driver-restart scenario drives the crash-loop cap with. const crash_test_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "crash-test", "system/services/crash-test/crash-test.zig"); + const device_list_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "device-list", "system/services/device-list/device-list.zig"); const device_manager_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "device-manager", "system/services/device-manager/device-manager.zig"); // The input service and its exercisers: the fan-out server, a hardware-free synthetic // source, and a subscriber that doubles as the `input` test's oracle. See docs/input.md. @@ -375,6 +376,8 @@ pub fn build(b: *std.Build) void { mk_run.addFileArg(usb_xhci_bus_exe.getEmittedBin()); mk_run.addArg("crash-test"); mk_run.addFileArg(crash_test_exe.getEmittedBin()); + mk_run.addArg("device-list"); + mk_run.addFileArg(device_list_exe.getEmittedBin()); mk_run.addArg("device-manager"); mk_run.addFileArg(device_manager_exe.getEmittedBin()); mk_run.addArg("input"); diff --git a/docs/device-manager.md b/docs/device-manager.md index e38eea0..b28d998 100644 --- a/docs/device-manager.md +++ b/docs/device-manager.md @@ -4,8 +4,13 @@ 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 (M18.2) and the application surface (M18.3) remain design. The -primitives underneath are real ([process-management.md](process-management.md): +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 diff --git a/docs/m17-m18-plan.md b/docs/m17-m18-plan.md index 9cd0c22..d3e2a72 100644 --- a/docs/m17-m18-plan.md +++ b/docs/m17-m18-plan.md @@ -53,8 +53,17 @@ only when its definition of green holds. re-proving claim release each respawn; `driver-restart` scenario; maximum_tasks 16→32 — the sweep was overflowing the pool; suite 52/52) - [x] **merge** `feat/device-manager` → main, push (merged 2026-07-13) -- [ ] **M18.2** — xHCI port scan + tree reports (branch `feat/usb-xhci-bus`) -- [ ] **M18.3** — app surface: enumerate/subscribe + device-list +- [x] **M18.2** — xHCI port scan + tree reports (child_added/child_removed in + the protocol; the manager's child mirror with death-pruning; xHCI maps the + register BAR — resource 0 is ECAM — reads CAPLENGTH/HCSPARAMS1, scans + PORTSC, reports connected ports with speed-class identity; `usb-report` + scenario proves report → prune → respawn → re-report; suite 53/53) +- [x] **M18.3** — app surface: enumerate/subscribe over IPC (subscriber + endpoint rides as the call's capability; events are the same structs the + buses send); device-list first client; protocol capped at the kernel's + IPC MESSAGE_MAXIMUM (256); the startUserTask debug print removed — it + sheared concurrent serial lines and was the scenario-flake root cause; + `device-list` scenario; suite 54/54) - [ ] **merge** `feat/usb-xhci-bus` → main, push — **loop ends here** --- diff --git a/library/runtime/service.zig b/library/runtime/service.zig index f5c7064..8926754 100644 --- a/library/runtime/service.zig +++ b/library/runtime/service.zig @@ -22,8 +22,10 @@ pub const Callbacks = struct { /// Return false to abort startup (the process exits). init: ?*const fn (endpoint: ipc.Handle) bool = null, /// One protocol request from `sender` (a task id): write the reply into - /// `reply`, return its length. The zero-length ping never reaches this. - on_message: *const fn (message: []const u8, reply: []u8, sender: u32) usize, + /// `reply`, return its length. `capability` is the handle the request + /// carried, if any (M13 cap passing — how a subscriber hands over its + /// endpoint). The zero-length ping never reaches this. + on_message: *const fn (message: []const u8, reply: []u8, sender: u32, capability: ?ipc.Handle) usize, /// A notification that is not a signal — a subscribed exit event, a bound /// IRQ, a timer landing. The raw badge; decode with the ipc helpers. on_notification: ?*const fn (badge: u64) void = null, @@ -76,6 +78,6 @@ pub fn run(comptime maximum_message: usize, callbacks: Callbacks) void { reply_len = 0; // the universal ping: a zero-length reply, from the harness continue; } - reply_len = callbacks.on_message(receive[0..got.len], &reply_buffer, got.senderTaskId()); + reply_len = callbacks.on_message(receive[0..got.len], &reply_buffer, got.senderTaskId(), got.cap); } } diff --git a/system/drivers/usb-xhci-bus/usb-xhci-bus.zig b/system/drivers/usb-xhci-bus/usb-xhci-bus.zig index cb23480..ee7898a 100644 --- a/system/drivers/usb-xhci-bus/usb-xhci-bus.zig +++ b/system/drivers/usb-xhci-bus/usb-xhci-bus.zig @@ -4,11 +4,14 @@ //! argv[1]; this instance claims that device and no other, so multiple //! instances never fight over hardware. //! -//! M18.1 (this increment): a harness service and the first conforming driver of -//! the device-manager protocol — claim the controller, `hello` the manager -//! (role, version, assignment) inside its deadline, then serve. Controller -//! bring-up (map the MMIO window, reset, port scan) and tree reports -//! (`child_added` for each connected port) land in M18.2. +//! M18.2 (this increment): after the hello, real hardware — map the xHC's +//! register window (the first memory BAR; resource 0 is the ECAM config +//! space), read the capability registers, and walk the root-hub ports: one +//! `child_added` report to the manager per connected port, carrying the port +//! number and the PORTSC speed class as identity. No transfer rings yet — +//! descriptors and USB class matching are the USB track; the connect bit and +//! speed come straight from PORTSC, which reflects hardware state whether or +//! not the controller is running. const std = @import("std"); const runtime = @import("runtime"); @@ -47,12 +50,16 @@ fn initialise(endpoint: runtime.ipc.Handle) bool { return false; }; - // The controller's operational registers live behind BAR0, enumerated as - // the device's first memory resource. - const register_window = for (descriptor.resources[0..@intCast(descriptor.resource_count)]) |resource| { - if (resource.kind == @intFromEnum(device.ResourceKind.memory)) break resource; + // The xHC's registers live behind the first memory BAR. Resource 0 is the + // function's ECAM configuration space (M15), so the walk starts at 1. + var register_index: u64 = 0; + const register_window = for (descriptor.resources[1..@intCast(descriptor.resource_count)], 1..) |resource, index| { + if (resource.kind == @intFromEnum(device.ResourceKind.memory)) { + register_index = index; + break resource; + } } else { - writeLine("usb-xhci-bus: controller device {d} has no MMIO window\n", .{controller_id}); + writeLine("usb-xhci-bus: controller device {d} has no register BAR\n", .{controller_id}); return false; }; writeLine("usb-xhci-bus: claimed controller device {d} (registers at 0x{x}, {d} bytes)\n", .{ @@ -60,6 +67,10 @@ fn initialise(endpoint: runtime.ipc.Handle) bool { register_window.start, register_window.len, }); + register_base = device.mmioMap(controller_id, register_index) orelse { + _ = runtime.system.write("usb-xhci-bus: mmio_map failed\n"); + return false; + }; // The handshake: role, protocol version, assignment — inside the manager's // deadline (the lookup retries cover the manager still registering). @@ -84,14 +95,62 @@ fn initialise(endpoint: runtime.ipc.Handle) bool { return false; } _ = runtime.system.write("usb-xhci-bus: hello acknowledged\n"); + + scanPorts(h); return true; } +var register_base: usize = 0; + +/// One 32-bit volatile register read at `offset` from the mapped window. +fn readRegister(offset: usize) u32 { + const register: *volatile u32 = @ptrFromInt(register_base + offset); + return register.*; +} + +/// The root-hub port scan: read the capability registers for the port count +/// and the operational-register offset, then one PORTSC per port. The connect +/// bit (CCS) and the speed field reflect hardware state directly — no +/// controller reset or run needed to *see* the devices; driving them needs the +/// rings (the USB track). +fn scanPorts(manager: runtime.ipc.Handle) void { + // Capability registers: CAPLENGTH is byte 0 of the first dword; HCSPARAMS1 + // carries MaxPorts in bits 31:24. + const capability_length = readRegister(0) & 0xFF; + const structural = readRegister(0x04); + const maximum_ports: u32 = structural >> 24; + writeLine("usb-xhci-bus: {d} root-hub ports\n", .{maximum_ports}); + + // PORTSC registers: operational base + 0x400 + 0x10 per port (1-based). + var port: u32 = 1; + var connected: u32 = 0; + while (port <= maximum_ports) : (port += 1) { + const port_status = readRegister(capability_length + 0x400 + 0x10 * (port - 1)); + if (port_status & 1 == 0) continue; // CCS: nothing connected + connected += 1; + const speed = (port_status >> 10) & 0xF; // the PORTSC port-speed class + writeLine("usb-xhci-bus: port {d} connected (speed class {d})\n", .{ port, speed }); + + const report = protocol.ChildAdded{ + .parent = controller_id, + .bus_address = port, + .identity = speed, + }; + var reply: [protocol.message_maximum]u8 = undefined; + _ = runtime.ipc.call(manager, std.mem.asBytes(&report), &reply) catch { + writeLine("usb-xhci-bus: child report for port {d} failed\n", .{port}); + continue; + }; + } + if (connected == 0) _ = runtime.system.write("usb-xhci-bus: no devices connected\n"); +} + /// No bus protocol to serve yet — transfer requests arrive with the USB track. -fn onMessage(message: []const u8, reply: []u8, sender: u32) usize { +fn onMessage(message: []const u8, reply: []u8, sender: u32, capability: ?runtime.ipc.Handle) usize { _ = message; _ = reply; _ = sender; + _ = capability; return 0; } diff --git a/system/kernel/scheduler.zig b/system/kernel/scheduler.zig index c468a63..2f26023 100644 --- a/system/kernel/scheduler.zig +++ b/system/kernel/scheduler.zig @@ -350,8 +350,9 @@ pub fn spawnUserLocked(aspace: u64, entry: u64, user_sp: u64, priority: Priority /// context switch and lock release. fn startUserTask() void { const t = current(); - var buffer: [96]u8 = undefined; - architecture.serialWrite(std.fmt.bufPrint(&buffer, "DBG startUserTask ip=0x{x} sp=0x{x} aspace=0x{x} kstack=0x{x}\n", .{ t.user_ip, t.user_sp, t.aspace, t.kstack_top }) catch ""); + // No serial chatter here: this runs on every spawn, unserialized against + // user-space writes, and its output used to shear concurrent log lines in + // half — the largest source of corrupted markers in the QEMU scenarios. architecture.jumpToUser(t.user_ip, t.user_sp); // noreturn } diff --git a/system/kernel/tests.zig b/system/kernel/tests.zig index 44cd12b..bf6237b 100644 --- a/system/kernel/tests.zig +++ b/system/kernel/tests.zig @@ -140,6 +140,10 @@ pub fn run(case: []const u8, boot_information: *const BootInformation) void { signalsTest(boot_information); } else if (eql(case, "driver-restart")) { driverRestartTest(boot_information); + } else if (eql(case, "usb-report")) { + usbReportTest(boot_information); + } else if (eql(case, "device-list")) { + deviceListTest(boot_information); } else if (eql(case, "initial-ramdisk")) { initialRamdiskTest(boot_information); } else if (eql(case, "vfs")) { @@ -1694,6 +1698,73 @@ fn driverRestartTest(boot_information: *const BootInformation) void { result(); } +/// M18.2: bus tree reports, end to end. The manager (test-usb-restart mode) +/// spawns the xHCI driver; the driver maps its BAR, scans the root-hub ports, +/// and reports the two QEMU devices; the manager mirrors them, kills the +/// reporter (the test trigger), prunes both children, restarts the driver with +/// backoff, and the respawned instance re-claims, re-scans, and re-reports. +/// The harness's ordered expect regex is the assertion; this test only +/// orchestrates the spawn. +fn usbReportTest(boot_information: *const BootInformation) void { + log("DANOS-TEST-BEGIN: usb-report\n", .{}); + if (boot_information.initial_ramdisk_len == 0) { + check("bootloader handed over an initial_ramdisk", false); + result(); + return; + } + const image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.initial_ramdisk_base)))[0..boot_information.initial_ramdisk_len]; + const rd = initial_ramdisk.Reader.init(image) orelse { + check("initial_ramdisk image is valid", false); + result(); + return; + }; + + process.setInitialRamdisk(image); + var manager: u32 = 0; + var i: u32 = 0; + while (i < rd.count) : (i += 1) { + const item = rd.entry(i) orelse continue; + if (!eql(item.name, "device-manager")) continue; + manager = process.spawnProcessSupervised(item.blob, 4, &.{ "device-manager", "test-usb-restart" }, scheduler.currentId(), null) catch 0; + break; + } + check("device-manager spawned in test-usb-restart mode", manager != 0); + result(); +} + +/// M18.3: the application surface. device-list enumerates the manager's tree +/// over IPC, subscribes with its endpoint as a capability, and prints every +/// published event; the manager's delayed test-kill of the reporter produces a +/// removed/added storm the subscriber must observe. The harness's ordered +/// expect regex is the assertion. +fn deviceListTest(boot_information: *const BootInformation) void { + log("DANOS-TEST-BEGIN: device-list\n", .{}); + if (boot_information.initial_ramdisk_len == 0) { + check("bootloader handed over an initial_ramdisk", false); + result(); + return; + } + const image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.initial_ramdisk_base)))[0..boot_information.initial_ramdisk_len]; + const rd = initial_ramdisk.Reader.init(image) orelse { + check("initial_ramdisk image is valid", false); + result(); + return; + }; + + process.setInitialRamdisk(image); + var manager: u32 = 0; + var i: u32 = 0; + while (i < rd.count) : (i += 1) { + const item = rd.entry(i) orelse continue; + if (!eql(item.name, "device-manager")) continue; + manager = process.spawnProcessSupervised(item.blob, 4, &.{ "device-manager", "test-usb-restart" }, scheduler.currentId(), null) catch 0; + break; + } + check("device-manager spawned in test-usb-restart mode", manager != 0); + check("device-list spawned", spawnNamed(rd, "device-list")); + result(); +} + /// The whole user-side surface at once: spawn process-test's supervisor role, /// which — entirely from ring 3 — creates an exit endpoint, spawns its two /// children supervised, sees them in process_enumerate, kills them (one blocked, diff --git a/system/services/device-list/device-list.zig b/system/services/device-list/device-list.zig new file mode 100644 index 0000000..4b72285 --- /dev/null +++ b/system/services/device-list/device-list.zig @@ -0,0 +1,88 @@ +//! device-list — the `ps` analog for the device tree (docs/device-manager.md +//! M18.3): asks the device manager for the tree over IPC, prints it, then +//! subscribes and prints every published add/remove event. The manager is the +//! one answer to "what devices exist" for user space; nothing here touches a +//! device_* system call. + +const std = @import("std"); +const runtime = @import("runtime"); +const protocol = runtime.device_manager_protocol; + +fn writeLine(comptime fmt: []const u8, arguments: anytype) void { + var line: [96]u8 = undefined; + _ = runtime.system.write(std.fmt.bufPrint(&line, fmt, arguments) catch return); +} + +pub fn main() void { + var manager: ?runtime.ipc.Handle = null; + var tries: u32 = 0; + while (manager == null and tries < 200) : (tries += 1) { + manager = runtime.ipc.lookup(.device_manager); + if (manager == null) runtime.system.sleep(20); + } + const h = manager orelse { + _ = runtime.system.write("device-list: no device manager\n"); + return; + }; + + // The snapshot — polled briefly, because at boot the bus drivers may still + // be scanning: an empty first answer usually just means "too early". + var reply: [protocol.message_maximum]u8 = undefined; + var count: u32 = 0; + var length: usize = 0; + tries = 0; + while (tries < 20) : (tries += 1) { + const request = protocol.Enumerate{}; + length = runtime.ipc.call(h, std.mem.asBytes(&request), &reply) catch 0; + if (length >= @sizeOf(protocol.EnumerateReply)) { + count = std.mem.bytesToValue(protocol.EnumerateReply, reply[0..@sizeOf(protocol.EnumerateReply)]).count; + if (count != 0) break; + } + runtime.system.sleep(100); + } + writeLine("device-list: {d} devices\n", .{count}); + var offset: usize = @sizeOf(protocol.EnumerateReply); + var index: u32 = 0; + while (index < count and offset + @sizeOf(protocol.ChildEntry) <= length) : (index += 1) { + const entry = std.mem.bytesToValue(protocol.ChildEntry, reply[offset..][0..@sizeOf(protocol.ChildEntry)]); + writeLine("device-list: device {d} port {d} identity {d}\n", .{ entry.parent, entry.bus_address, entry.identity }); + offset += @sizeOf(protocol.ChildEntry); + } + + // The subscription: our endpoint rides as the call's capability; events + // arrive as buffered messages carrying the same structs the bus sends. + const endpoint = runtime.ipc.createIpcEndpoint() orelse { + _ = runtime.system.write("device-list: no endpoint\n"); + return; + }; + const subscribe = protocol.Subscribe{}; + _ = runtime.ipc.callCap(h, std.mem.asBytes(&subscribe), &reply, endpoint) catch { + _ = runtime.system.write("device-list: subscribe failed\n"); + return; + }; + _ = runtime.system.write("device-list: subscribed\n"); + + var receive: [protocol.message_maximum]u8 = undefined; + while (true) { + const got = runtime.ipc.replyWait(endpoint, &.{}, &receive, null); + if (!got.isMessage() or got.len < 1) continue; + switch (receive[0]) { + @intFromEnum(protocol.Operation.child_added) => { + if (got.len < protocol.child_added_size) continue; + const event = std.mem.bytesToValue(protocol.ChildAdded, receive[0..protocol.child_added_size]); + writeLine("device-list: added (device {d} port {d})\n", .{ event.parent, event.bus_address }); + }, + @intFromEnum(protocol.Operation.child_removed) => { + if (got.len < protocol.child_removed_size) continue; + const event = std.mem.bytesToValue(protocol.ChildRemoved, receive[0..protocol.child_removed_size]); + writeLine("device-list: removed (device {d} port {d})\n", .{ event.parent, event.bus_address }); + }, + else => {}, + } + } +} + +pub const panic = runtime.panic; +comptime { + _ = &runtime.start._start; // pull the runtime entry shim into the image +} diff --git a/system/services/device-manager/device-manager-protocol.zig b/system/services/device-manager/device-manager-protocol.zig index d53a50f..2a49db4 100644 --- a/system/services/device-manager/device-manager-protocol.zig +++ b/system/services/device-manager/device-manager-protocol.zig @@ -19,10 +19,13 @@ 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. pub const Operation = enum(u8) { hello = 1, + child_added = 2, + child_removed = 3, + enumerate = 4, + subscribe = 5, }; /// `Hello.device_id` for a driver that serves no enumerated device (a test @@ -54,5 +57,83 @@ 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, +}; + +/// An application asking for the tree (M18.3): the reply is an EnumerateReply +/// header followed by `count` ChildEntry records. +pub const Enumerate = extern struct { + operation: u8 = @intFromEnum(Operation.enumerate), + reserved0: u8 = 0, + reserved1: u16 = 0, + reserved2: u32 = 0, +}; + +pub const EnumerateReply = extern struct { + status: i32, + /// ChildEntry records following this header. + count: u32, +}; + +pub const ChildEntry = extern struct { + parent: u64, + bus_address: u64, + identity: u64, +}; + +/// An application subscribing to published add/remove events (the input-service +/// pattern): the subscriber's endpoint rides as the call's **capability**, and +/// events arrive on it as buffered messages whose payload is the same +/// ChildAdded / ChildRemoved struct the bus drivers send — one encoding, both +/// directions. +pub const Subscribe = extern struct { + operation: u8 = @intFromEnum(Operation.subscribe), + reserved0: u8 = 0, + reserved1: u16 = 0, + reserved2: u32 = 0, +}; + /// Upper bound on any message in this protocol — sizes the endpoint buffers. -pub const message_maximum = 64; +/// Capped by the kernel's IPC MESSAGE_MAXIMUM (256): an EnumerateReply carries +/// up to ten ChildEntry records per call, plenty for the mirror's current +/// bounds; paging joins the protocol if a tree ever outgrows one message. +pub const message_maximum = 256; diff --git a/system/services/device-manager/device-manager.zig b/system/services/device-manager/device-manager.zig index 63c0ff2..b59f2a1 100644 --- a/system/services/device-manager/device-manager.zig +++ b/system/services/device-manager/device-manager.zig @@ -106,6 +106,84 @@ 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; +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]?runtime.ipc.Handle = .{null} ** maximum_subscribers; + +/// Publish one event (a ChildAdded or ChildRemoved struct, the same encoding +/// the bus drivers send) to every subscriber. +fn publishEvent(event: []const u8) void { + for (&subscribers) |*slot| { + if (slot.*) |handle| { + if (!runtime.ipc.send(handle, event)) 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, + 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. +/// 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) { + writeLine("device-manager: child removed (device {d} port {d})\n", .{ child.parent, child.bus_address }); + child.used = false; + const event = protocol.ChildRemoved{ .parent = child.parent, .bus_address = child.bus_address }; + publishEvent(std.mem.asBytes(&event)); + } + } +} + +/// 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 +249,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; @@ -201,6 +281,11 @@ fn onDriverExit(driver: *Driver) void { /// sweep serves every armed deadline. fn sweepDeadlines() void { const now = system.clock(); + if (test_kill_pid != 0 and now >= test_kill_due_ns) { + writeLine("device-manager: test mode: killing the reporter\n", .{}); + _ = system.kill(test_kill_pid); + test_kill_pid = 0; + } for (&drivers) |*driver| { if (!driver.used) continue; switch (driver.state) { @@ -260,10 +345,18 @@ fn initialise(endpoint: runtime.ipc.Handle) bool { return true; } -fn onMessage(message: []const u8, reply: []u8, sender: u32) usize { +fn onMessage(message: []const u8, reply: []u8, sender: u32, capability: ?runtime.ipc.Handle) 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.enumerate) => return onEnumerate(reply), + @intFromEnum(protocol.Operation.subscribe) => return onSubscribe(reply, capability), + @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 +374,85 @@ 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() }); + if (status == 0) publishEvent(message[0..protocol.child_added_size]); + } 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) { + // Delayed, not immediate: the device-list scenario's subscriber needs a + // window to enumerate and subscribe before the events start. + test_usb_killed = true; + test_kill_pid = sender; + test_kill_due_ns = system.clock() + 2_000_000_000; + _ = system.timerOnce(manager_endpoint, 2100); + } + 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); +} + +/// An application asked for the tree: the mirror, as a header plus entries. +fn onEnumerate(reply: []u8) usize { + var count: u32 = 0; + var offset: usize = @sizeOf(protocol.EnumerateReply); + for (&children) |*child| { + if (!child.used) continue; + if (offset + @sizeOf(protocol.ChildEntry) > reply.len) break; + const entry = protocol.ChildEntry{ .parent = child.parent, .bus_address = child.bus_address, .identity = child.identity }; + @memcpy(reply[offset..][0..@sizeOf(protocol.ChildEntry)], std.mem.asBytes(&entry)); + offset += @sizeOf(protocol.ChildEntry); + count += 1; + } + const header = protocol.EnumerateReply{ .status = 0, .count = count }; + @memcpy(reply[0..@sizeOf(protocol.EnumerateReply)], std.mem.asBytes(&header)); + return offset; +} + +/// An application subscribed: its endpoint arrived as the call's capability. +fn onSubscribe(reply: []u8, capability: ?runtime.ipc.Handle) usize { + var status: i32 = -1; + if (capability) |handle| { + for (&subscribers) |*slot| { + if (slot.* == null) { + slot.* = handle; + status = 0; + break; + } + } + } + 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 +465,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, diff --git a/system/services/process-test/process-test.zig b/system/services/process-test/process-test.zig index 0cc2c50..0e5f353 100644 --- a/system/services/process-test/process-test.zig +++ b/system/services/process-test/process-test.zig @@ -51,8 +51,9 @@ fn awaitChildExit(endpoint: runtime.ipc.Handle) u32 { /// The harness-run child of the signals test: echoes requests, logs the two /// signals it handles. Terminate makes run() return, and returning from main is /// the clean exit the parent reads as ExitReason.exited. -fn echo(message: []const u8, reply: []u8, sender: u32) usize { +fn echo(message: []const u8, reply: []u8, sender: u32, capability: ?runtime.ipc.Handle) usize { _ = sender; + _ = capability; const n = @min(message.len, reply.len); @memcpy(reply[0..n], message[0..n]); return n; diff --git a/system/services/vfs/vfs.zig b/system/services/vfs/vfs.zig index 9e17af8..25a528a 100644 --- a/system/services/vfs/vfs.zig +++ b/system/services/vfs/vfs.zig @@ -91,7 +91,8 @@ fn releaseClientHandles(client: u32) void { } /// Handle one request from `sender`; write the reply into `out`, return its length. -fn handle(message: []const u8, out: []u8, sender: u32) usize { +fn handle(message: []const u8, out: []u8, sender: u32, capability: ?runtime.ipc.Handle) usize { + _ = capability; if (message.len < protocol.request_size) return fail(out); const request = std.mem.bytesToValue(protocol.Request, message[0..protocol.request_size]); const payload = message[protocol.request_size..]; diff --git a/test/qemu_test.py b/test/qemu_test.py index e9eec0c..79e168d 100644 --- a/test/qemu_test.py +++ b/test/qemu_test.py @@ -258,6 +258,37 @@ CASES = [ "smp": 4, "expect": r"DANOS-TEST-RESULT: PASS", "fail": r"DANOS-TEST-RESULT: FAIL"}, + # M18.2: bus tree reports — the xHCI driver scans its root-hub ports and + # reports both QEMU devices; the manager mirrors, prunes on the reporter's + # death, and the respawned driver re-reports (docs/device-manager.md). + {"name": "usb-report", + "smp": 4, + "timeout": 90, + "qemu_extra": ["-device", "qemu-xhci,id=xhci", + "-device", "usb-kbd,bus=xhci.0", + "-device", "usb-mouse,bus=xhci.0"], + "expect": r"device-manager: child added[\s\S]*" + r"device-manager: child added[\s\S]*" + r"device-manager: test mode: killing the reporter[\s\S]*" + r"device-manager: child removed[\s\S]*" + r"device-manager: restarting usb-xhci-bus[\s\S]*" + r"device-manager: child added", + "fail": r"DANOS-TEST-RESULT: FAIL"}, + # M18.3: the application surface — device-list enumerates the tree over IPC, + # subscribes (endpoint as capability), and observes the removed/added events + # the reporter's test-kill produces (docs/device-manager.md). + {"name": "device-list", + "smp": 4, + "timeout": 90, + "qemu_extra": ["-device", "qemu-xhci,id=xhci", + "-device", "usb-kbd,bus=xhci.0", + "-device", "usb-mouse,bus=xhci.0"], + "expect": r"device-list: 2 devices[\s\S]*" + r"device-list: subscribed[\s\S]*" + r"device-manager: test mode: killing the reporter[\s\S]*" + r"device-list: removed \(device[\s\S]*" + r"device-list: added \(device", + "fail": r"DANOS-TEST-RESULT: FAIL"}, # M18.1: the device manager's hello + restart policy — xHCI hellos clean and # stays; crash-test faults, is restarted with backoff (re-claiming its device # each time), and hits the crash-loop cap (docs/device-manager.md).