library: the last three protocols speak the envelope

These were the awkward ones. Each began with an operation packed into a
single byte — two of them with a version wedged in beside it — so there was
no wrapping them: the layouts had to be rebuilt. The device manager's own
enumerate and subscribe become the reserved verbs that mean the same thing
everywhere, its replies lose three status structs the envelope already
carries, and a device id becomes the packet's target. Power drops the
version it repeated on every request, because describe is the handshake,
and stops claiming a 64-byte ceiling it never needed for calls. USB moves a
control transfer's data to the packet tail in both directions, which makes
the status length the transferred length and retires a field that had been
saying the same thing twice.

The danger in this one was not the protocols but their readers. Init
recognised a power button by two bytes at the head of a message, the ACPI
service dispatched on the first byte, the xHCI driver read its operation
with a raw integer load, and the HID drivers reinterpreted a report
wholesale — none of which would have failed to compile once the layouts
moved. They would simply have stopped: no shutdown on the power button, no
reports from the keyboard. Every one of them now reads through the
generated types, and the shutdown gate that answers only a subscriber is
the same code it was.

Two sizes were decided by measuring rather than assuming. The child-added
message is both a request and the event broadcast to subscribers, and
alignment rounds it to 48 bytes, which puts its packet exactly on the
64-byte push floor — a test pins that, because a field added carelessly
would now overflow it. The interrupt report gives up eight bytes of inline
room to make space for the header; the two drivers that produce reports
send eight and four.

Suite 110/110.
This commit is contained in:
Daniel Samson 2026-08-01 07:20:37 +01:00
parent d2dfbcabf8
commit 2719b93530
No known key found for this signature in database
GPG Key ID: A9EB2589C60F7268
27 changed files with 1058 additions and 685 deletions

View File

@ -64,19 +64,38 @@ restarted instance to rebuild exactly the same ids.
## The protocol ## The protocol
A `device-manager-protocol` module (the vfs-protocol pattern): extern-struct A `device-manager-protocol` module, defined through the
messages, a version in the handshake, reserved fields everywhere. The manager is a [envelope](../os-development/protocol-namespace.md): every packet — request,
well-known endpoint (`ipc.register(.device_manager)`); the badge tells it who is 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, talking; the same endpoint receives its children's exit notifications — one loop,
one world. one world.
| Direction | Message | Purpose | | Direction | Packet | Purpose |
|---|---|---| |---|---|---|
| driver → manager | `hello { version, role, device_id }` | confirms the argv assignment, starts the deadline clock | | driver → manager | `hello { role, version }` @ the assigned device | confirms the argv assignment, starts the deadline clock |
| bus → manager | `child_added { parent, bus_address, identity, device_id, hid }` | one node the bus discovered | | 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 | | bus → manager | `child_removed { parent, bus_address }` | unplug, or the bus lost it |
| app → manager | `enumerate` | snapshot of the tree (read-only) | | app → manager | `enumerate` (reserved verb 1) | snapshot of the tree: one `ChildEntry` per record in the reply's tail |
| app → manager | `subscribe` | receive published add/remove events | | 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 `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 deadline means wrong binary, wrong protocol version, or wedged before main — apply

View File

@ -28,28 +28,39 @@ unchanged.
## The protocol ## The protocol
The `power-protocol` module ([library/protocol/power/power-protocol.zig](../../library/protocol/power/power-protocol.zig)) 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 is defined through the [envelope](protocol-namespace.md), so every packet begins
fields. Three operations: 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) | | 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` | orderly shutdown's last step: enter S5 (soft off) | | init → service | `shutdown` (verb 16) | 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) | | 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 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 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: vocabulary is hardware-neutral:
- `power_button` — the button was pressed (a fixed ACPI event on x86). - `power_button` (event 16) — the button was pressed (a fixed ACPI event on x86).
- `lid`, `ac`, `battery` — the named GPE-driven events. - `lid` (17), `ac` (18), `battery` (19) — the named GPE-driven events.
- `notify` — a device notification that maps to none of the above; its `code` - `notify` (20) — a device notification that maps to none of the above; its
(the ACPI `Notify` argument) and the notifying device's `hid` say which device `code` (the ACPI `Notify` argument) and the notifying device's `hid` say which
and what happened. device and what happened.
An `EventMessage` carries the `event` tag plus `code` and an 8-byte `hid`, so a The payload every one of them carries is a `Notice`: `code` plus an 8-byte `hid`,
generic `notify` is fully described without a second round trip. 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 **`shutdown` is authority, not information.** It is the only operation that
*does* something irreversible, so it is gated: the contract is that only init *does* something irreversible, so it is gated: the contract is that only init

View File

@ -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) | | Branch carrying it | `feat/security-group-3` (pushed to origin) |
| On `main` | Phase 0, PM, H1, P1, P2, P3 — groups 1 and 2 merged | | 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 | | Suite | 110 cases, all passing |
| Last updated | 2026-08-01 | | Last updated | 2026-08-01 |
@ -66,7 +66,7 @@ group boundary.
bind attestation and every refusal it makes untouched (suite 109/109) bind attestation and every refusal it makes untouched (suite 109/109)
- [x] **merge** group 2 → main, push - [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) - [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 - [ ] **P4c** — harness subscriber lift + badge-scoped per-client integers
- [ ] **merge** group 3 → main, push - [ ] **merge** group 3 → main, push
- [ ] **H2** — SMEP on every core - [ ] **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 USB storage/HID all exercise these wires); the conformance case now covers
three more providers. Suite 110. 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 ## P4c — harness subscriber lift + badge scoping
- `library/kernel/service.zig` grows the subscriber table, exit- - `library/kernel/service.zig` grows the subscriber table, exit-

View File

