From d8778b4b70547c0fb74d5a0b4cdd4e16a89177e3 Mon Sep 17 00:00:00 2001 From: Daniel Samson <12231216+daniel-samson@users.noreply.github.com> Date: Mon, 13 Jul 2026 00:49:03 +0100 Subject: [PATCH] The application surface: enumerate, subscribe, and device-list (M18.3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Applications ask the device manager for the tree (enumerate: a header plus ChildEntry records) and subscribe to published add/remove events by handing their endpoint over as the call's capability — the input-service pattern; events are the same ChildAdded/ChildRemoved structs the bus drivers send, one encoding in both directions. device-list is the first client: it prints the tree, subscribes, and narrates the events through a driver restart. The protocol's message maximum is capped at the kernel's IPC MESSAGE_MAXIMUM (256 bytes, ten entries per reply; paging joins the protocol when a tree outgrows one message). The startUserTask debug print is gone: it wrote to serial unserialized against user-space lines and sheared concurrent log markers in half — the root cause of the scenario flakes. --- build.zig | 3 + docs/device-manager.md | 6 +- docs/m17-m18-plan.md | 7 +- library/runtime/service.zig | 8 +- system/drivers/usb-xhci-bus/usb-xhci-bus.zig | 3 +- system/kernel/scheduler.zig | 5 +- system/kernel/tests.zig | 35 ++++++++ system/services/device-list/device-list.zig | 88 +++++++++++++++++++ .../device-manager-protocol.zig | 42 ++++++++- .../device-manager/device-manager.zig | 73 ++++++++++++++- system/services/process-test/process-test.zig | 3 +- system/services/vfs/vfs.zig | 3 +- test/qemu_test.py | 15 ++++ 13 files changed, 275 insertions(+), 16 deletions(-) create mode 100644 system/services/device-list/device-list.zig 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 a93f6b3..b28d998 100644 --- a/docs/device-manager.md +++ b/docs/device-manager.md @@ -7,8 +7,10 @@ cap are in — usb-xhci-bus is the first conforming driver, and the 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 -(M18.3) remains design. The primitives underneath are real ([process-management.md](process-management.md): +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 3b35e92..d3e2a72 100644 --- a/docs/m17-m18-plan.md +++ b/docs/m17-m18-plan.md @@ -58,7 +58,12 @@ only when its definition of green holds. 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) -- [ ] **M18.3** — app surface: enumerate/subscribe + device-list +- [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 7f3048f..ee7898a 100644 --- a/system/drivers/usb-xhci-bus/usb-xhci-bus.zig +++ b/system/drivers/usb-xhci-bus/usb-xhci-bus.zig @@ -146,10 +146,11 @@ fn scanPorts(manager: runtime.ipc.Handle) void { } /// 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 8310fd1..bf6237b 100644 --- a/system/kernel/tests.zig +++ b/system/kernel/tests.zig @@ -142,6 +142,8 @@ pub fn run(case: []const u8, boot_information: *const BootInformation) void { 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")) { @@ -1730,6 +1732,39 @@ fn usbReportTest(boot_information: *const BootInformation) void { 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 f359d3b..2a49db4 100644 --- a/system/services/device-manager/device-manager-protocol.zig +++ b/system/services/device-manager/device-manager-protocol.zig @@ -19,11 +19,13 @@ pub const Role = enum(u8) { device = 2, }; -/// The message kinds. `enumerate`/`subscribe` land 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 @@ -97,5 +99,41 @@ pub const ReportReply = extern struct { 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 c3dece0..b59f2a1 100644 --- a/system/services/device-manager/device-manager.zig +++ b/system/services/device-manager/device-manager.zig @@ -108,6 +108,25 @@ 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 @@ -144,11 +163,14 @@ fn addChild(parent: u64, bus_address: u64, identity: u64, reporter: u32) bool { /// 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)); } } } @@ -259,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) { @@ -318,11 +345,13 @@ 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, } @@ -355,15 +384,19 @@ fn onChildAdded(message: []const u8, reply: []u8, sender: u32) usize { 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; - writeLine("device-manager: test mode: killing the reporter\n", .{}); - _ = system.kill(sender); + test_kill_pid = sender; + test_kill_due_ns = system.clock() + 2_000_000_000; + _ = system.timerOnce(manager_endpoint, 2100); } return @sizeOf(protocol.ReportReply); } @@ -386,6 +419,40 @@ fn onChildRemoved(message: []const u8, reply: []u8, sender: u32) usize { 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)); 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 9e212dc..79e168d 100644 --- a/test/qemu_test.py +++ b/test/qemu_test.py @@ -274,6 +274,21 @@ CASES = [ 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).