runtime: consolidate the device-manager hello into runtime.device_manager

Every supervised driver owes the device manager a hello at startup, and
the retry-lookup-call-check for it had been copied into five drivers:
usb.helloManager (misfiled in the USB client) plus hand-rolled twins in
display, virtio-gpu, pci-bus, and usb-xhci-bus.

Extract it once as a runtime client, runtime.device_manager.hello(role,
device_id) ?Handle — returns the manager endpoint (bus drivers keep it to
report children through), null when there is no manager or it refused the
handshake, and logs the outcome itself so each call site is one line.

Also correct two roles while collapsing their calls: virtio-gpu and the
display driver each claim one PCI function and report no children, so they
are Role.device, not Role.bus. The manager ignores role today, so this is
cosmetic, but it matches the protocol's own definition (bus = reports
children via child_added).

Behavior-preserving otherwise: pci-bus and usb-xhci-bus move their hello
logging from raw serial writes to std.log, which the kernel renders with
the same "<path>: " prefix, so driver-restart still matches
"usb-xhci-bus: hello acknowledged". zig build clean; 8 QEMU cases pass
(driver-restart, pci-scan, usb-hid, usb-storage, virtio-gpu,
display-reattach, device-list, device-manager).
This commit is contained in:
Daniel Samson
2026-07-22 19:27:20 +01:00
parent d27377ab1e
commit ea470afe84
10 changed files with 85 additions and 108 deletions
+59
View File
@@ -0,0 +1,59 @@
//! Client side of the device-manager protocol (docs/device-manager.md): the
//! calls a supervised driver makes *to* the manager, as opposed to
//! `device-manager-protocol.zig`, which is the wire contract both sides share.
//!
//! Today this is just the hello handshake every spawned driver owes. The
//! manager arms a hello deadline when it spawns a driver
//! (system/services/device-manager/device-manager.zig): a driver that stays
//! silent past it is assumed wedged before `main` and stopped, so every driver
//! calls `hello` early in its startup. The retry-lookup-call-check this used to
//! be — copied byte-for-byte into each bus and class driver — lives here once.
const std = @import("std");
const ipc = @import("ipc.zig");
const system = @import("system.zig");
const protocol = @import("device-manager-protocol");
/// What kind of driver is announcing itself (a bus that reports children, or a
/// leaf device). Re-exported so callers name it without importing the protocol.
pub const Role = protocol.Role;
/// Endpoint lookups while the manager is still registering, and the pause
/// between them: 100 x 20 ms = ~2 s, comfortably inside the manager's 3 s hello
/// deadline (device-manager.zig `hello_deadline_ms`).
const lookup_attempts: u32 = 100;
const lookup_pause_ms: u64 = 20;
/// Say hello to the device manager and return its endpoint, or null if there is
/// no manager (best-effort standalone bring-up) or it refused the handshake
/// (version mismatch, unknown sender). Bus drivers keep the returned handle to
/// report children through; a driver that runs fine unsupervised discards it
/// with `_ =`, and a driver that requires supervision bails on null.
///
/// Logs the outcome itself (attribution is the kernel's, via the process's
/// binary path), so callers stay a single line.
pub fn hello(role: Role, device_id: u64) ?ipc.Handle {
var attempts: u32 = 0;
const manager = while (attempts < lookup_attempts) : (attempts += 1) {
if (ipc.lookup(.device_manager)) |handle| break handle;
system.sleep(lookup_pause_ms);
} else {
std.log.info("no device manager to hello", .{});
return null;
};
const message = protocol.Hello{ .role = @intFromEnum(role), .device_id = device_id };
var reply: [protocol.reply_size]u8 = undefined;
const length = ipc.call(manager, std.mem.asBytes(&message), &reply) catch {
std.log.info("hello call failed", .{});
return null;
};
if (length < protocol.reply_size or
std.mem.bytesToValue(protocol.HelloReply, reply[0..protocol.reply_size]).status != 0)
{
std.log.info("hello refused", .{});
return null;
}
std.log.info("hello acknowledged", .{});
return manager;
}
+3
View File
@@ -23,6 +23,9 @@ pub const vfs_protocol = @import("vfs-protocol");
/// The device-manager protocol: hello + tree reports (docs/device-manager.md). /// The device-manager protocol: hello + tree reports (docs/device-manager.md).
pub const device_manager_protocol = @import("device-manager-protocol"); pub const device_manager_protocol = @import("device-manager-protocol");
/// Client for talking to the device manager (the hello handshake a supervised
/// driver owes at startup). See library/runtime/device-manager.zig.
pub const device_manager = @import("device-manager.zig");
/// The power protocol: events (button, lid, battery) + shutdown (docs/power.md). /// The power protocol: events (button, lid, battery) + shutdown (docs/power.md).
pub const power_protocol = @import("power-protocol"); pub const power_protocol = @import("power-protocol");
+1 -18
View File
@@ -5,7 +5,7 @@
//! service and `device.zig` over the raw device calls. //! service and `device.zig` over the raw device calls.
//! //!
//! A class driver, spawned with its interface's assigned device id as argv[1]: //! A class driver, spawned with its interface's assigned device id as argv[1]:
//! if (!usb.helloManager(id)) return; // meet the spawn deadline //! if (device_manager.hello(.device, id) == null) return; // meet the spawn deadline
//! var device = usb.open(id) orelse return; // open + get its endpoints //! var device = usb.open(id) orelse return; // open + get its endpoints
//! _ = device.controlOut(usb_abi.setProtocol(...));// class requests, descriptors //! _ = device.controlOut(usb_abi.setProtocol(...));// class requests, descriptors
//! _ = device.subscribeInterrupt(address, length); // reports arrive asynchronously //! _ = device.subscribeInterrupt(address, length); // reports arrive asynchronously
@@ -19,7 +19,6 @@ const std = @import("std");
const ipc = @import("ipc.zig"); const ipc = @import("ipc.zig");
const system = @import("system.zig"); const system = @import("system.zig");
const protocol = @import("usb-transfer-protocol"); const protocol = @import("usb-transfer-protocol");
const device_manager = @import("device-manager-protocol");
pub const Endpoint = protocol.Endpoint; pub const Endpoint = protocol.Endpoint;
pub const InterruptReport = protocol.InterruptReport; pub const InterruptReport = protocol.InterruptReport;
@@ -144,19 +143,3 @@ pub fn open(device_id: u64) ?Device {
for (0..device.endpoint_count) |index| device.endpoints[index] = open_reply.endpoints[index]; for (0..device.endpoint_count) |index| device.endpoints[index] = open_reply.endpoints[index];
return device; return device;
} }
/// Hello the device manager as a class driver (Role.device) so a supervised
/// spawn meets its hello deadline. Retries while the manager comes up.
pub fn helloManager(device_id: u64) bool {
var attempts: usize = 0;
const manager = while (attempts < 100) : (attempts += 1) {
if (ipc.lookup(.device_manager)) |handle| break handle;
system.sleep(20);
} else return false;
const hello = device_manager.Hello{ .role = @intFromEnum(device_manager.Role.device), .device_id = device_id };
var reply: [device_manager.message_maximum]u8 = undefined;
const length = ipc.call(manager, std.mem.asBytes(&hello), &reply) catch return false;
if (length < device_manager.reply_size) return false;
return std.mem.bytesToValue(device_manager.HelloReply, reply[0..device_manager.reply_size]).status == 0;
}
+5 -2
View File
@@ -21,12 +21,15 @@ const system = runtime.system;
const ipc = runtime.ipc; const ipc = runtime.ipc;
const display_protocol = runtime.display_protocol; const display_protocol = runtime.display_protocol;
const scanout_protocol = runtime.scanout_protocol; const scanout_protocol = runtime.scanout_protocol;
const device_manager_protocol = runtime.device_manager_protocol;
var device_id: u64 = 0; var device_id: u64 = 0;
fn initialise(endpoint: ipc.Handle) bool { fn initialise(endpoint: ipc.Handle) bool {
_ = endpoint; _ = endpoint;
// Hello the device manager (role: device — we claim one GPU's PCI function
// and serve its display engine; we report no children). Best-effort: without a
// manager the driver still runs standalone; when present, the manager marks us
// up before the hello deadline and restarts us if we die.
_ = runtime.device_manager.hello(.device, device_id);
return true; return true;
} }
+4 -22
View File
@@ -103,28 +103,10 @@ fn initialise(endpoint: runtime.ipc.Handle) bool {
return false; return false;
}; };
// The handshake, then the scan (reports join in M19.2). // The handshake (role: bus — we enumerate PCI and report the functions we
var manager: ?runtime.ipc.Handle = null; // find), then the scan. Keep the manager handle to report children through;
var tries: u32 = 0; // a supervised bus that cannot reach its manager has nothing to serve.
while (manager == null and tries < 100) : (tries += 1) { manager_handle = runtime.device_manager.hello(.bus, bridge_id) orelse return false;
manager = runtime.ipc.lookup(.device_manager);
if (manager == null) runtime.system.sleep(20);
}
const h = manager orelse {
_ = runtime.system.write("/system/drivers/pci-bus: no device manager to hello\n");
return false;
};
const hello = protocol.Hello{ .role = @intFromEnum(protocol.Role.bus), .device_id = bridge_id };
var reply: [protocol.message_maximum]u8 = undefined;
const n = runtime.ipc.call(h, std.mem.asBytes(&hello), &reply) catch {
_ = runtime.system.write("/system/drivers/pci-bus: hello call failed\n");
return false;
};
if (n < protocol.reply_size or std.mem.bytesToValue(protocol.HelloReply, reply[0..protocol.reply_size]).status != 0) {
_ = runtime.system.write("/system/drivers/pci-bus: hello refused\n");
return false;
}
manager_handle = h;
scan(); scan();
return true; return true;
+1 -4
View File
@@ -72,10 +72,7 @@ pub fn main(init: runtime.process.Init) void {
const layout = xkb.byName(init.arguments.get(2) orelse "us") orelse xkb.us; const layout = xkb.byName(init.arguments.get(2) orelse "us") orelse xkb.us;
// Hello the manager first (meet the spawn deadline), then open the device. // Hello the manager first (meet the spawn deadline), then open the device.
if (!runtime.usb.helloManager(device_id)) { if (runtime.device_manager.hello(.device, device_id) == null) return;
_ = runtime.system.write("/system/drivers/usb-hid/keyboard: hello to device manager failed\n");
return;
}
var device = runtime.usb.open(device_id) orelse { var device = runtime.usb.open(device_id) orelse {
std.log.info("could not open device {d}", .{device_id}); std.log.info("could not open device {d}", .{device_id});
return; return;
+1 -4
View File
@@ -37,10 +37,7 @@ pub fn main(init: runtime.process.Init) void {
return; return;
}; };
if (!runtime.usb.helloManager(device_id)) { if (runtime.device_manager.hello(.device, device_id) == null) return;
_ = runtime.system.write("/system/drivers/usb-hid/mouse: hello to device manager failed\n");
return;
}
var device = runtime.usb.open(device_id) orelse { var device = runtime.usb.open(device_id) orelse {
std.log.info("could not open device {d}", .{device_id}); std.log.info("could not open device {d}", .{device_id});
return; return;
+1 -4
View File
@@ -69,10 +69,7 @@ var bring_up_failed = false;
fn initialise(endpoint: runtime.ipc.Handle) bool { fn initialise(endpoint: runtime.ipc.Handle) bool {
_ = endpoint; _ = endpoint;
if (!runtime.usb.helloManager(device_id)) { if (runtime.device_manager.hello(.device, device_id) == null) return false;
_ = runtime.system.write("/system/drivers/usb-storage: hello to device manager failed\n");
return false;
}
device = runtime.usb.open(device_id) orelse { device = runtime.usb.open(device_id) orelse {
std.log.info("could not open device {d}", .{device_id}); std.log.info("could not open device {d}", .{device_id});
return false; return false;
+6 -25
View File
@@ -131,32 +131,13 @@ fn initialise(endpoint: runtime.ipc.Handle) bool {
return false; return false;
} }
// The handshake: role, protocol version, assignment — inside the manager's // The handshake (role: bus — we enumerate USB ports and report the devices
// deadline (the lookup retries cover the manager still registering). // behind them), inside the manager's hello deadline. Keep the handle: the
var manager: ?runtime.ipc.Handle = null; // tick's hot-plug dispatch reports through it.
var tries: u32 = 0; const handle = runtime.device_manager.hello(.bus, controller_id) orelse return false;
while (manager == null and tries < 100) : (tries += 1) { manager_handle = handle;
manager = runtime.ipc.lookup(.device_manager);
if (manager == null) runtime.system.sleep(20);
}
const h = manager orelse {
_ = runtime.system.write("/system/drivers/usb-xhci-bus: no device manager to hello\n");
return false;
};
manager_handle = h; // the tick's hot-plug dispatch reports through this
const hello = protocol.Hello{ .role = @intFromEnum(protocol.Role.bus), .device_id = controller_id };
var reply: [protocol.message_maximum]u8 = undefined;
const n = runtime.ipc.call(h, std.mem.asBytes(&hello), &reply) catch {
_ = runtime.system.write("/system/drivers/usb-xhci-bus: hello call failed\n");
return false;
};
if (n < protocol.reply_size or std.mem.bytesToValue(protocol.HelloReply, reply[0..protocol.reply_size]).status != 0) {
_ = runtime.system.write("/system/drivers/usb-xhci-bus: hello refused\n");
return false;
}
_ = runtime.system.write("/system/drivers/usb-xhci-bus: hello acknowledged\n");
scanPorts(h); scanPorts(handle);
// Arm the poll timer that drains interrupt reports from the event ring. It is // Arm the poll timer that drains interrupt reports from the event ring. It is
// re-armed on each tick in onNotification; class drivers subscribe later. // re-armed on each tick in onNotification; class drivers subscribe later.
+4 -29
View File
@@ -23,7 +23,6 @@ const system = runtime.system;
const ipc = runtime.ipc; const ipc = runtime.ipc;
const dp = runtime.display_protocol; const dp = runtime.display_protocol;
const sp = runtime.scanout_protocol; const sp = runtime.scanout_protocol;
const dm = runtime.device_manager_protocol;
const vp = @import("virtio-pci.zig"); const vp = @import("virtio-pci.zig");
const vg = @import("virtio-gpu-protocol.zig"); const vg = @import("virtio-gpu-protocol.zig");
@@ -400,9 +399,10 @@ fn initialise(endpoint: ipc.Handle) bool {
std.log.info("scanout {d}x{d} online", .{ current_width, current_height }); std.log.info("scanout {d}x{d} online", .{ current_width, current_height });
// Hello the device manager so it counts us as up (and does not stop us at the hello // Hello the device manager so it counts us as up (and does not stop us at the hello
// deadline). A restarted instance re-hellos here and re-announces below — the compositor // deadline). Role: device — we claim one PCI function and serve its scanout; we report
// re-attaches to the fresh scanout (V6). // no children. A restarted instance re-hellos here and re-announces below — the compositor
helloManager(); // re-attaches to the fresh scanout (V6). Best-effort: standalone bring-up has no manager.
_ = runtime.device_manager.hello(.device, device_id);
// Read the monitor's EDID (best-effort, when the device offers it) — the mode list a real // Read the monitor's EDID (best-effort, when the device offers it) — the mode list a real
// driver derives from it; we log the preferred mode and keep our fixed offered list. // driver derives from it; we log the preferred mode and keep our fixed offered list.
@@ -513,31 +513,6 @@ fn presentFull() bool {
return true; return true;
} }
/// Hello the device manager (role: bus — we own a PCI function, though we report no children):
/// the handshake that marks us up so the manager doesn't stop us at the hello deadline, and
/// (as a supervised driver) restarts us if we die. Best-effort: without a manager we still run.
fn helloManager() void {
var tries: u32 = 0;
const manager = while (tries < 100) : (tries += 1) {
if (ipc.lookup(.device_manager)) |h| break h;
system.sleep(20);
} else {
std.log.info("no device manager to hello", .{});
return;
};
const hello = dm.Hello{ .role = @intFromEnum(dm.Role.bus), .device_id = device_id };
var reply: [dm.reply_size]u8 = undefined;
const n = ipc.call(manager, std.mem.asBytes(&hello), &reply) catch {
std.log.info("hello call failed", .{});
return;
};
if (n < dm.reply_size or std.mem.bytesToValue(dm.HelloReply, reply[0..dm.reply_size]).status != 0) {
std.log.info("hello refused", .{});
return;
}
std.log.info("hello acknowledged", .{});
}
/// Announce the scanout to the display service so it upgrades off the GOP framebuffer: hand it /// Announce the scanout to the display service so it upgrades off the GOP framebuffer: hand it
/// the shared surface as a capability plus the geometry. Best-effort and non-fatal — without a /// the shared surface as a capability plus the geometry. Best-effort and non-fatal — without a
/// display service (the standalone virtio-gpu bring-up test) the driver is still a valid /// display service (the standalone virtio-gpu bring-up test) the driver is still a valid