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:
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user