@ -61,6 +61,7 @@ pub fn build(b: *std.Build) void {
.{ .name = "abi", .module = abi }, .{ .name = "abi", .module = abi },
.{ .name = "channel", .module = channel }, .{ .name = "channel", .module = channel },
.{ .name = "device-abi", .module = device_abi }, .{ .name = "device-abi", .module = device_abi },
.{ .name = "envelope", .module = protocol.module("envelope") },
.{ .name = "system-call", .module = system_call }, .{ .name = "system-call", .module = system_call },
.{ .name = "ipc", .module = ipc }, .{ .name = "ipc", .module = ipc },
.{ .name = "time", .module = time }, .{ .name = "time", .module = time },
@ -87,6 +88,7 @@ pub fn build(b: *std.Build) void {
.root_source_file = b.path("usb/usb.zig"), .root_source_file = b.path("usb/usb.zig"),
.imports = &.{ .imports = &.{
.{ .name = "channel", .module = channel }, .{ .name = "channel", .module = channel },
.{ .name = "envelope", .module = protocol.module("envelope") },
.{ .name = "ipc", .module = ipc }, .{ .name = "ipc", .module = ipc },
.{ .name = "time", .module = time }, .{ .name = "time", .module = time },
.{ .name = "usb-transfer-protocol", .module = protocol.module("usb-transfer-protocol") }, .{ .name = "usb-transfer-protocol", .module = protocol.module("usb-transfer-protocol") },

View File

@ -8,6 +8,7 @@ const abi = @import("abi");
const device_abi = @import("device-abi"); const device_abi = @import("device-abi");
const sc = @import("system-call"); const sc = @import("system-call");
const channel = @import("channel"); const channel = @import("channel");
const envelope = @import("envelope");
const ipc = @import("ipc"); const ipc = @import("ipc");
const time = @import("time"); const time = @import("time");
const device_manager_protocol = @import("device-manager-protocol"); 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 /// (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 `_ =`, /// 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. /// 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 { pub fn hello(role: Role, device_id: u64) ?ipc.Handle {
var attempts: u32 = 0; var attempts: u32 = 0;
const manager = while (attempts < lookup_attempts) : (attempts += 1) { const manager = while (attempts < lookup_attempts) : (attempts += 1) {
@ -178,15 +182,25 @@ pub fn hello(role: Role, device_id: u64) ?ipc.Handle {
return null; return null;
}; };
const message = device_manager_protocol.Hello{ .role = @intFromEnum(role), .device_id = device_id }; var packet: [device_manager_protocol.message_maximum]u8 = undefined;
var reply: [device_manager_protocol.reply_size]u8 = undefined; const framed = device_manager_protocol.Protocol.encodeRequest(
const length = ipc.call(manager, std.mem.asBytes(&message), &reply) catch { .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", .{}); std.log.info("hello call failed", .{});
return null; return null;
}; };
if (length < device_manager_protocol.reply_size or const status = envelope.statusOf(reply[0..length]) orelse {
std.mem.bytesToValue(device_manager_protocol.HelloReply, reply[0..device_manager_protocol.reply_size]).status != 0) std.log.info("hello answered nothing readable", .{});
{ return null;
};
if (status.status != 0) {
std.log.info("hello refused", .{}); std.log.info("hello refused", .{});
return null; return null;
} }

View File

@ -11,16 +11,24 @@
//! _ = device.subscribeInterrupt(address, length); // reports arrive asynchronously //! _ = device.subscribeInterrupt(address, length); // reports arrive asynchronously
//! while (true) { ... ipc.replyWait(device.endpoint, ...) ... } // its own loop //! while (true) { ... ipc.replyWait(device.endpoint, ...) ... } // its own loop
//! //!
//! Reports are delivered to `device.endpoint` as asynchronous `InterruptReport` //! Reports are delivered to `device.endpoint` as asynchronous `interrupt_report`
//! messages (the class driver runs a bare `replyWait` loop to read them, because //! event packets, decoded with `reportOf` (the class driver runs a bare `replyWait`
//! the service harness drops buffered-message payloads see service.zig). //! 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 std = @import("std");
const channel = @import("channel"); const channel = @import("channel");
const envelope = @import("envelope");
const ipc = @import("ipc"); const ipc = @import("ipc");
const time = @import("time"); const time = @import("time");
const usb_transfer_protocol = @import("usb-transfer-protocol"); 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 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`). /// the whole USB domain through its one `usb` import (`usb.abi.getDescriptor`, `usb.ids.Class`).
pub const abi = @import("usb-abi"); pub const abi = @import("usb-abi");
@ -57,21 +65,41 @@ pub const Device = struct {
return null; 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 { fn controlTransfer(self: *Device, setup: [8]u8, direction_in: bool, data: []u8) ?usize {
var request = usb_transfer_protocol.ControlRequest{ if (data.len > usb_transfer_protocol.max_inline_data) return null;
.device_token = self.token, 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, .setup = setup,
.direction_in = @intFromBool(direction_in), .direction_in = @intFromBool(direction_in),
.data_length = @intCast(data.len), .data_length = @intCast(data.len),
}; }, outgoing, null, &reply) orelse return null;
if (!direction_in and data.len > 0) @memcpy(request.data[0..data.len], data);
var reply: [@sizeOf(usb_transfer_protocol.ControlReply)]u8 = undefined; const returned = Protocol.replyTail(.control, answered);
const length = ipc.call(self.bus, std.mem.asBytes(&request), &reply) catch return null; const actual = @min(returned.len, data.len);
if (length < @sizeOf(usb_transfer_protocol.ControlReply)) return null; if (direction_in and actual > 0) @memcpy(data[0..actual], returned[0..actual]);
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]);
return actual; return actual;
} }
@ -89,15 +117,11 @@ pub const Device = struct {
/// Begin periodic IN polling of an interrupt endpoint; reports flow back to /// Begin periodic IN polling of an interrupt endpoint; reports flow back to
/// `self.endpoint` as asynchronous `InterruptReport` messages. /// `self.endpoint` as asynchronous `InterruptReport` messages.
pub fn subscribeInterrupt(self: *Device, endpoint_address: u8, max_length: u16) bool { pub fn subscribeInterrupt(self: *Device, endpoint_address: u8, max_length: u16) bool {
var request = usb_transfer_protocol.InterruptSubscribeRequest{ var reply: [usb_transfer_protocol.message_maximum]u8 = undefined;
.device_token = self.token, return self.call(.interrupt_subscribe, .{
.endpoint_address = endpoint_address, .endpoint_address = endpoint_address,
.max_length = max_length, .max_length = max_length,
}; }, &.{}, null, &reply) != null;
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;
} }
/// Hand the controller a DMA-region capability (`handle` from a `shareable` /// 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 /// will name in a `bulk` transfer, before the transfer. Harmless (and a no-op
/// success) when no IOMMU is enforcing. Returns false on failure. /// success) when no IOMMU is enforcing. Returns false on failure.
pub fn attachDma(self: *Device, handle: ipc.Handle) bool { pub fn attachDma(self: *Device, handle: ipc.Handle) bool {
var request = usb_transfer_protocol.DmaAttachRequest{ .device_token = self.token }; var reply: [usb_transfer_protocol.message_maximum]u8 = undefined;
var reply: [@sizeOf(usb_transfer_protocol.DmaAttachReply)]u8 = undefined; return self.call(.dma_attach, {}, &.{}, handle, &reply) != null;
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;
} }
/// One bulk transfer (IN or OUT per `endpoint_address`'s direction bit) to or /// 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. /// 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 { pub fn bulk(self: *Device, endpoint_address: u8, physical: u64, length: u32) ?u32 {
var request = usb_transfer_protocol.BulkRequest{ var reply: [usb_transfer_protocol.message_maximum]u8 = undefined;
.device_token = self.token, const answered = self.call(.bulk, .{
.physical_address = physical, .physical_address = physical,
.length = length, .length = length,
.endpoint_address = endpoint_address, .endpoint_address = endpoint_address,
}; }, &.{}, null, &reply) orelse return null;
var reply: [@sizeOf(usb_transfer_protocol.BulkReply)]u8 = undefined; return (Protocol.decodeReply(.bulk, answered) orelse return null).actual_length;
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;
} }
}; };
/// 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 /// Open `/protocol/usb-transfer` and, on that channel, open the device with the
/// assigned id, handing over a freshly created endpoint for asynchronous interrupt /// 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 /// 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; } else return null;
const endpoint = ipc.createIpcEndpoint() orelse return null; const endpoint = ipc.createIpcEndpoint() orelse return null;
var request = usb_transfer_protocol.OpenRequest{ .device_id = device_id }; // The assigned device id is the target: it is what the caller has before a
var reply: [@sizeOf(usb_transfer_protocol.OpenReply)]u8 = undefined; // token exists, and the token the reply hands back addresses every packet
const result = ipc.callCap(bus, std.mem.asBytes(&request), &reply, endpoint) catch return null; // after this one.
if (result.len < @sizeOf(usb_transfer_protocol.OpenReply)) return null; var packet: [usb_transfer_protocol.message_maximum]u8 = undefined;
const open_reply = std.mem.bytesToValue(usb_transfer_protocol.OpenReply, reply[0..@sizeOf(usb_transfer_protocol.OpenReply)]); const framed = Protocol.encodeRequest(.open, device_id, {}, &.{}, &packet) orelse return null;
if (open_reply.status != 0) 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{ var device = Device{
.bus = bus, .bus = bus,

View File

@ -58,6 +58,9 @@ pub fn build(b: *std.Build) void {
"vfs/vfs-protocol.zig", // NodeKind / DirectoryEntry sizes + op values "vfs/vfs-protocol.zig", // NodeKind / DirectoryEntry sizes + op values
"input/input-protocol.zig", // event numbering + the push-floor budget "input/input-protocol.zig", // event numbering + the push-floor budget
"display/display-protocol.zig", // pack(): native pixel encoding per format "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| { }) |root| {
const protocol_tests = b.addTest(.{ const protocol_tests = b.addTest(.{
.root_module = b.createModule(.{ .root_module = b.createModule(.{

View File

@ -1,15 +1,48 @@
//! The device-manager protocol (docs/device-manager.md): what drivers and //! The device-manager protocol (docs/device-driver-development/device-manager.md):
//! applications say to the device manager over its well-known endpoint. The //! what drivers and applications say to the device manager over
//! vfs-protocol pattern extern-struct messages, a version in the handshake, //! `/protocol/device-manager`. Defined through the envelope
//! reserved fields so both sides depend on the contract by name. Deliberately //! (docs/os-development/protocol-namespace.md), so every packet request, reply,
//! contains nothing lifecycle-shaped: stopping, liveness (the zero-length ping), //! and pushed event alike begins with the folded `Header`.
//! and exit reasons are the universal vocabulary of //!
//! **`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. //! docs/process-lifecycle.md, not this protocol.
/// The protocol version a driver states in its hello. A manager that cannot const std = @import("std");
/// serve a driver's version refuses the hello, and the mismatch is loud at const envelope = @import("envelope");
/// startup instead of quiet corruption later.
pub const version: u16 = 1; /// 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 /// 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 /// 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, 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) { pub const Role = enum(u8) {
/// Owns a controller and reports the devices behind it (`child_added`). /// Owns a controller and reports the devices behind it (`child_added`).
bus = 1, bus = 1,
@ -32,58 +65,45 @@ pub const Role = enum(u8) {
device = 2, device = 2,
}; };
/// The message kinds. // --- the per-operation request parts ----------------------------------------
pub const Operation = enum(u8) { //
hello = 1, // Each names the bytes AFTER the prefix. Nothing here carries an operation or a
child_added = 2, // device id: those are the packet header's, folded in once. No reply part
child_removed = 3, // carries a status either that is the `Status` every reply already begins
enumerate = 4, // with, so the manager's old three `{status, reserved}` reply structs are gone.
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 handshake, sent once by every driver the manager spawns the manager's /// 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, /// 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 { pub const Hello = extern struct {
operation: u8 = @intFromEnum(Operation.hello), /// A `Role` value.
/// A Role value.
role: u8, role: u8,
_padding: u8 = 0,
/// The protocol version this driver was built against (`version`). /// The protocol version this driver was built against (`version`).
version: u16 = 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 /// A bus driver reporting one device it discovered behind its controller
/// (docs/device-manager.md "the tree"). Identity is the bus's native language /// (docs/device-driver-development/device-manager.md "the tree"), and the payload
/// for USB a port-speed class; the (class, subclass, protocol) triple joins it /// the manager pushes to its subscribers for the same event. Identity is the
/// once control transfers exist (the USB track). The manager mirrors the child /// bus's native language for USB a port-speed class, for PCI the class triple.
/// into its tree; when the reporting driver dies, the manager prunes everything /// The manager mirrors the child into its tree; when the reporting driver dies,
/// it reported (the children describe protocol state that died with it) and the /// the manager prunes everything it reported (the children describe protocol
/// restarted instance rediscovers and re-reports. /// 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 { 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. /// The reporting driver's own device (the controller) the child's parent.
parent: u64, parent: u64,
/// Where on the bus (for USB: the root port number, 1-based). /// 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: /// Bus-specific identity (for USB: the PORTSC port-speed class; for PCI:
/// the class triple; for ACPI devices, 0 identity is the hid below). /// the class triple; for ACPI devices, 0 identity is the hid below).
identity: u64, identity: u64,
/// The kernel device id this child was `device_register`ed as what the /// The PCI subsystem id, packed `(subsystem_vendor << 16) | subsystem_device`
/// manager hands a matched driver as its argv assignment or `no_device` /// (so it reads vendor-first, matching the CSV's `ssvid:ssid`), or 0 when the
/// for an unregistered leaf (a USB port before the descriptor track). /// device has no subsystem id (a bridge, or a non-PCI bus).
device_id: u64 = no_device, subsystem: u32 = 0,
/// The vendor id (PCI vendor / USB idVendor), or 0 when the bus has no such /// 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 /// 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. /// 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 /// 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. /// virtio display function without the driver re-confirming after it is spawned.
device: u16 = 0, 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 /// The ACPI hardware id (`_HID`), EISA-decoded (e.g. "PNP0303"), for devices
/// discovered by firmware string rather than a numeric bus identity. Empty /// discovered by firmware string rather than a numeric bus identity. Empty
/// (all zero) otherwise. Widens for FDT `compatible` strings later. /// (all zero) otherwise. Widens for FDT `compatible` strings later.
hid: [8]u8 = .{0} ** 8, 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), and the payload pushed to
/// subscribers for it.
/// 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; /// **This is the one message whose target stays 0.** A removal is addressed by
/// death-pruning covers removal until hotplug lands. /// 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 { pub const ChildRemoved = extern struct {
operation: u8 = @intFromEnum(Operation.child_removed),
reserved0: u8 = 0,
reserved1: u16 = 0,
reserved2: u32 = 0,
parent: u64, parent: u64,
bus_address: u64, bus_address: u64,
}; };
pub const child_removed_size = @sizeOf(ChildRemoved); /// One record of the reserved `enumerate` reply: the manager's mirror, one
/// entry per known child, packed into the reply tail. The count is
/// The manager's answer to a tree report. /// `Status.len / @sizeOf(ChildEntry)` the envelope's reply length says how
pub const ReportReply = extern struct { /// many arrived, so no count header is spent on saying it twice.
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,
};
pub const ChildEntry = extern struct { pub const ChildEntry = extern struct {
parent: u64, parent: u64,
bus_address: u64, bus_address: u64,
identity: u64, identity: u64,
}; };
/// An application subscribing to published add/remove events (the input-service /// How many `ChildEntry` records one `enumerate` reply can carry. Paging joins
/// pattern): the subscriber's endpoint rides as the call's **capability**, and /// the protocol if a tree ever outgrows one packet.
/// events arrive on it as buffered messages whose payload is the same pub const entries_per_reply: usize = (envelope.packet_maximum - envelope.prefix_size) / @sizeOf(ChildEntry);
/// ChildAdded / ChildRemoved struct the bus drivers send one encoding, both
/// directions. pub const Protocol = envelope.Define(.{
pub const Subscribe = extern struct { .name = "device-manager",
operation: u8 = @intFromEnum(Operation.subscribe), .version = version,
reserved0: u8 = 0, .operations = &.{
reserved1: u16 = 0, // The driver-facing half. `enumerate` and `subscribe` are not here: they
reserved2: u32 = 0, // 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,
}; };
/// Upper bound on any message in this protocol sizes the endpoint buffers. var call: [message_maximum]u8 = undefined;
/// Capped by the kernel's IPC MESSAGE_MAXIMUM (256): an EnumerateReply carries const called = Protocol.encodeRequest(.child_added, 42, report, &.{}, &call).?;
/// up to ten ChildEntry records per call, plenty for the mirror's current try std.testing.expectEqual(Operation.child_added, Protocol.operationOf(called).?);
/// bounds; paging joins the protocol if a tree ever outgrows one message. try std.testing.expectEqual(@as(u64, 42), envelope.headerOf(called).?.target);
pub const message_maximum = 256;
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..]);
}

