241 lines
12 KiB
Zig
241 lines
12 KiB
Zig
//! The device-manager protocol (docs/device-driver-development/device-manager.md):
|
|
//! what drivers and applications say to the device manager over
|
|
//! `/protocol/device-manager`. Defined through the envelope
|
|
//! (docs/os-development/protocol-namespace.md), so every packet — request, reply,
|
|
//! and pushed event alike — begins with the folded `Header`.
|
|
//!
|
|
//! **`Header.target` is the device id.** It was the `device_id` field of three
|
|
//! different messages; folding it into the header is what made the packed
|
|
//! leading operation byte disappear along with it. `no_device` addresses a
|
|
//! driver that serves no enumerated device.
|
|
//!
|
|
//! Two of the manager's four old operations were the reserved verbs under
|
|
//! another name and are gone from this protocol's own numbering: `enumerate`
|
|
//! (the tree, one `ChildEntry` per record in the reply tail) and `subscribe`
|
|
//! (the watcher's endpoint rides as the call's capability). What is left is the
|
|
//! driver-facing half — the handshake and the two tree reports.
|
|
//!
|
|
//! **`ChildAdded` travels in both directions, and says so twice.** A bus driver
|
|
//! *calls* `child_added` to report a device; the manager then *pushes* the same
|
|
//! struct to every subscriber as the `child_added` event. Operations and events
|
|
//! are numbered in separate spaces, so one struct under two numbers is exactly
|
|
//! how the envelope spells "one encoding, both directions" — and the direction
|
|
//! (call vs. send) already tells them apart.
|
|
//!
|
|
//! Deliberately contains nothing lifecycle-shaped: stopping, liveness (the
|
|
//! zero-length ping), and exit reasons are the universal vocabulary of
|
|
//! docs/process-lifecycle.md, not this protocol.
|
|
|
|
const std = @import("std");
|
|
const envelope = @import("envelope");
|
|
|
|
/// The protocol version a driver states in its hello, and the version this
|
|
/// contract answers `describe` with. A manager that cannot serve a driver's
|
|
/// version refuses the hello, and the mismatch is loud at startup instead of
|
|
/// quiet corruption later.
|
|
///
|
|
/// `describe` publishes the same number, but it cannot replace this: it tells a
|
|
/// *client* what the provider is, and here it is the **provider** that has to
|
|
/// learn what the client was built against in order to refuse it.
|
|
pub const version = 1;
|
|
|
|
/// `Header.target` for a driver that serves no enumerated device (a test
|
|
/// fixture, a synthetic source), and `ChildAdded`'s answer for a leaf that was
|
|
/// never `device_register`ed.
|
|
pub const no_device: u64 = ~@as(u64, 0);
|
|
|
|
/// Which bus a `child_added` came from — stated by the reporting bus driver so
|
|
/// the manager's /system/configuration/devices.csv matcher knows how to read the report's identity
|
|
/// (a PCI class triple vs a USB class triple are the same 24 bits but different
|
|
/// namespaces) and which `bus` column a rule must name to bind it. `unknown` is
|
|
/// the zero default, so an un-upgraded reporter fails to match rather than
|
|
/// binding to the wrong bus's rule.
|
|
pub const BusKind = enum(u8) {
|
|
unknown = 0,
|
|
pci = 1,
|
|
usb = 2,
|
|
acpi = 3,
|
|
};
|
|
|
|
/// What kind of driver is talking (docs/device-driver-development/driver-model.md's shapes).
|
|
pub const Role = enum(u8) {
|
|
/// Owns a controller and reports the devices behind it (`child_added`).
|
|
bus = 1,
|
|
/// Serves one device, reached through a bus's transfer protocol.
|
|
device = 2,
|
|
};
|
|
|
|
// --- the per-operation request parts ----------------------------------------
|
|
//
|
|
// Each names the bytes AFTER the prefix. Nothing here carries an operation or a
|
|
// device id: those are the packet header's, folded in once. No reply part
|
|
// carries a status either — that is the `Status` every reply already begins
|
|
// with, so the manager's old three `{status, reserved}` reply structs are gone.
|
|
|
|
/// The handshake, sent once by every driver the manager spawns — the manager's
|
|
/// one self-enforced deadline: spawned and silent past it means wrong binary,
|
|
/// wrong version, or wedged before main, and the stop sequence follows. The
|
|
/// device this driver was assigned (its argv[1]) is `Header.target`.
|
|
pub const Hello = extern struct {
|
|
/// A `Role` value.
|
|
role: u8,
|
|
_padding: u8 = 0,
|
|
/// The protocol version this driver was built against (`version`).
|
|
version: u16 = version,
|
|
};
|
|
|
|
/// A bus driver reporting one device it discovered behind its controller
|
|
/// (docs/device-driver-development/device-manager.md "the tree"), and the payload
|
|
/// the manager pushes to its subscribers for the same event. Identity is the
|
|
/// bus's native language — for USB a port-speed class, for PCI the class triple.
|
|
/// 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.
|
|
///
|
|
/// `Header.target` is the kernel device id this child was `device_register`ed
|
|
/// as — what the manager hands a matched driver as its argv assignment — or
|
|
/// `no_device` for an unregistered leaf (a USB port before the descriptor
|
|
/// track). That is the field that used to sit at the end of this struct.
|
|
///
|
|
/// **The field order is the size budget.** An event packet is the header plus
|
|
/// this, within 64 bytes, and three `u64`s round the whole struct up to a
|
|
/// multiple of eight whatever order they sit in — so the small fields are
|
|
/// packed tail-first into the space the rounding pays for anyway. `Define`
|
|
/// checks the result; this comment is why there is no slack in it.
|
|
pub const ChildAdded = extern struct {
|
|
/// 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; for PCI:
|
|
/// the class triple; for ACPI devices, 0 — identity is the hid below).
|
|
identity: u64,
|
|
/// The PCI subsystem id, packed `(subsystem_vendor << 16) | subsystem_device`
|
|
/// (so it reads vendor-first, matching the CSV's `ssvid:ssid`), or 0 when the
|
|
/// device has no subsystem id (a bridge, or a non-PCI bus).
|
|
subsystem: u32 = 0,
|
|
/// The vendor id (PCI vendor / USB idVendor), or 0 when the bus has no such
|
|
/// concept (ACPI). Carried so the manager's /system/configuration/devices.csv matcher can bind
|
|
/// on vendor — a level the bus-native `identity` (a class triple) cannot express.
|
|
vendor: u16 = 0,
|
|
/// The device id (PCI device / USB idProduct), or 0. The most specific numeric
|
|
/// level: this is what lets one virtio-gpu (1AF4:1050) be told from any other
|
|
/// virtio display function without the driver re-confirming after it is spawned.
|
|
device: u16 = 0,
|
|
/// The ACPI hardware id (`_HID`), EISA-decoded (e.g. "PNP0303"), for devices
|
|
/// discovered by firmware string rather than a numeric bus identity. Empty
|
|
/// (all zero) otherwise. Widens for FDT `compatible` strings later.
|
|
hid: [8]u8 = .{0} ** 8,
|
|
/// A `BusKind` value: which bus reported this child, so the manager reads the
|
|
/// identity in the right namespace and matches against the right `bus` column.
|
|
bus: u8 = @intFromEnum(BusKind.unknown),
|
|
_padding: [7]u8 = .{0} ** 7,
|
|
};
|
|
|
|
/// A bus driver reporting a device gone (hot-unplug), and the payload pushed to
|
|
/// subscribers for it.
|
|
///
|
|
/// **This is the one message whose target stays 0.** A removal is addressed by
|
|
/// the composite (parent, bus address) — the reporter knows where the device
|
|
/// *was*, not necessarily what id it had been registered under — and a single
|
|
/// `u64` cannot carry a pair. So the address stays in the payload, where it
|
|
/// always was, and the header addresses the provider itself.
|
|
pub const ChildRemoved = extern struct {
|
|
parent: u64,
|
|
bus_address: u64,
|
|
};
|
|
|
|
/// One record of the reserved `enumerate` reply: the manager's mirror, one
|
|
/// entry per known child, packed into the reply tail. The count is
|
|
/// `Status.len / @sizeOf(ChildEntry)` — the envelope's reply length says how
|
|
/// many arrived, so no count header is spent on saying it twice.
|
|
pub const ChildEntry = extern struct {
|
|
parent: u64,
|
|
bus_address: u64,
|
|
identity: u64,
|
|
};
|
|
|
|
/// How many `ChildEntry` records one `enumerate` reply can carry. Paging joins
|
|
/// the protocol if a tree ever outgrows one packet.
|
|
pub const entries_per_reply: usize = (envelope.packet_maximum - envelope.prefix_size) / @sizeOf(ChildEntry);
|
|
|
|
pub const Protocol = envelope.Define(.{
|
|
.name = "device-manager",
|
|
.version = version,
|
|
.operations = &.{
|
|
// The driver-facing half. `enumerate` and `subscribe` are not here: they
|
|
// are the reserved verbs, which mean the same thing at every provider.
|
|
.{ .name = "hello", .request = Hello },
|
|
.{ .name = "child_added", .request = ChildAdded },
|
|
.{ .name = "child_removed", .request = ChildRemoved },
|
|
},
|
|
.events = &.{
|
|
// The watcher-facing half — the same two structs, pushed rather than
|
|
// called, in the events' own numbering space.
|
|
.{ .name = "child_added", .payload = ChildAdded },
|
|
.{ .name = "child_removed", .payload = ChildRemoved },
|
|
},
|
|
});
|
|
|
|
pub const Operation = Protocol.Operation;
|
|
pub const Event = Protocol.Event;
|
|
|
|
/// What the manager sizes its buffers to — the call floor, as every protocol does.
|
|
pub const message_maximum: usize = Protocol.message_maximum;
|
|
|
|
test "a tree report fits the push floor with the header folded in" {
|
|
// The dual-use struct is the tight one: `child_added` is both a call and an
|
|
// event, and the event floor is 64 bytes *including* the header. Forty-one
|
|
// bytes of content, rounded to 48 by the three u64s' alignment, plus the
|
|
// 16-byte header — exactly on the floor, which is what folding the operation
|
|
// byte and the device id out of the payload bought.
|
|
try std.testing.expectEqual(@as(usize, 48), @sizeOf(ChildAdded));
|
|
try std.testing.expectEqual(envelope.post_maximum, Protocol.event_maximum);
|
|
try std.testing.expect(Protocol.event_maximum <= envelope.post_maximum);
|
|
// Ten records per enumerate reply — what the old count-header layout carried.
|
|
try std.testing.expectEqual(@as(usize, 10), entries_per_reply);
|
|
}
|
|
|
|
test "the verb and event numbering, and the device id in the header" {
|
|
try std.testing.expectEqual(@as(u32, 16), @intFromEnum(Operation.hello));
|
|
try std.testing.expectEqual(@as(u32, 17), @intFromEnum(Operation.child_added));
|
|
try std.testing.expectEqual(@as(u32, 18), @intFromEnum(Operation.child_removed));
|
|
// Events number in their own space, so the same two reports start at 16 too.
|
|
try std.testing.expectEqual(@as(u32, 16), @intFromEnum(Event.child_added));
|
|
try std.testing.expectEqual(@as(u32, 17), @intFromEnum(Event.child_removed));
|
|
// The manager's own enumerate/subscribe became the RESERVED verbs, below the
|
|
// protocol range entirely.
|
|
try std.testing.expectEqual(@as(u32, 1), envelope.operation_enumerate);
|
|
try std.testing.expectEqual(@as(u32, 2), envelope.operation_subscribe);
|
|
|
|
var buffer: [message_maximum]u8 = undefined;
|
|
const hello = Protocol.encodeRequest(.hello, 7, .{ .role = @intFromEnum(Role.bus) }, &.{}, &buffer).?;
|
|
try std.testing.expectEqual(@as(u64, 7), envelope.headerOf(hello).?.target);
|
|
try std.testing.expectEqual(@as(u16, 1), Protocol.decodeRequest(.hello, hello).?.version);
|
|
}
|
|
|
|
test "one struct, two numbers: the report a bus calls and the event a watcher is pushed" {
|
|
const report = ChildAdded{
|
|
.parent = 3,
|
|
.bus_address = 1,
|
|
.identity = 0x030000,
|
|
.bus = @intFromEnum(BusKind.pci),
|
|
.vendor = 0x1AF4,
|
|
};
|
|
|
|
var call: [message_maximum]u8 = undefined;
|
|
const called = Protocol.encodeRequest(.child_added, 42, report, &.{}, &call).?;
|
|
try std.testing.expectEqual(Operation.child_added, Protocol.operationOf(called).?);
|
|
try std.testing.expectEqual(@as(u64, 42), envelope.headerOf(called).?.target);
|
|
|
|
var push: [envelope.post_maximum]u8 = undefined;
|
|
const pushed = Protocol.encodeEvent(.child_added, 42, report, &push).?;
|
|
try std.testing.expectEqual(envelope.post_maximum, pushed.len);
|
|
try std.testing.expectEqual(Event.child_added, Protocol.eventOf(pushed).?);
|
|
try std.testing.expectEqual(@as(u16, 0x1AF4), Protocol.decodeEvent(.child_added, pushed).?.vendor);
|
|
// Same bytes after the prefix, different verb in it — the direction is what
|
|
// tells a call from a push, and the numbering spaces never collide.
|
|
try std.testing.expectEqualSlices(u8, called[envelope.prefix_size..], pushed[envelope.prefix_size..]);
|
|
}
|