//! 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..]); }