The first driver to stop claiming its own hardware. The device manager holds the controller and transfers it in the hello reply, so its matching becomes authoritative instead of advisory — until now the driver claimed the id it found in argv[1], and any process could have claimed the same integer first. The manager claims before it spawns, so there is no window in which anything else could take the device, and transfers in onHello using invocation.sender — the kernel-stamped task id, which cannot be forged by the caller. hello is synchronous, so the transfer has completed before the reply lands: no gap between being told yes and holding the thing. usb-xhci-bus's hello moves from after controller bring-up to before anything that needs the device, which is the bring-up reorder the design predicted. It is the first member of an explicit delegated set, so every unconverted driver keeps claiming exactly as before and the suite stays green; the set and device_claim both go at D6. D3 and D4 could not be separated and the plan records why: the moment the manager claims, any driver still calling device_claim is refused, and D3 applied to nothing changes no behaviour and cannot be tested. This step introduced a regression and the incremental conversion is what caught it. confineDevice runs inside systemDeviceClaim, so a device arriving by transfer was never confined for its new owner. Three IOMMU+USB cases failed on the driver's DMA rings going unbound, and two worse consequences were latent: a manager death would have torn down a domain a live driver was using, and a driver death would have leaked one. iommu.reassign now moves the confinement with the device, keeping the domain and its attachment intact so it never translates through nothing. Converting all five drivers at once would have produced the same three failures with five suspects. A log line of mine claimed "holding controller device N" before anything verified it — it printed even in the failure case, where the driver held nothing. Reworded to state only what is known there: where the registers are. usb-hid asserts the delegation with the device id backreferenced, so the id delegated and the id the driver ends up with must match. Emptying the delegated set fails it with "hello acknowledged" then "mmio_map failed". usb-hub failed once in a full run and has passed six times since (four isolated, two full) — recorded in the plan as a suspected instance of the known intermittent AP fault, not dismissed, since this step did shift boot timing. Suite 118/118.
617 lines
28 KiB
Zig
617 lines
28 KiB
Zig
//! /system/services/device-manager — the ring-3 process that turns the device
|
|
//! tree into a running system: **the matcher and the supervisor**
|
|
//! (docs/device-manager.md). The kernel enumerates the hardware and enforces the
|
|
//! claim capability (mechanism); this decides which driver serves which device,
|
|
//! spawns it, and keeps it alive (policy). Keeping that split in user space is
|
|
//! the whole point of the microkernel: the manager is an ordinary, restartable
|
|
//! process with no special privilege.
|
|
//!
|
|
//! M18.1 (this increment): the manager is a harness service on the well-known
|
|
//! `.device_manager` endpoint. Every driver is spawned **supervised** — exit
|
|
//! notifications land in the same loop as protocol messages. Drivers with an
|
|
//! assignment must `hello` within a deadline or be stopped; a driver that dies
|
|
//! is restarted with backoff, and a crash loop (three fast deaths) marks it
|
|
//! failed instead of respawning forever. Exit reasons (M17.2) drive the
|
|
//! decision: a clean exit meant to stop; only faults and missed deadlines
|
|
//! restart. Tree reports (`child_added`) land in M18.2.
|
|
|
|
const std = @import("std");
|
|
const device = @import("driver");
|
|
const ipc = @import("ipc");
|
|
const process = @import("process");
|
|
const service = @import("service");
|
|
const time = @import("time");
|
|
const memory = @import("memory");
|
|
const logging = @import("logging");
|
|
const device_manager_protocol = @import("device-manager-protocol");
|
|
const envelope = @import("envelope");
|
|
const registry = @import("device-registry");
|
|
|
|
/// The generated device-manager dispatch, plus the subscriber machinery the
|
|
/// harness owns (P4c): the watcher table, the reserved `subscribe` verb, the
|
|
/// exit sweep, and the fan-out. One manager per system, so the handler context is
|
|
/// empty and the tables stay in this file's globals.
|
|
const Serve = service.Subscribers(device_manager_protocol.Protocol, void);
|
|
|
|
const Invocation = envelope.Invocation;
|
|
const Answer = envelope.Answer;
|
|
const fs = @import("file-system");
|
|
|
|
// --- the device registry ------------------------------------------------------
|
|
// Driver matching is data-driven and authoritative: /system/configuration/devices.csv (parsed by
|
|
// the device-registry module) names, per bus, which driver binds a reported
|
|
// device, the most-specific match winning. There is no compiled-in fallback — a
|
|
// device no row matches goes unbound and is logged. This retired the hand-kept
|
|
// pciDriverForIdentity / hidDriverFor / usbDriverForIdentity switch tables
|
|
// (docs/device-manager.md: "matching stays code until the third bus").
|
|
|
|
/// The CSV bytes, held for the life of the process because the parsed rules'
|
|
/// string fields (hid, driver) slice into this buffer.
|
|
var registry_source: [8192]u8 = undefined;
|
|
var registry_rules: [64]registry.Rule = undefined;
|
|
var registry_count: usize = 0;
|
|
|
|
/// Read and parse /system/configuration/devices.csv once at boot. The file lives in the initial
|
|
/// ramdisk, which the kernel serves directly — no filesystem service need be up
|
|
/// (fat is spawned after the manager), so this is a plain fs.open + read.
|
|
fn loadRegistry() void {
|
|
var file = fs.open("/system/configuration/devices.csv", .{}) orelse {
|
|
_ = logging.write("/system/services/device-manager: /system/configuration/devices.csv missing — nothing will match\n");
|
|
return;
|
|
};
|
|
defer file.close();
|
|
var used: usize = 0;
|
|
while (used < registry_source.len) {
|
|
const n = file.read(registry_source[used..]) orelse break;
|
|
if (n == 0) break;
|
|
used += n;
|
|
}
|
|
const result = registry.parse(registry_source[0..used], ®istry_rules);
|
|
registry_count = result.count;
|
|
if (result.malformed != 0) std.log.info("/system/configuration/devices.csv: {d} malformed line(s) skipped", .{result.malformed});
|
|
if (result.truncated) _ = logging.write("/system/services/device-manager: /system/configuration/devices.csv has more rules than the table holds\n");
|
|
std.log.info("/system/configuration/devices.csv: {d} rule(s) loaded", .{registry_count});
|
|
}
|
|
|
|
/// Build a registry Identity from a bus driver's report: the bus it named, the
|
|
/// class triple unpacked from `identity` (0xCCSSPP — the same packing for a PCI
|
|
/// class code and a USB class triple), the widened numeric ids, and the ACPI hid.
|
|
fn identityFromReport(report: device_manager_protocol.ChildAdded) registry.Identity {
|
|
const bus: registry.Bus = switch (report.bus) {
|
|
@intFromEnum(device_manager_protocol.BusKind.pci) => .pci,
|
|
@intFromEnum(device_manager_protocol.BusKind.usb) => .usb,
|
|
@intFromEnum(device_manager_protocol.BusKind.acpi) => .acpi,
|
|
else => .unknown,
|
|
};
|
|
const hid_len = std.mem.indexOfScalar(u8, &report.hid, 0) orelse report.hid.len;
|
|
return .{
|
|
.bus = bus,
|
|
.base = @truncate(report.identity >> 16),
|
|
.subclass = @truncate(report.identity >> 8),
|
|
.prog_if = @truncate(report.identity),
|
|
.vendor = report.vendor,
|
|
.device = report.device,
|
|
.subsystem = report.subsystem,
|
|
.hid = report.hid[0..hid_len],
|
|
};
|
|
}
|
|
|
|
/// Whether some driver entry already serves registered device `device_id` —
|
|
/// a re-report after a bus restart must not spawn a second instance.
|
|
fn driverForDevice(device_id: u64) bool {
|
|
for (&drivers) |*driver| {
|
|
if (driver.used and driver.device_id == device_id) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// --- supervision -------------------------------------------------------------
|
|
|
|
/// How long a protocol driver has to hello after its spawn.
|
|
const hello_deadline_ms: u64 = 3000;
|
|
/// Deaths faster than this count toward the crash loop; slower ones reset it.
|
|
const fast_death_ns: u64 = 2_000_000_000;
|
|
/// Consecutive fast deaths before the manager gives up on a driver.
|
|
const crash_loop_cap: u32 = 3;
|
|
/// Restart backoff: base << (restarts - 1), so 300 ms, 600 ms, 1200 ms.
|
|
const backoff_base_ms: u64 = 300;
|
|
|
|
const DriverState = enum {
|
|
awaiting_hello, // spawned; the deadline is armed (protocol drivers only)
|
|
running,
|
|
restarting, // dead; respawn due at restart_due_ns
|
|
stopped, // exited cleanly — it meant to; not restarted
|
|
failed, // crash loop, or unspawnable; the manager gave up
|
|
};
|
|
|
|
const Driver = struct {
|
|
used: bool = false,
|
|
name_buffer: [64]u8 = undefined, // fits a full binary path (abi.maximum_process_name)
|
|
name_len: usize = 0,
|
|
// The assigned device id (becomes argv[1]), or device_manager_protocol.no_device.
|
|
device_id: u64 = device_manager_protocol.no_device,
|
|
// Whether this driver speaks the protocol (hello expected, deadline
|
|
// enforced). Legacy drivers (e.g. ps2-bus) are supervised and restarted
|
|
// but not yet required to hello.
|
|
speaks_protocol: bool = false,
|
|
process_id: u32 = 0,
|
|
state: DriverState = .running,
|
|
restarts: u32 = 0,
|
|
spawn_ns: u64 = 0,
|
|
hello_deadline_ns: u64 = 0,
|
|
restart_due_ns: u64 = 0,
|
|
|
|
fn name(driver: *const Driver) []const u8 {
|
|
return driver.name_buffer[0..driver.name_len];
|
|
}
|
|
};
|
|
|
|
const maximum_drivers = 16;
|
|
var drivers: [maximum_drivers]Driver = .{Driver{}} ** maximum_drivers;
|
|
var manager_endpoint: ipc.Handle = 0;
|
|
var test_restart_mode = false;
|
|
var test_usb_restart_mode = false;
|
|
var test_usb_killed = false;
|
|
var test_pci_restart_mode = false;
|
|
var test_scanout_restart_mode = false;
|
|
var test_scanout_killed = false;
|
|
var test_kill_pid: u32 = 0;
|
|
var test_kill_due_ns: u64 = 0;
|
|
|
|
/// 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,
|
|
// The kernel device id (registered by the reporter), or device_manager_protocol.no_device.
|
|
device_id: u64 = 0,
|
|
reporter: u32 = 0, // the reporting driver instance's process id
|
|
};
|
|
|
|
const maximum_children = 64; // ACPI adds ~34 device nodes (M20.2), plus PCI + USB
|
|
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, device_id: 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.device_id = device_id;
|
|
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, .device_id = device_id, .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) {
|
|
std.log.info("child removed (device {d} port {d})", .{ child.parent, child.bus_address });
|
|
child.used = false;
|
|
Serve.publish(.child_removed, 0, .{ .parent = child.parent, .bus_address = child.bus_address });
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 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;
|
|
}
|
|
|
|
/// The driver entry a live process id belongs to. Zero is not a process id here:
|
|
/// it is what `onDriverExit` writes back to retire an id it has already acted on,
|
|
/// so a second notification for the same death matches nothing.
|
|
fn driverByProcess(process_id: u32) ?*Driver {
|
|
if (process_id == 0) return null;
|
|
for (&drivers) |*driver| {
|
|
if (driver.used and driver.process_id == process_id) return driver;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// Whether a singleton driver is already in the table (two ACPI nodes can both
|
|
/// map to ps2-bus; one instance serves both).
|
|
fn alreadySupervised(name: []const u8) bool {
|
|
for (&drivers) |*driver| {
|
|
if (driver.used and std.mem.eql(u8, driver.name(), name)) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// Drivers that receive their device from the manager rather than claiming it
|
|
/// themselves. Scaffolding for the conversion, not a permanent concept: it exists so
|
|
/// each driver can move across one at a time with the suite green throughout, and it
|
|
/// disappears at D6 when `device_claim` stops being a way to acquire a device at all
|
|
/// (docs/bounds-track-plan.md, Run 2).
|
|
///
|
|
/// `usb-xhci-bus` is first because it was the first driver to conform to `hello`
|
|
/// (device-manager.md, M18.1), so it is the one whose handshake is best proven.
|
|
const delegated_drivers = [_][]const u8{
|
|
"/system/drivers/usb-xhci-bus",
|
|
};
|
|
|
|
fn isDelegated(name: []const u8) bool {
|
|
for (delegated_drivers) |candidate| {
|
|
if (std.mem.eql(u8, name, candidate)) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// Record a driver in the table and spawn its first instance.
|
|
fn addDriver(name: []const u8, device_id: u64, speaks_protocol: bool) void {
|
|
for (&drivers) |*driver| {
|
|
if (driver.used) continue;
|
|
const n = @min(name.len, driver.name_buffer.len);
|
|
@memcpy(driver.name_buffer[0..n], name[0..n]);
|
|
driver.name_len = n;
|
|
driver.device_id = device_id;
|
|
driver.speaks_protocol = speaks_protocol;
|
|
driver.used = true;
|
|
spawnDriver(driver);
|
|
return;
|
|
}
|
|
std.log.info("driver table full; cannot supervise {s}", .{name});
|
|
}
|
|
|
|
/// (Re)spawn a driver instance: supervised on the manager's own endpoint, the
|
|
/// device id as argv[1] when it has one, the hello deadline armed when it
|
|
/// speaks the protocol.
|
|
fn spawnDriver(driver: *Driver) void {
|
|
// Take the device before the driver exists, so there is no window in which anyone
|
|
// else could claim it — which is the whole of what makes the handover authoritative
|
|
// rather than advisory. Re-claiming across a restart is expected to say
|
|
// AlreadyClaimed once the manager already holds it, and that is fine: it means the
|
|
// device never left our hands while the driver was dead.
|
|
if (isDelegated(driver.name()) and driver.device_id != device_manager_protocol.no_device) {
|
|
device.claim(driver.device_id) catch |e| switch (e) {
|
|
error.AlreadyClaimed => {}, // ours already, from a previous spawn of this driver
|
|
else => {
|
|
std.log.warn("cannot hold device {d} for {s}: {s}", .{ driver.device_id, driver.name(), @errorName(e) });
|
|
driver.state = .failed;
|
|
return;
|
|
},
|
|
};
|
|
}
|
|
|
|
var id_text: [20]u8 = undefined;
|
|
var arguments: [1][]const u8 = undefined;
|
|
var argument_count: usize = 0;
|
|
if (driver.device_id != device_manager_protocol.no_device) {
|
|
arguments[0] = std.fmt.bufPrint(&id_text, "{d}", .{driver.device_id}) catch return;
|
|
argument_count = 1;
|
|
}
|
|
const child = process.spawnSupervised(driver.name(), arguments[0..argument_count], manager_endpoint) orelse {
|
|
std.log.info("failed to spawn {s}", .{driver.name()});
|
|
driver.state = .failed;
|
|
return;
|
|
};
|
|
driver.process_id = child;
|
|
driver.spawn_ns = time.clock();
|
|
if (driver.speaks_protocol) {
|
|
driver.state = .awaiting_hello;
|
|
driver.hello_deadline_ns = driver.spawn_ns + hello_deadline_ms * 1_000_000;
|
|
_ = time.timerOnce(manager_endpoint, hello_deadline_ms + 100);
|
|
} else {
|
|
driver.state = .running;
|
|
}
|
|
if (driver.device_id != device_manager_protocol.no_device) {
|
|
std.log.info("spawned {s} for device {d}", .{ driver.name(), driver.device_id });
|
|
} else {
|
|
std.log.info("spawned {s}", .{driver.name()});
|
|
}
|
|
}
|
|
|
|
/// 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 {
|
|
const dead = driver.process_id;
|
|
// One death, two notifications: the manager is this driver's supervisor (its
|
|
// spawn named this endpoint) *and*, since P4c put the watcher table in the
|
|
// harness, a subscriber to published exits. Both badges carry the same id, and
|
|
// the ring delivers them separately — so the id is retired here, before any
|
|
// decision is taken, and the second notification finds no driver to act on.
|
|
// Without this the backoff would count one death twice and the crash-loop cap
|
|
// would fire at half the deaths it names.
|
|
driver.process_id = 0;
|
|
pruneChildrenOf(dead);
|
|
const reason = process.exitReason(dead) orelse .fault;
|
|
if (reason == .exited) {
|
|
driver.state = .stopped;
|
|
std.log.info("{s} exited cleanly; not restarting", .{driver.name()});
|
|
return;
|
|
}
|
|
const now = time.clock();
|
|
const alive_ns = now - driver.spawn_ns;
|
|
driver.restarts = if (alive_ns < fast_death_ns) driver.restarts + 1 else 1;
|
|
if (driver.restarts >= crash_loop_cap) {
|
|
driver.state = .failed;
|
|
std.log.info("{s} is failing repeatedly (crash loop); giving up", .{driver.name()});
|
|
return;
|
|
}
|
|
const delay_ms = backoff_base_ms << @intCast(driver.restarts - 1);
|
|
driver.state = .restarting;
|
|
driver.restart_due_ns = now + delay_ms * 1_000_000;
|
|
std.log.info("restarting {s} in {d} ms (died: {s})", .{ driver.name(), delay_ms, @tagName(reason) });
|
|
_ = time.timerOnce(manager_endpoint, delay_ms + 50);
|
|
}
|
|
|
|
/// A timer landed: sweep every deadline. Overdue hellos are killed (the exit
|
|
/// notification then routes through the normal restart policy); due restarts
|
|
/// respawn. Timers carry no id on purpose — the table is the state, and one
|
|
/// sweep serves every armed deadline.
|
|
fn sweepDeadlines() void {
|
|
const now = time.clock();
|
|
if (test_kill_pid != 0 and now >= test_kill_due_ns) {
|
|
std.log.info("test mode: killing the reporter", .{});
|
|
_ = process.kill(test_kill_pid);
|
|
test_kill_pid = 0;
|
|
}
|
|
for (&drivers) |*driver| {
|
|
if (!driver.used) continue;
|
|
switch (driver.state) {
|
|
.awaiting_hello => if (now >= driver.hello_deadline_ns) {
|
|
std.log.info("{s} missed its hello deadline", .{driver.name()});
|
|
_ = process.kill(driver.process_id);
|
|
// The exit notification finishes the job via onDriverExit.
|
|
},
|
|
.restarting => if (now >= driver.restart_due_ns) spawnDriver(driver),
|
|
else => {},
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- the harness callbacks -----------------------------------------------------
|
|
|
|
fn initialise(endpoint: ipc.Handle) bool {
|
|
manager_endpoint = endpoint;
|
|
|
|
// Load the authoritative driver-match registry before any bus driver can
|
|
// report a device to match against it.
|
|
loadRegistry();
|
|
|
|
// Enumerate into a heap buffer (too big for the one-page user stack).
|
|
const buffer = memory.allocator().alloc(device.DeviceDescriptor, 64) catch {
|
|
_ = logging.write("/system/services/device-manager: out of memory\n");
|
|
return false;
|
|
};
|
|
const total = device.enumerate(buffer);
|
|
const count = @min(total, buffer.len);
|
|
|
|
var matched: usize = 0;
|
|
for (buffer[0..count]) |descriptor| {
|
|
if (descriptor.class == @intFromEnum(device.DeviceClass.pci_host_bridge)) {
|
|
// The PCI bus driver: enumeration in ring 3 (M19), one instance
|
|
// per bridge, the bridge id as its assignment.
|
|
matched += 1;
|
|
addDriver("pci-bus", descriptor.id, true);
|
|
continue;
|
|
}
|
|
// Nothing else is matched from the boot snapshot today. The kernel-seeded
|
|
// HPET timer node is served by the kernel's own clock (docs/timers.md), not
|
|
// a user-space driver; PCI functions and PS/2 _HID devices arrive later as
|
|
// pci-bus / acpi-service reports and match in onChildAdded (docs/discovery.md).
|
|
// A fuller system's static class->driver manifest (docs/device-manager.md)
|
|
// would slot in here.
|
|
}
|
|
|
|
// The discovery service (docs/discovery.md): one per firmware, packed
|
|
// under the neutral name "discovery", spawned once at startup. It finds and
|
|
// claims the acpi-tables (or devicetree-blob) node itself. Not a per-device
|
|
// match — it is the discoverer, not a driver bound to one device.
|
|
addDriver("discovery", device_manager_protocol.no_device, false);
|
|
|
|
if (test_restart_mode) {
|
|
// The driver-restart scenario's fixture: claims device 0 (the tree
|
|
// root, otherwise unclaimed), hellos, then faults — driving backoff,
|
|
// re-claim-after-death, and the crash-loop cap deterministically.
|
|
addDriver("crash-test", 0, true);
|
|
}
|
|
|
|
if (matched == 0) {
|
|
_ = logging.write("/system/services/device-manager: no matchable devices\n");
|
|
} else {
|
|
_ = logging.write("/system/services/device-manager: ok\n");
|
|
}
|
|
return true;
|
|
}
|
|
|
|
fn onMessage(message: []const u8, reply: []u8, sender: u32, arrived: *ipc.Arrival) usize {
|
|
return Serve.dispatch({}, handlers, message, sender, arrived, reply);
|
|
}
|
|
|
|
/// `subscribe` and `unsubscribe` are absent on purpose: the harness answers both,
|
|
/// and its table is what `publish` fans out over.
|
|
const handlers = Serve.Handlers{
|
|
.hello = onHello,
|
|
.child_added = onChildAdded,
|
|
.child_removed = onChildRemoved,
|
|
.enumerate = onEnumerate,
|
|
};
|
|
|
|
/// The handshake. The device this driver was assigned is the packet's target.
|
|
fn onHello(_: void, invocation: Invocation(device_manager_protocol.Hello), _: Answer(void)) isize {
|
|
if (invocation.request.version != device_manager_protocol.version) {
|
|
std.log.info("refused hello (version {d}) from process {d}", .{ invocation.request.version, invocation.sender });
|
|
return -envelope.EPROTO;
|
|
}
|
|
const driver = driverByProcess(invocation.sender) orelse {
|
|
std.log.info("hello from unknown process {d}", .{invocation.sender});
|
|
return -envelope.EPERM;
|
|
};
|
|
driver.state = .running;
|
|
|
|
// **Delegation.** The manager holds this driver's device and hands it over here —
|
|
// what replaces first-come-first-served `device_claim` with policy
|
|
// (docs/os-development/device-authority.md). `invocation.sender` is the driver's
|
|
// task id stamped by the kernel, so the manager cannot be lied to about who is
|
|
// asking, and the transfer is a move: the manager stops holding it.
|
|
//
|
|
// Gated on the delegated set so an unconverted driver still claims for itself and
|
|
// its path is untouched; the set and `device_claim` both go at D6.
|
|
if (isDelegated(driver.name()) and driver.device_id != device_manager_protocol.no_device) {
|
|
device.transfer(driver.device_id, invocation.sender) catch |e| {
|
|
std.log.warn("could not delegate device {d} to {s}: {s}", .{ driver.device_id, driver.name(), @errorName(e) });
|
|
return -envelope.EPERM;
|
|
};
|
|
std.log.info("delegated device {d} to {s}", .{ driver.device_id, driver.name() });
|
|
}
|
|
|
|
std.log.info("hello from {s} (device {d})", .{ driver.name(), invocation.target });
|
|
// Resilience drill (V6): once, kill the virtio-gpu driver a moment after it hellos, so
|
|
// the normal restart policy respawns it — the compositor must survive and re-attach.
|
|
if (test_scanout_restart_mode and !test_scanout_killed and std.mem.eql(u8, driver.name(), "/system/drivers/virtio-gpu")) {
|
|
test_scanout_killed = true;
|
|
test_kill_pid = invocation.sender;
|
|
test_kill_due_ns = time.clock() + 1_500_000_000;
|
|
_ = time.timerOnce(manager_endpoint, 1600);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/// A bus driver reported a discovered device: mirror it, publish it, match a
|
|
/// driver for it — and in the restart drills kill the reporter once, the
|
|
/// deterministic trigger for prune -> backoff -> respawn -> re-report.
|
|
fn onChildAdded(_: void, invocation: Invocation(device_manager_protocol.ChildAdded), _: Answer(void)) isize {
|
|
const report = invocation.request;
|
|
const sender = invocation.sender;
|
|
// The registered kernel device id is the packet's target, not a field: what
|
|
// the manager hands a matched driver as its argv assignment.
|
|
const device_id = invocation.target;
|
|
|
|
var status: isize = 0;
|
|
if (driverByProcess(sender)) |driver| {
|
|
if (!addChild(report.parent, report.bus_address, report.identity, device_id, sender)) status = -envelope.ENOSPC;
|
|
std.log.info("child added (device {d} port {d}, identity {d}) by {s}", .{ report.parent, report.bus_address, report.identity, driver.name() });
|
|
if (status == 0) Serve.publish(.child_added, device_id, report);
|
|
// Matching from reports (M19.3), now data-driven via the /system/configuration/devices.csv
|
|
// registry: a registered child gets the most-specific driver its identity
|
|
// matches, once — re-reports after a bus restart dedupe on the registered
|
|
// id, exactly like the registrations do.
|
|
if (status == 0 and device_id != device_manager_protocol.no_device) {
|
|
const id = identityFromReport(report);
|
|
if (registry.matchDriver(registry_rules[0..registry_count], id)) |match| {
|
|
if (match.ambiguous)
|
|
std.log.info("/system/configuration/devices.csv: multiple equally-specific rules match the device {s} reported; binding {s}", .{ driver.name(), match.driver });
|
|
if (id.bus == .acpi) {
|
|
// An hid-matched driver (ps2-bus) is a singleton that finds its
|
|
// own devices once spawned — spawn it once, no device assignment.
|
|
if (!alreadySupervised(match.driver)) addDriver(match.driver, device_manager_protocol.no_device, false);
|
|
} else {
|
|
// A per-device driver: one instance, the registered id as argv[1].
|
|
if (!driverForDevice(device_id)) addDriver(match.driver, device_id, true);
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
status = -envelope.EPERM;
|
|
}
|
|
if (test_pci_restart_mode and !test_usb_killed) {
|
|
if (driverByProcess(sender)) |driver| {
|
|
if (std.mem.eql(u8, driver.name(), "pci-bus") and childCountOf(sender) >= 3) {
|
|
// The pci restart drill: kill the enumerator after it has
|
|
// reported; the respawn must re-register without duplicates
|
|
// (M19.0 idempotence, proven end to end by pci-scan).
|
|
test_usb_killed = true;
|
|
test_kill_pid = sender;
|
|
test_kill_due_ns = time.clock() + 1_000_000_000;
|
|
_ = time.timerOnce(manager_endpoint, 1100);
|
|
}
|
|
}
|
|
}
|
|
if (test_usb_restart_mode and !test_usb_killed and childCountOf(sender) >= 2) {
|
|
// Only the xHCI reporter is the drill's victim — pci-bus also reports
|
|
// now, and whichever finishes second must not trigger the kill.
|
|
if (driverByProcess(sender)) |driver| {
|
|
if (std.mem.eql(u8, driver.name(), "/system/drivers/usb-xhci-bus")) {
|
|
// Delayed, not immediate: the device-list scenario's subscriber
|
|
// needs a window to enumerate and subscribe before the events.
|
|
test_usb_killed = true;
|
|
test_kill_pid = sender;
|
|
test_kill_due_ns = time.clock() + 2_000_000_000;
|
|
_ = time.timerOnce(manager_endpoint, 2100);
|
|
}
|
|
}
|
|
}
|
|
return status;
|
|
}
|
|
|
|
/// A bus driver reported a device gone (hot-unplug). Addressed by the composite
|
|
/// (parent, bus address) the reporter knows, which is why that pair is the
|
|
/// packet's body rather than its target.
|
|
fn onChildRemoved(_: void, invocation: Invocation(device_manager_protocol.ChildRemoved), _: Answer(void)) isize {
|
|
const report = invocation.request;
|
|
var status: isize = -envelope.ENOENT;
|
|
for (&children) |*child| {
|
|
if (child.used and child.parent == report.parent and child.bus_address == report.bus_address and child.reporter == invocation.sender) {
|
|
std.log.info("child removed (device {d} port {d})", .{ child.parent, child.bus_address });
|
|
child.used = false;
|
|
status = 0;
|
|
}
|
|
}
|
|
return status;
|
|
}
|
|
|
|
/// The reserved `enumerate` verb: the mirror, one `ChildEntry` per known child,
|
|
/// packed into the reply's tail. How many arrived is the reply's own length —
|
|
/// `Status.len` — so no count header is spent saying it twice.
|
|
fn onEnumerate(_: void, _: Invocation(void), answer: Answer(void)) isize {
|
|
const entry_size = @sizeOf(device_manager_protocol.ChildEntry);
|
|
const tail = answer.tail();
|
|
var written: usize = 0;
|
|
for (&children) |*child| {
|
|
if (!child.used) continue;
|
|
if (written + entry_size > tail.len) break;
|
|
const entry = device_manager_protocol.ChildEntry{ .parent = child.parent, .bus_address = child.bus_address, .identity = child.identity };
|
|
@memcpy(tail[written..][0..entry_size], std.mem.asBytes(&entry));
|
|
written += entry_size;
|
|
}
|
|
return @intCast(written);
|
|
}
|
|
|
|
fn onNotification(badge: u64) void {
|
|
if (badge & ipc.notify_exit_bit != 0) {
|
|
const dead: u32 = @intCast(badge & ~(ipc.notify_badge_bit | ipc.notify_exit_bit));
|
|
// The harness has already swept the watcher table for this death; what is
|
|
// left is the manager's own concern, its supervised drivers.
|
|
if (driverByProcess(dead)) |driver| onDriverExit(driver);
|
|
return;
|
|
}
|
|
if (badge & ipc.notify_timer_bit != 0) sweepDeadlines();
|
|
}
|
|
|
|
pub fn main(init: 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");
|
|
test_pci_restart_mode = std.mem.eql(u8, mode, "test-pci-restart");
|
|
test_scanout_restart_mode = std.mem.eql(u8, mode, "test-scanout-restart");
|
|
}
|
|
service.run(device_manager_protocol.message_maximum, .{
|
|
.service = "device-manager",
|
|
.init = initialise,
|
|
.on_message = onMessage,
|
|
.on_notification = onNotification,
|
|
.subscribers = Serve.hooks,
|
|
});
|
|
}
|