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