View File

@ -1,70 +1,104 @@
//! The power protocol (docs/power.md): system power's domain-named surface, //! The power protocol (docs/os-development/power.md): system power's
//! bound at `/protocol/power`. On x86 the acpi service provides it; on ARM a //! domain-named surface, bound at `/protocol/power`. On x86 the acpi service
//! PSCI/mailbox service will bind the same name subscribers never learn which //! provides it; on ARM a PSCI/mailbox service will bind the same name
//! firmware they are on (docs/discovery.md firmware neutrality), which is the //! subscribers never learn which firmware they are on (docs/discovery.md
//! whole point of naming the contract rather than the provider //! firmware neutrality), which is the whole point of naming the contract rather
//! (docs/os-development/protocol-namespace.md). //! than the provider (docs/os-development/protocol-namespace.md).
//! The vfs-protocol pattern: extern-struct messages, a version, reserved fields. //!
//! 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 const std = @import("std");
/// handshake needs it; requests carry it so a mismatch can be refused loudly. const envelope = @import("envelope");
pub const version: u16 = 1;
pub const Operation = enum(u8) { /// What a published event carries beyond its kind. The kind is the packet's
/// Subscribe to power events: the subscriber's endpoint rides as the /// operation, so nothing here repeats it; `power_button`, `lid`, `ac` and
/// call's capability (the input/device-manager pattern); events arrive on /// `battery` leave both fields zero and are fully described by the verb alone.
/// it as buffered messages carrying an `EventMessage`. pub const Notice = extern struct {
subscribe = 1, /// The device notification code (ACPI `Notify`'s second argument), or 0.
/// 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.
code: u32 = 0, code: u32 = 0,
/// The notifying device's hardware id (EISA-decoded), or all zero. /// The notifying device's hardware id (EISA-decoded), or all zero.
hid: [8]u8 = .{0} ** 8, hid: [8]u8 = .{0} ** 8,
}; };
pub const Reply = extern struct { pub const Protocol = envelope.Define(.{
status: i32, .name = "power",
reserved: u32 = 0, .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 Operation = Protocol.Operation;
pub const message_maximum = 64;
/// 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);
}

View File

@ -1,57 +1,66 @@
//! The USB transfer protocol: what a USB class driver (a keyboard, mouse, or //! 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 //! mass-storage driver) says to the xHCI bus driver over `/protocol/usb-transfer`
//! `.usb_bus` endpoint to drive its device. The class driver owns no hardware //! to drive its device. The class driver owns no hardware it reaches its device
//! it reaches its device entirely through these messages, the way a PS/2 keyboard //! entirely through these packets, the way a PS/2 keyboard driver reaches the
//! driver reaches the 8042 through the ps2-bus. Extern-struct messages tagged by //! 8042 through the ps2-bus.
//! `Operation`, the vfs-protocol / device-manager-protocol pattern. //!
//! 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: //! The shape:
//! - **open** (a capability-passing `ipc.callCap`): the class driver hands over //! - **open** (a capability-passing call): the class driver hands over its own
//! its own endpoint (for asynchronous interrupt reports) and its assigned //! endpoint (for asynchronous interrupt reports); the target is its assigned
//! device id, and receives a `device_token` plus its interface's endpoints. //! device id, and the reply carries a `device_token` plus its interface's
//! - **control / bulk** (synchronous `ipc.call`): one transfer, answered when //! endpoints.
//! it completes. Control data travels inline (descriptors, HID/MSC class //! - **control / bulk** (synchronous calls): one transfer, answered when it
//! requests are all small); bulk data travels by **physical address** the //! completes. Control data travels **in the packet's tail** in both
//! class driver's own `dma_alloc`'d buffer so a 512-byte sector never has //! directions (descriptors, HID/MSC class requests are all small), so the
//! to cross the 256-byte IPC boundary. //! 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_subscribe** (synchronous): arm periodic IN polling of an
//! interrupt endpoint; each report the device produces is then pushed to the //! interrupt endpoint; each report the device produces is then pushed to the
//! class driver's endpoint as an asynchronous `InterruptReport` (`ipc.send`), //! class driver's endpoint as an asynchronous `interrupt_report` event.
//! exactly how the input service delivers events. //! 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. //! Single controller assumption: one provider serves QEMU's one xHCI. A
//! A multi-controller machine would need a per-controller endpoint (the device //! multi-controller machine would need the controller in the target (or the
//! manager handing each class driver the right one); noted, not built. //! spawner wiring each class driver its own channel); noted, not built.
/// Fits one synchronous IPC message (kernel MESSAGE_MAXIMUM). const std = @import("std");
pub const message_maximum: usize = 256; const envelope = @import("envelope");
/// The largest inline control-transfer payload. Sized so a whole message /// The largest control-transfer data stage. It rides the packet's tail, so the
/// (header + data) stays under `message_maximum`: descriptors and HID/MSC class /// bound is the call floor less the header and the fixed request part derived
/// requests are all far smaller. /// rather than declared, which is what keeps it honest when a field moves.
pub const max_inline_data: usize = 200; pub const max_inline_data: usize = envelope.packet_maximum - envelope.prefix_size - @sizeOf(Control);
/// The largest interrupt report pushed asynchronously. Sized so `InterruptReport` /// The largest interrupt report pushed asynchronously. An event packet is the
/// fits an `ipc_send` payload slot (POST_MAXIMUM = 64): boot keyboard reports are /// header plus the payload within 64 bytes, so this is what is left after the
/// 8 bytes, boot mouse reports 34. /// report's own four bytes of framing: boot keyboard reports are 8 bytes, boot
pub const max_report_data: usize = 48; /// mouse reports 34, 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 /// Endpoints per interface reported back in an open reply (a boot HID interface
/// has one interrupt endpoint, a mass-storage interface two bulk endpoints). /// has one interrupt endpoint, a mass-storage interface two bulk endpoints).
pub const max_reported_endpoints: usize = 4; 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 endpoint facts a class driver needs, lifted from the endpoint descriptor
/// the bus driver already parsed during enumeration. /// the bus driver already parsed during enumeration.
pub const Endpoint = extern struct { pub const Endpoint = extern struct {
@ -64,114 +73,146 @@ pub const Endpoint = extern struct {
reserved: [3]u8 = .{ 0, 0, 0 }, reserved: [3]u8 = .{ 0, 0, 0 },
}; };
/// open: the class driver's receive endpoint rides as the call's capability, and // --- the per-operation request and reply parts ------------------------------
/// `device_id` is the interface's assigned id (its argv[1]). //
pub const OpenRequest = extern struct { // Each names the bytes AFTER the prefix. Nothing here carries an operation or a
operation: u32 = @intFromEnum(Operation.open), // device token: those are the packet header's, folded in once. No reply carries
reserved: u32 = 0, // a status either that is the `Status` every reply begins with.
device_id: u64,
};
/// The answer to open: a token scoping every later request to this device, the /// The answer to `open`: the token every later packet puts in `Header.target`,
/// interface's class triple (a sanity check), and its endpoints. /// the interface's class triple (a sanity check), and its endpoints.
pub const OpenReply = extern struct { pub const Opened = extern struct {
status: i32,
endpoint_count: u32,
device_token: u64, device_token: u64,
endpoint_count: u32,
interface_class: u8, interface_class: u8,
interface_subclass: u8, interface_subclass: u8,
interface_protocol: u8, interface_protocol: u8,
interface_number: 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, 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`. /// `control`: one EP0 control transfer on `Header.target`. `setup` is a bit-cast
/// For an OUT transfer `data[0..data_length]` is sent; for an IN transfer the /// `usb_abi.Request`. For an OUT transfer the data stage is the request's tail;
/// reply carries up to `data_length` bytes back. /// for an IN transfer it comes back as the reply's tail, and `Status.len` is how
pub const ControlRequest = extern struct { /// much of it arrived.
operation: u32 = @intFromEnum(Operation.control), pub const Control = extern struct {
reserved: u32 = 0,
device_token: u64,
setup: [8]u8, setup: [8]u8,
direction_in: u8, // 1 = device-to-host (IN), 0 = host-to-device (OUT) /// 1 = device-to-host (IN), 0 = host-to-device (OUT).
reserved2: u8 = 0, 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, data_length: u16,
reserved3: u32 = 0, _padding2: u32 = 0,
data: [max_inline_data]u8 = [_]u8{0} ** max_inline_data,
}; };
pub const ControlReply = extern struct { /// `interrupt_subscribe`: begin periodic IN polling of an interrupt endpoint of
status: i32, // 0 success, negative on failure/stall /// `Header.target`. Each report the device returns is pushed to the endpoint the
actual_length: u32, /// caller handed over at `open`, as an `interrupt_report` event.
data: [max_inline_data]u8 = [_]u8{0} ** max_inline_data, pub const InterruptSubscribe = extern struct {
};
/// 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,
endpoint_address: u8, endpoint_address: u8,
reserved2: u8 = 0, _padding: u8 = 0,
max_length: u16, // bytes to request per poll (the endpoint's max packet size) /// Bytes to request per poll (the endpoint's max packet size).
max_length: u16,
}; };
pub const InterruptSubscribeReply = extern struct { /// `bulk`: one bulk IN or OUT transfer on `Header.target`. `physical_address` is
status: i32, /// the class driver's own `dma_alloc`'d buffer the controller DMAs straight
reserved: u32 = 0, /// to/from it, so the bulk data never crosses IPC. `endpoint_address`'s bit 7
}; /// selects IN vs OUT.
pub const Bulk = extern struct {
/// 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,
physical_address: u64, physical_address: u64,
length: u32, length: u32,
endpoint_address: u8, endpoint_address: u8,
reserved2: u8 = 0, _padding: u8 = 0,
reserved3: u16 = 0, _padding2: u16 = 0,
}; };
pub const BulkReply = extern struct { /// How many bytes a bulk transfer actually moved. It cannot ride `Status.len`
status: i32, /// the way a control transfer's does: nothing follows a bulk reply, because the
actual_length: u32, /// 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 /// One asynchronous interrupt report, pushed to the endpoint the class driver
/// the device token (scoping) so the controller knows which caller is attaching. /// handed over at `open`. The device it came from is `Header.target`.
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.
pub const InterruptReport = extern struct { pub const InterruptReport = extern struct {
device_token: u64,
endpoint_address: u8, endpoint_address: u8,
length: u8, length: u8,
reserved: u16 = 0, _padding: u16 = 0,
data: [max_report_data]u8 = [_]u8{0} ** max_report_data, data: [max_report_data]u8 = [_]u8{0} ** max_report_data,
}; };
comptime { pub const Protocol = envelope.Define(.{
const std = @import("std"); .name = "usb-transfer",
// Every synchronous message must fit one IPC message; the async report must .version = 1,
// fit an ipc_send payload slot. .operations = &.{
std.debug.assert(@sizeOf(ControlRequest) <= message_maximum); // open: the target is the interface's assigned device id (its argv[1]),
std.debug.assert(@sizeOf(ControlReply) <= message_maximum); // and the class driver's receive endpoint rides as the capability.
std.debug.assert(@sizeOf(OpenReply) <= message_maximum); .{ .name = "open", .reply = Opened },
std.debug.assert(@sizeOf(InterruptReport) <= 64); .{ .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));
} }

View File

@ -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 }); std.log.info("register refused for {d}:{d}.{d}", .{ bus, dev, function });
return; 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), .bus = @intFromEnum(device_manager_protocol.BusKind.pci),
.parent = bridge_id, .parent = bridge_id,
.bus_address = (bus << 8) | (dev << 3) | function, .bus_address = (bus << 8) | (dev << 3) | function,
.identity = class_triple, .identity = class_triple,
.device_id = registered,
.vendor = descriptor.vendor, .vendor = descriptor.vendor,
.device = descriptor.device, .device = descriptor.device,
.subsystem = descriptor.subsystem, .subsystem = descriptor.subsystem,
}; }, &.{}, &packet) orelse return;
var reply: [device_manager_protocol.message_maximum]u8 = undefined; 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 }); std.log.info("child report for {d}:{d}.{d} failed", .{ bus, dev, function });
}; };
} }

View File

@ -112,9 +112,10 @@ pub fn main(init: process.Init) void {
if (signals.has(.terminate)) return; if (signals.has(.terminate)) return;
continue; continue;
} }
if (!got.isMessage() or got.len < @sizeOf(usb.InterruptReport)) continue; if (!got.isMessage()) continue;
// An `interrupt_report` event packet: the verb in its folded header, the
const message = std.mem.bytesToValue(usb.InterruptReport, receive[0..@sizeOf(usb.InterruptReport)]); // 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; if (message.length < @sizeOf(hid.KeyboardReport)) continue;
const report = std.mem.bytesToValue(hid.KeyboardReport, message.data[0..@sizeOf(hid.KeyboardReport)]); const report = std.mem.bytesToValue(hid.KeyboardReport, message.data[0..@sizeOf(hid.KeyboardReport)]);
const transitions = decoder.feed(report); const transitions = decoder.feed(report);

View File

@ -74,9 +74,10 @@ pub fn main(init: process.Init) void {
if (signals.has(.terminate)) return; if (signals.has(.terminate)) return;
continue; continue;
} }
if (!got.isMessage() or got.len < @sizeOf(usb.InterruptReport)) continue; if (!got.isMessage()) continue;
// An `interrupt_report` event packet: the verb in its folded header, the
const message = std.mem.bytesToValue(usb.InterruptReport, receive[0..@sizeOf(usb.InterruptReport)]); // 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 length = @min(message.length, message.data.len);
const report = hid.parseMouse(message.data[0..length]) orelse continue; const report = hid.parseMouse(message.data[0..length]) orelse continue;
const mask = buttonMask(report.buttons); const mask = buttonMask(report.buttons);

View File

@ -10,10 +10,10 @@ pub fn build(b: *std.Build) void {
.name = "usb-xhci-bus", .name = "usb-xhci-bus",
.root_source_file = b.path("usb-xhci-bus.zig"), .root_source_file = b.path("usb-xhci-bus.zig"),
.imports = &.{ .imports = &.{
"channel", "device-manager-protocol", "driver", "input-client", "channel", "device-manager-protocol", "driver", "envelope",
"ipc", "logging", "memory", "mmio", "input-client", "ipc", "logging", "memory",
"pci", "process", "service", "time", "mmio", "pci", "process", "service",
"usb-abi", "usb-ids", "usb-transfer-protocol", "time", "usb-abi", "usb-ids", "usb-transfer-protocol",
}, },
}); });
b.installArtifact(exe); b.installArtifact(exe);

View File

@ -25,6 +25,7 @@ const device_manager = @import("driver");
const memory = @import("memory"); const memory = @import("memory");
const logging = @import("logging"); const logging = @import("logging");
const device_manager_protocol = @import("device-manager-protocol"); const device_manager_protocol = @import("device-manager-protocol");
const envelope = @import("envelope");
const usb_ids = @import("usb-ids"); const usb_ids = @import("usb-ids");
const usb_abi = @import("usb-abi"); const usb_abi = @import("usb-abi");
const usb_transfer_protocol = @import("usb-transfer-protocol"); 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); 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 /// 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, /// own downstream devices first if it is a hub, report each interface removed,
/// then Disable Slot. Mirrors tearDownPort for a hub-attached device. /// 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); const key = hubPortKey(dev.parent_slot, dev.parent_port);
for (dev.interfaces[0..dev.interface_count]) |*interface| { for (dev.interfaces[0..dev.interface_count]) |*interface| {
if (interface.registered_device_id == 0) continue; if (interface.registered_device_id == 0) continue;
const event = device_manager_protocol.ChildRemoved{ reportRemoved(manager, (@as(u64, key) << 8) | interface.number, null);
.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 {};
interface.registered_device_id = 0; interface.registered_device_id = 0;
} }
engine.tearDownDevice(dev); 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}); std.log.info("port {d} disconnected", .{port});
for (usb_device.interfaces[0..usb_device.interface_count]) |*interface| { for (usb_device.interfaces[0..usb_device.interface_count]) |*interface| {
if (interface.registered_device_id == 0) continue; if (interface.registered_device_id == 0) continue;
const event = device_manager_protocol.ChildRemoved{ reportRemoved(manager, (@as(u64, port) << 8) | interface.number, .{ .port = port, .interface = interface.number });
.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 });
};
interface.registered_device_id = 0; interface.registered_device_id = 0;
} }
engine.tearDownDevice(usb_device); engine.tearDownDevice(usb_device);
@ -469,15 +475,17 @@ fn reportInterface(manager: ipc.Handle, port: u32, interface: library.InterfaceI
return null; 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), .bus = @intFromEnum(device_manager_protocol.BusKind.usb),
.parent = controller_id, .parent = controller_id,
.bus_address = (@as(u64, port) << 8) | interface.number, .bus_address = (@as(u64, port) << 8) | interface.number,
.identity = identity, .identity = identity,
.device_id = registered, }, &.{}, &packet) orelse return null;
};
var reply: [device_manager_protocol.message_maximum]u8 = undefined; 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 }); std.log.info("child report for port {d} interface {d} failed", .{ port, interface.number });
return null; return null;
}; };
@ -496,59 +504,55 @@ fn reportInterface(manager: ipc.Handle, port: u32, interface: library.InterfaceI
return registered; return registered;
} }
/// Serve the USB transfer protocol: a class driver opens its device, then issues /// The generated transfer dispatch. One controller per process, so the handler
/// control / interrupt-subscribe / bulk requests against it. /// 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 { fn onMessage(message: []const u8, reply: []u8, sender: u32, arrived: *ipc.Arrival) usize {
_ = sender; capability_claimed = false;
if (message.len < 4) return 0; const written = Serve.dispatch({}, handlers, message, sender, arrived.peek(), reply);
const operation = std.mem.readInt(u32, message[0..4], .little); if (capability_claimed) _ = arrived.take();
return switch (operation) { return written;
@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), const handlers = Serve.Handlers{
@intFromEnum(usb_transfer_protocol.Operation.bulk) => handleBulk(message, reply), .open = onOpen,
@intFromEnum(usb_transfer_protocol.Operation.dma_attach) => handleDmaAttach(message, reply, arrived), .control = onControl,
else => 0, .interrupt_subscribe = onInterruptSubscribe,
.bulk = onBulk,
.dma_attach = onDmaAttach,
}; };
}
/// dma_attach: bind the class driver's DMA-region capability into the controller's IOMMU /// open: the target is the class driver's assigned device id. Resolve it to an
/// domain, so the controller may DMA to the physical addresses inside that buffer. The /// interface, remember the caller's endpoint (for interrupt reports), and answer
/// binding holds its own kernel reference, so this never claims the arriving handle /// with a device token the target of every later packet plus the interface's
/// the turn's `defer` in the harness is the close, on the failure paths as well as this /// endpoints, so the class driver need not re-read the configuration descriptor.
/// one. fn onOpen(_: void, invocation: Invocation(void), answer: Answer(usb_transfer_protocol.Opened)) isize {
fn handleDmaAttach(message: []const u8, reply: []u8, arrived: *ipc.Arrival) usize { const engine = if (controller) |*c| c else return refused;
if (message.len < @sizeOf(usb_transfer_protocol.DmaAttachRequest)) return writeReply(reply, usb_transfer_protocol.DmaAttachReply{ .status = -1 }); const found = engine.findInterface(invocation.target) orelse return refused;
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 });
}
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 });
// The report endpoint is claimed only if the open table actually keeps it; // The report endpoint is claimed only if the open table actually keeps it;
// a full table leaves it to the turn to close. // a full table leaves it to the turn to close.
if (arrived.peek()) |endpoint| { if (invocation.capability) |endpoint| {
if (recordOpen(request.device_id, endpoint)) _ = arrived.take(); if (recordOpen(invocation.target, endpoint)) capability_claimed = true;
} }
var open_reply = usb_transfer_protocol.OpenReply{ var opened = usb_transfer_protocol.Opened{
.status = 0, .device_token = invocation.target,
.endpoint_count = found.interface.endpoint_count, .endpoint_count = found.interface.endpoint_count,
.device_token = request.device_id,
.interface_class = found.interface.class, .interface_class = found.interface.class,
.interface_subclass = found.interface.subclass, .interface_subclass = found.interface.subclass,
.interface_protocol = found.interface.protocol, .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); const count = @min(found.interface.endpoint_count, usb_transfer_protocol.max_reported_endpoints);
for (found.interface.endpoints[0..count], 0..) |endpoint, index| { for (found.interface.endpoints[0..count], 0..) |endpoint, index| {
open_reply.endpoints[index] = .{ opened.endpoints[index] = .{
.address = endpoint.address, .address = endpoint.address,
.transfer_type = endpoint.transfer_type, .transfer_type = endpoint.transfer_type,
.max_packet_size = endpoint.max_packet_size, .max_packet_size = endpoint.max_packet_size,
.interval = endpoint.interval, .interval = endpoint.interval,
}; };
} }
return writeReply(reply, open_reply); answer.set(opened);
return 0;
} }
/// control: one EP0 control transfer, small data inline both ways. /// control: one EP0 control transfer. The data stage rides the tail in both
fn handleControl(message: []const u8, reply: []u8) usize { /// directions, so an IN transfer's answer is simply however many bytes were
if (message.len < @sizeOf(usb_transfer_protocol.ControlRequest)) return writeReply(reply, usb_transfer_protocol.ControlReply{ .status = -1, .actual_length = 0 }); /// written into `answer.tail()` which is what `Status.len` then reports.
const request = std.mem.bytesToValue(usb_transfer_protocol.ControlRequest, message[0..@sizeOf(usb_transfer_protocol.ControlRequest)]); fn onControl(_: void, invocation: Invocation(usb_transfer_protocol.Control), answer: Answer(void)) isize {
const engine = if (controller) |*c| c else return writeReply(reply, usb_transfer_protocol.ControlReply{ .status = -1, .actual_length = 0 }); const engine = if (controller) |*c| c else return refused;
const found = engine.findInterface(request.device_token) orelse return writeReply(reply, usb_transfer_protocol.ControlReply{ .status = -1, .actual_length = 0 }); 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; 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);
const ok = engine.controlTransfer(found.device, setup, data[0..data_length], direction_in); @memcpy(data[0..supplied], invocation.tail[0..supplied]);
var control_reply = usb_transfer_protocol.ControlReply{ .status = if (ok) 0 else -1, .actual_length = if (ok) data_length else 0 }; if (supplied < data_length) @memset(data[supplied..data_length], 0);
if (ok and direction_in) @memcpy(control_reply.data[0..data_length], data[0..data_length]);
return writeReply(reply, control_reply);
} }
/// interrupt_subscribe: arm periodic IN polling; reports flow back asynchronously. if (!engine.controlTransfer(found.device, setup, data[0..data_length], direction_in)) return refused;
fn handleSubscribe(message: []const u8, reply: []u8) usize { if (!direction_in) return 0; // nothing follows an OUT: the status is the whole answer
if (message.len < @sizeOf(usb_transfer_protocol.InterruptSubscribeRequest)) return writeReply(reply, usb_transfer_protocol.InterruptSubscribeReply{ .status = -1 }); @memcpy(answer.tail()[0..data_length], data[0..data_length]);
const request = std.mem.bytesToValue(usb_transfer_protocol.InterruptSubscribeRequest, message[0..@sizeOf(usb_transfer_protocol.InterruptSubscribeRequest)]); return @intCast(data_length);
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 }); /// interrupt_subscribe: arm periodic IN polling; reports flow back asynchronously
const report_endpoint = reportEndpointFor(request.device_token) orelse return writeReply(reply, usb_transfer_protocol.InterruptSubscribeReply{ .status = -1 }); /// to the endpoint this device's `open` handed over.
const ok = engine.subscribeInterrupt(found.device, endpoint, request.device_token, report_endpoint); fn onInterruptSubscribe(_: void, invocation: Invocation(usb_transfer_protocol.InterruptSubscribe), _: Answer(void)) isize {
return writeReply(reply, usb_transfer_protocol.InterruptSubscribeReply{ .status = if (ok) 0 else -1 }); 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 /// bulk: one bulk transfer to/from the class driver's own DMA buffer (by physical
/// address), so sector-sized data never crosses IPC. /// address), so sector-sized data never crosses IPC.
fn handleBulk(message: []const u8, reply: []u8) usize { fn onBulk(_: void, invocation: Invocation(usb_transfer_protocol.Bulk), answer: Answer(usb_transfer_protocol.Transferred)) isize {
if (message.len < @sizeOf(usb_transfer_protocol.BulkRequest)) return writeReply(reply, usb_transfer_protocol.BulkReply{ .status = -1, .actual_length = 0 }); const engine = if (controller) |*c| c else return refused;
const request = std.mem.bytesToValue(usb_transfer_protocol.BulkRequest, message[0..@sizeOf(usb_transfer_protocol.BulkRequest)]); const found = engine.findInterface(invocation.target) orelse return refused;
const engine = if (controller) |*c| c else return writeReply(reply, usb_transfer_protocol.BulkReply{ .status = -1, .actual_length = 0 }); const endpoint = library.Controller.endpointForAddress(found.interface, invocation.request.endpoint_address) orelse return refused;
const found = engine.findInterface(request.device_token) orelse return writeReply(reply, usb_transfer_protocol.BulkReply{ .status = -1, .actual_length = 0 }); const transferred = engine.bulkTransfer(found.device, endpoint, invocation.request.physical_address, invocation.request.length) orelse return refused;
const endpoint = library.Controller.endpointForAddress(found.interface, request.endpoint_address) orelse return writeReply(reply, usb_transfer_protocol.BulkReply{ .status = -1, .actual_length = 0 }); answer.set(.{ .actual_length = transferred });
const transferred = engine.bulkTransfer(found.device, endpoint, request.physical_address, request.length); return 0;
return writeReply(reply, usb_transfer_protocol.BulkReply{ .status = if (transferred != null) 0 else -1, .actual_length = transferred orelse 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. /// 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; if (serviced >= 32) break;
} }
while (engine.takeReport()) |report| { while (engine.takeReport()) |report| {
var message = usb_transfer_protocol.InterruptReport{ // One `interrupt_report` event packet: the device token in the folded
.device_token = report.device_token, // header, the report after it. A device that produced more than the
.endpoint_address = report.endpoint_address, // push floor admits has its report truncated here, never split.
.length = @intCast(@min(report.length, usb_transfer_protocol.max_report_data)),
};
const n = @min(report.length, usb_transfer_protocol.max_report_data); const n = @min(report.length, usb_transfer_protocol.max_report_data);
@memcpy(message.data[0..n], report.data[0..n]); var payload = usb_transfer_protocol.InterruptReport{
_ = ipc.send(report.report_endpoint, std.mem.asBytes(&message)); .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);
} }
} }
} }

View File

@ -22,6 +22,7 @@ const logging = @import("logging");
const aml = @import("aml"); const aml = @import("aml");
const acpi_ids = @import("acpi-ids"); const acpi_ids = @import("acpi-ids");
const device_manager_protocol = @import("device-manager-protocol"); const device_manager_protocol = @import("device-manager-protocol");
const envelope = @import("envelope");
const power_protocol = @import("power-protocol"); const power_protocol = @import("power-protocol");
/// AML opcode/prefix bytes by name (`zero_opcode`, `byte_prefix`, ) so the `_HID` /// 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). /// 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 else
std.log.info("device {d} bus=acpi hid={s} ({d} resources)", .{ entry.device_id, hid, entry.resource_count }); std.log.info("device {d} bus=acpi hid={s} ({d} resources)", .{ entry.device_id, hid, entry.resource_count });
if (manager) |h| { 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]); @memcpy(report.hid[0..entry.hid_len], entry.hid[0..entry.hid_len]);
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; var reply: [device_manager_protocol.message_maximum]u8 = undefined;
_ = ipc.call(h, std.mem.asBytes(&report), &reply) catch {}; _ = ipc.call(h, framed, &reply) catch {};
}
} }
} }
std.log.info("reported {d} device(s) to the manager", .{registered_count}); 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 { fn publishNotify(node: *aml.Node, code: u64) void {
// Map the notified device's _HID to a domain event where we recognize it. // 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; var hid: [8]u8 = .{0} ** 8;
if (readHid(node, &global_interpreter)) |h| hid = h; 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; const notice = power_protocol.Notice{ .code = @truncate(code), .hid = hid };
var event = power_protocol.EventMessage{ .event = @intFromEnum(which), .code = @truncate(code) };
event.hid = hid;
std.log.info("power: notify {s} code {d}", .{ hid[0..7], code }); 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]`. /// Two lowercase hex digits of `n` into `out[0..2]`.
@ -406,14 +425,19 @@ fn writeHex2(out: []u8, n: u32) void {
} }
fn publishButton() void { fn publishButton() void {
const event = power_protocol.EventMessage{ .event = @intFromEnum(power_protocol.Event.power_button) }; publish(.power_button, .{});
publishEvent(std.mem.asBytes(&event));
} }
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| { for (&subscribers) |*slot| {
if (slot.*) |handle| { 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(); onSci();
} }
/// The `.power` protocol: subscribe (endpoint as the call's capability), /// The generated power dispatch. One provider per system, so the handler context
/// shutdown (PID 1 only). Device discovery uses a different endpoint (the /// is empty and the subscriber table stays in this file's globals.
/// device manager's), so nothing here handles ChildAdded. 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 { fn onMessage(message: []const u8, reply: []u8, sender: u32, arrived: *ipc.Arrival) usize {
if (message.len < 1) return 0; capability_claimed = false;
switch (message[0]) { const written = Serve.dispatch({}, handlers, message, sender, arrived.peek(), reply);
@intFromEnum(power_protocol.Operation.subscribe) => { if (capability_claimed) _ = arrived.take();
// The subscriber's endpoint is claimed only when a slot takes it; return written;
// a full table refuses and the turn closes what arrived. }
var status: i32 = -1;
if (arrived.peek() != null) { const handlers = Serve.Handlers{ .shutdown = onShutdown, .subscribe = onSubscribe };
for (&subscribers, 0..) |*slot, si| {
/// 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) { if (slot.* == null) {
slot.* = arrived.take(); slot.* = endpoint;
subscriber_tasks[si] = sender; subscriber_tasks[index] = invocation.sender;
status = 0; capability_claimed = true;
break; return 0;
} }
} }
return -envelope.ENOSPC;
} }
const r = power_protocol.Reply{ .status = status };
@memcpy(reply[0..@sizeOf(power_protocol.Reply)], std.mem.asBytes(&r)); /// Honored only from a power subscriber init, which has already run the stop
return @sizeOf(power_protocol.Reply); /// 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
@intFromEnum(power_protocol.Operation.shutdown) => { /// init's policy. The badge is the whole gate: it is kernel-stamped, so nothing
// Honored only from a power subscriber init, which has already run /// in the packet can claim to be init.
// the stop sequence over everything else. The power service is fn onShutdown(_: void, invocation: Invocation(void), _: Answer(void)) isize {
// mechanism (write S5); deciding *when* to shut down and stopping if (!isSubscriber(invocation.sender)) return -envelope.EPERM;
// the rest of the system first is init's policy. enterS5();
const allowed = isSubscriber(sender); return 0;
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,
}
} }
/// Depth-first walk: register + report each present device with a _HID, then /// Depth-first walk: register + report each present device with a _HID, then

View File

@ -14,8 +14,8 @@ pub fn build(b: *std.Build) void {
.name = "discovery", .name = "discovery",
.root_source_file = b.path("acpi.zig"), .root_source_file = b.path("acpi.zig"),
.imports = &.{ .imports = &.{
"acpi-ids", "aml", "channel", "device-manager-protocol", "driver", "ipc", "logging", "acpi-ids", "aml", "channel", "device-manager-protocol", "driver", "envelope",
"memory", "power-protocol", "process", "service", "time", "ipc", "logging", "memory", "power-protocol", "process", "service", "time",
}, },
}); });
b.installArtifact(exe); b.installArtifact(exe);

