diff --git a/docs/device-driver-development/device-manager.md b/docs/device-driver-development/device-manager.md index 5ddc587..118c80d 100644 --- a/docs/device-driver-development/device-manager.md +++ b/docs/device-driver-development/device-manager.md @@ -64,19 +64,38 @@ restarted instance to rebuild exactly the same ids. ## The protocol -A `device-manager-protocol` module (the vfs-protocol pattern): extern-struct -messages, a version in the handshake, reserved fields everywhere. The manager is a -well-known endpoint (`ipc.register(.device_manager)`); the badge tells it who is +A `device-manager-protocol` module, defined through the +[envelope](../os-development/protocol-namespace.md): every packet — request, +reply, and pushed event alike — begins with the folded `Header`, and **the device +id is `Header.target`**, the manager's object addressing. The contract is bound at +`/protocol/device-manager`; the kernel-stamped badge tells the manager who is talking; the same endpoint receives its children's exit notifications — one loop, one world. -| Direction | Message | Purpose | +| Direction | Packet | Purpose | |---|---|---| -| driver → manager | `hello { version, role, device_id }` | confirms the argv assignment, starts the deadline clock | -| bus → manager | `child_added { parent, bus_address, identity, device_id, hid }` | one node the bus discovered | +| driver → manager | `hello { role, version }` @ the assigned device | confirms the argv assignment, starts the deadline clock | +| bus → manager | `child_added { parent, bus_address, identity, bus, vendor, device, subsystem, hid }` @ the registered device id | one node the bus discovered | | bus → manager | `child_removed { parent, bus_address }` | unplug, or the bus lost it | -| app → manager | `enumerate` | snapshot of the tree (read-only) | -| app → manager | `subscribe` | receive published add/remove events | +| app → manager | `enumerate` (reserved verb 1) | snapshot of the tree: one `ChildEntry` per record in the reply's tail | +| app → manager | `subscribe` (reserved verb 2) | receive published add/remove events; the subscriber's endpoint rides as the call's capability | +| manager → app | `child_added` / `child_removed` events | the same two structs, pushed rather than called | + +Two of what used to be the manager's own operations are the envelope's **reserved** +verbs, which mean the same thing at every provider in the system, so this protocol +numbers only three of its own (`hello` = 16, `child_added` = 17, +`child_removed` = 18) and its two events in their own space (`child_added` = 16, +`child_removed` = 17). No reply carries a status field: that is the `Status` every +reply begins with. + +`child_added` is the one struct that travels both ways — a bus *calls* it, the +manager *pushes* it — which is why the operation and event numbering spaces are +separate: one encoding, both directions, told apart by which way the packet went. +Folding the operation byte and the device id out of it is also what makes it fit: +a pushed event is 64 bytes at most, header included, and this one lands exactly on +that floor. `child_removed` is the single message whose target stays 0, because it +is addressed by the composite (parent, bus address) and no single `u64` carries a +pair. `hello` is the one deadline the manager enforces itself: spawned and silent past the deadline means wrong binary, wrong protocol version, or wedged before main — apply diff --git a/docs/os-development/power.md b/docs/os-development/power.md index 968328f..1453a12 100644 --- a/docs/os-development/power.md +++ b/docs/os-development/power.md @@ -28,28 +28,39 @@ unchanged. ## The protocol The `power-protocol` module ([library/protocol/power/power-protocol.zig](../../library/protocol/power/power-protocol.zig)) -follows the vfs-protocol pattern — extern-struct messages, a version, reserved -fields. Three operations: +is defined through the [envelope](protocol-namespace.md), so every packet begins +with the folded `Header`. `Header.target` is unused in both directions: the +provider is the only object either side addresses. -| Direction | Operation | Purpose | +| Direction | Packet | Purpose | |---|---|---| -| subscriber → service | `subscribe` | receive published events; the subscriber's endpoint rides as the call's **capability** (the input/device-manager pattern) | -| init → service | `shutdown` | orderly shutdown's last step: enter S5 (soft off) | -| service → subscriber | `event` | a published `EventMessage`, delivered as a buffered message (never sent *to* the service) | +| subscriber → service | `subscribe` (reserved verb 2) | receive published events; the subscriber's endpoint rides as the call's **capability** (the input/device-manager pattern) | +| init → service | `shutdown` (verb 16) | orderly shutdown's last step: enter S5 (soft off) | +| service → subscriber | one event per kind | a published `Notice`, `ipc_send`t as a buffered packet (never sent *to* the service) | + +`subscribe` is not one of this protocol's own verbs: a synchronous call whose +attached capability is the subscriber's endpoint is exactly what the envelope's +reserved `subscribe` means everywhere, so power adopts it wholesale. And no +packet carries a version — the reserved `describe` verb is the version handshake, +asked once at connect time rather than out of every packet's budget. Events are published, not polled: like the input service, the service holds subscriber endpoints as capabilities and `ipc_send`s each event as a buffered -message, so a slow or dead subscriber can never wedge the source. The event +packet, so a slow or dead subscriber can never wedge the source. **The kind is +the packet's operation** — one declared event per named kind, exactly as the +input service delivers one per device class — so a subscriber reads *what +happened* out of the header rather than out of a tag inside the payload. The vocabulary is hardware-neutral: -- `power_button` — the button was pressed (a fixed ACPI event on x86). -- `lid`, `ac`, `battery` — the named GPE-driven events. -- `notify` — a device notification that maps to none of the above; its `code` - (the ACPI `Notify` argument) and the notifying device's `hid` say which device - and what happened. +- `power_button` (event 16) — the button was pressed (a fixed ACPI event on x86). +- `lid` (17), `ac` (18), `battery` (19) — the named GPE-driven events. +- `notify` (20) — a device notification that maps to none of the above; its + `code` (the ACPI `Notify` argument) and the notifying device's `hid` say which + device and what happened. -An `EventMessage` carries the `event` tag plus `code` and an 8-byte `hid`, so a -generic `notify` is fully described without a second round trip. +The payload every one of them carries is a `Notice`: `code` plus an 8-byte `hid`, +so a generic `notify` is fully described without a second round trip, and the +four named kinds leave both fields zero because the verb already said it all. **`shutdown` is authority, not information.** It is the only operation that *does* something irreversible, so it is gated: the contract is that only init diff --git a/docs/security-track-plan.md b/docs/security-track-plan.md index c9c1b09..8d34a4c 100644 --- a/docs/security-track-plan.md +++ b/docs/security-track-plan.md @@ -36,10 +36,10 @@ plain `main` checkout always tells the truth about where the work is.** | | | |---|---| -| Working on | **P4b** — device-manager, power, usb-transfer onto `Define` | +| Working on | **P4c** — harness subscriber lift + badge-scoped client ids | | Branch carrying it | `feat/security-group-3` (pushed to origin) | | On `main` | Phase 0, PM, H1, P1, P2, P3 — groups 1 and 2 merged | -| Awaiting merge | P4a — lands with the group 3 merge | +| Awaiting merge | P4a, P4b — land with the group 3 merge | | Suite | 110 cases, all passing | | Last updated | 2026-08-01 | @@ -66,7 +66,7 @@ group boundary. bind attestation and every refusal it makes untouched (suite 109/109) - [x] **merge** group 2 → main, push - [x] **P4a** — clean protocols rebased onto `Define` (vfs, block, display, scanout, input; display's one overloaded request split per-operation and its field abuse ended, scanout's bogus 64-byte maximum deleted, directory EOF re-spelled as a nameless entry, input moved onto the service harness; new `protocol-conformance` case asks every reachable provider for `describe` and requires `-ENOSYS` for an undefined verb; suite 110/110) -- [ ] **P4b** — misfit protocols rebased (device-manager, power, usb-transfer) +- [x] **P4b** — misfit protocols rebased (device-manager, power, usb-transfer; every leading operation byte folded into the header, and with it the `device_id`/`device_token` that followed it — `Header.target` now carries the device in all three. device-manager's own `enumerate`/`subscribe` became the reserved verbs and its three `{status, reserved}` reply structs the envelope's `Status`; `ChildAdded` is one struct under two numbers, a call and an event, landing exactly on the 64-byte push floor. power's kinds became one declared event each, the input protocol's shape, so init reads *what happened* from the header; usb-transfer's control data stage moved to the packet tail in both directions, which made `Status.len` the transferred length and `actual_length` redundant. The two silent-breakage sites — init's byte-offset power parse and acpi's `message[0]` dispatch — are gone, the shutdown badge gate unchanged; three more rows in the conformance table. Suite 110/110) - [ ] **P4c** — harness subscriber lift + badge-scoped per-client integers - [ ] **merge** group 3 → main, push - [ ] **H2** — SMEP on every core @@ -413,6 +413,32 @@ re-layouts and raw-offset readers): USB storage/HID all exercise these wires); the conformance case now covers three more providers. Suite 110. +*Landed. Three judgment calls the plan left open, recorded because a reader of +the wire formats will want them:* + +- *`ChildAdded` is 48 bytes, not the 44 the "60 B" estimate assumed: three `u64`s + give the struct eight-byte alignment, so 41 bytes of content round up whatever + order the fields sit in. The packet is therefore **exactly** 64 — on the push + floor, not under it — which `Define` accepts and the module pins in a test. The + fields are ordered small-tail-last deliberately, so the slack the rounding pays + for is where the small ones live.* +- *power's events are declared **per kind** (`power_button`, `lid`, `ac`, + `battery`, `notify`), not one `event` with the kind in the payload. That is the + shape P4a gave input — "the class is the header's operation, so a subscriber + reads the kind from the packet rather than from a tag inside the payload" — and + it is what makes `Header.operation` carry information here at all. It also kept + every call site's spelling: `Protocol.Event` is re-exported as the protocol's + own `Event`, with the members it always had.* +- *the conformance case still checks two providers, and the three new rows report + as unbound. All three P4b contracts arrive with the device manager — it is the + first, it spawns the discovery service that binds the second, and the xHCI + driver that binds the third — so booting one means booting the driver tree, and + the fixture takes **one snapshot** of `/protocol`: a scenario whose bound set + depends on how far that tree got would make the case's own summary line a boot + race. The rows still earn their place — a future scenario that binds one gets it + checked with no edit here, and `/test/*` already holds the `open` grant for + `device-manager`.* + ## P4c — harness subscriber lift + badge scoping - `library/kernel/service.zig` grows the subscriber table, exit- diff --git a/library/device/build.zig b/library/device/build.zig index e9ee388..1bb4f99 100644 --- a/library/device/build.zig +++ b/library/device/build.zig @@ -61,6 +61,7 @@ pub fn build(b: *std.Build) void { .{ .name = "abi", .module = abi }, .{ .name = "channel", .module = channel }, .{ .name = "device-abi", .module = device_abi }, + .{ .name = "envelope", .module = protocol.module("envelope") }, .{ .name = "system-call", .module = system_call }, .{ .name = "ipc", .module = ipc }, .{ .name = "time", .module = time }, @@ -87,6 +88,7 @@ pub fn build(b: *std.Build) void { .root_source_file = b.path("usb/usb.zig"), .imports = &.{ .{ .name = "channel", .module = channel }, + .{ .name = "envelope", .module = protocol.module("envelope") }, .{ .name = "ipc", .module = ipc }, .{ .name = "time", .module = time }, .{ .name = "usb-transfer-protocol", .module = protocol.module("usb-transfer-protocol") }, diff --git a/library/device/driver/driver.zig b/library/device/driver/driver.zig index 470bef7..e2651de 100644 --- a/library/device/driver/driver.zig +++ b/library/device/driver/driver.zig @@ -8,6 +8,7 @@ const abi = @import("abi"); const device_abi = @import("device-abi"); const sc = @import("system-call"); const channel = @import("channel"); +const envelope = @import("envelope"); const ipc = @import("ipc"); const time = @import("time"); const device_manager_protocol = @import("device-manager-protocol"); @@ -168,6 +169,9 @@ const lookup_pause_ms: u64 = 20; /// (best-effort standalone bring-up) or it refused the handshake. Bus drivers keep the handle /// to report children through; a driver that runs fine unsupervised discards it with `_ =`, /// and one that requires supervision bails on null. Logs the outcome itself. +/// +/// The device this driver was assigned is the packet's `Header.target` — the manager's +/// object addressing, so `no_device` here is a driver that serves none. pub fn hello(role: Role, device_id: u64) ?ipc.Handle { var attempts: u32 = 0; const manager = while (attempts < lookup_attempts) : (attempts += 1) { @@ -178,15 +182,25 @@ pub fn hello(role: Role, device_id: u64) ?ipc.Handle { return null; }; - const message = device_manager_protocol.Hello{ .role = @intFromEnum(role), .device_id = device_id }; - var reply: [device_manager_protocol.reply_size]u8 = undefined; - const length = ipc.call(manager, std.mem.asBytes(&message), &reply) catch { + var packet: [device_manager_protocol.message_maximum]u8 = undefined; + const framed = device_manager_protocol.Protocol.encodeRequest( + .hello, + device_id, + .{ .role = @intFromEnum(role) }, + &.{}, + &packet, + ) orelse return null; + + var reply: [device_manager_protocol.message_maximum]u8 = undefined; + const length = ipc.call(manager, framed, &reply) catch { std.log.info("hello call failed", .{}); return null; }; - if (length < device_manager_protocol.reply_size or - std.mem.bytesToValue(device_manager_protocol.HelloReply, reply[0..device_manager_protocol.reply_size]).status != 0) - { + const status = envelope.statusOf(reply[0..length]) orelse { + std.log.info("hello answered nothing readable", .{}); + return null; + }; + if (status.status != 0) { std.log.info("hello refused", .{}); return null; } diff --git a/library/device/usb/usb.zig b/library/device/usb/usb.zig index fadb96d..d3aa34a 100644 --- a/library/device/usb/usb.zig +++ b/library/device/usb/usb.zig @@ -11,16 +11,24 @@ //! _ = device.subscribeInterrupt(address, length); // reports arrive asynchronously //! while (true) { ... ipc.replyWait(device.endpoint, ...) ... } // its own loop //! -//! Reports are delivered to `device.endpoint` as asynchronous `InterruptReport` -//! messages (the class driver runs a bare `replyWait` loop to read them, because -//! the service harness drops buffered-message payloads — see service.zig). +//! Reports are delivered to `device.endpoint` as asynchronous `interrupt_report` +//! event packets, decoded with `reportOf` (the class driver runs a bare `replyWait` +//! loop to read them, because the service harness drops buffered-message payloads +//! — see service.zig). +//! +//! Every packet this file lays down is an envelope packet: the verb and the +//! device token in the folded `Header`, the transfer's own fields after it, and +//! a control transfer's data stage in the tail. const std = @import("std"); const channel = @import("channel"); +const envelope = @import("envelope"); const ipc = @import("ipc"); const time = @import("time"); const usb_transfer_protocol = @import("usb-transfer-protocol"); +const Protocol = usb_transfer_protocol.Protocol; + /// The USB chapter-9 wire ABI and the class taxonomy, re-exported so a class driver reaches /// the whole USB domain through its one `usb` import (`usb.abi.getDescriptor`, `usb.ids.Class`). pub const abi = @import("usb-abi"); @@ -57,21 +65,41 @@ pub const Device = struct { return null; } + /// One request at the bus driver, addressing this device by its token — the + /// packet's `Header.target`, so no request body ever names the device again. + /// Null covers both a failed transport and a refusal: a class driver has the + /// same recourse either way. + fn call( + self: *Device, + comptime operation: Protocol.Operation, + request: Protocol.RequestOf(operation), + tail: []const u8, + capability: ?ipc.Handle, + reply: []u8, + ) ?[]u8 { + var packet: [usb_transfer_protocol.message_maximum]u8 = undefined; + const framed = Protocol.encodeRequest(operation, self.token, request, tail, &packet) orelse return null; + const answer = ipc.callCap(self.bus, framed, reply, capability) catch return null; + const status = envelope.statusOf(reply[0..answer.len]) orelse return null; + if (status.status != 0) return null; + return reply[0..answer.len]; + } + + /// The data stage rides the tail in both directions, so the answer's length + /// *is* the transferred length — `Status.len`, which the envelope stamps. fn controlTransfer(self: *Device, setup: [8]u8, direction_in: bool, data: []u8) ?usize { - var request = usb_transfer_protocol.ControlRequest{ - .device_token = self.token, + if (data.len > usb_transfer_protocol.max_inline_data) return null; + const outgoing: []const u8 = if (direction_in) &.{} else data; + var reply: [usb_transfer_protocol.message_maximum]u8 = undefined; + const answered = self.call(.control, .{ .setup = setup, .direction_in = @intFromBool(direction_in), .data_length = @intCast(data.len), - }; - if (!direction_in and data.len > 0) @memcpy(request.data[0..data.len], data); - var reply: [@sizeOf(usb_transfer_protocol.ControlReply)]u8 = undefined; - const length = ipc.call(self.bus, std.mem.asBytes(&request), &reply) catch return null; - if (length < @sizeOf(usb_transfer_protocol.ControlReply)) return null; - const control_reply = std.mem.bytesToValue(usb_transfer_protocol.ControlReply, reply[0..@sizeOf(usb_transfer_protocol.ControlReply)]); - if (control_reply.status != 0) return null; - const actual = @min(control_reply.actual_length, data.len); - if (direction_in and actual > 0) @memcpy(data[0..actual], control_reply.data[0..actual]); + }, outgoing, null, &reply) orelse return null; + + const returned = Protocol.replyTail(.control, answered); + const actual = @min(returned.len, data.len); + if (direction_in and actual > 0) @memcpy(data[0..actual], returned[0..actual]); return actual; } @@ -89,15 +117,11 @@ pub const Device = struct { /// Begin periodic IN polling of an interrupt endpoint; reports flow back to /// `self.endpoint` as asynchronous `InterruptReport` messages. pub fn subscribeInterrupt(self: *Device, endpoint_address: u8, max_length: u16) bool { - var request = usb_transfer_protocol.InterruptSubscribeRequest{ - .device_token = self.token, + var reply: [usb_transfer_protocol.message_maximum]u8 = undefined; + return self.call(.interrupt_subscribe, .{ .endpoint_address = endpoint_address, .max_length = max_length, - }; - var reply: [@sizeOf(usb_transfer_protocol.InterruptSubscribeReply)]u8 = undefined; - const length = ipc.call(self.bus, std.mem.asBytes(&request), &reply) catch return false; - if (length < @sizeOf(usb_transfer_protocol.InterruptSubscribeReply)) return false; - return std.mem.bytesToValue(usb_transfer_protocol.InterruptSubscribeReply, reply[0..@sizeOf(usb_transfer_protocol.InterruptSubscribeReply)]).status == 0; + }, &.{}, null, &reply) != null; } /// Hand the controller a DMA-region capability (`handle` — from a `shareable` @@ -106,31 +130,33 @@ pub const Device = struct { /// will name in a `bulk` transfer, before the transfer. Harmless (and a no-op /// success) when no IOMMU is enforcing. Returns false on failure. pub fn attachDma(self: *Device, handle: ipc.Handle) bool { - var request = usb_transfer_protocol.DmaAttachRequest{ .device_token = self.token }; - var reply: [@sizeOf(usb_transfer_protocol.DmaAttachReply)]u8 = undefined; - const result = ipc.callCap(self.bus, std.mem.asBytes(&request), &reply, handle) catch return false; - if (result.len < @sizeOf(usb_transfer_protocol.DmaAttachReply)) return false; - return std.mem.bytesToValue(usb_transfer_protocol.DmaAttachReply, reply[0..@sizeOf(usb_transfer_protocol.DmaAttachReply)]).status == 0; + var reply: [usb_transfer_protocol.message_maximum]u8 = undefined; + return self.call(.dma_attach, {}, &.{}, handle, &reply) != null; } /// One bulk transfer (IN or OUT per `endpoint_address`'s direction bit) to or /// from the caller's own DMA buffer at `physical`. Returns the bytes moved. pub fn bulk(self: *Device, endpoint_address: u8, physical: u64, length: u32) ?u32 { - var request = usb_transfer_protocol.BulkRequest{ - .device_token = self.token, + var reply: [usb_transfer_protocol.message_maximum]u8 = undefined; + const answered = self.call(.bulk, .{ .physical_address = physical, .length = length, .endpoint_address = endpoint_address, - }; - var reply: [@sizeOf(usb_transfer_protocol.BulkReply)]u8 = undefined; - const replied = ipc.call(self.bus, std.mem.asBytes(&request), &reply) catch return null; - if (replied < @sizeOf(usb_transfer_protocol.BulkReply)) return null; - const bulk_reply = std.mem.bytesToValue(usb_transfer_protocol.BulkReply, reply[0..@sizeOf(usb_transfer_protocol.BulkReply)]); - if (bulk_reply.status != 0) return null; - return bulk_reply.actual_length; + }, &.{}, null, &reply) orelse return null; + return (Protocol.decodeReply(.bulk, answered) orelse return null).actual_length; } }; +/// Decode one asynchronous interrupt report out of a packet that arrived on the +/// class driver's own endpoint. Null when it is not one — a stray message, or a +/// packet too short to carry the report it names. The device it came from is the +/// packet's `Header.target`, which a single-device class driver never has to read. +pub fn reportOf(packet: []const u8) ?InterruptReport { + const event = Protocol.eventOf(packet) orelse return null; + if (event != .interrupt_report) return null; + return Protocol.decodeEvent(.interrupt_report, packet); +} + /// Open `/protocol/usb-transfer` and, on that channel, open the device with the /// assigned id, handing over a freshly created endpoint for asynchronous interrupt /// reports. Retries while the bus is still coming up (a class driver races the bus @@ -144,12 +170,17 @@ pub fn open(device_id: u64) ?Device { } else return null; const endpoint = ipc.createIpcEndpoint() orelse return null; - var request = usb_transfer_protocol.OpenRequest{ .device_id = device_id }; - var reply: [@sizeOf(usb_transfer_protocol.OpenReply)]u8 = undefined; - const result = ipc.callCap(bus, std.mem.asBytes(&request), &reply, endpoint) catch return null; - if (result.len < @sizeOf(usb_transfer_protocol.OpenReply)) return null; - const open_reply = std.mem.bytesToValue(usb_transfer_protocol.OpenReply, reply[0..@sizeOf(usb_transfer_protocol.OpenReply)]); - if (open_reply.status != 0) return null; + // The assigned device id is the target: it is what the caller has before a + // token exists, and the token the reply hands back addresses every packet + // after this one. + var packet: [usb_transfer_protocol.message_maximum]u8 = undefined; + const framed = Protocol.encodeRequest(.open, device_id, {}, &.{}, &packet) orelse return null; + var reply: [usb_transfer_protocol.message_maximum]u8 = undefined; + const result = ipc.callCap(bus, framed, &reply, endpoint) catch return null; + const answered = reply[0..result.len]; + const status = envelope.statusOf(answered) orelse return null; + if (status.status != 0) return null; + const open_reply = Protocol.decodeReply(.open, answered) orelse return null; var device = Device{ .bus = bus, diff --git a/library/protocol/build.zig b/library/protocol/build.zig index c78b123..2a81968 100644 --- a/library/protocol/build.zig +++ b/library/protocol/build.zig @@ -58,6 +58,9 @@ pub fn build(b: *std.Build) void { "vfs/vfs-protocol.zig", // NodeKind / DirectoryEntry sizes + op values "input/input-protocol.zig", // event numbering + the push-floor budget "display/display-protocol.zig", // pack(): native pixel encoding per format + "device-manager/device-manager-protocol.zig", // the dual-use report, exactly on the push floor + "power/power-protocol.zig", // the event kind as the packet's verb + "usb-transfer/usb-transfer-protocol.zig", // the tail-carried control stage + the trimmed report }) |root| { const protocol_tests = b.addTest(.{ .root_module = b.createModule(.{ diff --git a/library/protocol/device-manager/device-manager-protocol.zig b/library/protocol/device-manager/device-manager-protocol.zig index a7228e8..3fcb104 100644 --- a/library/protocol/device-manager/device-manager-protocol.zig +++ b/library/protocol/device-manager/device-manager-protocol.zig @@ -1,15 +1,48 @@ -//! The device-manager protocol (docs/device-manager.md): what drivers and -//! applications say to the device manager over its well-known endpoint. The -//! vfs-protocol pattern — extern-struct messages, a version in the handshake, -//! reserved fields — so both sides depend on the contract by name. Deliberately -//! contains nothing lifecycle-shaped: stopping, liveness (the zero-length ping), -//! and exit reasons are the universal vocabulary of +//! 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. -/// The protocol version a driver states in its hello. A manager that cannot -/// serve a driver's version refuses the hello, and the mismatch is loud at -/// startup instead of quiet corruption later. -pub const version: u16 = 1; +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 @@ -24,7 +57,7 @@ pub const BusKind = enum(u8) { acpi = 3, }; -/// What kind of driver is talking (docs/driver-model.md's shapes). +/// 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, @@ -32,58 +65,45 @@ pub const Role = enum(u8) { device = 2, }; -/// The message kinds. -pub const Operation = enum(u8) { - hello = 1, - child_added = 2, - child_removed = 3, - enumerate = 4, - subscribe = 5, -}; - -/// `Hello.device_id` for a driver that serves no enumerated device (a test -/// fixture, a synthetic source). -pub const no_device: u64 = ~@as(u64, 0); +// --- 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. +/// 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 { - operation: u8 = @intFromEnum(Operation.hello), - /// A Role value. + /// A `Role` value. role: u8, + _padding: u8 = 0, /// The protocol version this driver was built against (`version`). version: u16 = version, - reserved: u32 = 0, - /// The device this driver was assigned (its argv[1]), or `no_device`. - device_id: u64, }; -pub const hello_size = @sizeOf(Hello); - -/// The manager's answer to a hello. Nonzero status = refused (version mismatch, -/// unknown sender); a refused driver should exit cleanly. -pub const HelloReply = extern struct { - status: i32, - reserved: u32 = 0, -}; - -pub const reply_size = @sizeOf(HelloReply); - /// A bus driver reporting one device it discovered behind its controller -/// (docs/device-manager.md "the tree"). Identity is the bus's native language — -/// for USB a port-speed class; the (class, subclass, protocol) triple joins it -/// once control transfers exist (the USB track). 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. +/// (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 { - operation: u8 = @intFromEnum(Operation.child_added), - /// 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), - reserved1: u16 = 0, - reserved2: u32 = 0, /// 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). @@ -91,10 +111,10 @@ pub const ChildAdded = extern struct { /// 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 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). - device_id: u64 = no_device, + /// 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. @@ -103,73 +123,118 @@ pub const ChildAdded = extern struct { /// 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 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 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, }; -pub const child_added_size = @sizeOf(ChildAdded); - -/// A bus driver reporting a device gone (hot-unplug). Not yet sent by any -/// driver — the port scan has no unplug interrupt — but the manager handles it; -/// death-pruning covers removal until hotplug lands. +/// 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 { - operation: u8 = @intFromEnum(Operation.child_removed), - reserved0: u8 = 0, - reserved1: u16 = 0, - reserved2: u32 = 0, parent: u64, bus_address: u64, }; -pub const child_removed_size = @sizeOf(ChildRemoved); - -/// The manager's answer to a tree report. -pub const ReportReply = extern struct { - status: i32, - reserved: u32 = 0, -}; - -/// An application asking for the tree (M18.3): the reply is an EnumerateReply -/// header followed by `count` ChildEntry records. -pub const Enumerate = extern struct { - operation: u8 = @intFromEnum(Operation.enumerate), - reserved0: u8 = 0, - reserved1: u16 = 0, - reserved2: u32 = 0, -}; - -pub const EnumerateReply = extern struct { - status: i32, - /// ChildEntry records following this header. - count: u32, -}; - +/// 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, }; -/// An application subscribing to published add/remove events (the input-service -/// pattern): the subscriber's endpoint rides as the call's **capability**, and -/// events arrive on it as buffered messages whose payload is the same -/// ChildAdded / ChildRemoved struct the bus drivers send — one encoding, both -/// directions. -pub const Subscribe = extern struct { - operation: u8 = @intFromEnum(Operation.subscribe), - reserved0: u8 = 0, - reserved1: u16 = 0, - reserved2: u32 = 0, -}; +/// 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); -/// Upper bound on any message in this protocol — sizes the endpoint buffers. -/// Capped by the kernel's IPC MESSAGE_MAXIMUM (256): an EnumerateReply carries -/// up to ten ChildEntry records per call, plenty for the mirror's current -/// bounds; paging joins the protocol if a tree ever outgrows one message. -pub const message_maximum = 256; +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..]); +} diff --git a/library/protocol/power/power-protocol.zig b/library/protocol/power/power-protocol.zig index 8df00bc..6424b9a 100644 --- a/library/protocol/power/power-protocol.zig +++ b/library/protocol/power/power-protocol.zig @@ -1,70 +1,104 @@ -//! The power protocol (docs/power.md): system power's domain-named surface, -//! bound at `/protocol/power`. On x86 the acpi service provides it; on ARM a -//! PSCI/mailbox service will bind the same name — subscribers never learn which -//! firmware they are on (docs/discovery.md — firmware neutrality), which is the -//! whole point of naming the contract rather than the provider -//! (docs/os-development/protocol-namespace.md). -//! The vfs-protocol pattern: extern-struct messages, a version, reserved fields. +//! The power protocol (docs/os-development/power.md): system power's +//! domain-named surface, bound at `/protocol/power`. On x86 the acpi service +//! provides it; on ARM a PSCI/mailbox service will bind the same name — +//! subscribers never learn which firmware they are on (docs/discovery.md — +//! firmware neutrality), which is the whole point of naming the contract rather +//! than the provider (docs/os-development/protocol-namespace.md). +//! +//! Defined through the envelope, so every packet begins with the folded +//! `Header`. Three shapes ride the channel, and the envelope names all three: +//! +//! - **subscribe** is the *reserved* verb, not one of this protocol's own: a +//! synchronous call whose attached capability is the subscriber's endpoint is +//! exactly what `envelope.operation_subscribe` means everywhere. +//! - **shutdown** is this protocol's one verb — the only operation that *does* +//! something irreversible, and the reason the provider gates it by badge. +//! - **the events** are pushes: the service `ipc_send`s each one to every +//! subscriber, no reply owed, so a slow or dead subscriber can never wedge the +//! source. **The kind is the packet's operation** — one declared event per +//! named kind, exactly as the input protocol delivers one per device class — +//! so a subscriber reads *what happened* out of the header instead of a tag +//! inside the payload. That is what the old `EventMessage`'s two leading bytes +//! (an operation byte saying "this is an event", then the kind) fold into. +//! +//! `Header.target` is unused (0) in both directions: the provider is the only +//! object either side addresses. And no packet carries a version any more — the +//! reserved `describe` verb is the version handshake, asked once at connect time +//! rather than re-carried out of every packet's budget. -/// The protocol version a client states nowhere yet — reserved for the day a -/// handshake needs it; requests carry it so a mismatch can be refused loudly. -pub const version: u16 = 1; +const std = @import("std"); +const envelope = @import("envelope"); -pub const Operation = enum(u8) { - /// Subscribe to power events: the subscriber's endpoint rides as the - /// call's capability (the input/device-manager pattern); events arrive on - /// it as buffered messages carrying an `EventMessage`. - subscribe = 1, - /// Orderly shutdown's last step: enter S5. Accepted only from PID 1 - /// (init) — the process that has already run the stop sequence over - /// everything else. - shutdown = 2, - /// The published event payload (never sent *to* the service). - event = 3, -}; - -/// What happened. The vocabulary is hardware-neutral: a lid is a lid whether -/// ACPI or a PSCI mailbox reported it. -pub const Event = enum(u8) { - power_button = 1, - lid = 2, - ac = 3, - battery = 4, - /// A device notification that maps to none of the named events — the - /// `code` and `hid` fields say which device and what code. - notify = 5, -}; - -pub const Subscribe = extern struct { - operation: u8 = @intFromEnum(Operation.subscribe), - reserved0: u8 = 0, - version: u16 = version, - reserved1: u32 = 0, -}; - -pub const Shutdown = extern struct { - operation: u8 = @intFromEnum(Operation.shutdown), - reserved0: u8 = 0, - version: u16 = version, - reserved1: u32 = 0, -}; - -/// A published event, as the buffered-message payload subscribers receive. -pub const EventMessage = extern struct { - operation: u8 = @intFromEnum(Operation.event), - /// An Event value. - event: u8, - reserved0: u16 = 0, - /// The device notification code (Notify's second argument), or 0. +/// What a published event carries beyond its kind. The kind is the packet's +/// operation, so nothing here repeats it; `power_button`, `lid`, `ac` and +/// `battery` leave both fields zero and are fully described by the verb alone. +pub const Notice = extern struct { + /// The device notification code (ACPI `Notify`'s second argument), or 0. code: u32 = 0, /// The notifying device's hardware id (EISA-decoded), or all zero. hid: [8]u8 = .{0} ** 8, }; -pub const Reply = extern struct { - status: i32, - reserved: u32 = 0, -}; +pub const Protocol = envelope.Define(.{ + .name = "power", + .version = 1, + .operations = &.{ + // Orderly shutdown's last step: enter S5. Honored only from a + // subscriber — init, the process that has already run the stop sequence + // over everything else (docs/os-development/power.md, "authority, not + // information"). Nothing to say and nothing to answer, so the verb and + // the reply's `Status` are the whole exchange. + .{ .name = "shutdown" }, + }, + .events = &.{ + // The vocabulary is hardware-neutral: a lid is a lid whether ACPI or a + // PSCI mailbox reported it. One event per kind, each carrying the same + // `Notice`, because what differs between them is which thing happened — + // and that is the header's job now. + .{ .name = "power_button", .payload = Notice }, + .{ .name = "lid", .payload = Notice }, + .{ .name = "ac", .payload = Notice }, + .{ .name = "battery", .payload = Notice }, + // A device notification that maps to none of the named events — the + // `code` and `hid` say which device and what happened. + .{ .name = "notify", .payload = Notice }, + }, +}); -/// Upper bound on any message in this protocol — sizes endpoint buffers. -pub const message_maximum = 64; +pub const Operation = Protocol.Operation; + +/// What happened. The event *is* the kind: this is the generated event +/// enumeration, re-exported under the name this protocol has always called its +/// vocabulary, with the same members it has always had. +pub const Event = Protocol.Event; + +/// What a provider and a subscriber size their buffers to. This module used to +/// declare 64 — the *push* floor — which was simply wrong for a protocol whose +/// requests ride `ipc_call`: a provider sizing its receive buffer to 64 refuses +/// any caller that sends up to the floor it is entitled to. +pub const message_maximum: usize = Protocol.message_maximum; + +test "the kind is the verb, and an event fits the push floor" { + try std.testing.expectEqual(@as(u32, 16), @intFromEnum(Operation.shutdown)); + try std.testing.expectEqual(@as(u32, 16), @intFromEnum(Event.power_button)); + try std.testing.expectEqual(@as(u32, 17), @intFromEnum(Event.lid)); + try std.testing.expectEqual(@as(u32, 18), @intFromEnum(Event.ac)); + try std.testing.expectEqual(@as(u32, 19), @intFromEnum(Event.battery)); + try std.testing.expectEqual(@as(u32, 20), @intFromEnum(Event.notify)); + // subscribe is the RESERVED verb, below the protocol range entirely. + try std.testing.expectEqual(@as(u32, 2), envelope.operation_subscribe); + try std.testing.expectEqual(envelope.prefix_size + @sizeOf(Notice), Protocol.event_maximum); + try std.testing.expect(Protocol.event_maximum <= envelope.post_maximum); + // The call floor, not the push floor: `shutdown` is a synchronous call. + try std.testing.expectEqual(envelope.packet_maximum, message_maximum); +} + +test "a pushed event names its kind in the header" { + var buffer: [envelope.post_maximum]u8 = undefined; + const packet = Protocol.encodeEvent(.power_button, 0, .{}, &buffer).?; + try std.testing.expectEqual(Event.power_button, Protocol.eventOf(packet).?); + + const notified = Protocol.encodeEvent(.notify, 0, .{ .code = 0x80, .hid = "PNP0C0A\x00".* }, &buffer).?; + try std.testing.expectEqual(Event.notify, Protocol.eventOf(notified).?); + try std.testing.expectEqual(@as(u32, 0x80), Protocol.decodeEvent(.notify, notified).?.code); +} diff --git a/library/protocol/usb-transfer/usb-transfer-protocol.zig b/library/protocol/usb-transfer/usb-transfer-protocol.zig index 3f40b26..f23d404 100644 --- a/library/protocol/usb-transfer/usb-transfer-protocol.zig +++ b/library/protocol/usb-transfer/usb-transfer-protocol.zig @@ -1,57 +1,66 @@ //! The USB transfer protocol: what a USB class driver (a keyboard, mouse, or -//! mass-storage driver) says to the xHCI bus driver over its well-known -//! `.usb_bus` endpoint to drive its device. The class driver owns no hardware — -//! it reaches its device entirely through these messages, the way a PS/2 keyboard -//! driver reaches the 8042 through the ps2-bus. Extern-struct messages tagged by -//! `Operation`, the vfs-protocol / device-manager-protocol pattern. +//! mass-storage driver) says to the xHCI bus driver over `/protocol/usb-transfer` +//! to drive its device. The class driver owns no hardware — it reaches its device +//! entirely through these packets, the way a PS/2 keyboard driver reaches the +//! 8042 through the ps2-bus. +//! +//! Defined through the envelope (docs/os-development/protocol-namespace.md), so +//! every packet begins with the folded `Header`. **`Header.target` is the device +//! token** — the per-open handle the bus driver hands back, which every request +//! but `open` addressed through a `device_token` field of its own before the +//! rebase. `open` itself addresses the *assigned device id*, because that is what +//! the caller has before there is a token. //! //! The shape: -//! - **open** (a capability-passing `ipc.callCap`): the class driver hands over -//! its own endpoint (for asynchronous interrupt reports) and its assigned -//! device id, and receives a `device_token` plus its interface's endpoints. -//! - **control / bulk** (synchronous `ipc.call`): one transfer, answered when -//! it completes. Control data travels inline (descriptors, HID/MSC class -//! requests are all small); bulk data travels by **physical address** — the -//! class driver's own `dma_alloc`'d buffer — so a 512-byte sector never has -//! to cross the 256-byte IPC boundary. +//! - **open** (a capability-passing call): the class driver hands over its own +//! endpoint (for asynchronous interrupt reports); the target is its assigned +//! device id, and the reply carries a `device_token` plus its interface's +//! endpoints. +//! - **control / bulk** (synchronous calls): one transfer, answered when it +//! completes. Control data travels **in the packet's tail** in both +//! directions (descriptors, HID/MSC class requests are all small), so the +//! fixed parts stay tiny and `Status.len` is the transferred length — the +//! envelope's own field for "how many bytes follow", which is precisely what +//! the old `actual_length` said. Bulk data travels by **physical address** — +//! the class driver's own `dma_alloc`'d buffer — so a 512-byte sector never +//! has to cross the packet floor. //! - **interrupt_subscribe** (synchronous): arm periodic IN polling of an //! interrupt endpoint; each report the device produces is then pushed to the -//! class driver's endpoint as an asynchronous `InterruptReport` (`ipc.send`), -//! exactly how the input service delivers events. +//! class driver's endpoint as an asynchronous `interrupt_report` event. +//! It stays one of **this protocol's own verbs**, not the reserved +//! `subscribe`: the reserved verb means "push me this provider's events" and +//! carries the subscriber's endpoint, while this names one endpoint address +//! on one device and a poll length, and the endpoint it pushes to was handed +//! over at `open`. Same word, different contract. +//! - **dma_attach**: a class driver hands the controller a DMA-region +//! capability (riding the call's cap slot) so the controller binds that +//! buffer into its IOMMU domain and may then DMA to the physical addresses +//! inside it. Needed once per buffer the class driver will name in a `bulk` +//! transfer (its own, or one forwarded to it). //! -//! Single controller assumption: one `.usb_bus` singleton serves QEMU's one xHCI. -//! A multi-controller machine would need a per-controller endpoint (the device -//! manager handing each class driver the right one); noted, not built. +//! Single controller assumption: one provider serves QEMU's one xHCI. A +//! multi-controller machine would need the controller in the target (or the +//! spawner wiring each class driver its own channel); noted, not built. -/// Fits one synchronous IPC message (kernel MESSAGE_MAXIMUM). -pub const message_maximum: usize = 256; +const std = @import("std"); +const envelope = @import("envelope"); -/// The largest inline control-transfer payload. Sized so a whole message -/// (header + data) stays under `message_maximum`: descriptors and HID/MSC class -/// requests are all far smaller. -pub const max_inline_data: usize = 200; +/// The largest control-transfer data stage. It rides the packet's tail, so the +/// bound is the call floor less the header and the fixed request part — derived +/// rather than declared, which is what keeps it honest when a field moves. +pub const max_inline_data: usize = envelope.packet_maximum - envelope.prefix_size - @sizeOf(Control); -/// The largest interrupt report pushed asynchronously. Sized so `InterruptReport` -/// fits an `ipc_send` payload slot (POST_MAXIMUM = 64): boot keyboard reports are -/// 8 bytes, boot mouse reports 3–4. -pub const max_report_data: usize = 48; +/// The largest interrupt report pushed asynchronously. An event packet is the +/// header plus the payload within 64 bytes, so this is what is left after the +/// report's own four bytes of framing: boot keyboard reports are 8 bytes, boot +/// mouse reports 3–4, and the whole HID boot vocabulary fits many times over. +/// A device that produces more has its report truncated, never split. +pub const max_report_data: usize = 40; /// Endpoints per interface reported back in an open reply (a boot HID interface /// has one interrupt endpoint, a mass-storage interface two bulk endpoints). pub const max_reported_endpoints: usize = 4; -pub const Operation = enum(u32) { - open = 0, - control = 1, - interrupt_subscribe = 2, - bulk = 3, - /// dma_attach: a class driver hands the controller a DMA-region capability (riding - /// the call's cap slot) so the controller binds that buffer into its IOMMU domain - /// and may then DMA to the physical addresses inside it. Needed once per buffer the - /// class driver will name in a `bulk` transfer (its own, or one forwarded to it). - dma_attach = 4, -}; - /// The endpoint facts a class driver needs, lifted from the endpoint descriptor /// the bus driver already parsed during enumeration. pub const Endpoint = extern struct { @@ -64,114 +73,146 @@ pub const Endpoint = extern struct { reserved: [3]u8 = .{ 0, 0, 0 }, }; -/// open: the class driver's receive endpoint rides as the call's capability, and -/// `device_id` is the interface's assigned id (its argv[1]). -pub const OpenRequest = extern struct { - operation: u32 = @intFromEnum(Operation.open), - reserved: u32 = 0, - device_id: u64, -}; +// --- the per-operation request and reply parts ------------------------------ +// +// Each names the bytes AFTER the prefix. Nothing here carries an operation or a +// device token: those are the packet header's, folded in once. No reply carries +// a status either — that is the `Status` every reply begins with. -/// The answer to open: a token scoping every later request to this device, the -/// interface's class triple (a sanity check), and its endpoints. -pub const OpenReply = extern struct { - status: i32, - endpoint_count: u32, +/// The answer to `open`: the token every later packet puts in `Header.target`, +/// the interface's class triple (a sanity check), and its endpoints. +pub const Opened = extern struct { device_token: u64, + endpoint_count: u32, interface_class: u8, interface_subclass: u8, interface_protocol: u8, interface_number: u8, - reserved2: u32 = 0, endpoints: [max_reported_endpoints]Endpoint = [_]Endpoint{.{ .address = 0, .transfer_type = 0, .max_packet_size = 0, .interval = 0 }} ** max_reported_endpoints, }; -/// control: one EP0 control transfer. `setup` is a bit-cast `usb_abi.Request`. -/// For an OUT transfer `data[0..data_length]` is sent; for an IN transfer the -/// reply carries up to `data_length` bytes back. -pub const ControlRequest = extern struct { - operation: u32 = @intFromEnum(Operation.control), - reserved: u32 = 0, - device_token: u64, +/// `control`: one EP0 control transfer on `Header.target`. `setup` is a bit-cast +/// `usb_abi.Request`. For an OUT transfer the data stage is the request's tail; +/// for an IN transfer it comes back as the reply's tail, and `Status.len` is how +/// much of it arrived. +pub const Control = extern struct { setup: [8]u8, - direction_in: u8, // 1 = device-to-host (IN), 0 = host-to-device (OUT) - reserved2: u8 = 0, + /// 1 = device-to-host (IN), 0 = host-to-device (OUT). + direction_in: u8, + _padding: u8 = 0, + /// Bytes of data stage: what an IN transfer asks for, and what an OUT + /// transfer's tail carries. data_length: u16, - reserved3: u32 = 0, - data: [max_inline_data]u8 = [_]u8{0} ** max_inline_data, + _padding2: u32 = 0, }; -pub const ControlReply = extern struct { - status: i32, // 0 success, negative on failure/stall - actual_length: u32, - data: [max_inline_data]u8 = [_]u8{0} ** max_inline_data, -}; - -/// interrupt_subscribe: begin periodic IN polling of an interrupt endpoint. Each -/// report the device returns is pushed to the caller's endpoint (handed over at -/// open) as an asynchronous `InterruptReport`. -pub const InterruptSubscribeRequest = extern struct { - operation: u32 = @intFromEnum(Operation.interrupt_subscribe), - reserved: u32 = 0, - device_token: u64, +/// `interrupt_subscribe`: begin periodic IN polling of an interrupt endpoint of +/// `Header.target`. Each report the device returns is pushed to the endpoint the +/// caller handed over at `open`, as an `interrupt_report` event. +pub const InterruptSubscribe = extern struct { endpoint_address: u8, - reserved2: u8 = 0, - max_length: u16, // bytes to request per poll (the endpoint's max packet size) + _padding: u8 = 0, + /// Bytes to request per poll (the endpoint's max packet size). + max_length: u16, }; -pub const InterruptSubscribeReply = extern struct { - status: i32, - reserved: u32 = 0, -}; - -/// bulk: one bulk IN or OUT transfer. `physical_address` is the class driver's own -/// `dma_alloc`'d buffer — the controller DMAs straight to/from it, so the bulk -/// data never crosses IPC. `endpoint_address`'s bit 7 selects IN vs OUT. -pub const BulkRequest = extern struct { - operation: u32 = @intFromEnum(Operation.bulk), - reserved: u32 = 0, - device_token: u64, +/// `bulk`: one bulk IN or OUT transfer on `Header.target`. `physical_address` is +/// the class driver's own `dma_alloc`'d buffer — the controller DMAs straight +/// to/from it, so the bulk data never crosses IPC. `endpoint_address`'s bit 7 +/// selects IN vs OUT. +pub const Bulk = extern struct { physical_address: u64, length: u32, endpoint_address: u8, - reserved2: u8 = 0, - reserved3: u16 = 0, + _padding: u8 = 0, + _padding2: u16 = 0, }; -pub const BulkReply = extern struct { - status: i32, - actual_length: u32, -}; +/// How many bytes a bulk transfer actually moved. It cannot ride `Status.len` +/// the way a control transfer's does: nothing follows a bulk reply, because the +/// data went to the caller's DMA buffer rather than into the packet. +pub const Transferred = extern struct { actual_length: u32 }; -/// dma_attach: the region capability rides the call's cap slot; the body only carries -/// the device token (scoping) so the controller knows which caller is attaching. -pub const DmaAttachRequest = extern struct { - operation: u32 = @intFromEnum(Operation.dma_attach), - reserved: u32 = 0, - device_token: u64, -}; - -pub const DmaAttachReply = extern struct { - status: i32, - reserved: u32 = 0, -}; - -/// An asynchronous interrupt report, pushed with `ipc.send` to a subscriber's -/// endpoint. `Received.isMessage()` is set; there is no reply owed. +/// One asynchronous interrupt report, pushed to the endpoint the class driver +/// handed over at `open`. The device it came from is `Header.target`. pub const InterruptReport = extern struct { - device_token: u64, endpoint_address: u8, length: u8, - reserved: u16 = 0, + _padding: u16 = 0, data: [max_report_data]u8 = [_]u8{0} ** max_report_data, }; -comptime { - const std = @import("std"); - // Every synchronous message must fit one IPC message; the async report must - // fit an ipc_send payload slot. - std.debug.assert(@sizeOf(ControlRequest) <= message_maximum); - std.debug.assert(@sizeOf(ControlReply) <= message_maximum); - std.debug.assert(@sizeOf(OpenReply) <= message_maximum); - std.debug.assert(@sizeOf(InterruptReport) <= 64); +pub const Protocol = envelope.Define(.{ + .name = "usb-transfer", + .version = 1, + .operations = &.{ + // open: the target is the interface's assigned device id (its argv[1]), + // and the class driver's receive endpoint rides as the capability. + .{ .name = "open", .reply = Opened }, + .{ .name = "control", .request = Control }, + .{ .name = "interrupt_subscribe", .request = InterruptSubscribe }, + .{ .name = "bulk", .request = Bulk, .reply = Transferred }, + // dma_attach: the region capability rides the call's cap slot; the + // target says which caller's device is attaching, so there is nothing + // left for a body to carry. + .{ .name = "dma_attach" }, + }, + .events = &.{ + .{ .name = "interrupt_report", .payload = InterruptReport }, + }, +}); + +pub const Operation = Protocol.Operation; +pub const Event = Protocol.Event; + +/// What both sides size their buffers to — the call floor, as every protocol does. +pub const message_maximum: usize = Protocol.message_maximum; + +test "the budgets, re-verified by Define rather than by hand" { + // What the hand-rolled comptime asserts used to say, now said by `Define` + // — and counting the header, which the old checks did not. + try std.testing.expectEqual(@as(usize, 224), max_inline_data); + try std.testing.expect(Protocol.request_maximum <= envelope.packet_maximum); + try std.testing.expect(Protocol.reply_maximum <= envelope.packet_maximum); + // The report was 48 bytes of data in a 64-byte struct that had no room left + // for a header. Folding the device token into the target and trimming the + // data to 40 leaves the whole packet at 60 of the 64-byte push floor. + try std.testing.expectEqual(@as(usize, 44), @sizeOf(InterruptReport)); + try std.testing.expectEqual(@as(usize, 60), Protocol.event_maximum); + try std.testing.expect(Protocol.event_maximum <= envelope.post_maximum); +} + +test "the verb numbering, and the device token in the header" { + try std.testing.expectEqual(@as(u32, 16), @intFromEnum(Operation.open)); + try std.testing.expectEqual(@as(u32, 17), @intFromEnum(Operation.control)); + try std.testing.expectEqual(@as(u32, 18), @intFromEnum(Operation.interrupt_subscribe)); + try std.testing.expectEqual(@as(u32, 19), @intFromEnum(Operation.bulk)); + try std.testing.expectEqual(@as(u32, 20), @intFromEnum(Operation.dma_attach)); + try std.testing.expectEqual(@as(u32, 16), @intFromEnum(Event.interrupt_report)); + + var buffer: [message_maximum]u8 = undefined; + const packet = Protocol.encodeRequest(.control, 9, .{ + .setup = .{ 0, 6, 0, 1, 0, 0, 18, 0 }, + .direction_in = 1, + .data_length = 18, + }, &.{}, &buffer).?; + try std.testing.expectEqual(@as(u64, 9), envelope.headerOf(packet).?.target); + try std.testing.expectEqual(@as(u16, 18), Protocol.decodeRequest(.control, packet).?.data_length); +} + +test "a control OUT carries its data stage as the packet's tail" { + var buffer: [message_maximum]u8 = undefined; + const payload = [_]u8{ 1, 2, 3, 4 }; + const packet = Protocol.encodeRequest(.control, 5, .{ + .setup = .{ 0x21, 11, 0, 0, 0, 0, 4, 0 }, + .direction_in = 0, + .data_length = payload.len, + }, &payload, &buffer).?; + try std.testing.expectEqualSlices(u8, &payload, Protocol.requestTail(.control, packet)); + + // And the answer to an IN: the bytes follow the (empty) fixed reply part, + // with `Status.len` counting exactly them. + const answered = Protocol.encodeReply(.control, 0, {}, &payload, &buffer).?; + try std.testing.expectEqual(@as(u32, payload.len), envelope.statusOf(answered).?.len); + try std.testing.expectEqualSlices(u8, &payload, Protocol.replyTail(.control, answered)); } diff --git a/system/drivers/pci-bus/pci-bus.zig b/system/drivers/pci-bus/pci-bus.zig index 0501f7d..4420664 100644 --- a/system/drivers/pci-bus/pci-bus.zig +++ b/system/drivers/pci-bus/pci-bus.zig @@ -229,18 +229,21 @@ fn registerAndReport(bus: u64, dev: u64, function: u64, class_triple: u32) void std.log.info("register refused for {d}:{d}.{d}", .{ bus, dev, function }); return; }; - const report = device_manager_protocol.ChildAdded{ + // The registered device id is the packet's target — the manager's object + // addressing — so the report body carries only where on the bus it sits and + // what it is. + var packet: [device_manager_protocol.message_maximum]u8 = undefined; + const framed = device_manager_protocol.Protocol.encodeRequest(.child_added, registered, .{ .bus = @intFromEnum(device_manager_protocol.BusKind.pci), .parent = bridge_id, .bus_address = (bus << 8) | (dev << 3) | function, .identity = class_triple, - .device_id = registered, .vendor = descriptor.vendor, .device = descriptor.device, .subsystem = descriptor.subsystem, - }; + }, &.{}, &packet) orelse return; var reply: [device_manager_protocol.message_maximum]u8 = undefined; - _ = ipc.call(manager_handle, std.mem.asBytes(&report), &reply) catch { + _ = ipc.call(manager_handle, framed, &reply) catch { std.log.info("child report for {d}:{d}.{d} failed", .{ bus, dev, function }); }; } diff --git a/system/drivers/usb-hid/keyboard.zig b/system/drivers/usb-hid/keyboard.zig index 21b305d..0416288 100644 --- a/system/drivers/usb-hid/keyboard.zig +++ b/system/drivers/usb-hid/keyboard.zig @@ -112,9 +112,10 @@ pub fn main(init: process.Init) void { if (signals.has(.terminate)) return; continue; } - if (!got.isMessage() or got.len < @sizeOf(usb.InterruptReport)) continue; - - const message = std.mem.bytesToValue(usb.InterruptReport, receive[0..@sizeOf(usb.InterruptReport)]); + if (!got.isMessage()) continue; + // An `interrupt_report` event packet: the verb in its folded header, the + // report after it. Anything else on this endpoint is not ours. + const message = usb.reportOf(receive[0..got.len]) orelse continue; if (message.length < @sizeOf(hid.KeyboardReport)) continue; const report = std.mem.bytesToValue(hid.KeyboardReport, message.data[0..@sizeOf(hid.KeyboardReport)]); const transitions = decoder.feed(report); diff --git a/system/drivers/usb-hid/mouse.zig b/system/drivers/usb-hid/mouse.zig index 7dc26a5..beb0a64 100644 --- a/system/drivers/usb-hid/mouse.zig +++ b/system/drivers/usb-hid/mouse.zig @@ -74,9 +74,10 @@ pub fn main(init: process.Init) void { if (signals.has(.terminate)) return; continue; } - if (!got.isMessage() or got.len < @sizeOf(usb.InterruptReport)) continue; - - const message = std.mem.bytesToValue(usb.InterruptReport, receive[0..@sizeOf(usb.InterruptReport)]); + if (!got.isMessage()) continue; + // An `interrupt_report` event packet: the verb in its folded header, the + // report after it. Anything else on this endpoint is not ours. + const message = usb.reportOf(receive[0..got.len]) orelse continue; const length = @min(message.length, message.data.len); const report = hid.parseMouse(message.data[0..length]) orelse continue; const mask = buttonMask(report.buttons); diff --git a/system/drivers/usb-xhci-bus/build.zig b/system/drivers/usb-xhci-bus/build.zig index 8183a6a..216dfa3 100644 --- a/system/drivers/usb-xhci-bus/build.zig +++ b/system/drivers/usb-xhci-bus/build.zig @@ -10,10 +10,10 @@ pub fn build(b: *std.Build) void { .name = "usb-xhci-bus", .root_source_file = b.path("usb-xhci-bus.zig"), .imports = &.{ - "channel", "device-manager-protocol", "driver", "input-client", - "ipc", "logging", "memory", "mmio", - "pci", "process", "service", "time", - "usb-abi", "usb-ids", "usb-transfer-protocol", + "channel", "device-manager-protocol", "driver", "envelope", + "input-client", "ipc", "logging", "memory", + "mmio", "pci", "process", "service", + "time", "usb-abi", "usb-ids", "usb-transfer-protocol", }, }); b.installArtifact(exe); diff --git a/system/drivers/usb-xhci-bus/usb-xhci-bus.zig b/system/drivers/usb-xhci-bus/usb-xhci-bus.zig index 22e46ce..e581cef 100644 --- a/system/drivers/usb-xhci-bus/usb-xhci-bus.zig +++ b/system/drivers/usb-xhci-bus/usb-xhci-bus.zig @@ -25,6 +25,7 @@ const device_manager = @import("driver"); const memory = @import("memory"); const logging = @import("logging"); const device_manager_protocol = @import("device-manager-protocol"); +const envelope = @import("envelope"); const usb_ids = @import("usb-ids"); const usb_abi = @import("usb-abi"); const usb_transfer_protocol = @import("usb-transfer-protocol"); @@ -386,6 +387,23 @@ fn bringUpBehindHub(manager: ipc.Handle, engine: *library.Controller, hub: *libr if (deviceIsHub(usb_device)) _ = engine.setupHub(usb_device); } +/// Report one interface gone. A removal is addressed by the composite (parent, +/// bus address) — a pair no single `Header.target` can carry — so that stays the +/// packet's body and the target addresses the manager itself. `where` names the +/// port and interface for the log line, or null where the caller stays quiet +/// (a hub subtree collapsing reports a great many at once). +fn reportRemoved(manager: ipc.Handle, bus_address: u64, where: ?struct { port: u32, interface: u8 }) void { + var packet: [device_manager_protocol.message_maximum]u8 = undefined; + const framed = device_manager_protocol.Protocol.encodeRequest(.child_removed, 0, .{ + .parent = controller_id, + .bus_address = bus_address, + }, &.{}, &packet) orelse return; + var reply: [device_manager_protocol.message_maximum]u8 = undefined; + _ = ipc.call(manager, framed, &reply) catch { + if (where) |place| std.log.info("child-removed report for port {d} interface {d} failed", .{ place.port, place.interface }); + }; +} + /// Tear down a device that disconnected from a hub: recursively tear down its /// own downstream devices first if it is a hub, report each interface removed, /// then Disable Slot. Mirrors tearDownPort for a hub-attached device. @@ -398,12 +416,7 @@ fn tearDownHubDevice(manager: ipc.Handle, engine: *library.Controller, dev: *lib const key = hubPortKey(dev.parent_slot, dev.parent_port); for (dev.interfaces[0..dev.interface_count]) |*interface| { if (interface.registered_device_id == 0) continue; - const event = device_manager_protocol.ChildRemoved{ - .parent = controller_id, - .bus_address = (@as(u64, key) << 8) | interface.number, - }; - var reply: [device_manager_protocol.message_maximum]u8 = undefined; - _ = ipc.call(manager, std.mem.asBytes(&event), &reply) catch {}; + reportRemoved(manager, (@as(u64, key) << 8) | interface.number, null); interface.registered_device_id = 0; } engine.tearDownDevice(dev); @@ -428,14 +441,7 @@ fn tearDownPort(manager: ipc.Handle, engine: *library.Controller, port: u32) voi std.log.info("port {d} disconnected", .{port}); for (usb_device.interfaces[0..usb_device.interface_count]) |*interface| { if (interface.registered_device_id == 0) continue; - const event = device_manager_protocol.ChildRemoved{ - .parent = controller_id, - .bus_address = (@as(u64, port) << 8) | interface.number, - }; - var reply: [device_manager_protocol.message_maximum]u8 = undefined; - _ = ipc.call(manager, std.mem.asBytes(&event), &reply) catch { - std.log.info("child-removed report for port {d} interface {d} failed", .{ port, interface.number }); - }; + reportRemoved(manager, (@as(u64, port) << 8) | interface.number, .{ .port = port, .interface = interface.number }); interface.registered_device_id = 0; } engine.tearDownDevice(usb_device); @@ -469,15 +475,17 @@ fn reportInterface(manager: ipc.Handle, port: u32, interface: library.InterfaceI return null; }; - const report = device_manager_protocol.ChildAdded{ + // The registered device id is the packet's target — the manager's object + // addressing — so the report body carries only where on the bus it sits. + var packet: [device_manager_protocol.message_maximum]u8 = undefined; + const framed = device_manager_protocol.Protocol.encodeRequest(.child_added, registered, .{ .bus = @intFromEnum(device_manager_protocol.BusKind.usb), .parent = controller_id, .bus_address = (@as(u64, port) << 8) | interface.number, .identity = identity, - .device_id = registered, - }; + }, &.{}, &packet) orelse return null; var reply: [device_manager_protocol.message_maximum]u8 = undefined; - _ = ipc.call(manager, std.mem.asBytes(&report), &reply) catch { + _ = ipc.call(manager, framed, &reply) catch { std.log.info("child report for port {d} interface {d} failed", .{ port, interface.number }); return null; }; @@ -496,59 +504,55 @@ fn reportInterface(manager: ipc.Handle, port: u32, interface: library.InterfaceI return registered; } -/// Serve the USB transfer protocol: a class driver opens its device, then issues -/// control / interrupt-subscribe / bulk requests against it. +/// The generated transfer dispatch. One controller per process, so the handler +/// context is empty and the open table stays in this file's globals. +const Serve = usb_transfer_protocol.Protocol.Provider(void); + +const Invocation = envelope.Invocation; +const Answer = envelope.Answer; + +/// Every refusal here is the same one — this controller does not (or no longer) +/// serve the device the packet addressed — so there is one errno for all of them. +const refused: isize = -envelope.ENOENT; + +/// Set by `onOpen` when the open table has taken ownership of the endpoint the +/// call carried, and read by `onMessage`, where the turn's `Arrival` lives. The +/// generated dispatch hands a handler the raw handle rather than the `Arrival`, +/// so the *claim* travels back out this way. One turn, one handler, one thread. +var capability_claimed = false; + fn onMessage(message: []const u8, reply: []u8, sender: u32, arrived: *ipc.Arrival) usize { - _ = sender; - if (message.len < 4) return 0; - const operation = std.mem.readInt(u32, message[0..4], .little); - return switch (operation) { - @intFromEnum(usb_transfer_protocol.Operation.open) => handleOpen(message, reply, arrived), - @intFromEnum(usb_transfer_protocol.Operation.control) => handleControl(message, reply), - @intFromEnum(usb_transfer_protocol.Operation.interrupt_subscribe) => handleSubscribe(message, reply), - @intFromEnum(usb_transfer_protocol.Operation.bulk) => handleBulk(message, reply), - @intFromEnum(usb_transfer_protocol.Operation.dma_attach) => handleDmaAttach(message, reply, arrived), - else => 0, - }; + capability_claimed = false; + const written = Serve.dispatch({}, handlers, message, sender, arrived.peek(), reply); + if (capability_claimed) _ = arrived.take(); + return written; } -/// dma_attach: bind the class driver's DMA-region capability into the controller's IOMMU -/// domain, so the controller may DMA to the physical addresses inside that buffer. The -/// binding holds its own kernel reference, so this never claims the arriving handle — -/// the turn's `defer` in the harness is the close, on the failure paths as well as this -/// one. -fn handleDmaAttach(message: []const u8, reply: []u8, arrived: *ipc.Arrival) usize { - if (message.len < @sizeOf(usb_transfer_protocol.DmaAttachRequest)) return writeReply(reply, usb_transfer_protocol.DmaAttachReply{ .status = -1 }); - const handle = arrived.peek() orelse return writeReply(reply, usb_transfer_protocol.DmaAttachReply{ .status = -1 }); - const ok = device.dmaBind(controller_id, handle); - return writeReply(reply, usb_transfer_protocol.DmaAttachReply{ .status = if (ok) 0 else -1 }); -} +const handlers = Serve.Handlers{ + .open = onOpen, + .control = onControl, + .interrupt_subscribe = onInterruptSubscribe, + .bulk = onBulk, + .dma_attach = onDmaAttach, +}; -fn writeReply(reply: []u8, value: anytype) usize { - const bytes = std.mem.asBytes(&value); - @memcpy(reply[0..bytes.len], bytes); - return bytes.len; -} - -/// open: resolve the assigned device id to an interface, remember the caller's -/// endpoint (for interrupt reports), and answer with a device token + the -/// interface's endpoints so the class driver need not re-read the config. -fn handleOpen(message: []const u8, reply: []u8, arrived: *ipc.Arrival) usize { - if (message.len < @sizeOf(usb_transfer_protocol.OpenRequest)) return writeReply(reply, usb_transfer_protocol.OpenReply{ .status = -1, .endpoint_count = 0, .device_token = 0, .interface_class = 0, .interface_subclass = 0, .interface_protocol = 0, .interface_number = 0 }); - const request = std.mem.bytesToValue(usb_transfer_protocol.OpenRequest, message[0..@sizeOf(usb_transfer_protocol.OpenRequest)]); - const engine = if (controller) |*c| c else return writeReply(reply, usb_transfer_protocol.OpenReply{ .status = -1, .endpoint_count = 0, .device_token = 0, .interface_class = 0, .interface_subclass = 0, .interface_protocol = 0, .interface_number = 0 }); - const found = engine.findInterface(request.device_id) orelse return writeReply(reply, usb_transfer_protocol.OpenReply{ .status = -1, .endpoint_count = 0, .device_token = 0, .interface_class = 0, .interface_subclass = 0, .interface_protocol = 0, .interface_number = 0 }); +/// open: the target is the class driver's assigned device id. Resolve it to an +/// interface, remember the caller's endpoint (for interrupt reports), and answer +/// with a device token — the target of every later packet — plus the interface's +/// endpoints, so the class driver need not re-read the configuration descriptor. +fn onOpen(_: void, invocation: Invocation(void), answer: Answer(usb_transfer_protocol.Opened)) isize { + const engine = if (controller) |*c| c else return refused; + const found = engine.findInterface(invocation.target) orelse return refused; // The report endpoint is claimed only if the open table actually keeps it; // a full table leaves it to the turn to close. - if (arrived.peek()) |endpoint| { - if (recordOpen(request.device_id, endpoint)) _ = arrived.take(); + if (invocation.capability) |endpoint| { + if (recordOpen(invocation.target, endpoint)) capability_claimed = true; } - var open_reply = usb_transfer_protocol.OpenReply{ - .status = 0, + var opened = usb_transfer_protocol.Opened{ + .device_token = invocation.target, .endpoint_count = found.interface.endpoint_count, - .device_token = request.device_id, .interface_class = found.interface.class, .interface_subclass = found.interface.subclass, .interface_protocol = found.interface.protocol, @@ -556,57 +560,71 @@ fn handleOpen(message: []const u8, reply: []u8, arrived: *ipc.Arrival) usize { }; const count = @min(found.interface.endpoint_count, usb_transfer_protocol.max_reported_endpoints); for (found.interface.endpoints[0..count], 0..) |endpoint, index| { - open_reply.endpoints[index] = .{ + opened.endpoints[index] = .{ .address = endpoint.address, .transfer_type = endpoint.transfer_type, .max_packet_size = endpoint.max_packet_size, .interval = endpoint.interval, }; } - return writeReply(reply, open_reply); + answer.set(opened); + return 0; } -/// control: one EP0 control transfer, small data inline both ways. -fn handleControl(message: []const u8, reply: []u8) usize { - if (message.len < @sizeOf(usb_transfer_protocol.ControlRequest)) return writeReply(reply, usb_transfer_protocol.ControlReply{ .status = -1, .actual_length = 0 }); - const request = std.mem.bytesToValue(usb_transfer_protocol.ControlRequest, message[0..@sizeOf(usb_transfer_protocol.ControlRequest)]); - const engine = if (controller) |*c| c else return writeReply(reply, usb_transfer_protocol.ControlReply{ .status = -1, .actual_length = 0 }); - const found = engine.findInterface(request.device_token) orelse return writeReply(reply, usb_transfer_protocol.ControlReply{ .status = -1, .actual_length = 0 }); +/// control: one EP0 control transfer. The data stage rides the tail in both +/// directions, so an IN transfer's answer is simply however many bytes were +/// written into `answer.tail()` — which is what `Status.len` then reports. +fn onControl(_: void, invocation: Invocation(usb_transfer_protocol.Control), answer: Answer(void)) isize { + const engine = if (controller) |*c| c else return refused; + const found = engine.findInterface(invocation.target) orelse return refused; + + const setup = std.mem.bytesToValue(usb_abi.Request, &invocation.request.setup); + const direction_in = invocation.request.direction_in != 0; + const room = @min(usb_transfer_protocol.max_inline_data, answer.tail().len); + const data_length = @min(@as(usize, invocation.request.data_length), room); - const setup = std.mem.bytesToValue(usb_abi.Request, &request.setup); - const direction_in = request.direction_in != 0; - const data_length = @min(request.data_length, usb_transfer_protocol.max_inline_data); var data: [usb_transfer_protocol.max_inline_data]u8 = undefined; - if (!direction_in) @memcpy(data[0..data_length], request.data[0..data_length]); + if (!direction_in) { + const supplied = @min(data_length, invocation.tail.len); + @memcpy(data[0..supplied], invocation.tail[0..supplied]); + if (supplied < data_length) @memset(data[supplied..data_length], 0); + } - const ok = engine.controlTransfer(found.device, setup, data[0..data_length], direction_in); - var control_reply = usb_transfer_protocol.ControlReply{ .status = if (ok) 0 else -1, .actual_length = if (ok) data_length else 0 }; - if (ok and direction_in) @memcpy(control_reply.data[0..data_length], data[0..data_length]); - return writeReply(reply, control_reply); + if (!engine.controlTransfer(found.device, setup, data[0..data_length], direction_in)) return refused; + if (!direction_in) return 0; // nothing follows an OUT: the status is the whole answer + @memcpy(answer.tail()[0..data_length], data[0..data_length]); + return @intCast(data_length); } -/// interrupt_subscribe: arm periodic IN polling; reports flow back asynchronously. -fn handleSubscribe(message: []const u8, reply: []u8) usize { - if (message.len < @sizeOf(usb_transfer_protocol.InterruptSubscribeRequest)) return writeReply(reply, usb_transfer_protocol.InterruptSubscribeReply{ .status = -1 }); - const request = std.mem.bytesToValue(usb_transfer_protocol.InterruptSubscribeRequest, message[0..@sizeOf(usb_transfer_protocol.InterruptSubscribeRequest)]); - const engine = if (controller) |*c| c else return writeReply(reply, usb_transfer_protocol.InterruptSubscribeReply{ .status = -1 }); - const found = engine.findInterface(request.device_token) orelse return writeReply(reply, usb_transfer_protocol.InterruptSubscribeReply{ .status = -1 }); - const endpoint = library.Controller.endpointForAddress(found.interface, request.endpoint_address) orelse return writeReply(reply, usb_transfer_protocol.InterruptSubscribeReply{ .status = -1 }); - const report_endpoint = reportEndpointFor(request.device_token) orelse return writeReply(reply, usb_transfer_protocol.InterruptSubscribeReply{ .status = -1 }); - const ok = engine.subscribeInterrupt(found.device, endpoint, request.device_token, report_endpoint); - return writeReply(reply, usb_transfer_protocol.InterruptSubscribeReply{ .status = if (ok) 0 else -1 }); +/// interrupt_subscribe: arm periodic IN polling; reports flow back asynchronously +/// to the endpoint this device's `open` handed over. +fn onInterruptSubscribe(_: void, invocation: Invocation(usb_transfer_protocol.InterruptSubscribe), _: Answer(void)) isize { + const engine = if (controller) |*c| c else return refused; + const found = engine.findInterface(invocation.target) orelse return refused; + const endpoint = library.Controller.endpointForAddress(found.interface, invocation.request.endpoint_address) orelse return refused; + const report_endpoint = reportEndpointFor(invocation.target) orelse return refused; + return if (engine.subscribeInterrupt(found.device, endpoint, invocation.target, report_endpoint)) 0 else refused; } /// bulk: one bulk transfer to/from the class driver's own DMA buffer (by physical /// address), so sector-sized data never crosses IPC. -fn handleBulk(message: []const u8, reply: []u8) usize { - if (message.len < @sizeOf(usb_transfer_protocol.BulkRequest)) return writeReply(reply, usb_transfer_protocol.BulkReply{ .status = -1, .actual_length = 0 }); - const request = std.mem.bytesToValue(usb_transfer_protocol.BulkRequest, message[0..@sizeOf(usb_transfer_protocol.BulkRequest)]); - const engine = if (controller) |*c| c else return writeReply(reply, usb_transfer_protocol.BulkReply{ .status = -1, .actual_length = 0 }); - const found = engine.findInterface(request.device_token) orelse return writeReply(reply, usb_transfer_protocol.BulkReply{ .status = -1, .actual_length = 0 }); - const endpoint = library.Controller.endpointForAddress(found.interface, request.endpoint_address) orelse return writeReply(reply, usb_transfer_protocol.BulkReply{ .status = -1, .actual_length = 0 }); - const transferred = engine.bulkTransfer(found.device, endpoint, request.physical_address, request.length); - return writeReply(reply, usb_transfer_protocol.BulkReply{ .status = if (transferred != null) 0 else -1, .actual_length = transferred orelse 0 }); +fn onBulk(_: void, invocation: Invocation(usb_transfer_protocol.Bulk), answer: Answer(usb_transfer_protocol.Transferred)) isize { + const engine = if (controller) |*c| c else return refused; + const found = engine.findInterface(invocation.target) orelse return refused; + const endpoint = library.Controller.endpointForAddress(found.interface, invocation.request.endpoint_address) orelse return refused; + const transferred = engine.bulkTransfer(found.device, endpoint, invocation.request.physical_address, invocation.request.length) orelse return refused; + answer.set(.{ .actual_length = transferred }); + return 0; +} + +/// dma_attach: bind the class driver's DMA-region capability into the controller's IOMMU +/// domain, so the controller may DMA to the physical addresses inside that buffer. The +/// binding holds its own kernel reference, so this never claims the arriving handle — +/// the turn's `defer` in the harness is the close, on the failure paths as well as this +/// one. +fn onDmaAttach(_: void, invocation: Invocation(void), _: Answer(void)) isize { + const handle = invocation.capability orelse return -envelope.EPROTO; + return if (device.dmaBind(controller_id, handle)) 0 else refused; } /// A timer tick or an MSI landed: drain the event ring, reconcile ports, and fan out. @@ -679,14 +697,18 @@ fn serviceController() void { if (serviced >= 32) break; } while (engine.takeReport()) |report| { - var message = usb_transfer_protocol.InterruptReport{ - .device_token = report.device_token, - .endpoint_address = report.endpoint_address, - .length = @intCast(@min(report.length, usb_transfer_protocol.max_report_data)), - }; + // One `interrupt_report` event packet: the device token in the folded + // header, the report after it. A device that produced more than the + // push floor admits has its report truncated here, never split. const n = @min(report.length, usb_transfer_protocol.max_report_data); - @memcpy(message.data[0..n], report.data[0..n]); - _ = ipc.send(report.report_endpoint, std.mem.asBytes(&message)); + var payload = usb_transfer_protocol.InterruptReport{ + .endpoint_address = report.endpoint_address, + .length = @intCast(n), + }; + @memcpy(payload.data[0..n], report.data[0..n]); + var packet: [envelope.post_maximum]u8 = undefined; + const framed = usb_transfer_protocol.Protocol.encodeEvent(.interrupt_report, report.device_token, payload, &packet) orelse continue; + _ = ipc.send(report.report_endpoint, framed); } } } diff --git a/system/services/acpi/acpi.zig b/system/services/acpi/acpi.zig index f56978e..169bf42 100644 --- a/system/services/acpi/acpi.zig +++ b/system/services/acpi/acpi.zig @@ -22,6 +22,7 @@ const logging = @import("logging"); const aml = @import("aml"); const acpi_ids = @import("acpi-ids"); const device_manager_protocol = @import("device-manager-protocol"); +const envelope = @import("envelope"); const power_protocol = @import("power-protocol"); /// AML opcode/prefix bytes by name (`zero_opcode`, `byte_prefix`, …) — so the `_HID` /// integer decode names the opcodes instead of bare 0x0A/0x0B/… (docs/coding-standards.md). @@ -241,10 +242,20 @@ fn onInit(endpoint: ipc.Handle) bool { else std.log.info("device {d} bus=acpi hid={s} ({d} resources)", .{ entry.device_id, hid, entry.resource_count }); if (manager) |h| { - var report = device_manager_protocol.ChildAdded{ .bus = @intFromEnum(device_manager_protocol.BusKind.acpi), .parent = node_id, .bus_address = entry.device_id, .identity = 0, .device_id = entry.device_id }; + // The registered device id is the packet's target, so the body only + // says where on the firmware tree the node sits and what it is. + var report = device_manager_protocol.ChildAdded{ + .bus = @intFromEnum(device_manager_protocol.BusKind.acpi), + .parent = node_id, + .bus_address = entry.device_id, + .identity = 0, + }; @memcpy(report.hid[0..entry.hid_len], entry.hid[0..entry.hid_len]); - var reply: [device_manager_protocol.message_maximum]u8 = undefined; - _ = ipc.call(h, std.mem.asBytes(&report), &reply) catch {}; + var packet: [device_manager_protocol.message_maximum]u8 = undefined; + if (device_manager_protocol.Protocol.encodeRequest(.child_added, entry.device_id, report, &.{}, &packet)) |framed| { + var reply: [device_manager_protocol.message_maximum]u8 = undefined; + _ = ipc.call(h, framed, &reply) catch {}; + } } } std.log.info("reported {d} device(s) to the manager", .{registered_count}); @@ -389,13 +400,21 @@ fn dispatchGpe(n: u32) void { fn publishNotify(node: *aml.Node, code: u64) void { // Map the notified device's _HID to a domain event where we recognize it. + // The kind IS the packet's verb, so the mapping picks which event to frame + // rather than which tag to put in a payload. var hid: [8]u8 = .{0} ** 8; if (readHid(node, &global_interpreter)) |h| hid = h; - const which: power_protocol.Event = if (std.mem.eql(u8, hid[0..7], "PNP0C0A")) .battery else if (std.mem.eql(u8, hid[0..7], "ACPI0003")) .ac else if (std.mem.eql(u8, hid[0..7], "PNP0C0D")) .lid else .notify; - var event = power_protocol.EventMessage{ .event = @intFromEnum(which), .code = @truncate(code) }; - event.hid = hid; + const notice = power_protocol.Notice{ .code = @truncate(code), .hid = hid }; std.log.info("power: notify {s} code {d}", .{ hid[0..7], code }); - publishEvent(std.mem.asBytes(&event)); + if (std.mem.eql(u8, hid[0..7], "PNP0C0A")) { + publish(.battery, notice); + } else if (std.mem.eql(u8, hid[0..7], "ACPI0003")) { + publish(.ac, notice); + } else if (std.mem.eql(u8, hid[0..7], "PNP0C0D")) { + publish(.lid, notice); + } else { + publish(.notify, notice); + } } /// Two lowercase hex digits of `n` into `out[0..2]`. @@ -406,14 +425,19 @@ fn writeHex2(out: []u8, n: u32) void { } fn publishButton() void { - const event = power_protocol.EventMessage{ .event = @intFromEnum(power_protocol.Event.power_button) }; - publishEvent(std.mem.asBytes(&event)); + publish(.power_button, .{}); } -fn publishEvent(bytes: []const u8) void { +/// Push one event to every subscriber. The kind is the packet's operation, so +/// this is framed once, outside the loop — every subscriber gets identical +/// bytes. A subscriber whose endpoint stops accepting (it died) is dropped on +/// the failed send, so a dead one can never stall the rest. +fn publish(comptime kind: power_protocol.Event, notice: power_protocol.Notice) void { + var packet: [envelope.post_maximum]u8 = undefined; + const framed = power_protocol.Protocol.encodeEvent(kind, 0, notice, &packet) orelse return; for (&subscribers) |*slot| { if (slot.*) |handle| { - if (!ipc.send(handle, bytes)) slot.* = null; + if (!ipc.send(handle, framed)) slot.* = null; } } } @@ -449,43 +473,54 @@ fn onNotification(badge: u64) void { onSci(); } -/// The `.power` protocol: subscribe (endpoint as the call's capability), -/// shutdown (PID 1 only). Device discovery uses a different endpoint (the -/// device manager's), so nothing here handles ChildAdded. +/// The generated power dispatch. One provider per system, so the handler context +/// is empty and the subscriber table stays in this file's globals. +const Serve = power_protocol.Protocol.Provider(void); + +const Invocation = envelope.Invocation; +const Answer = envelope.Answer; + +/// Set by `onSubscribe` when the subscriber table has taken the capability the +/// call carried, and read by `onMessage`, where the turn's `Arrival` lives. +var capability_claimed = false; + +/// The power contract: the reserved `subscribe` (the subscriber's endpoint as +/// the call's capability) and `shutdown` (subscribers only). Device discovery +/// uses a different endpoint — the device manager's — so nothing here handles a +/// tree report. fn onMessage(message: []const u8, reply: []u8, sender: u32, arrived: *ipc.Arrival) usize { - if (message.len < 1) return 0; - switch (message[0]) { - @intFromEnum(power_protocol.Operation.subscribe) => { - // The subscriber's endpoint is claimed only when a slot takes it; - // a full table refuses and the turn closes what arrived. - var status: i32 = -1; - if (arrived.peek() != null) { - for (&subscribers, 0..) |*slot, si| { - if (slot.* == null) { - slot.* = arrived.take(); - subscriber_tasks[si] = sender; - status = 0; - break; - } - } - } - const r = power_protocol.Reply{ .status = status }; - @memcpy(reply[0..@sizeOf(power_protocol.Reply)], std.mem.asBytes(&r)); - return @sizeOf(power_protocol.Reply); - }, - @intFromEnum(power_protocol.Operation.shutdown) => { - // Honored only from a power subscriber — init, which has already run - // the stop sequence over everything else. The power service is - // mechanism (write S5); deciding *when* to shut down and stopping - // the rest of the system first is init's policy. - const allowed = isSubscriber(sender); - const r = power_protocol.Reply{ .status = if (allowed) 0 else -1 }; - @memcpy(reply[0..@sizeOf(power_protocol.Reply)], std.mem.asBytes(&r)); - if (allowed) enterS5(); - return @sizeOf(power_protocol.Reply); - }, - else => return 0, + capability_claimed = false; + const written = Serve.dispatch({}, handlers, message, sender, arrived.peek(), reply); + if (capability_claimed) _ = arrived.take(); + return written; +} + +const handlers = Serve.Handlers{ .shutdown = onShutdown, .subscribe = onSubscribe }; + +/// The subscriber's endpoint is claimed only when a slot takes it; a full table +/// refuses and the turn closes what arrived. +fn onSubscribe(_: void, invocation: Invocation(void), _: Answer(void)) isize { + const endpoint = invocation.capability orelse return -envelope.EPROTO; + for (&subscribers, 0..) |*slot, index| { + if (slot.* == null) { + slot.* = endpoint; + subscriber_tasks[index] = invocation.sender; + capability_claimed = true; + return 0; + } } + return -envelope.ENOSPC; +} + +/// Honored only from a power subscriber — init, which has already run the stop +/// sequence over everything else. The power service is mechanism (write S5); +/// deciding *when* to shut down and stopping the rest of the system first is +/// init's policy. The badge is the whole gate: it is kernel-stamped, so nothing +/// in the packet can claim to be init. +fn onShutdown(_: void, invocation: Invocation(void), _: Answer(void)) isize { + if (!isSubscriber(invocation.sender)) return -envelope.EPERM; + enterS5(); + return 0; } /// Depth-first walk: register + report each present device with a _HID, then diff --git a/system/services/acpi/build.zig b/system/services/acpi/build.zig index 96fb107..48263fc 100644 --- a/system/services/acpi/build.zig +++ b/system/services/acpi/build.zig @@ -14,8 +14,8 @@ pub fn build(b: *std.Build) void { .name = "discovery", .root_source_file = b.path("acpi.zig"), .imports = &.{ - "acpi-ids", "aml", "channel", "device-manager-protocol", "driver", "ipc", "logging", - "memory", "power-protocol", "process", "service", "time", + "acpi-ids", "aml", "channel", "device-manager-protocol", "driver", "envelope", + "ipc", "logging", "memory", "power-protocol", "process", "service", "time", }, }); b.installArtifact(exe); diff --git a/system/services/device-manager/build.zig b/system/services/device-manager/build.zig index 256f8d3..80b8f92 100644 --- a/system/services/device-manager/build.zig +++ b/system/services/device-manager/build.zig @@ -10,8 +10,8 @@ pub fn build(b: *std.Build) void { .name = "device-manager", .root_source_file = b.path("device-manager.zig"), .imports = &.{ - "device-manager-protocol", "device-registry", "driver", "file-system", "ipc", - "logging", "memory", "process", "service", "time", + "device-manager-protocol", "device-registry", "driver", "envelope", "file-system", + "ipc", "logging", "memory", "process", "service", "time", }, }); b.installArtifact(exe); diff --git a/system/services/device-manager/device-manager.zig b/system/services/device-manager/device-manager.zig index 9d7e6e3..4954834 100644 --- a/system/services/device-manager/device-manager.zig +++ b/system/services/device-manager/device-manager.zig @@ -24,7 +24,15 @@ 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. One manager per system, so the handler +/// context is empty and the tables stay in this file's globals. +const Serve = device_manager_protocol.Protocol.Provider(void); + +const Invocation = envelope.Invocation; +const Answer = envelope.Answer; const fs = @import("file-system"); // --- the device registry ------------------------------------------------------ @@ -155,12 +163,20 @@ var test_kill_due_ns: u64 = 0; const maximum_subscribers = 8; var subscribers: [maximum_subscribers]?ipc.Handle = .{null} ** maximum_subscribers; -/// Publish one event (a ChildAdded or ChildRemoved struct, the same encoding -/// the bus drivers send) to every subscriber. -fn publishEvent(event: []const u8) void { +/// Push one event to every subscriber: the same struct a bus driver *called* +/// with, framed as an event instead — one encoding, both directions, told apart +/// by the packet's verb rather than by anything inside it. A subscriber whose +/// endpoint stops accepting (it died) is dropped on the failed send. +fn publish( + comptime event: device_manager_protocol.Event, + target: u64, + payload: device_manager_protocol.Protocol.PayloadOf(event), +) void { + var packet: [envelope.post_maximum]u8 = undefined; + const framed = device_manager_protocol.Protocol.encodeEvent(event, target, payload, &packet) orelse return; for (&subscribers) |*slot| { if (slot.*) |handle| { - if (!ipc.send(handle, event)) slot.* = null; // dead subscriber + if (!ipc.send(handle, framed)) slot.* = null; // dead subscriber } } } @@ -209,8 +225,7 @@ fn pruneChildrenOf(reporter: u32) void { if (child.used and child.reporter == reporter) { std.log.info("child removed (device {d} port {d})", .{ child.parent, child.bus_address }); child.used = false; - const event = device_manager_protocol.ChildRemoved{ .parent = child.parent, .bus_address = child.bus_address }; - publishEvent(std.mem.asBytes(&event)); + publish(.child_removed, 0, .{ .parent = child.parent, .bus_address = child.bus_address }); } } } @@ -395,59 +410,72 @@ fn initialise(endpoint: ipc.Handle) bool { return true; } -fn onMessage(message: []const u8, reply: []u8, sender: u32, arrived: *ipc.Arrival) usize { - if (message.len < 1) return 0; - switch (message[0]) { - @intFromEnum(device_manager_protocol.Operation.child_added) => return onChildAdded(message, reply, sender), - @intFromEnum(device_manager_protocol.Operation.child_removed) => return onChildRemoved(message, reply, sender), - @intFromEnum(device_manager_protocol.Operation.enumerate) => return onEnumerate(reply), - @intFromEnum(device_manager_protocol.Operation.subscribe) => return onSubscribe(reply, arrived), - @intFromEnum(device_manager_protocol.Operation.hello) => {}, - else => return 0, - } - if (message.len < device_manager_protocol.hello_size) return 0; - const hello = std.mem.bytesToValue(device_manager_protocol.Hello, message[0..device_manager_protocol.hello_size]); +/// Set by `onSubscribe` when the subscriber table has taken ownership of the +/// capability the call carried, and read by `onMessage`, where the turn's +/// `Arrival` lives. The generated dispatch hands a handler the raw handle rather +/// than the `Arrival` — deliberately, since a handler has no business closing the +/// turn's property — so the *claim* travels back out this way. One turn, one +/// handler, one thread: there is nothing here to race. +var capability_claimed = false; - var status: i32 = 0; - if (hello.version != device_manager_protocol.version) { - status = -1; - std.log.info("refused hello (version {d}) from process {d}", .{ hello.version, sender }); - } else if (driverByProcess(sender)) |driver| { - driver.state = .running; - std.log.info("hello from {s} (device {d})", .{ driver.name(), hello.device_id }); - // 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 = sender; - test_kill_due_ns = time.clock() + 1_500_000_000; - _ = time.timerOnce(manager_endpoint, 1600); - } - } else { - status = -1; - std.log.info("hello from unknown process {d}", .{sender}); - } - const hello_reply = device_manager_protocol.HelloReply{ .status = status }; - @memcpy(reply[0..device_manager_protocol.reply_size], std.mem.asBytes(&hello_reply)); - return device_manager_protocol.reply_size; +fn onMessage(message: []const u8, reply: []u8, sender: u32, arrived: *ipc.Arrival) usize { + capability_claimed = false; + const written = Serve.dispatch({}, handlers, message, sender, arrived.peek(), reply); + if (capability_claimed) _ = arrived.take(); + return written; } -/// A bus driver reported a discovered device: mirror it, and in -/// test-usb-restart mode kill the reporter once after its second child — the +const handlers = Serve.Handlers{ + .hello = onHello, + .child_added = onChildAdded, + .child_removed = onChildRemoved, + .enumerate = onEnumerate, + .subscribe = onSubscribe, +}; + +/// 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; + 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(message: []const u8, reply: []u8, sender: u32) usize { - if (message.len < device_manager_protocol.child_added_size) return 0; - const report = std.mem.bytesToValue(device_manager_protocol.ChildAdded, message[0..device_manager_protocol.child_added_size]); - var status: i32 = 0; +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, report.device_id, sender)) status = -1; + 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) publishEvent(message[0..device_manager_protocol.child_added_size]); + if (status == 0) 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 report.device_id != device_manager_protocol.no_device) { + 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) @@ -458,15 +486,13 @@ fn onChildAdded(message: []const u8, reply: []u8, sender: u32) usize { 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(report.device_id)) addDriver(match.driver, report.device_id, true); + if (!driverForDevice(device_id)) addDriver(match.driver, device_id, true); } } } } else { - status = -1; + status = -envelope.EPERM; } - const report_reply = device_manager_protocol.ReportReply{ .status = status }; - @memcpy(reply[0..@sizeOf(device_manager_protocol.ReportReply)], std.mem.asBytes(&report_reply)); 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) { @@ -494,61 +520,55 @@ fn onChildAdded(message: []const u8, reply: []u8, sender: u32) usize { } } } - return @sizeOf(device_manager_protocol.ReportReply); + return status; } -/// A bus driver reported a device gone (hot-unplug; no sender exists yet, but -/// the handler is protocol-complete — death-pruning covers removal until then). -fn onChildRemoved(message: []const u8, reply: []u8, sender: u32) usize { - if (message.len < device_manager_protocol.child_removed_size) return 0; - const report = std.mem.bytesToValue(device_manager_protocol.ChildRemoved, message[0..device_manager_protocol.child_removed_size]); - var status: i32 = -1; +/// 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 == sender) { + 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; } } - const report_reply = device_manager_protocol.ReportReply{ .status = status }; - @memcpy(reply[0..@sizeOf(device_manager_protocol.ReportReply)], std.mem.asBytes(&report_reply)); - return @sizeOf(device_manager_protocol.ReportReply); + return status; } -/// An application asked for the tree: the mirror, as a header plus entries. -fn onEnumerate(reply: []u8) usize { - var count: u32 = 0; - var offset: usize = @sizeOf(device_manager_protocol.EnumerateReply); +/// 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 (offset + @sizeOf(device_manager_protocol.ChildEntry) > reply.len) break; + 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(reply[offset..][0..@sizeOf(device_manager_protocol.ChildEntry)], std.mem.asBytes(&entry)); - offset += @sizeOf(device_manager_protocol.ChildEntry); - count += 1; + @memcpy(tail[written..][0..entry_size], std.mem.asBytes(&entry)); + written += entry_size; } - const header = device_manager_protocol.EnumerateReply{ .status = 0, .count = count }; - @memcpy(reply[0..@sizeOf(device_manager_protocol.EnumerateReply)], std.mem.asBytes(&header)); - return offset; + return @intCast(written); } -/// An application subscribed: its endpoint arrived as the call's capability. The -/// table taking a slot is what claims it (`take`); a full table refuses and lets -/// the turn close it, so a subscribe storm cannot spend the handle table too. -fn onSubscribe(reply: []u8, arrived: *ipc.Arrival) usize { - var status: i32 = -1; - if (arrived.peek() != null) { - for (&subscribers) |*slot| { - if (slot.* == null) { - slot.* = arrived.take(); // claimed: the table holds it from here - status = 0; - break; - } +/// The reserved `subscribe` verb: an application's endpoint arrived as the call's +/// capability. The table taking a slot is what claims it; a full table refuses +/// and lets the turn close it, so a subscribe storm cannot spend the handle table. +fn onSubscribe(_: void, invocation: Invocation(void), _: Answer(void)) isize { + const endpoint = invocation.capability orelse return -envelope.EPROTO; + for (&subscribers) |*slot| { + if (slot.* == null) { + slot.* = endpoint; + capability_claimed = true; // the table holds it from here + return 0; } } - const report_reply = device_manager_protocol.ReportReply{ .status = status }; - @memcpy(reply[0..@sizeOf(device_manager_protocol.ReportReply)], std.mem.asBytes(&report_reply)); - return @sizeOf(device_manager_protocol.ReportReply); + return -envelope.ENOSPC; } fn onNotification(badge: u64) void { diff --git a/system/services/init/init.zig b/system/services/init/init.zig index 43ba009..ab679d3 100644 --- a/system/services/init/init.zig +++ b/system/services/init/init.zig @@ -902,9 +902,12 @@ fn onPowerEvent(sender: u32, payload: []const u8) void { std.log.info("ignored a power event from pid {d}: /protocol/power is pid {d}", .{ sender, authorized }); return; } - if (payload.len < 2) return; - if (payload[0] != @intFromEnum(power_protocol.Operation.event)) return; - if (payload[1] == @intFromEnum(power_protocol.Event.power_button)) shutDown(); + // Read as an envelope packet, never by byte offset: the kind IS the packet's + // verb, so a power event is decoded exactly the way every other event in the + // system is. A packet whose operation is not one of this protocol's events — + // anything else that lands in this mailbox — decodes to null and is dropped. + const kind = power_protocol.Protocol.eventOf(payload) orelse return; + if (kind == .power_button) shutDown(); } /// A supervised boot service died. Find which one and restart it — unless it exited @@ -955,9 +958,11 @@ fn restartChild(id: u32) void { /// init calls owes the same discipline. fn subscribePower() void { const handle = power_endpoint orelse return; - const request = power_protocol.Subscribe{}; + // The reserved `subscribe` verb: nothing but the header, with our own + // endpoint riding as the call's capability. + const header = envelope.Header{ .operation = envelope.operation_subscribe }; var reply: [power_protocol.message_maximum]u8 = undefined; - _ = ipc.callCap(handle, std.mem.asBytes(&request), &reply, supervision_endpoint) catch {}; + _ = ipc.callCap(handle, std.mem.asBytes(&header), &reply, supervision_endpoint) catch {}; } /// The stop sequence: persist the log while storage is still up, then terminate @@ -976,9 +981,11 @@ fn shutDown() void { if (child_ids[i] != 0) process.stop(child_ids[i], 2000, supervision_endpoint); } if (power_endpoint) |h| { - const request = power_protocol.Shutdown{}; - var reply: [power_protocol.message_maximum]u8 = undefined; - _ = ipc.call(h, std.mem.asBytes(&request), &reply) catch {}; + var packet: [power_protocol.message_maximum]u8 = undefined; + if (power_protocol.Protocol.encodeRequest(.shutdown, 0, {}, &.{}, &packet)) |framed| { + var reply: [power_protocol.message_maximum]u8 = undefined; + _ = ipc.call(h, framed, &reply) catch {}; + } } // If S5 did not take, init has nothing left to do but idle. while (true) time.sleepMillis(1000); diff --git a/test/qemu_test.py b/test/qemu_test.py index a273b77..2b7cab3 100644 --- a/test/qemu_test.py +++ b/test/qemu_test.py @@ -896,10 +896,14 @@ CASES = [ # it finds is the proof that `Define` hands those verbs to everyone alike. # The scenario boots the registry, the input service and the compositor: two # protocols of different sizes and verb counts, both reached through the real - # registry under the manifest's own grants. vfs, block and scanout are the - # P4a protocols whose providers need hardware chains this case does not boot; - # the fixture names them as unchecked rather than skipping them quietly, and - # the existing fat-mount / usb-storage / virtio-gpu scenarios are their proof. + # registry under the manifest's own grants. The other six the fixture knows — + # vfs, block, scanout, device-manager, power and usb-transfer — need provider + # chains this case does not boot (the last three all arrive with the device + # manager, i.e. with the whole driver tree, whose timing would make this + # fixture's one namespace snapshot a boot race). It names them as unchecked + # rather than skipping them quietly, and the fat-mount / usb-storage / + # virtio-gpu / device-list / driver-restart / pci-scan / usb-* / + # orderly-shutdown scenarios are their proof. {"name": "protocol-conformance", "expect": r"(?s)(?=.*protocol-conformance: input v\d+ describes itself)" r"(?=.*protocol-conformance: display v\d+ describes itself)" diff --git a/test/system/services/crash-test/crash-test.zig b/test/system/services/crash-test/crash-test.zig index 40fb952..8107a2e 100644 --- a/test/system/services/crash-test/crash-test.zig +++ b/test/system/services/crash-test/crash-test.zig @@ -34,9 +34,17 @@ pub fn main(init: process.Init) void { if (manager == null) time.sleepMillis(20); } const h = manager orelse return; - const hello = device_manager_protocol.Hello{ .role = @intFromEnum(device_manager_protocol.Role.device), .device_id = assigned }; + // The assigned device is the packet's target, the manager's object addressing. + var packet: [device_manager_protocol.message_maximum]u8 = undefined; + const framed = device_manager_protocol.Protocol.encodeRequest( + .hello, + assigned, + .{ .role = @intFromEnum(device_manager_protocol.Role.device) }, + &.{}, + &packet, + ) orelse return; var reply: [device_manager_protocol.message_maximum]u8 = undefined; - _ = ipc.call(h, std.mem.asBytes(&hello), &reply) catch return; + _ = ipc.call(h, framed, &reply) catch return; _ = logging.write("crash-test: faulting now\n"); const poison: *volatile u32 = @ptrFromInt(0xdead0000); diff --git a/test/system/services/device-list/build.zig b/test/system/services/device-list/build.zig index 821d42d..09bb551 100644 --- a/test/system/services/device-list/build.zig +++ b/test/system/services/device-list/build.zig @@ -9,7 +9,7 @@ pub fn build(b: *std.Build) void { const exe = build_support.userBinary(b, .{ .name = "device-list", .root_source_file = b.path("device-list.zig"), - .imports = &.{ "channel", "device-manager-protocol", "ipc", "logging", "time" }, + .imports = &.{ "channel", "device-manager-protocol", "envelope", "ipc", "logging", "time" }, }); b.installArtifact(exe); } diff --git a/test/system/services/device-list/device-list.zig b/test/system/services/device-list/device-list.zig index 6df4d9c..f7b84f3 100644 --- a/test/system/services/device-list/device-list.zig +++ b/test/system/services/device-list/device-list.zig @@ -6,6 +6,7 @@ const std = @import("std"); const channel = @import("channel"); +const envelope = @import("envelope"); const ipc = @import("ipc"); const time = @import("time"); const logging = @import("logging"); @@ -29,36 +30,41 @@ pub fn main() void { }; // The snapshot — polled briefly, because at boot the bus drivers may still - // be scanning: an empty first answer usually just means "too early". + // be scanning: an empty first answer usually just means "too early". This is + // the envelope's reserved `enumerate` verb, so the request is nothing but a + // header and the answer is one `ChildEntry` per record in the reply's tail — + // how many arrived is the reply's own length, which is why no count header + // says it a second time. + const Entry = device_manager_protocol.ChildEntry; + const enumerate = envelope.Header{ .operation = envelope.operation_enumerate }; var reply: [device_manager_protocol.message_maximum]u8 = undefined; - var count: u32 = 0; - var length: usize = 0; + var count: usize = 0; tries = 0; while (tries < 20) : (tries += 1) { - const request = device_manager_protocol.Enumerate{}; - length = ipc.call(h, std.mem.asBytes(&request), &reply) catch 0; - if (length >= @sizeOf(device_manager_protocol.EnumerateReply)) { - count = std.mem.bytesToValue(device_manager_protocol.EnumerateReply, reply[0..@sizeOf(device_manager_protocol.EnumerateReply)]).count; - if (count != 0) break; + const length = ipc.call(h, std.mem.asBytes(&enumerate), &reply) catch 0; + if (envelope.statusOf(reply[0..length])) |status| { + if (status.status == 0) { + const carried = @min(@as(usize, status.len), length - envelope.prefix_size); + count = carried / @sizeOf(Entry); + if (count != 0) break; + } } time.sleepMillis(100); } writeLine("device-list: {d} devices\n", .{count}); - var offset: usize = @sizeOf(device_manager_protocol.EnumerateReply); - var index: u32 = 0; - while (index < count and offset + @sizeOf(device_manager_protocol.ChildEntry) <= length) : (index += 1) { - const entry = std.mem.bytesToValue(device_manager_protocol.ChildEntry, reply[offset..][0..@sizeOf(device_manager_protocol.ChildEntry)]); + for (0..count) |index| { + const entry = std.mem.bytesToValue(Entry, reply[envelope.prefix_size + index * @sizeOf(Entry) ..][0..@sizeOf(Entry)]); writeLine("device-list: device {d} port {d} identity {d}\n", .{ entry.parent, entry.bus_address, entry.identity }); - offset += @sizeOf(device_manager_protocol.ChildEntry); } - // The subscription: our endpoint rides as the call's capability; events - // arrive as buffered messages carrying the same structs the bus sends. + // The subscription — the reserved `subscribe` verb: our endpoint rides as + // the call's capability, and events arrive as buffered packets carrying the + // same structs the bus drivers send, under the events' own numbering. const endpoint = ipc.createIpcEndpoint() orelse { _ = logging.write("device-list: no endpoint\n"); return; }; - const subscribe = device_manager_protocol.Subscribe{}; + const subscribe = envelope.Header{ .operation = envelope.operation_subscribe }; _ = ipc.callCap(h, std.mem.asBytes(&subscribe), &reply, endpoint) catch { _ = logging.write("device-list: subscribe failed\n"); return; @@ -68,19 +74,18 @@ pub fn main() void { var receive: [device_manager_protocol.message_maximum]u8 = undefined; while (true) { const got = ipc.replyWait(endpoint, &.{}, &receive, null); - if (!got.isMessage() or got.len < 1) continue; - switch (receive[0]) { - @intFromEnum(device_manager_protocol.Operation.child_added) => { - if (got.len < device_manager_protocol.child_added_size) continue; - const event = std.mem.bytesToValue(device_manager_protocol.ChildAdded, receive[0..device_manager_protocol.child_added_size]); - writeLine("device-list: added (device {d} port {d})\n", .{ event.parent, event.bus_address }); + if (!got.isMessage()) continue; + const packet = receive[0..got.len]; + const event = device_manager_protocol.Protocol.eventOf(packet) orelse continue; + switch (event) { + .child_added => { + const added = device_manager_protocol.Protocol.decodeEvent(.child_added, packet) orelse continue; + writeLine("device-list: added (device {d} port {d})\n", .{ added.parent, added.bus_address }); }, - @intFromEnum(device_manager_protocol.Operation.child_removed) => { - if (got.len < device_manager_protocol.child_removed_size) continue; - const event = std.mem.bytesToValue(device_manager_protocol.ChildRemoved, receive[0..device_manager_protocol.child_removed_size]); - writeLine("device-list: removed (device {d} port {d})\n", .{ event.parent, event.bus_address }); + .child_removed => { + const removed = device_manager_protocol.Protocol.decodeEvent(.child_removed, packet) orelse continue; + writeLine("device-list: removed (device {d} port {d})\n", .{ removed.parent, removed.bus_address }); }, - else => {}, } } } diff --git a/test/system/services/protocol-conformance-test/build.zig b/test/system/services/protocol-conformance-test/build.zig index 0040ed2..6b40342 100644 --- a/test/system/services/protocol-conformance-test/build.zig +++ b/test/system/services/protocol-conformance-test/build.zig @@ -12,15 +12,18 @@ pub fn build(b: *std.Build) void { .imports = &.{ "block-protocol", "channel", + "device-manager-protocol", "display-protocol", "envelope", "file-system", "input-protocol", "ipc", "logging", + "power-protocol", "process", "scanout-protocol", "time", + "usb-transfer-protocol", "vfs-protocol", }, }); diff --git a/test/system/services/protocol-conformance-test/protocol-conformance-test.zig b/test/system/services/protocol-conformance-test/protocol-conformance-test.zig index e5dade4..879d013 100644 --- a/test/system/services/protocol-conformance-test/protocol-conformance-test.zig +++ b/test/system/services/protocol-conformance-test/protocol-conformance-test.zig @@ -24,31 +24,37 @@ //! exactly the two contracts its scenario boots, and an ungranted name is absent //! for it like any other client's. //! -//! **What it covers, and what it cannot — the honest list at P4a.** The scenario -//! boots the registry, the input service, and the compositor, so `input` and -//! `display` are checked end to end over real IPC. The other three protocols P4a -//! rebased are not asked here, and the reason is the provider, not the protocol: +//! **What it covers, and what it cannot — the honest list.** The scenario boots +//! the registry, the input service, and the compositor, so `input` and `display` +//! are checked end to end over real IPC. The other six contracts in the table +//! are not asked here, and the reason is the provider, not the protocol: //! //! - `vfs` — the FAT server, which needs a mounted volume behind the whole USB //! storage chain (the `fat-mount` scenario); //! - `block` — the usb-storage driver, which the device manager spawns after //! enumerating an xHCI bus (the `usb-storage` scenario); //! - `scanout` — the virtio-gpu driver, which needs an emulated virtio-gpu the -//! default harness does not attach (the `virtio-gpu` scenario). +//! default harness does not attach (the `virtio-gpu` scenario); +//! - `device-manager`, `power` and `usb-transfer` — the three P4b rebased. All +//! three come with the device manager: it *is* the first, it spawns the +//! discovery service that binds the second, and the xHCI driver it spawns +//! binds the third. So booting a provider for any one of them means booting +//! the whole driver tree here. //! -//! Booting any of those chains here would buy conformance for a third and fourth -//! provider at the price of a case that boots half the system to send two -//! packets; their rebase is proven instead by the scenarios that already drive -//! them. All three sit in the table below anyway, so if a future scenario binds -//! one, this fixture checks it without being edited — and prints, every run, the -//! ones it found no provider for. +//! That is the reason this scenario stays at two providers rather than five or +//! eight. It is not only the cost of booting half the system to send two +//! packets: this fixture takes **one snapshot** of `/protocol` and checks what +//! is in it, so a scenario whose bound set depends on how far a driver tree got +//! by that instant would make the case's own summary a boot race. What proves +//! the six instead is the scenarios that already drive them end to end — +//! `fat-mount`, `usb-storage`, `virtio-gpu`, and for the P4b three the +//! `device-list`, `driver-restart`, `pci-scan`, `usb-*`, `power-button` and +//! `orderly-shutdown` cases, every one of which is a live conversation over +//! these wires. //! -//! **And the ones it must not ask.** `device-manager`, `power` and -//! `usb-transfer` are still hand-numbered (P4b): to them, operation 0 is a verb -//! of their own, not `describe`. So the table is not "every contract" but "every -//! contract already built on `Define`" — anything listed that is not in it is -//! reported as skipped by name, never silently. P4b adds three rows here and the -//! coverage follows. +//! All six sit in the table below regardless, so a scenario that binds one gets +//! it conformance-checked without this file being edited — and every run prints, +//! by name, the ones it found no provider for. //! //! The registry itself — PID 1 serving `/protocol` — is the one vfs backend //! deliberately NOT dispatched through the generated table (it reads a @@ -68,9 +74,12 @@ const logging = @import("logging"); const process = @import("process"); const time = @import("time"); const block_protocol = @import("block-protocol"); +const device_manager_protocol = @import("device-manager-protocol"); const display_protocol = @import("display-protocol"); const input_protocol = @import("input-protocol"); +const power_protocol = @import("power-protocol"); const scanout_protocol = @import("scanout-protocol"); +const usb_transfer_protocol = @import("usb-transfer-protocol"); const vfs_protocol = @import("vfs-protocol"); // --- what conformance means, per contract ----------------------------------- @@ -100,16 +109,23 @@ fn contractOf(comptime Protocol: type, required: bool) Contract { }; } -/// The protocols built on `envelope.Define`, and nothing else. A name listed by -/// `/protocol` that is absent from here is reported and left alone — see the -/// header: asking a hand-numbered provider for operation 0 would name one of its -/// own verbs. +/// The protocols built on `envelope.Define`. A name listed by `/protocol` that +/// is absent from here is reported and left alone rather than probed: a +/// hand-numbered provider would read operation 0 as one of its own verbs, so +/// asking it for `describe` would *do* something. Only `ps2-bus` is still in +/// that state today. const contracts = [_]Contract{ contractOf(input_protocol.Protocol, true), // the input fan-out service contractOf(display_protocol.Protocol, true), // the compositor contractOf(vfs_protocol.Protocol, false), // the FAT server — needs a volume contractOf(block_protocol.Protocol, false), // usb-storage — needs the xHCI chain contractOf(scanout_protocol.Protocol, false), // virtio-gpu — needs the device + // The three P4b rebased. Each needs the device manager (and, for the last + // two, what the device manager starts), which is more than this scenario + // boots — see the header. + contractOf(device_manager_protocol.Protocol, false), + contractOf(power_protocol.Protocol, false), // the discovery service + contractOf(usb_transfer_protocol.Protocol, false), // the xHCI bus driver }; /// A verb number no protocol in the system defines, and none plausibly will: far diff --git a/test/system/services/protocol-registry-test/protocol-registry-test.zig b/test/system/services/protocol-registry-test/protocol-registry-test.zig index 14ede1e..24b6a5d 100644 --- a/test/system/services/protocol-registry-test/protocol-registry-test.zig +++ b/test/system/services/protocol-registry-test/protocol-registry-test.zig @@ -288,8 +288,10 @@ fn run() void { // — the strongest one available, because a regression does not fail this // line, it takes the entire boot down with it. const registry = registryEndpoint() orelse fail("resolve /protocol"); - const forged = power_protocol.EventMessage{ .event = @intFromEnum(power_protocol.Event.power_button) }; - if (!ipc.send(registry, std.mem.asBytes(&forged))) fail("post a forged power event"); + var forged: [envelope.post_maximum]u8 = undefined; + const packet = power_protocol.Protocol.encodeEvent(.power_button, 0, .{}, &forged) orelse + fail("frame a forged power event"); + if (!ipc.send(registry, packet)) fail("post a forged power event"); const still_serving = verdictWithin(forbidden, spare) orelse fail("the registrar went silent after a forged power event — it acted on it"); if (still_serving != -envelope.EPERM) fail("the registry misanswered after a forged power event");