danos/library/protocol/device-manager/device-manager-protocol.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..]);
}