View File

@ -10,8 +10,8 @@ pub fn build(b: *std.Build) void {
.name = "device-manager", .name = "device-manager",
.root_source_file = b.path("device-manager.zig"), .root_source_file = b.path("device-manager.zig"),
.imports = &.{ .imports = &.{
"device-manager-protocol", "device-registry", "driver", "file-system", "ipc", "device-manager-protocol", "device-registry", "driver", "envelope", "file-system",
"logging", "memory", "process", "service", "time", "ipc", "logging", "memory", "process", "service", "time",
}, },
}); });
b.installArtifact(exe); b.installArtifact(exe);

View File

@ -24,7 +24,15 @@ const time = @import("time");
const memory = @import("memory"); const memory = @import("memory");
const logging = @import("logging"); const logging = @import("logging");
const device_manager_protocol = @import("device-manager-protocol"); const device_manager_protocol = @import("device-manager-protocol");
const envelope = @import("envelope");
const registry = @import("device-registry"); 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"); const fs = @import("file-system");
// --- the device registry ------------------------------------------------------ // --- the device registry ------------------------------------------------------
@ -155,12 +163,20 @@ var test_kill_due_ns: u64 = 0;
const maximum_subscribers = 8; const maximum_subscribers = 8;
var subscribers: [maximum_subscribers]?ipc.Handle = .{null} ** maximum_subscribers; var subscribers: [maximum_subscribers]?ipc.Handle = .{null} ** maximum_subscribers;
/// Publish one event (a ChildAdded or ChildRemoved struct, the same encoding /// Push one event to every subscriber: the same struct a bus driver *called*
/// the bus drivers send) to every subscriber. /// with, framed as an event instead one encoding, both directions, told apart
fn publishEvent(event: []const u8) void { /// 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| { for (&subscribers) |*slot| {
if (slot.*) |handle| { 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) { if (child.used and child.reporter == reporter) {
std.log.info("child removed (device {d} port {d})", .{ child.parent, child.bus_address }); std.log.info("child removed (device {d} port {d})", .{ child.parent, child.bus_address });
child.used = false; child.used = false;
const event = device_manager_protocol.ChildRemoved{ .parent = child.parent, .bus_address = child.bus_address }; publish(.child_removed, 0, .{ .parent = child.parent, .bus_address = child.bus_address });
publishEvent(std.mem.asBytes(&event));
} }
} }
} }
@ -395,59 +410,72 @@ fn initialise(endpoint: ipc.Handle) bool {
return true; return true;
} }
fn onMessage(message: []const u8, reply: []u8, sender: u32, arrived: *ipc.Arrival) usize { /// Set by `onSubscribe` when the subscriber table has taken ownership of the
if (message.len < 1) return 0; /// capability the call carried, and read by `onMessage`, where the turn's
switch (message[0]) { /// `Arrival` lives. The generated dispatch hands a handler the raw handle rather
@intFromEnum(device_manager_protocol.Operation.child_added) => return onChildAdded(message, reply, sender), /// than the `Arrival` deliberately, since a handler has no business closing the
@intFromEnum(device_manager_protocol.Operation.child_removed) => return onChildRemoved(message, reply, sender), /// turn's property so the *claim* travels back out this way. One turn, one
@intFromEnum(device_manager_protocol.Operation.enumerate) => return onEnumerate(reply), /// handler, one thread: there is nothing here to race.
@intFromEnum(device_manager_protocol.Operation.subscribe) => return onSubscribe(reply, arrived), var capability_claimed = false;
@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]);
var status: i32 = 0; fn onMessage(message: []const u8, reply: []u8, sender: u32, arrived: *ipc.Arrival) usize {
if (hello.version != device_manager_protocol.version) { capability_claimed = false;
status = -1; const written = Serve.dispatch({}, handlers, message, sender, arrived.peek(), reply);
std.log.info("refused hello (version {d}) from process {d}", .{ hello.version, sender }); if (capability_claimed) _ = arrived.take();
} else if (driverByProcess(sender)) |driver| { return written;
}
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; driver.state = .running;
std.log.info("hello from {s} (device {d})", .{ driver.name(), hello.device_id }); 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 // 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. // 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")) { 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_scanout_killed = true;
test_kill_pid = sender; test_kill_pid = invocation.sender;
test_kill_due_ns = time.clock() + 1_500_000_000; test_kill_due_ns = time.clock() + 1_500_000_000;
_ = time.timerOnce(manager_endpoint, 1600); _ = time.timerOnce(manager_endpoint, 1600);
} }
} else { return 0;
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;
} }
/// A bus driver reported a discovered device: mirror it, and in /// A bus driver reported a discovered device: mirror it, publish it, match a
/// test-usb-restart mode kill the reporter once after its second child the /// driver for it and in the restart drills kill the reporter once, the
/// deterministic trigger for prune -> backoff -> respawn -> re-report. /// deterministic trigger for prune -> backoff -> respawn -> re-report.
fn onChildAdded(message: []const u8, reply: []u8, sender: u32) usize { fn onChildAdded(_: void, invocation: Invocation(device_manager_protocol.ChildAdded), _: Answer(void)) isize {
if (message.len < device_manager_protocol.child_added_size) return 0; const report = invocation.request;
const report = std.mem.bytesToValue(device_manager_protocol.ChildAdded, message[0..device_manager_protocol.child_added_size]); const sender = invocation.sender;
var status: i32 = 0; // 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 (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() }); 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 // 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 // registry: a registered child gets the most-specific driver its identity
// matches, once re-reports after a bus restart dedupe on the registered // matches, once re-reports after a bus restart dedupe on the registered
// id, exactly like the registrations do. // 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); const id = identityFromReport(report);
if (registry.matchDriver(registry_rules[0..registry_count], id)) |match| { if (registry.matchDriver(registry_rules[0..registry_count], id)) |match| {
if (match.ambiguous) 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); if (!alreadySupervised(match.driver)) addDriver(match.driver, device_manager_protocol.no_device, false);
} else { } else {
// A per-device driver: one instance, the registered id as argv[1]. // 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 { } 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 (test_pci_restart_mode and !test_usb_killed) {
if (driverByProcess(sender)) |driver| { if (driverByProcess(sender)) |driver| {
if (std.mem.eql(u8, driver.name(), "pci-bus") and childCountOf(sender) >= 3) { 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 /// A bus driver reported a device gone (hot-unplug). Addressed by the composite
/// the handler is protocol-complete death-pruning covers removal until then). /// (parent, bus address) the reporter knows, which is why that pair is the
fn onChildRemoved(message: []const u8, reply: []u8, sender: u32) usize { /// packet's body rather than its target.
if (message.len < device_manager_protocol.child_removed_size) return 0; fn onChildRemoved(_: void, invocation: Invocation(device_manager_protocol.ChildRemoved), _: Answer(void)) isize {
const report = std.mem.bytesToValue(device_manager_protocol.ChildRemoved, message[0..device_manager_protocol.child_removed_size]); const report = invocation.request;
var status: i32 = -1; var status: isize = -envelope.ENOENT;
for (&children) |*child| { 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 }); std.log.info("child removed (device {d} port {d})", .{ child.parent, child.bus_address });
child.used = false; child.used = false;
status = 0; status = 0;
} }
} }
const report_reply = device_manager_protocol.ReportReply{ .status = status }; return status;
@memcpy(reply[0..@sizeOf(device_manager_protocol.ReportReply)], std.mem.asBytes(&report_reply));
return @sizeOf(device_manager_protocol.ReportReply);
} }
/// An application asked for the tree: the mirror, as a header plus entries. /// The reserved `enumerate` verb: the mirror, one `ChildEntry` per known child,
fn onEnumerate(reply: []u8) usize { /// packed into the reply's tail. How many arrived is the reply's own length
var count: u32 = 0; /// `Status.len` so no count header is spent saying it twice.
var offset: usize = @sizeOf(device_manager_protocol.EnumerateReply); 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| { for (&children) |*child| {
if (!child.used) continue; 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 }; 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)); @memcpy(tail[written..][0..entry_size], std.mem.asBytes(&entry));
offset += @sizeOf(device_manager_protocol.ChildEntry); written += entry_size;
count += 1;
} }
const header = device_manager_protocol.EnumerateReply{ .status = 0, .count = count }; return @intCast(written);
@memcpy(reply[0..@sizeOf(device_manager_protocol.EnumerateReply)], std.mem.asBytes(&header));
return offset;
} }
/// An application subscribed: its endpoint arrived as the call's capability. The /// The reserved `subscribe` verb: an application's endpoint arrived as the call's
/// table taking a slot is what claims it (`take`); a full table refuses and lets /// capability. The table taking a slot is what claims it; a full table refuses
/// the turn close it, so a subscribe storm cannot spend the handle table too. /// and lets the turn close it, so a subscribe storm cannot spend the handle table.
fn onSubscribe(reply: []u8, arrived: *ipc.Arrival) usize { fn onSubscribe(_: void, invocation: Invocation(void), _: Answer(void)) isize {
var status: i32 = -1; const endpoint = invocation.capability orelse return -envelope.EPROTO;
if (arrived.peek() != null) {
for (&subscribers) |*slot| { for (&subscribers) |*slot| {
if (slot.* == null) { if (slot.* == null) {
slot.* = arrived.take(); // claimed: the table holds it from here slot.* = endpoint;
status = 0; capability_claimed = true; // the table holds it from here
break; return 0;
} }
} }
} return -envelope.ENOSPC;
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);
} }
fn onNotification(badge: u64) void { fn onNotification(badge: u64) void {

View File

@ -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 }); std.log.info("ignored a power event from pid {d}: /protocol/power is pid {d}", .{ sender, authorized });
return; return;
} }
if (payload.len < 2) return; // Read as an envelope packet, never by byte offset: the kind IS the packet's
if (payload[0] != @intFromEnum(power_protocol.Operation.event)) return; // verb, so a power event is decoded exactly the way every other event in the
if (payload[1] == @intFromEnum(power_protocol.Event.power_button)) shutDown(); // 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 /// 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. /// init calls owes the same discipline.
fn subscribePower() void { fn subscribePower() void {
const handle = power_endpoint orelse return; 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; 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 /// 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 (child_ids[i] != 0) process.stop(child_ids[i], 2000, supervision_endpoint);
} }
if (power_endpoint) |h| { if (power_endpoint) |h| {
const request = power_protocol.Shutdown{}; 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; var reply: [power_protocol.message_maximum]u8 = undefined;
_ = ipc.call(h, std.mem.asBytes(&request), &reply) catch {}; _ = ipc.call(h, framed, &reply) catch {};
}
} }
// If S5 did not take, init has nothing left to do but idle. // If S5 did not take, init has nothing left to do but idle.
while (true) time.sleepMillis(1000); while (true) time.sleepMillis(1000);

