The application surface: enumerate, subscribe, and device-list (M18.3)

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.
This commit is contained in:
Daniel Samson
2026-07-13 00:49:03 +01:00
parent 79d859a111
commit d8778b4b70
13 changed files with 275 additions and 16 deletions
@@ -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
}
@@ -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;
@@ -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));
@@ -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;
+2 -1
View File
@@ -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..];