View File

@ -896,10 +896,14 @@ CASES = [
# it finds is the proof that `Define` hands those verbs to everyone alike. # 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 # The scenario boots the registry, the input service and the compositor: two
# protocols of different sizes and verb counts, both reached through the real # 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 # registry under the manifest's own grants. The other six the fixture knows —
# P4a protocols whose providers need hardware chains this case does not boot; # vfs, block, scanout, device-manager, power and usb-transfer — need provider
# the fixture names them as unchecked rather than skipping them quietly, and # chains this case does not boot (the last three all arrive with the device
# the existing fat-mount / usb-storage / virtio-gpu scenarios are their proof. # 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", {"name": "protocol-conformance",
"expect": r"(?s)(?=.*protocol-conformance: input v\d+ describes itself)" "expect": r"(?s)(?=.*protocol-conformance: input v\d+ describes itself)"
r"(?=.*protocol-conformance: display v\d+ describes itself)" r"(?=.*protocol-conformance: display v\d+ describes itself)"

View File

@ -34,9 +34,17 @@ pub fn main(init: process.Init) void {
if (manager == null) time.sleepMillis(20); if (manager == null) time.sleepMillis(20);
} }
const h = manager orelse return; 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; 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"); _ = logging.write("crash-test: faulting now\n");
const poison: *volatile u32 = @ptrFromInt(0xdead0000); const poison: *volatile u32 = @ptrFromInt(0xdead0000);

View File

@ -9,7 +9,7 @@ pub fn build(b: *std.Build) void {
const exe = build_support.userBinary(b, .{ const exe = build_support.userBinary(b, .{
.name = "device-list", .name = "device-list",
.root_source_file = b.path("device-list.zig"), .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); b.installArtifact(exe);
} }

View File

@ -6,6 +6,7 @@
const std = @import("std"); const std = @import("std");
const channel = @import("channel"); const channel = @import("channel");
const envelope = @import("envelope");
const ipc = @import("ipc"); const ipc = @import("ipc");
const time = @import("time"); const time = @import("time");
const logging = @import("logging"); const logging = @import("logging");
@ -29,36 +30,41 @@ pub fn main() void {
}; };
// The snapshot polled briefly, because at boot the bus drivers may still // 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 reply: [device_manager_protocol.message_maximum]u8 = undefined;
var count: u32 = 0; var count: usize = 0;
var length: usize = 0;
tries = 0; tries = 0;
while (tries < 20) : (tries += 1) { while (tries < 20) : (tries += 1) {
const request = device_manager_protocol.Enumerate{}; const length = ipc.call(h, std.mem.asBytes(&enumerate), &reply) catch 0;
length = ipc.call(h, std.mem.asBytes(&request), &reply) catch 0; if (envelope.statusOf(reply[0..length])) |status| {
if (length >= @sizeOf(device_manager_protocol.EnumerateReply)) { if (status.status == 0) {
count = std.mem.bytesToValue(device_manager_protocol.EnumerateReply, reply[0..@sizeOf(device_manager_protocol.EnumerateReply)]).count; const carried = @min(@as(usize, status.len), length - envelope.prefix_size);
count = carried / @sizeOf(Entry);
if (count != 0) break; if (count != 0) break;
} }
}
time.sleepMillis(100); time.sleepMillis(100);
} }
writeLine("device-list: {d} devices\n", .{count}); writeLine("device-list: {d} devices\n", .{count});
var offset: usize = @sizeOf(device_manager_protocol.EnumerateReply); for (0..count) |index| {
var index: u32 = 0; const entry = std.mem.bytesToValue(Entry, reply[envelope.prefix_size + index * @sizeOf(Entry) ..][0..@sizeOf(Entry)]);
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)]);
writeLine("device-list: device {d} port {d} identity {d}\n", .{ entry.parent, entry.bus_address, entry.identity }); 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 // The subscription the reserved `subscribe` verb: our endpoint rides as
// arrive as buffered messages carrying the same structs the bus sends. // 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 { const endpoint = ipc.createIpcEndpoint() orelse {
_ = logging.write("device-list: no endpoint\n"); _ = logging.write("device-list: no endpoint\n");
return; 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 { _ = ipc.callCap(h, std.mem.asBytes(&subscribe), &reply, endpoint) catch {
_ = logging.write("device-list: subscribe failed\n"); _ = logging.write("device-list: subscribe failed\n");
return; return;
@ -68,19 +74,18 @@ pub fn main() void {
var receive: [device_manager_protocol.message_maximum]u8 = undefined; var receive: [device_manager_protocol.message_maximum]u8 = undefined;
while (true) { while (true) {
const got = ipc.replyWait(endpoint, &.{}, &receive, null); const got = ipc.replyWait(endpoint, &.{}, &receive, null);
if (!got.isMessage() or got.len < 1) continue; if (!got.isMessage()) continue;
switch (receive[0]) { const packet = receive[0..got.len];
@intFromEnum(device_manager_protocol.Operation.child_added) => { const event = device_manager_protocol.Protocol.eventOf(packet) orelse continue;
if (got.len < device_manager_protocol.child_added_size) continue; switch (event) {
const event = std.mem.bytesToValue(device_manager_protocol.ChildAdded, receive[0..device_manager_protocol.child_added_size]); .child_added => {
writeLine("device-list: added (device {d} port {d})\n", .{ event.parent, event.bus_address }); 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) => { .child_removed => {
if (got.len < device_manager_protocol.child_removed_size) continue; const removed = device_manager_protocol.Protocol.decodeEvent(.child_removed, packet) orelse 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", .{ removed.parent, removed.bus_address });
writeLine("device-list: removed (device {d} port {d})\n", .{ event.parent, event.bus_address });
}, },
else => {},
} }
} }
} }

View File

@ -12,15 +12,18 @@ pub fn build(b: *std.Build) void {
.imports = &.{ .imports = &.{
"block-protocol", "block-protocol",
"channel", "channel",
"device-manager-protocol",
"display-protocol", "display-protocol",
"envelope", "envelope",
"file-system", "file-system",
"input-protocol", "input-protocol",
"ipc", "ipc",
"logging", "logging",
"power-protocol",
"process", "process",
"scanout-protocol", "scanout-protocol",
"time", "time",
"usb-transfer-protocol",
"vfs-protocol", "vfs-protocol",
}, },
}); });

View File

@ -24,31 +24,37 @@
//! exactly the two contracts its scenario boots, and an ungranted name is absent //! exactly the two contracts its scenario boots, and an ungranted name is absent
//! for it like any other client's. //! for it like any other client's.
//! //!
//! **What it covers, and what it cannot the honest list at P4a.** The scenario //! **What it covers, and what it cannot the honest list.** The scenario boots
//! boots the registry, the input service, and the compositor, so `input` and //! the registry, the input service, and the compositor, so `input` and `display`
//! `display` are checked end to end over real IPC. The other three protocols P4a //! are checked end to end over real IPC. The other six contracts in the table
//! rebased are not asked here, and the reason is the provider, not the protocol: //! 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 //! - `vfs` the FAT server, which needs a mounted volume behind the whole USB
//! storage chain (the `fat-mount` scenario); //! storage chain (the `fat-mount` scenario);
//! - `block` the usb-storage driver, which the device manager spawns after //! - `block` the usb-storage driver, which the device manager spawns after
//! enumerating an xHCI bus (the `usb-storage` scenario); //! enumerating an xHCI bus (the `usb-storage` scenario);
//! - `scanout` the virtio-gpu driver, which needs an emulated virtio-gpu the //! - `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 //! That is the reason this scenario stays at two providers rather than five or
//! provider at the price of a case that boots half the system to send two //! eight. It is not only the cost of booting half the system to send two
//! packets; their rebase is proven instead by the scenarios that already drive //! packets: this fixture takes **one snapshot** of `/protocol` and checks what
//! them. All three sit in the table below anyway, so if a future scenario binds //! is in it, so a scenario whose bound set depends on how far a driver tree got
//! one, this fixture checks it without being edited and prints, every run, the //! by that instant would make the case's own summary a boot race. What proves
//! ones it found no provider for. //! 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 //! All six sit in the table below regardless, so a scenario that binds one gets
//! `usb-transfer` are still hand-numbered (P4b): to them, operation 0 is a verb //! it conformance-checked without this file being edited and every run prints,
//! of their own, not `describe`. So the table is not "every contract" but "every //! by name, the ones it found no provider for.
//! 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.
//! //!
//! The registry itself PID 1 serving `/protocol` is the one vfs backend //! The registry itself PID 1 serving `/protocol` is the one vfs backend
//! deliberately NOT dispatched through the generated table (it reads a //! deliberately NOT dispatched through the generated table (it reads a
@ -68,9 +74,12 @@ const logging = @import("logging");
const process = @import("process"); const process = @import("process");
const time = @import("time"); const time = @import("time");
const block_protocol = @import("block-protocol"); const block_protocol = @import("block-protocol");
const device_manager_protocol = @import("device-manager-protocol");
const display_protocol = @import("display-protocol"); const display_protocol = @import("display-protocol");
const input_protocol = @import("input-protocol"); const input_protocol = @import("input-protocol");
const power_protocol = @import("power-protocol");
const scanout_protocol = @import("scanout-protocol"); const scanout_protocol = @import("scanout-protocol");
const usb_transfer_protocol = @import("usb-transfer-protocol");
const vfs_protocol = @import("vfs-protocol"); const vfs_protocol = @import("vfs-protocol");
// --- what conformance means, per contract ----------------------------------- // --- 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 /// The protocols built on `envelope.Define`. A name listed by `/protocol` that
/// `/protocol` that is absent from here is reported and left alone see the /// is absent from here is reported and left alone rather than probed: a
/// header: asking a hand-numbered provider for operation 0 would name one of its /// hand-numbered provider would read operation 0 as one of its own verbs, so
/// own verbs. /// asking it for `describe` would *do* something. Only `ps2-bus` is still in
/// that state today.
const contracts = [_]Contract{ const contracts = [_]Contract{
contractOf(input_protocol.Protocol, true), // the input fan-out service contractOf(input_protocol.Protocol, true), // the input fan-out service
contractOf(display_protocol.Protocol, true), // the compositor contractOf(display_protocol.Protocol, true), // the compositor
contractOf(vfs_protocol.Protocol, false), // the FAT server needs a volume contractOf(vfs_protocol.Protocol, false), // the FAT server needs a volume
contractOf(block_protocol.Protocol, false), // usb-storage needs the xHCI chain contractOf(block_protocol.Protocol, false), // usb-storage needs the xHCI chain
contractOf(scanout_protocol.Protocol, false), // virtio-gpu needs the device 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 /// A verb number no protocol in the system defines, and none plausibly will: far

View File

@ -288,8 +288,10 @@ fn run() void {
// the strongest one available, because a regression does not fail this // the strongest one available, because a regression does not fail this
// line, it takes the entire boot down with it. // line, it takes the entire boot down with it.
const registry = registryEndpoint() orelse fail("resolve /protocol"); const registry = registryEndpoint() orelse fail("resolve /protocol");
const forged = power_protocol.EventMessage{ .event = @intFromEnum(power_protocol.Event.power_button) }; var forged: [envelope.post_maximum]u8 = undefined;
if (!ipc.send(registry, std.mem.asBytes(&forged))) fail("post a forged power event"); 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 const still_serving = verdictWithin(forbidden, spare) orelse
fail("the registrar went silent after a forged power event — it acted on it"); 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"); if (still_serving != -envelope.EPERM) fail("the registry misanswered after a forged power event");