merge: security track group 3 — every protocol on the envelope, ids owned
# Conflicts: # docs/security-track-plan.md
This commit is contained in:
commit
b3011fbb40
|
|
@ -326,6 +326,7 @@ pub fn build(b: *std.Build) void {
|
||||||
if (test_case != null) for ([_][]const u8{
|
if (test_case != null) for ([_][]const u8{
|
||||||
"vfs-test", // the user-space VFS round-trip client
|
"vfs-test", // the user-space VFS round-trip client
|
||||||
"fat-test",
|
"fat-test",
|
||||||
|
"badge-scope-test", // the guessable-id probe: a second process names the first's node and layer
|
||||||
"shared-memory-server",
|
"shared-memory-server",
|
||||||
"shared-memory-client",
|
"shared-memory-client",
|
||||||
"crash-test", // hellos to the device manager, then faults — drives the crash-loop cap
|
"crash-test", // hellos to the device manager, then faults — drives the crash-loop cap
|
||||||
|
|
@ -340,6 +341,7 @@ pub fn build(b: *std.Build) void {
|
||||||
"user-memory-test", // aims deliberately bad user pointers at the checked copy layer
|
"user-memory-test", // aims deliberately bad user pointers at the checked copy layer
|
||||||
"protocol-registry-test", // drives the registrar: ungranted bind, collision, restart
|
"protocol-registry-test", // drives the registrar: ungranted bind, collision, restart
|
||||||
"protocol-denied-test", // restriction stage one: an ungranted open answers as absence
|
"protocol-denied-test", // restriction stage one: an ungranted open answers as absence
|
||||||
|
"protocol-conformance-test", // the reserved verbs, asked of every provider the boot bound
|
||||||
}) |fixture| {
|
}) |fixture| {
|
||||||
const package = b.lazyDependency(fixture, .{}) orelse
|
const package = b.lazyDependency(fixture, .{}) orelse
|
||||||
@panic("a test fixture package is missing under test/system/services");
|
@panic("a test fixture package is missing under test/system/services");
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,7 @@
|
||||||
.@"virtio-gpu" = .{ .path = "system/drivers/virtio-gpu" },
|
.@"virtio-gpu" = .{ .path = "system/drivers/virtio-gpu" },
|
||||||
.@"vfs-test" = .{ .path = "test/system/services/vfs-test", .lazy = true },
|
.@"vfs-test" = .{ .path = "test/system/services/vfs-test", .lazy = true },
|
||||||
.@"fat-test" = .{ .path = "test/system/services/fat-test", .lazy = true },
|
.@"fat-test" = .{ .path = "test/system/services/fat-test", .lazy = true },
|
||||||
|
.@"badge-scope-test" = .{ .path = "test/system/services/badge-scope-test", .lazy = true },
|
||||||
.@"shared-memory-server" = .{ .path = "test/system/services/shared-memory-server", .lazy = true },
|
.@"shared-memory-server" = .{ .path = "test/system/services/shared-memory-server", .lazy = true },
|
||||||
.@"shared-memory-client" = .{ .path = "test/system/services/shared-memory-client", .lazy = true },
|
.@"shared-memory-client" = .{ .path = "test/system/services/shared-memory-client", .lazy = true },
|
||||||
.@"crash-test" = .{ .path = "test/system/services/crash-test", .lazy = true },
|
.@"crash-test" = .{ .path = "test/system/services/crash-test", .lazy = true },
|
||||||
|
|
@ -77,6 +78,7 @@
|
||||||
.@"user-memory-test" = .{ .path = "test/system/services/user-memory-test", .lazy = true },
|
.@"user-memory-test" = .{ .path = "test/system/services/user-memory-test", .lazy = true },
|
||||||
.@"protocol-registry-test" = .{ .path = "test/system/services/protocol-registry-test", .lazy = true },
|
.@"protocol-registry-test" = .{ .path = "test/system/services/protocol-registry-test", .lazy = true },
|
||||||
.@"protocol-denied-test" = .{ .path = "test/system/services/protocol-denied-test", .lazy = true },
|
.@"protocol-denied-test" = .{ .path = "test/system/services/protocol-denied-test", .lazy = true },
|
||||||
|
.@"protocol-conformance-test" = .{ .path = "test/system/services/protocol-conformance-test", .lazy = true },
|
||||||
// See `zig fetch --save <url>` for a command-line interface for adding dependencies.
|
// See `zig fetch --save <url>` for a command-line interface for adding dependencies.
|
||||||
//.example = .{
|
//.example = .{
|
||||||
// // When updating this field to a new URL, be sure to delete the corresponding
|
// // When updating this field to a new URL, be sure to delete the corresponding
|
||||||
|
|
|
||||||
|
|
@ -64,19 +64,48 @@ restarted instance to rebuild exactly the same ids.
|
||||||
|
|
||||||
## The protocol
|
## The protocol
|
||||||
|
|
||||||
A `device-manager-protocol` module (the vfs-protocol pattern): extern-struct
|
A `device-manager-protocol` module, defined through the
|
||||||
messages, a version in the handshake, reserved fields everywhere. The manager is a
|
[envelope](../os-development/protocol-namespace.md): every packet — request,
|
||||||
well-known endpoint (`ipc.register(.device_manager)`); the badge tells it who is
|
reply, and pushed event alike — begins with the folded `Header`, and **the device
|
||||||
|
id is `Header.target`**, the manager's object addressing. The contract is bound at
|
||||||
|
`/protocol/device-manager`; the kernel-stamped badge tells the manager who is
|
||||||
talking; the same endpoint receives its children's exit notifications — one loop,
|
talking; the same endpoint receives its children's exit notifications — one loop,
|
||||||
one world.
|
one world.
|
||||||
|
|
||||||
| Direction | Message | Purpose |
|
| Direction | Packet | Purpose |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| driver → manager | `hello { version, role, device_id }` | confirms the argv assignment, starts the deadline clock |
|
| driver → manager | `hello { role, version }` @ the assigned device | confirms the argv assignment, starts the deadline clock |
|
||||||
| bus → manager | `child_added { parent, bus_address, identity, device_id, hid }` | one node the bus discovered |
|
| bus → manager | `child_added { parent, bus_address, identity, bus, vendor, device, subsystem, hid }` @ the registered device id | one node the bus discovered |
|
||||||
| bus → manager | `child_removed { parent, bus_address }` | unplug, or the bus lost it |
|
| bus → manager | `child_removed { parent, bus_address }` | unplug, or the bus lost it |
|
||||||
| app → manager | `enumerate` | snapshot of the tree (read-only) |
|
| app → manager | `enumerate` (reserved verb 1) | snapshot of the tree: one `ChildEntry` per record in the reply's tail |
|
||||||
| app → manager | `subscribe` | receive published add/remove events |
|
| app → manager | `subscribe` (reserved verb 2) | receive published add/remove events; the subscriber's endpoint rides as the call's capability |
|
||||||
|
| manager → app | `child_added` / `child_removed` events | the same two structs, pushed rather than called |
|
||||||
|
|
||||||
|
The watcher table behind those last two rows is the **service harness's**
|
||||||
|
(`service.Subscribers`, shared with input and power), not the manager's: it
|
||||||
|
answers `subscribe`/`unsubscribe`, frames each event once for the fan-out, and
|
||||||
|
sweeps a watcher on its exit notification — where the manager previously had no
|
||||||
|
sweep for watchers at all. Its own supervised-driver exits are a different thing
|
||||||
|
and unchanged, except that a driver's death now arrives twice (the manager is
|
||||||
|
both its supervisor and a subscriber to published exits), so the manager retires
|
||||||
|
a dead driver's process id as it handles the first and the second finds nothing
|
||||||
|
to act on.
|
||||||
|
|
||||||
|
Two of what used to be the manager's own operations are the envelope's **reserved**
|
||||||
|
verbs, which mean the same thing at every provider in the system, so this protocol
|
||||||
|
numbers only three of its own (`hello` = 16, `child_added` = 17,
|
||||||
|
`child_removed` = 18) and its two events in their own space (`child_added` = 16,
|
||||||
|
`child_removed` = 17). No reply carries a status field: that is the `Status` every
|
||||||
|
reply begins with.
|
||||||
|
|
||||||
|
`child_added` is the one struct that travels both ways — a bus *calls* it, the
|
||||||
|
manager *pushes* it — which is why the operation and event numbering spaces are
|
||||||
|
separate: one encoding, both directions, told apart by which way the packet went.
|
||||||
|
Folding the operation byte and the device id out of it is also what makes it fit:
|
||||||
|
a pushed event is 64 bytes at most, header included, and this one lands exactly on
|
||||||
|
that floor. `child_removed` is the single message whose target stays 0, because it
|
||||||
|
is addressed by the composite (parent, bus address) and no single `u64` carries a
|
||||||
|
pair.
|
||||||
|
|
||||||
`hello` is the one deadline the manager enforces itself: spawned and silent past the
|
`hello` is the one deadline the manager enforces itself: spawned and silent past the
|
||||||
deadline means wrong binary, wrong protocol version, or wedged before main — apply
|
deadline means wrong binary, wrong protocol version, or wedged before main — apply
|
||||||
|
|
|
||||||
|
|
@ -106,8 +106,10 @@ The bring-up sequence mirrors a hardware driver's — it is the
|
||||||
[`usb-xhci-bus` `initialise`](../../system/drivers/usb-xhci-bus/usb-xhci-bus.zig) shape
|
[`usb-xhci-bus` `initialise`](../../system/drivers/usb-xhci-bus/usb-xhci-bus.zig) shape
|
||||||
(claim → `mmio_map` → run loop) — and the request/reply service shell is the
|
(claim → `mmio_map` → run loop) — and the request/reply service shell is the
|
||||||
[FAT](../../system/services/fat/fat.zig) / [input](../../system/services/input/input.zig) shape
|
[FAT](../../system/services/fat/fat.zig) / [input](../../system/services/input/input.zig) shape
|
||||||
([`service.run`](../../library/kernel/service.zig) with a `protocol.zig` of
|
([`service.run`](../../library/kernel/service.zig) over the dispatch table its
|
||||||
`extern struct` messages and an `Operation` tag).
|
protocol module generates through
|
||||||
|
[`envelope.Define`](../os-development/protocol-namespace.md) — one request and
|
||||||
|
reply type per verb, and the layer id in the packet header's `target`).
|
||||||
|
|
||||||
**One process, for now.** v1 is a *single* service that both owns the framebuffer and
|
**One process, for now.** v1 is a *single* service that both owns the framebuffer and
|
||||||
composites — it does not split a "framebuffer driver" from a "compositor" the way input
|
composites — it does not split a "framebuffer driver" from a "compositor" the way input
|
||||||
|
|
@ -209,6 +211,18 @@ shell, a terminal, a cursor, and a wallpaper:
|
||||||
| `damage` | mark a region of a layer dirty |
|
| `damage` | mark a region of a layer dirty |
|
||||||
| `present` | request a repaint: composited at the next frame-clock tick |
|
| `present` | request a repaint: composited at the next frame-clock tick |
|
||||||
|
|
||||||
|
**A layer belongs to the client that created it.** The id is a slot in a
|
||||||
|
sixteen-entry table — small, dense, guessable — so every verb above that names one is
|
||||||
|
answered only for the task whose `create_layer` produced it, and a layer that is
|
||||||
|
somebody else's is refused exactly as one that never existed (`-ENOENT`), so a client
|
||||||
|
cannot use the refusal to learn which ids are live
|
||||||
|
([protocol-namespace.md](../os-development/protocol-namespace.md): handles are scoped
|
||||||
|
per client, validated against the badge). The compositor's own layers — the cursor
|
||||||
|
sprite and the startup self-check's pair — are marked service-owned and are created by
|
||||||
|
direct call rather than over the protocol, so no client can move or destroy the
|
||||||
|
cursor. A dead client's layers are released on its exit notification, the same sweep
|
||||||
|
the FAT server runs for open files.
|
||||||
|
|
||||||
Text is intentionally *not* an operation — a client renders glyphs by blitting tiles
|
Text is intentionally *not* an operation — a client renders glyphs by blitting tiles
|
||||||
(the [PSF font](../../system/kernel/font.psf) path the console already uses can move into a
|
(the [PSF font](../../system/kernel/font.psf) path the console already uses can move into a
|
||||||
client). Keeping the protocol to rectangles and tiles keeps the compositor small and the
|
client). Keeping the protocol to rectangles and tiles keeps the compositor small and the
|
||||||
|
|
|
||||||
|
|
@ -22,12 +22,22 @@ event:
|
||||||
- `JoystickEvent` — `axis` moves (a signed value on a `control` index) and
|
- `JoystickEvent` — `axis` moves (a signed value on a `control` index) and
|
||||||
`button_down`/`button_up`.
|
`button_down`/`button_up`.
|
||||||
|
|
||||||
All three travel in one **`InputEvent` envelope** tagged with a `DeviceKind`, so the
|
A source publishes any of the three as one **`InputEvent`** tagged with a `DeviceKind`, so
|
||||||
fan-out is a single code path and a subscriber can take a mix of classes on one stream.
|
`publish` is a single verb; decode one with `asKeyboard()` / `asMouse()` / `asJoystick()`
|
||||||
Decode an envelope with `asKeyboard()` / `asMouse()` / `asJoystick()` (each returns null
|
(each returns null unless the tag matches). On the *delivery* wire the class is the
|
||||||
unless the tag matches). A subscriber names the classes it wants with a **`device_mask`**,
|
packet's own operation instead — the protocol declares one event per class
|
||||||
and the service routes each event only to subscribers whose mask includes its class — so a
|
([protocol-namespace.md](../os-development/protocol-namespace.md)), so a pushed packet is
|
||||||
mouse-only listener never wakes for keystrokes.
|
the 16-byte header plus the typed event and nothing carries a tag twice. The client
|
||||||
|
helpers re-tag what arrives back into an `InputEvent`, so a subscriber can still take a
|
||||||
|
mix of classes on one stream. A subscriber names the classes it wants with a
|
||||||
|
**`device_mask`**, and the service routes each event only to subscribers whose mask
|
||||||
|
includes its class — so a mouse-only listener never wakes for keystrokes.
|
||||||
|
|
||||||
|
**`subscribe` is not this protocol's verb.** Its shape — a synchronous call whose attached
|
||||||
|
capability is the subscriber's own endpoint — is what the envelope's *reserved* subscribe
|
||||||
|
means at every provider in the system, so the input protocol adopts it rather than
|
||||||
|
defining a second spelling of the same thing. The interest mask rides as the packet's
|
||||||
|
tail. `publish` is the one verb the protocol defines for itself.
|
||||||
|
|
||||||
## Why this needed a new kernel primitive
|
## Why this needed a new kernel primitive
|
||||||
|
|
||||||
|
|
@ -98,12 +108,25 @@ This is the async counterpart of `ipc_call`, and the input service is its first
|
||||||
`publishJoystickEvent`. Publishing is a short synchronous `ipc_call` the service answers at
|
`publishJoystickEvent`. Publishing is a short synchronous `ipc_call` the service answers at
|
||||||
once; the service's own fan-out is asynchronous, so publishing never blocks on a slow
|
once; the service's own fan-out is asynchronous, so publishing never blocks on a slow
|
||||||
subscriber.
|
subscriber.
|
||||||
- The **service** ([input.zig](../../system/services/input/input.zig)) keeps a small subscriber
|
- The **service** ([input.zig](../../system/services/input/input.zig)) owns none of that
|
||||||
table (endpoint handle + owning task id + `device_mask`). On `publish` it `ipc_send`s the
|
machinery any more: the subscriber table (endpoint handle + owning task + interest mask),
|
||||||
event to every subscriber whose mask includes the event's device class. On `subscribe` it
|
the reserved `subscribe`/`unsubscribe` verbs, the fan-out, and the dead-subscriber sweep
|
||||||
stores the passed capability and mask and, as housekeeping, prunes any slot whose owning
|
are the shared harness's (`service.Subscribers` in
|
||||||
process has exited (checked against `process_enumerate`) — not for correctness (an async
|
[service.zig](../../library/kernel/service.zig)), so every event stream in the system has
|
||||||
send to an orphaned endpoint is harmless) but to reclaim the slot.
|
identical semantics. What is left in this file is what is actually about input: which
|
||||||
|
class an event belongs to, and which classes a subscriber asked for. On `publish` it names
|
||||||
|
the event's class and the harness `ipc_send`s the packet — framed once — to every
|
||||||
|
subscriber whose mask includes it.
|
||||||
|
- **A dead subscriber goes away on its exit notification**, not on a poll. The service used
|
||||||
|
to walk `process_enumerate` on every subscribe and drop slots whose owner had gone; it now
|
||||||
|
subscribes to the kernel's published exits like the FAT server and the compositor do
|
||||||
|
([process-lifecycle.md](../os-development/process-lifecycle.md)), which reclaims the slot
|
||||||
|
*and* closes the endpoint capability in it promptly rather than at the next subscribe.
|
||||||
|
(The fan-out also drops a subscriber whose `ipc_send` fails, as a backstop for a
|
||||||
|
notification a full ring dropped.)
|
||||||
|
- The service runs on the shared harness like every other, so it answers the universal ping
|
||||||
|
and exits on `terminate`; it was the last hand-rolled receive loop in the tree, and the
|
||||||
|
last service a shutdown had to kill rather than ask.
|
||||||
|
|
||||||
Publisher and subscriber must be **separate processes**: a single thread that both
|
Publisher and subscriber must be **separate processes**: a single thread that both
|
||||||
published and serviced its own subscription would deadlock (its `publish` call blocks until
|
published and serviced its own subscription would deadlock (its `publish` call blocks until
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,10 @@
|
||||||
> serves it directly (the read-only /system initrd mount, via `fs_node`) or
|
> serves it directly (the read-only /system initrd mount, via `fs_node`) or
|
||||||
> redirects the caller to the owning backend's endpoint plus the rewritten
|
> redirects the caller to the owning backend's endpoint plus the rewritten
|
||||||
> mount-relative path — after which the client speaks THIS protocol to the
|
> mount-relative path — after which the client speaks THIS protocol to the
|
||||||
> backend, unchanged. The Zig source of truth is `library/protocol/vfs/vfs-protocol.zig`
|
> backend, unchanged. Since P4a the contract is expressed through
|
||||||
|
> `envelope.Define` (docs/os-development/protocol-namespace.md), so every
|
||||||
|
> packet begins with the universal 16-byte prefix and the open-node id rides
|
||||||
|
> in it. The Zig source of truth is `library/protocol/vfs/vfs-protocol.zig`
|
||||||
> (the `vfs-protocol` module), whose unit test pins a sample of the sizes
|
> (the `vfs-protocol` module), whose unit test pins a sample of the sizes
|
||||||
> and values below. This page is the **language-neutral wire specification**
|
> and values below. This page is the **language-neutral wire specification**
|
||||||
> of that contract — what a Rust or C client implements ([vdso.md](../os-development/vdso.md)
|
> of that contract — what a Rust or C client implements ([vdso.md](../os-development/vdso.md)
|
||||||
|
|
@ -22,12 +25,17 @@ also hands back the path rewritten relative to the mount — not from a
|
||||||
registry lookup. (Service id 1, the old userspace router, is retired.)
|
registry lookup. (Service id 1, the old userspace router, is retired.)
|
||||||
|
|
||||||
- A message is at most **256 bytes** (`message_maximum`).
|
- A message is at most **256 bytes** (`message_maximum`).
|
||||||
- A request is a fixed 32-byte **Request** header followed by an inline
|
- Every packet begins with the 16-byte **envelope prefix**
|
||||||
payload of at most **224 bytes** (`maximum_payload`) — a path, or write
|
([protocol-namespace.md](../os-development/protocol-namespace.md)): a
|
||||||
bytes. There is no multi-message request: paths and single reads/writes
|
`Header` on a request, a `Status` on a reply. The prefix is **folded, not
|
||||||
must fit, and larger transfers loop (see *read* / *write*).
|
stacked** — the verb and the object being addressed live in it, and no
|
||||||
- A reply is a fixed 24-byte **Reply** header followed by an inline payload —
|
request or reply below repeats either.
|
||||||
read bytes, a `FileStatus`, or a `DirectoryEntry`.
|
- A request is the header, then the verb's own fixed part (0–16 bytes), then
|
||||||
|
an inline tail of at most **224 bytes** (`maximum_payload`) — a path, or
|
||||||
|
write bytes. There is no multi-message request: paths and single
|
||||||
|
reads/writes must fit, and larger transfers loop (see *read* / *write*).
|
||||||
|
- A reply is the status, then the verb's own fixed part, then an inline tail
|
||||||
|
— read bytes, or a directory entry's name.
|
||||||
- All integers are **little-endian**; layouts are C layout for x86-64
|
- All integers are **little-endian**; layouts are C layout for x86-64
|
||||||
(`extern struct`), offsets given below so nothing need be inferred.
|
(`extern struct`), offsets given below so nothing need be inferred.
|
||||||
|
|
||||||
|
|
@ -37,91 +45,108 @@ With clients holding backend node ids directly, a backend records each open
|
||||||
handle's owner and sweeps a dead client's handles via the published process
|
handle's owner and sweeps a dead client's handles via the published process
|
||||||
exit events.
|
exit events.
|
||||||
|
|
||||||
## Request header — 32 bytes
|
## Request header — 16 bytes
|
||||||
|
|
||||||
|
The envelope's `Header`, identical in every danos protocol:
|
||||||
|
|
||||||
| offset | size | field | meaning |
|
| offset | size | field | meaning |
|
||||||
|-------:|-----:|-------|---------|
|
|-------:|-----:|-------|---------|
|
||||||
| 0 | 4 | `operation` | an **Operation** value (below) |
|
| 0 | 4 | `operation` | an **Operation** value (below); 0–15 are the reserved universal verbs |
|
||||||
| 4 | 4 | — | padding |
|
| 4 | 4 | — | padding |
|
||||||
| 8 | 8 | `node` | the server-side open-node id from a prior `open`; 0 for path-based operations |
|
| 8 | 8 | `target` | **the open-node id** from a prior `open`; 0 for `open` itself and the path-based verbs |
|
||||||
| 16 | 8 | `offset` | byte position for read/write; entry index (cursor) for readdir; else 0 |
|
|
||||||
| 24 | 4 | `len` | payload length for path/write operations; requested byte count for read |
|
|
||||||
| 28 | 4 | `flags` | open flags (below); else 0 |
|
|
||||||
|
|
||||||
## Reply header — 24 bytes
|
## Reply header — 16 bytes
|
||||||
|
|
||||||
|
The envelope's `Status`:
|
||||||
|
|
||||||
| offset | size | field | meaning |
|
| offset | size | field | meaning |
|
||||||
|-------:|-----:|-------|---------|
|
|-------:|-----:|-------|---------|
|
||||||
| 0 | 4 | `status` | **0 = success**, negative = failure (signed) |
|
| 0 | 4 | `status` | **0 = success**, negative = failure (signed) |
|
||||||
| 4 | 4 | — | padding |
|
| 4 | 4 | — | padding |
|
||||||
| 8 | 8 | `node` | the new open-node id (for `open`); else 0 |
|
| 8 | 4 | `len` | reply bytes following this header: the verb's fixed part plus its tail |
|
||||||
| 16 | 4 | `len` | reply payload length in bytes |
|
| 12 | 4 | — | padding |
|
||||||
| 20 | 4 | — | padding |
|
|
||||||
|
|
||||||
On failure the backend replies `status = -1`, and that reply reaches the
|
A failing backend replies with the status alone (`len` = 0) and no fixed
|
||||||
client directly — there is no party between them on the wire. (Kernel-served
|
part, and that reply reaches the client directly — there is no party between
|
||||||
paths produce no wire replies at all: `fs_resolve`/`fs_node` failures are
|
them on the wire. (Kernel-served paths produce no wire replies at all:
|
||||||
syscall register statuses.) A richer errno vocabulary is future work —
|
`fs_resolve`/`fs_node` failures are syscall register statuses.) The errno
|
||||||
clients must treat *any* negative status as failure, not match on -1.
|
vocabulary is the kernel's, continued by the envelope: `ENOENT` = 4 is what a
|
||||||
|
backend answers for anything it cannot find or cannot do, `ENOSYS` = 10 for a
|
||||||
|
verb it does not implement, `EPROTO` = 11 for a packet shorter than the verb
|
||||||
|
it names. Clients must treat *any* negative status as failure rather than
|
||||||
|
matching a particular one.
|
||||||
|
|
||||||
## Operations
|
## Operations
|
||||||
|
|
||||||
Values are append-only and never renumbered (the same evolution rule every
|
Values number from 16 (`first_protocol_operation`) in declaration order, and
|
||||||
danos protocol follows). Send only values from this table: the shipped server
|
are frozen once shipped. Values 0–15 are the envelope's reserved universal
|
||||||
decodes the operation into an exhaustive enum, so an out-of-range value is
|
verbs, which mean the same thing at every provider in the system: `describe`
|
||||||
not answered with a `status = -1` reply — it trips a safety check in safe
|
(0) answers the protocol's name and version and is implemented by the
|
||||||
builds and is undefined otherwise. (The `-1` replies cover recognised but
|
envelope itself, so every backend answers it. A verb outside this table is
|
||||||
refused operations, such as `mount` sent to a backend.)
|
answered `-ENOSYS`; it is never a safety check any more, because the
|
||||||
|
dispatch compares numbers rather than decoding an enum.
|
||||||
|
|
||||||
| value | operation | request payload | reply |
|
Each row's *request* and *reply* name the bytes **after** the 16-byte prefix.
|
||||||
|------:|-----------|-----------------|-------|
|
|
||||||
| 0 | `open` | the path (`len` = its length), `flags` as below | `node` = open-node id |
|
| value | operation | request | tail | reply | reply tail |
|
||||||
| 1 | `close` | — (`node` set) | status only |
|
|------:|-----------|---------|------|-------|-----------|
|
||||||
| 2 | `read` | — (`node`, `offset`, `len` = wanted count) | `len` bytes read, payload = the bytes; `len` 0 at end of file |
|
| 16 | `open` | `flags` (4 bytes, below) | the path | `node` (8 bytes) = the open-node id | — |
|
||||||
| 3 | `write` | the bytes (`node`, `offset`, `len` = count) | `len` = bytes accepted (may be short — loop) |
|
| 17 | `close` | — | — | — | — |
|
||||||
| 4 | `status` | — (`node` set) | payload = **FileStatus** (24 bytes) |
|
| 18 | `read` | `offset` (8), `len` (4) = wanted count | — | — | the bytes read; `Status.len` 0 at end of file |
|
||||||
| 5 | `readdir` | — (`node` = a directory, `offset` = cursor) | payload = one **DirectoryEntry** + name; `len` 0 at end |
|
| 19 | `write` | `offset` (8), `len` (4) = count | the bytes | `count` (4) = bytes accepted (may be short — loop) | — |
|
||||||
| 6 | `mount` | the mount-point path; the backend endpoint rides as the call's **capability** | status only |
|
| 20 | `status` | — | — | **FileStatus** (24 bytes) | — |
|
||||||
| 7 | `unmount` | the mount-point path | status only |
|
| 21 | `readdir` | `cursor` (8) | — | one **DirectoryEntry** (16 bytes) | the name |
|
||||||
| 8 | `mkdir` | the path | status only |
|
| 22 | `mount` | — | the mount-point path; the backend endpoint rides as the call's **capability** | — | — |
|
||||||
| 9 | `unlink` | the path | status only |
|
| 23 | `unmount` | — | the mount-point path | — | — |
|
||||||
| 10 | `rename` | old path, one `0x00`, new path (`len` = total) | status only |
|
| 24 | `mkdir` | — | the path | — | — |
|
||||||
|
| 25 | `unlink` | — | the path | — | — |
|
||||||
|
| 26 | `rename` | — | old path, one `0x00`, new path | — | — |
|
||||||
|
| 27 | `bind` | — | the contract name; the provider's endpoint rides as the call's **capability** | — | — |
|
||||||
|
|
||||||
Notes per operation:
|
Notes per operation:
|
||||||
|
|
||||||
- **open** — the path is the mount-relative path `fs_resolve` handed back
|
- **open** — the path is the mount-relative path `fs_resolve` handed back
|
||||||
(absolute-shaped: `/notes.txt` under fat's `/mnt/usb` mount). Bare names
|
(absolute-shaped: `/notes.txt` under fat's `/volumes/usb` mount). Bare names
|
||||||
(`greeting`) resolve nowhere — the flat ramfs is retired, and `fs_resolve`
|
(`greeting`) resolve nowhere — the flat ramfs is retired, and `fs_resolve`
|
||||||
refuses non-absolute paths. The returned `node` is the *backend's* own
|
refuses non-absolute paths. The returned `node` is the *backend's* own
|
||||||
open-node id: with the router in the kernel there is no forwarding table,
|
open-node id: with the router in the kernel there is no forwarding table,
|
||||||
and clients hold backend ids directly (see *Lifetimes and trust*).
|
and clients hold backend ids directly (see *Lifetimes and trust*). Every
|
||||||
|
later packet carries it in `Header.target` — the path is spoken once, here,
|
||||||
|
and integers do the rest.
|
||||||
- **read / write** — a single exchange moves at most 224 bytes
|
- **read / write** — a single exchange moves at most 224 bytes
|
||||||
(`maximum_payload`); the client loops, advancing `offset` by the returned
|
(`maximum_payload`); the client loops, advancing its own offset by what
|
||||||
`len`, until done (read) or the slice is written (write). A `write` reply
|
came back, until done (read) or the slice is written (write). A `write`
|
||||||
shorter than requested is progress, not an error; a `len` of 0 means no
|
reply shorter than requested is progress, not an error; a count of 0 means
|
||||||
forward progress — stop rather than spin.
|
no forward progress — stop rather than spin.
|
||||||
- **readdir** — `offset` is a **cursor: the entry index**, not a byte
|
- **readdir** — `cursor` is the **entry index**, not a byte position. Each
|
||||||
position. Each call returns exactly one entry; the client increments the
|
call returns exactly one entry; the client increments the cursor by 1. **A
|
||||||
cursor by 1. A reply with `len` 0 is end-of-directory. The directory must
|
`name_len` of 0 is end-of-directory** — the reply's own length cannot say
|
||||||
have been opened with the `directory` flag.
|
so, because the envelope always sends the fixed reply part. The directory
|
||||||
|
must have been opened with the `directory` flag.
|
||||||
- **mount / unmount** — RETIRED from the wire: mounting is the `fs_mount`
|
- **mount / unmount** — RETIRED from the wire: mounting is the `fs_mount`
|
||||||
syscall now (a filesystem server passes its endpoint handle; possession is
|
syscall now (a filesystem server passes its endpoint handle; possession is
|
||||||
the capability, exactly the trust of the old cap-passing op). The op
|
the capability, exactly the trust of the old cap-passing op). The verb
|
||||||
numbers stay reserved. Mount-prefix semantics are unchanged: prefixes
|
numbers stay reserved. Mount-prefix semantics are unchanged: prefixes
|
||||||
match at path boundaries only (`/mnt/usb` never captures `/mnt/usbextra`),
|
match at path boundaries only (`/volumes/usb` never captures
|
||||||
the longest matching prefix wins, and an optional backend-side rewrite
|
`/volumes/usbextra`), the longest matching prefix wins, and an optional
|
||||||
prefix maps a mount into the backend's namespace (fat serves `/mnt/usb`
|
backend-side rewrite prefix maps a mount into the backend's namespace (fat
|
||||||
from its volume root and `/var` from its `/var` subtree).
|
serves `/volumes/usb` from its volume root and `/system/logs` from its
|
||||||
|
`/system/logs` subtree).
|
||||||
- **rename** — same-directory rename only: the backend compares the old and
|
- **rename** — same-directory rename only: the backend compares the old and
|
||||||
new parent paths and refuses a mismatch. The client (`file_system`) refuses
|
new parent paths and refuses a mismatch. The client (`file_system`) refuses
|
||||||
earlier when the two paths resolve to different backend endpoints, but that
|
earlier when the two paths resolve to different backend endpoints, but that
|
||||||
check is coarser than "one mount" — one endpoint can serve several mounts
|
check is coarser than "one mount" — one endpoint can serve several mounts
|
||||||
(fat serves `/mnt/usb` and `/var`), so a cross-mount rename reaches the
|
(fat serves `/volumes/usb`, `/system/configuration` and `/system/logs`), so
|
||||||
backend and fails on its same-directory check.
|
a cross-mount rename reaches the backend and fails on its same-directory
|
||||||
|
check.
|
||||||
|
- **bind** — the protocol registry's claim verb, implemented only by the
|
||||||
|
synthetic `/protocol` backend inside PID 1
|
||||||
|
([protocol-namespace.md](../os-development/protocol-namespace.md)). A file
|
||||||
|
backend answers `-ENOSYS`.
|
||||||
|
|
||||||
## Open flags
|
## Open flags
|
||||||
|
|
||||||
Bitwise OR in `Request.flags`, meaningful for `open` only:
|
Bitwise OR in `open`'s `flags`, meaningful for `open` only:
|
||||||
|
|
||||||
| bit | name | meaning |
|
| bit | name | meaning |
|
||||||
|----:|------|---------|
|
|----:|------|---------|
|
||||||
|
|
@ -129,7 +154,7 @@ Bitwise OR in `Request.flags`, meaningful for `open` only:
|
||||||
| 2 | `directory` | open a directory node for `readdir` rather than a file |
|
| 2 | `directory` | open a directory node for `readdir` rather than a file |
|
||||||
| 4 | `truncate` | truncate an existing file to zero length on open (replace, don't overwrite in place) |
|
| 4 | `truncate` | truncate an existing file to zero length on open (replace, don't overwrite in place) |
|
||||||
|
|
||||||
## FileStatus — 24 bytes (the `status` reply payload)
|
## FileStatus — 24 bytes (the `status` reply's fixed part)
|
||||||
|
|
||||||
| offset | size | field | meaning |
|
| offset | size | field | meaning |
|
||||||
|-------:|-----:|-------|---------|
|
|-------:|-----:|-------|---------|
|
||||||
|
|
@ -138,12 +163,12 @@ Bitwise OR in `Request.flags`, meaningful for `open` only:
|
||||||
| 12 | 4 | — | padding |
|
| 12 | 4 | — | padding |
|
||||||
| 16 | 8 | `mtime` | modification time, Unix epoch seconds UTC; 0 if the backend keeps none |
|
| 16 | 8 | `mtime` | modification time, Unix epoch seconds UTC; 0 if the backend keeps none |
|
||||||
|
|
||||||
## DirectoryEntry — 16 bytes + name (the `readdir` reply payload)
|
## DirectoryEntry — 16 bytes + name (the `readdir` reply)
|
||||||
|
|
||||||
| offset | size | field | meaning |
|
| offset | size | field | meaning |
|
||||||
|-------:|-----:|-------|---------|
|
|-------:|-----:|-------|---------|
|
||||||
| 0 | 4 | `kind` | a **NodeKind** value |
|
| 0 | 4 | `kind` | a **NodeKind** value |
|
||||||
| 4 | 4 | `name_len` | length of the name that follows |
|
| 4 | 4 | `name_len` | length of the name that follows; **0 means end of directory** |
|
||||||
| 8 | 8 | `size` | the entry's size in bytes |
|
| 8 | 8 | `size` | the entry's size in bytes |
|
||||||
| 16 | `name_len` | name | the entry's name, not NUL-terminated |
|
| 16 | `name_len` | name | the entry's name, not NUL-terminated |
|
||||||
|
|
||||||
|
|
@ -174,7 +199,7 @@ volumes, reserved) remain part of the design.
|
||||||
## An open reply may carry a capability
|
## An open reply may carry a capability
|
||||||
|
|
||||||
`open` rides `ipc_call`, whose reply direction can hand back an endpoint
|
`open` rides `ipc_call`, whose reply direction can hand back an endpoint
|
||||||
capability alongside the `Reply` header. A file backend never uses it — FAT
|
capability alongside the reply. A file backend never uses it — FAT
|
||||||
answers with a node id and nothing else — but a **synthetic** backend does:
|
answers with a node id and nothing else — but a **synthetic** backend does:
|
||||||
opening a `protocol` node returns the provider's endpoint, and possession of
|
opening a `protocol` node returns the provider's endpoint, and possession of
|
||||||
that endpoint *is* the channel. The convention is per-backend, not
|
that endpoint *is* the channel. The convention is per-backend, not
|
||||||
|
|
@ -187,10 +212,24 @@ Open-node ids live in the backend. A client that dies without closing leaks
|
||||||
nothing permanently: the backend (the FAT server) subscribes to the kernel's
|
nothing permanently: the backend (the FAT server) subscribes to the kernel's
|
||||||
published process-exit events (docs/process-lifecycle.md) and releases a dead
|
published process-exit events (docs/process-lifecycle.md) and releases a dead
|
||||||
client's handles. The kernel VFS root needs no sweep at all — its node tokens
|
client's handles. The kernel VFS root needs no sweep at all — its node tokens
|
||||||
are permanent for a boot and carry no open state. Ids are plain integers, not
|
are permanent for a boot and carry no open state.
|
||||||
capabilities — a backend trusts its callers with each other's ids today, which
|
|
||||||
is acceptable while every client is part of the system image and worth
|
Ids are plain integers rather than capabilities, so the backend **scopes them
|
||||||
revisiting (per-client id namespaces) before third-party binaries arrive.
|
to the caller's badge**: an open node belongs to the task that opened it, and
|
||||||
|
every verb that names one — read, write, status, readdir, close — is answered
|
||||||
|
only for that task. A node id is a small number drawn from a table of
|
||||||
|
thirty-two, trivially guessable, and until this rule a backend honoured every
|
||||||
|
client's ids from every other client
|
||||||
|
(docs/os-development/protocol-namespace.md: *handles must be scoped per
|
||||||
|
client — validated against the badge, or drawn from a per-client id
|
||||||
|
namespace*).
|
||||||
|
|
||||||
|
The refusal is deliberately **identical to absence**: a node that is somebody
|
||||||
|
else's answers `-ENOENT`, exactly as one that was never opened, so a prober
|
||||||
|
learns nothing about which ids are live — the same discipline the protocol
|
||||||
|
namespace applies to a refused open. The owner is a *task*, because the badge
|
||||||
|
is: a threaded client uses a node from the thread that opened it, which is
|
||||||
|
already the granularity of the exit sweep that releases it.
|
||||||
|
|
||||||
## Evolution rules
|
## Evolution rules
|
||||||
|
|
||||||
|
|
@ -198,11 +237,15 @@ What a non-Zig implementation may rely on, and what it must not:
|
||||||
|
|
||||||
- Operation values, flag bits, `NodeKind` values, and struct layouts are
|
- Operation values, flag bits, `NodeKind` values, and struct layouts are
|
||||||
**append-only and frozen once shipped**. The unit test in
|
**append-only and frozen once shipped**. The unit test in
|
||||||
`library/protocol/vfs/vfs-protocol.zig` pins a sample of them (the `DirectoryEntry`
|
`library/protocol/vfs/vfs-protocol.zig` pins a sample of them (the
|
||||||
size, `NodeKind` 0–1 and 6–7, `Operation` values 0, 4 and 5); this page is
|
`DirectoryEntry` size, `NodeKind` 0–1 and 6–7, `Operation` values 16–21, 26
|
||||||
the full record of the frozen values.
|
and 27); this page is the full record of the frozen values.
|
||||||
|
*The one renumbering this contract has had was the rebase onto the envelope
|
||||||
|
(P4a), which moved every verb above the reserved range — a deliberate
|
||||||
|
flag-day across a system with no third-party clients yet, not a precedent.*
|
||||||
- The 256-byte message ceiling is a property of the current IPC transport,
|
- The 256-byte message ceiling is a property of the current IPC transport,
|
||||||
not a promise; clients should read `maximum_payload`-shaped limits from the
|
not a promise; clients should read `maximum_payload`-shaped limits from the
|
||||||
reply lengths they actually get (loop-until-done), not hard-code 224.
|
reply lengths they actually get (loop-until-done), not hard-code 224.
|
||||||
- Negative statuses beyond -1 will appear (an errno vocabulary); success is
|
- Success is exactly 0, and the negative statuses come from one system-wide
|
||||||
exactly 0.
|
errno vocabulary (the kernel's, continued by the envelope) rather than from
|
||||||
|
this protocol.
|
||||||
|
|
|
||||||
|
|
@ -28,28 +28,43 @@ unchanged.
|
||||||
## The protocol
|
## The protocol
|
||||||
|
|
||||||
The `power-protocol` module ([library/protocol/power/power-protocol.zig](../../library/protocol/power/power-protocol.zig))
|
The `power-protocol` module ([library/protocol/power/power-protocol.zig](../../library/protocol/power/power-protocol.zig))
|
||||||
follows the vfs-protocol pattern — extern-struct messages, a version, reserved
|
is defined through the [envelope](protocol-namespace.md), so every packet begins
|
||||||
fields. Three operations:
|
with the folded `Header`. `Header.target` is unused in both directions: the
|
||||||
|
provider is the only object either side addresses.
|
||||||
|
|
||||||
| Direction | Operation | Purpose |
|
| Direction | Packet | Purpose |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| subscriber → service | `subscribe` | receive published events; the subscriber's endpoint rides as the call's **capability** (the input/device-manager pattern) |
|
| subscriber → service | `subscribe` (reserved verb 2) | receive published events; the subscriber's endpoint rides as the call's **capability** (the input/device-manager pattern) |
|
||||||
| init → service | `shutdown` | orderly shutdown's last step: enter S5 (soft off) |
|
| init → service | `shutdown` (verb 16) | orderly shutdown's last step: enter S5 (soft off) |
|
||||||
| service → subscriber | `event` | a published `EventMessage`, delivered as a buffered message (never sent *to* the service) |
|
| service → subscriber | one event per kind | a published `Notice`, `ipc_send`t as a buffered packet (never sent *to* the service) |
|
||||||
|
|
||||||
|
`subscribe` is not one of this protocol's own verbs: a synchronous call whose
|
||||||
|
attached capability is the subscriber's endpoint is exactly what the envelope's
|
||||||
|
reserved `subscribe` means everywhere, so power adopts it wholesale. And no
|
||||||
|
packet carries a version — the reserved `describe` verb is the version handshake,
|
||||||
|
asked once at connect time rather than out of every packet's budget.
|
||||||
|
|
||||||
Events are published, not polled: like the input service, the service holds
|
Events are published, not polled: like the input service, the service holds
|
||||||
subscriber endpoints as capabilities and `ipc_send`s each event as a buffered
|
subscriber endpoints as capabilities and `ipc_send`s each event as a buffered
|
||||||
message, so a slow or dead subscriber can never wedge the source. The event
|
packet, so a slow or dead subscriber can never wedge the source. The table, the
|
||||||
|
reserved `subscribe`/`unsubscribe` verbs and the fan-out are the **service
|
||||||
|
harness's** (`service.Subscribers`), shared with input and the device manager, so
|
||||||
|
the acpi service's own code is the ACPI half only — and a subscriber that dies is
|
||||||
|
now swept on its exit notification, where before this service had no sweep at
|
||||||
|
all. **The kind is the packet's operation** — one declared event per named kind, exactly as the
|
||||||
|
input service delivers one per device class — so a subscriber reads *what
|
||||||
|
happened* out of the header rather than out of a tag inside the payload. The
|
||||||
vocabulary is hardware-neutral:
|
vocabulary is hardware-neutral:
|
||||||
|
|
||||||
- `power_button` — the button was pressed (a fixed ACPI event on x86).
|
- `power_button` (event 16) — the button was pressed (a fixed ACPI event on x86).
|
||||||
- `lid`, `ac`, `battery` — the named GPE-driven events.
|
- `lid` (17), `ac` (18), `battery` (19) — the named GPE-driven events.
|
||||||
- `notify` — a device notification that maps to none of the above; its `code`
|
- `notify` (20) — a device notification that maps to none of the above; its
|
||||||
(the ACPI `Notify` argument) and the notifying device's `hid` say which device
|
`code` (the ACPI `Notify` argument) and the notifying device's `hid` say which
|
||||||
and what happened.
|
device and what happened.
|
||||||
|
|
||||||
An `EventMessage` carries the `event` tag plus `code` and an 8-byte `hid`, so a
|
The payload every one of them carries is a `Notice`: `code` plus an 8-byte `hid`,
|
||||||
generic `notify` is fully described without a second round trip.
|
so a generic `notify` is fully described without a second round trip, and the
|
||||||
|
four named kinds leave both fields zero because the verb already said it all.
|
||||||
|
|
||||||
**`shutdown` is authority, not information.** It is the only operation that
|
**`shutdown` is authority, not information.** It is the only operation that
|
||||||
*does* something irreversible, so it is gated: the contract is that only init
|
*does* something irreversible, so it is gated: the contract is that only init
|
||||||
|
|
@ -58,7 +73,10 @@ sequence over everything else. The acpi service implements this as a **soft
|
||||||
gate** — it honors `shutdown` only from a process that is a *subscriber*, and
|
gate** — it honors `shutdown` only from a process that is a *subscriber*, and
|
||||||
init is the one subscriber. That stands in for "only the system supervisor may
|
init is the one subscriber. That stands in for "only the system supervisor may
|
||||||
power off" without hard-coding a pid, so it still holds under tests where PID 1
|
power off" without hard-coding a pid, so it still holds under tests where PID 1
|
||||||
is not init.
|
is not init. The question is asked of the harness's table now
|
||||||
|
(`Subscribers.has(sender)`), which is why the harness exposes it: the gate is
|
||||||
|
unchanged, including the badge being the whole of it — the badge is
|
||||||
|
kernel-stamped, so nothing inside a packet can claim to be init.
|
||||||
|
|
||||||
## Orderly shutdown
|
## Orderly shutdown
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -188,9 +188,15 @@ zombie state or privileged snooping:
|
||||||
state by all along is the id the exit event carries.
|
state by all along is the id the exit event carries.
|
||||||
|
|
||||||
Subscription, not broadcast-to-everyone: only processes that asked receive
|
Subscription, not broadcast-to-everyone: only processes that asked receive
|
||||||
events, the kernel keeps a bounded subscriber table, and delivery is the same
|
events, the kernel keeps a bounded subscriber table (sixteen — a normal boot
|
||||||
non-blocking coalescing notification as everything else — a dying process never
|
already fields six, since this is what *every* provider with per-client state
|
||||||
waits on its mourners. Subscribing is ungated, like `process_enumerate`: what is
|
releases on), and delivery is the same non-blocking coalescing notification as
|
||||||
|
everything else — a dying process never waits on its mourners.
|
||||||
|
A service does not usually write the sweep itself: the shared service harness
|
||||||
|
subscribes for it and drops a dead task's event subscriptions
|
||||||
|
(`service.Subscribers`), and a provider adds its own handler only for state the
|
||||||
|
harness knows nothing about — open files, layers, device tokens.
|
||||||
|
Subscribing is ungated, like `process_enumerate`: what is
|
||||||
running (and dying) is not a secret between cooperating processes. Subscribers
|
running (and dying) is not a secret between cooperating processes. Subscribers
|
||||||
do not receive the exit reason — the filesystem server does not care *why*
|
do not receive the exit reason — the filesystem server does not care *why*
|
||||||
the client died.
|
the client died.
|
||||||
|
|
@ -285,6 +291,10 @@ callbacks (`on_terminate`, `on_reload`) for programs that want defaults.
|
||||||
|
|
||||||
`service` owns the `replyWait` loop and folds every event source — signals,
|
`service` owns the `replyWait` loop and folds every event source — signals,
|
||||||
child exits, protocol messages — into callbacks, with the vocabulary's defaults:
|
child exits, protocol messages — into callbacks, with the vocabulary's defaults:
|
||||||
|
it also owns the **subscriber side** of any protocol that declares events
|
||||||
|
(`service.Subscribers`: the table, the reserved `subscribe`/`unsubscribe` verbs,
|
||||||
|
the fan-out, and the sweep on a subscriber's published exit), so every event
|
||||||
|
stream in the system behaves identically.
|
||||||
`terminate` returns from the loop (clean exit), the common `ping` is answered automatically,
|
`terminate` returns from the loop (clean exit), the common `ping` is answered automatically,
|
||||||
`reload` is ignored unless overridden. One loop, no locking, nothing reentrant. A
|
`reload` is ignored unless overridden. One loop, no locking, nothing reentrant. A
|
||||||
service author writes domain logic; the lifecycle contract is satisfied by the
|
service author writes domain logic; the lifecycle contract is satisfied by the
|
||||||
|
|
|
||||||
|
|
@ -36,11 +36,11 @@ plain `main` checkout always tells the truth about where the work is.**
|
||||||
|
|
||||||
| | |
|
| | |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Working on | **P4c** — harness subscriber lift + badge-scoped client ids |
|
| Working on | **H2** — SMEP (group 4) |
|
||||||
| Branch carrying it | `feat/security-group-3` (pushed to origin) |
|
| Branch carrying it | `feat/security-group-4` (cut next) |
|
||||||
| On `main` | Phase 0, PM, H1, P1, P2, P3 — groups 1 and 2 merged |
|
| On `main` | everything through P4c — groups 1, 2 and 3 merged |
|
||||||
| Awaiting merge | P4a, P4b — land with the group 3 merge |
|
| Awaiting merge | nothing |
|
||||||
| Suite | 110 cases, all passing |
|
| Suite | 111 cases, all passing |
|
||||||
| Last updated | 2026-08-01 |
|
| Last updated | 2026-08-01 |
|
||||||
|
|
||||||
A checkbox below means the phase met its definition of green and was
|
A checkbox below means the phase met its definition of green and was
|
||||||
|
|
@ -67,8 +67,19 @@ group boundary.
|
||||||
- [x] **merge** group 2 → main, push
|
- [x] **merge** group 2 → main, push
|
||||||
- [x] **P4a** — clean protocols rebased onto `Define` (vfs, block, display, scanout, input; display's one overloaded request split per-operation and its field abuse ended, scanout's bogus 64-byte maximum deleted, directory EOF re-spelled as a nameless entry, input moved onto the service harness; new `protocol-conformance` case asks every reachable provider for `describe` and requires `-ENOSYS` for an undefined verb; suite 110/110)
|
- [x] **P4a** — clean protocols rebased onto `Define` (vfs, block, display, scanout, input; display's one overloaded request split per-operation and its field abuse ended, scanout's bogus 64-byte maximum deleted, directory EOF re-spelled as a nameless entry, input moved onto the service harness; new `protocol-conformance` case asks every reachable provider for `describe` and requires `-ENOSYS` for an undefined verb; suite 110/110)
|
||||||
- [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)
|
- [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
|
- [x] **P4c** — harness subscriber lift + badge-scoped per-client integers (the
|
||||||
- [ ] **merge** group 3 → main, push
|
subscriber table, the reserved subscribe/unsubscribe verbs, the fan-out and the
|
||||||
|
dead-subscriber sweep are `service.Subscribers` now; input, acpi and
|
||||||
|
device-manager deleted three hand-rolled variants and their three different
|
||||||
|
ideas of when a subscriber goes away, standardizing on published exit
|
||||||
|
notifications — acpi had no sweep at all and input polled the process list on
|
||||||
|
every subscribe. The three guessable-id namespaces are scoped to the opening
|
||||||
|
badge: FAT node ids on every verb that names one, xHCI device tokens on open,
|
||||||
|
control, bulk and interrupt_subscribe, display layers on configure, fill, blit,
|
||||||
|
damage and destroy — each refusing a wrong owner with the *same* answer as an id
|
||||||
|
nobody holds. New `badge-scope` case, two processes of one fixture, every
|
||||||
|
refusal paired with a control; suite 111/111)
|
||||||
|
- [x] **merge** group 3 → main, push
|
||||||
- [ ] **H2** — SMEP on every core
|
- [ ] **H2** — SMEP on every core
|
||||||
- [ ] **HS** — SYSRET canonical-RIP guard
|
- [ ] **HS** — SYSRET canonical-RIP guard
|
||||||
- [ ] **H3** — SMAP + boot-patched `clac`; `-cpu max` in the harness; negative tests
|
- [ ] **H3** — SMAP + boot-patched `clac`; `-cpu max` in the harness; negative tests
|
||||||
|
|
@ -456,6 +467,33 @@ the wire formats will want them:*
|
||||||
node id and asserts refusal; kernel-side unit test for the harness sweep.
|
node id and asserts refusal; kernel-side unit test for the harness sweep.
|
||||||
Suite 111.
|
Suite 111.
|
||||||
|
|
||||||
|
*Landed. Four things the plan had not foreseen:*
|
||||||
|
|
||||||
|
- *One sweep idiom means one more kernel subscriber per provider, and the kernel's
|
||||||
|
published-exit table held **eight**. A normal boot now fields six (fat, input,
|
||||||
|
power, device-manager, display, and one per xHCI controller), so the table grew
|
||||||
|
to sixteen. It is not a table anyone notices until a service silently loses its
|
||||||
|
sweep, which is exactly the failure the old ceiling was two subscriptions away
|
||||||
|
from.*
|
||||||
|
- *The device manager hears each of its drivers die **twice** now — it is both the
|
||||||
|
supervisor its spawn named and, through the harness, a subscriber to published
|
||||||
|
exits — and the notify ring delivers the two badges separately. Untreated, one
|
||||||
|
death counted as two: the restart backoff doubled and the crash-loop cap fired
|
||||||
|
at half the deaths it names. `onDriverExit` therefore retires the dead process
|
||||||
|
id before it decides anything, and the second notification finds nothing to act
|
||||||
|
on. (The `driver-restart` and `pci-scan` drills are what would have caught it.)*
|
||||||
|
- *Refusal-equals-absence has a corollary for the verbs that **release**: FAT's
|
||||||
|
`close` used to answer 0 for an unknown node, so scoping it had to change that
|
||||||
|
too — a foreign node and a free one both answer `-ENOENT`, or the pair would
|
||||||
|
have been an oracle for which ids are live. The same applies to the harness's
|
||||||
|
`unsubscribe`.*
|
||||||
|
- *Ownership is per **task**, not per process, because the badge is: the kernel
|
||||||
|
stamps the sending thread's id, which is already the granularity of the exit
|
||||||
|
sweep that releases the state (a worker thread's death releases the handles that
|
||||||
|
worker opened). Nothing in the tree shares an id across its own threads today;
|
||||||
|
a per-process notion would need the kernel to stamp the leader, and belongs with
|
||||||
|
P5's spawner-wired namespaces if it is ever wanted.*
|
||||||
|
|
||||||
## H2 — SMEP
|
## H2 — SMEP
|
||||||
|
|
||||||
- Generalize the cpuid helper (`apic.zig:351-365`, private, subleaf-0) to
|
- Generalize the cpuid helper (`apic.zig:351-365`, private, subleaf-0) to
|
||||||
|
|
|
||||||
|
|
@ -17,10 +17,15 @@ pub fn build(b: *std.Build) void {
|
||||||
// (docs/os-development/protocol-namespace.md).
|
// (docs/os-development/protocol-namespace.md).
|
||||||
const channel = kernel.module("channel");
|
const channel = kernel.module("channel");
|
||||||
|
|
||||||
|
// A client frames its own packets, so it needs the envelope alongside the
|
||||||
|
// protocol whose verbs it speaks.
|
||||||
|
const envelope = protocol.module("envelope");
|
||||||
|
|
||||||
_ = b.addModule("display-client", .{
|
_ = b.addModule("display-client", .{
|
||||||
.root_source_file = b.path("display/display-client.zig"),
|
.root_source_file = b.path("display/display-client.zig"),
|
||||||
.imports = &.{
|
.imports = &.{
|
||||||
.{ .name = "channel", .module = channel },
|
.{ .name = "channel", .module = channel },
|
||||||
|
.{ .name = "envelope", .module = envelope },
|
||||||
.{ .name = "ipc", .module = ipc },
|
.{ .name = "ipc", .module = ipc },
|
||||||
.{ .name = "time", .module = time },
|
.{ .name = "time", .module = time },
|
||||||
.{ .name = "display-protocol", .module = protocol.module("display-protocol") },
|
.{ .name = "display-protocol", .module = protocol.module("display-protocol") },
|
||||||
|
|
@ -30,6 +35,7 @@ pub fn build(b: *std.Build) void {
|
||||||
.root_source_file = b.path("input/input-client.zig"),
|
.root_source_file = b.path("input/input-client.zig"),
|
||||||
.imports = &.{
|
.imports = &.{
|
||||||
.{ .name = "channel", .module = channel },
|
.{ .name = "channel", .module = channel },
|
||||||
|
.{ .name = "envelope", .module = envelope },
|
||||||
.{ .name = "ipc", .module = ipc },
|
.{ .name = "ipc", .module = ipc },
|
||||||
.{ .name = "time", .module = time },
|
.{ .name = "time", .module = time },
|
||||||
.{ .name = "input-protocol", .module = protocol.module("input-protocol") },
|
.{ .name = "input-protocol", .module = protocol.module("input-protocol") },
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,13 @@
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const channel = @import("channel");
|
const channel = @import("channel");
|
||||||
|
const envelope = @import("envelope");
|
||||||
const ipc = @import("ipc");
|
const ipc = @import("ipc");
|
||||||
const time = @import("time");
|
const time = @import("time");
|
||||||
const display_protocol = @import("display-protocol");
|
const display_protocol = @import("display-protocol");
|
||||||
|
|
||||||
|
const Protocol = display_protocol.Protocol;
|
||||||
|
|
||||||
/// The display's current mode, as `info()` reports it.
|
/// The display's current mode, as `info()` reports it.
|
||||||
pub const Info = struct {
|
pub const Info = struct {
|
||||||
width: u32,
|
width: u32,
|
||||||
|
|
@ -35,29 +38,45 @@ fn service() ?ipc.Handle {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send one request, receive its reply; true on a zero status. `out` receives the reply
|
/// A reply the compositor answered with, kept whole so the caller can decode the
|
||||||
/// so callers can read `info`/`layer` fields on success.
|
/// verb's own fixed part out of it.
|
||||||
fn transact(request: display_protocol.Request, out: *display_protocol.Reply) bool {
|
const Answered = struct {
|
||||||
const h = service() orelse return false;
|
packet: [display_protocol.message_maximum]u8,
|
||||||
var req = request;
|
len: usize,
|
||||||
var reply: [display_protocol.reply_size]u8 = undefined;
|
|
||||||
const len = ipc.call(h, std.mem.asBytes(&req), &reply) catch return false;
|
fn bytes(self: *const Answered) []const u8 {
|
||||||
if (len < display_protocol.reply_size) return false;
|
return self.packet[0..self.len];
|
||||||
out.* = std.mem.bytesToValue(display_protocol.Reply, reply[0..display_protocol.reply_size]);
|
}
|
||||||
return out.status == 0;
|
};
|
||||||
|
|
||||||
|
/// Send one request (`target` addresses a layer, or 0 for the compositor itself)
|
||||||
|
/// and keep the reply. Null when the transport failed or the compositor refused.
|
||||||
|
fn transact(
|
||||||
|
comptime operation: Protocol.Operation,
|
||||||
|
target: u64,
|
||||||
|
request: Protocol.RequestOf(operation),
|
||||||
|
tail: []const u8,
|
||||||
|
) ?Answered {
|
||||||
|
const h = service() orelse return null;
|
||||||
|
var packet: [display_protocol.message_maximum]u8 = undefined;
|
||||||
|
const framed = Protocol.encodeRequest(operation, target, request, tail, &packet) orelse return null;
|
||||||
|
var answered: Answered = .{ .packet = undefined, .len = 0 };
|
||||||
|
answered.len = ipc.call(h, framed, &answered.packet) catch return null;
|
||||||
|
const status = envelope.statusOf(answered.bytes()) orelse return null;
|
||||||
|
if (status.status != 0) return null;
|
||||||
|
return answered;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The display's current mode, or null if the service never came up.
|
/// The display's current mode, or null if the service never came up.
|
||||||
pub fn info() ?Info {
|
pub fn info() ?Info {
|
||||||
var reply: display_protocol.Reply = undefined;
|
const answered = transact(.info, 0, {}, &.{}) orelse return null;
|
||||||
if (!transact(.{ .operation = @intFromEnum(display_protocol.Operation.info) }, &reply)) return null;
|
const reply = Protocol.decodeReply(.info, answered.bytes()) orelse return null;
|
||||||
return .{ .width = reply.width, .height = reply.height, .pitch = reply.pitch, .format = reply.format };
|
return .{ .width = reply.width, .height = reply.height, .pitch = reply.pitch, .format = reply.format };
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Composite the dirty layers and flush the frame to the screen.
|
/// Composite the dirty layers and flush the frame to the screen.
|
||||||
pub fn present() bool {
|
pub fn present() bool {
|
||||||
var reply: display_protocol.Reply = undefined;
|
return transact(.present, 0, {}, &.{}) != null;
|
||||||
return transact(.{ .operation = @intFromEnum(display_protocol.Operation.present) }, &reply);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One selectable display mode.
|
/// One selectable display mode.
|
||||||
|
|
@ -66,23 +85,17 @@ pub const Mode = display_protocol.Mode;
|
||||||
/// Fill `out` with the resolutions the display can switch to; returns how many were written
|
/// Fill `out` with the resolutions the display can switch to; returns how many were written
|
||||||
/// (zero on the GOP floor, or if the service never came up).
|
/// (zero on the GOP floor, or if the service never came up).
|
||||||
pub fn modes(out: []Mode) usize {
|
pub fn modes(out: []Mode) usize {
|
||||||
const h = service() orelse return 0;
|
const answered = transact(.get_modes, 0, {}, &.{}) orelse return 0;
|
||||||
var request = display_protocol.Request{ .operation = @intFromEnum(display_protocol.Operation.get_modes) };
|
const offered = Protocol.decodeReply(.get_modes, answered.bytes()) orelse return 0;
|
||||||
var reply: [display_protocol.modes_reply_size]u8 = undefined;
|
const count = @min(@min(offered.count, display_protocol.max_modes), out.len);
|
||||||
const len = ipc.call(h, std.mem.asBytes(&request), &reply) catch return 0;
|
for (0..count) |i| out[i] = offered.modes[i];
|
||||||
if (len < display_protocol.modes_reply_size) return 0;
|
|
||||||
const answer = std.mem.bytesToValue(display_protocol.ModesReply, reply[0..display_protocol.modes_reply_size]);
|
|
||||||
if (answer.status != 0) return 0;
|
|
||||||
const count = @min(@min(answer.count, display_protocol.max_modes), out.len);
|
|
||||||
for (0..count) |i| out[i] = answer.modes[i];
|
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Change the display resolution. Only a native backend that supports mode-setting honours it
|
/// Change the display resolution. Only a native backend that supports mode-setting honours it
|
||||||
/// (on the GOP floor it returns false); on success the display's `info()` reports the new mode.
|
/// (on the GOP floor it returns false); on success the display's `info()` reports the new mode.
|
||||||
pub fn setMode(width: u32, height: u32) bool {
|
pub fn setMode(width: u32, height: u32) bool {
|
||||||
var reply: display_protocol.Reply = undefined;
|
const changed = transact(.set_mode, 0, .{ .width = width, .height = height }, &.{}) != null;
|
||||||
const changed = transact(.{ .operation = @intFromEnum(display_protocol.Operation.set_mode), .width = width, .height = height }, &reply);
|
|
||||||
if (changed) mode = null; // the cached mode is stale now
|
if (changed) mode = null; // the cached mode is stale now
|
||||||
return changed;
|
return changed;
|
||||||
}
|
}
|
||||||
|
|
@ -107,93 +120,46 @@ pub fn color(r: u8, g: u8, b: u8) u32 {
|
||||||
/// A handle to a server-owned layer: a positioned, z-ordered surface the client draws
|
/// A handle to a server-owned layer: a positioned, z-ordered surface the client draws
|
||||||
/// into by command. Create with `createLayer`; drawing and moves take effect on the next
|
/// into by command. Create with `createLayer`; drawing and moves take effect on the next
|
||||||
/// `present`. Coordinates are signed (a layer may sit partly off-screen).
|
/// `present`. Coordinates are signed (a layer may sit partly off-screen).
|
||||||
|
///
|
||||||
|
/// The id is the packet header's `target` on every call below, so it is named once
|
||||||
|
/// per request rather than repeated inside one.
|
||||||
pub const Layer = struct {
|
pub const Layer = struct {
|
||||||
id: u32,
|
id: u32,
|
||||||
|
|
||||||
/// Fill a rectangle of this layer (layer-local coordinates) with a native `colour`.
|
/// Fill a rectangle of this layer (layer-local coordinates) with a native `colour`.
|
||||||
pub fn fill(self: Layer, x: i32, y: i32, w: u32, h: u32, colour: u32) bool {
|
pub fn fill(self: Layer, x: i32, y: i32, w: u32, h: u32, colour: u32) bool {
|
||||||
var reply: display_protocol.Reply = undefined;
|
return transact(.fill_rect, self.id, .{ .x = x, .y = y, .width = w, .height = h, .colour = colour }, &.{}) != null;
|
||||||
return transact(.{
|
|
||||||
.operation = @intFromEnum(display_protocol.Operation.fill_rect),
|
|
||||||
.layer = self.id,
|
|
||||||
.x = @bitCast(x),
|
|
||||||
.y = @bitCast(y),
|
|
||||||
.width = w,
|
|
||||||
.height = h,
|
|
||||||
.colour = colour,
|
|
||||||
}, &reply);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Copy a `w`×`h` tile of native pixels (row-major, little-endian bytes) into this
|
/// Copy a `w`×`h` tile of native pixels (row-major, little-endian bytes) into this
|
||||||
/// layer at (`x`, `y`). The tile rides inline in the request, so `w*h*4` must fit
|
/// layer at (`x`, `y`). The tile rides inline as the request's tail, so `w*h*4` must
|
||||||
/// `display_protocol.maximum_payload`.
|
/// fit `display_protocol.maximum_payload` — the bound the protocol derives from this
|
||||||
|
/// verb's own fixed part, so the check here can never drift from what fits.
|
||||||
pub fn blitTile(self: Layer, x: i32, y: i32, w: u32, h: u32, pixels: []const u8) bool {
|
pub fn blitTile(self: Layer, x: i32, y: i32, w: u32, h: u32, pixels: []const u8) bool {
|
||||||
var request = display_protocol.Request{
|
if (pixels.len > display_protocol.maximum_payload) return false;
|
||||||
.operation = @intFromEnum(display_protocol.Operation.blit_tile),
|
return transact(.blit_tile, self.id, .{ .x = x, .y = y, .width = w, .height = h }, pixels) != null;
|
||||||
.layer = self.id,
|
|
||||||
.x = @bitCast(x),
|
|
||||||
.y = @bitCast(y),
|
|
||||||
.width = w,
|
|
||||||
.height = h,
|
|
||||||
};
|
|
||||||
const header = std.mem.asBytes(&request);
|
|
||||||
if (header.len + pixels.len > display_protocol.message_maximum) return false;
|
|
||||||
var buffer: [display_protocol.message_maximum]u8 = undefined;
|
|
||||||
@memcpy(buffer[0..header.len], header);
|
|
||||||
@memcpy(buffer[header.len..][0..pixels.len], pixels);
|
|
||||||
const h_svc = service() orelse return false;
|
|
||||||
var reply: [display_protocol.reply_size]u8 = undefined;
|
|
||||||
const len = ipc.call(h_svc, buffer[0 .. header.len + pixels.len], &reply) catch return false;
|
|
||||||
if (len < display_protocol.reply_size) return false;
|
|
||||||
return std.mem.bytesToValue(display_protocol.Reply, reply[0..display_protocol.reply_size]).status == 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Move / restack / show or hide the layer.
|
/// Move / restack / show or hide the layer.
|
||||||
pub fn configure(self: Layer, x: i32, y: i32, z: u32, visible: bool) bool {
|
pub fn configure(self: Layer, x: i32, y: i32, z: u32, visible: bool) bool {
|
||||||
var reply: display_protocol.Reply = undefined;
|
return transact(.configure_layer, self.id, .{ .x = x, .y = y, .z = z, .visible = if (visible) 1 else 0 }, &.{}) != null;
|
||||||
return transact(.{
|
|
||||||
.operation = @intFromEnum(display_protocol.Operation.configure_layer),
|
|
||||||
.layer = self.id,
|
|
||||||
.x = @bitCast(x),
|
|
||||||
.y = @bitCast(y),
|
|
||||||
.z = z,
|
|
||||||
.visible = if (visible) 1 else 0,
|
|
||||||
}, &reply);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Mark a rectangle of this layer (layer-local) dirty for the next present — for when
|
/// Mark a rectangle of this layer (layer-local) dirty for the next present — for when
|
||||||
/// the layer's pixels changed without a drawing call the compositor already tracked.
|
/// the layer's pixels changed without a drawing call the compositor already tracked.
|
||||||
pub fn damage(self: Layer, x: i32, y: i32, w: u32, h: u32) bool {
|
pub fn damage(self: Layer, x: i32, y: i32, w: u32, h: u32) bool {
|
||||||
var reply: display_protocol.Reply = undefined;
|
return transact(.damage, self.id, .{ .x = x, .y = y, .width = w, .height = h }, &.{}) != null;
|
||||||
return transact(.{
|
|
||||||
.operation = @intFromEnum(display_protocol.Operation.damage),
|
|
||||||
.layer = self.id,
|
|
||||||
.x = @bitCast(x),
|
|
||||||
.y = @bitCast(y),
|
|
||||||
.width = w,
|
|
||||||
.height = h,
|
|
||||||
}, &reply);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Release the layer and its surface.
|
/// Release the layer and its surface.
|
||||||
pub fn destroy(self: Layer) bool {
|
pub fn destroy(self: Layer) bool {
|
||||||
var reply: display_protocol.Reply = undefined;
|
return transact(.destroy_layer, self.id, {}, &.{}) != null;
|
||||||
return transact(.{ .operation = @intFromEnum(display_protocol.Operation.destroy_layer), .layer = self.id }, &reply);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Create a server-owned layer of `w`×`h` pixels at screen (`x`, `y`) with stacking order
|
/// Create a server-owned layer of `w`×`h` pixels at screen (`x`, `y`) with stacking order
|
||||||
/// `z` (higher is nearer the front), initially visible. Returns a handle, or null.
|
/// `z` (higher is nearer the front), initially visible. Returns a handle, or null.
|
||||||
pub fn createLayer(x: i32, y: i32, w: u32, h: u32, z: u32) ?Layer {
|
pub fn createLayer(x: i32, y: i32, w: u32, h: u32, z: u32) ?Layer {
|
||||||
var reply: display_protocol.Reply = undefined;
|
const answered = transact(.create_layer, 0, .{ .x = x, .y = y, .width = w, .height = h, .z = z, .visible = 1 }, &.{}) orelse return null;
|
||||||
if (!transact(.{
|
return .{ .id = (Protocol.decodeReply(.create_layer, answered.bytes()) orelse return null).layer };
|
||||||
.operation = @intFromEnum(display_protocol.Operation.create_layer),
|
|
||||||
.x = @bitCast(x),
|
|
||||||
.y = @bitCast(y),
|
|
||||||
.width = w,
|
|
||||||
.height = h,
|
|
||||||
.z = z,
|
|
||||||
.visible = 1,
|
|
||||||
}, &reply)) return null;
|
|
||||||
return .{ .id = reply.layer };
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,12 +21,14 @@
|
||||||
//! if (event.asKeyboard()) |k| { ... } else if (event.asMouse()) |m| { ... }
|
//! if (event.asKeyboard()) |k| { ... } else if (event.asMouse()) |m| { ... }
|
||||||
//! }
|
//! }
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
const channel = @import("channel");
|
const channel = @import("channel");
|
||||||
|
const envelope = @import("envelope");
|
||||||
const ipc = @import("ipc");
|
const ipc = @import("ipc");
|
||||||
const time = @import("time");
|
const time = @import("time");
|
||||||
const input_protocol = @import("input-protocol");
|
const input_protocol = @import("input-protocol");
|
||||||
|
|
||||||
|
const Protocol = input_protocol.Protocol;
|
||||||
|
|
||||||
pub const DeviceKind = input_protocol.DeviceKind;
|
pub const DeviceKind = input_protocol.DeviceKind;
|
||||||
pub const InputEvent = input_protocol.InputEvent;
|
pub const InputEvent = input_protocol.InputEvent;
|
||||||
pub const KeyEvent = input_protocol.KeyEvent;
|
pub const KeyEvent = input_protocol.KeyEvent;
|
||||||
|
|
@ -66,31 +68,44 @@ pub const Subscriber = struct {
|
||||||
/// The endpoint the service delivers events to (created and owned by us; its handle
|
/// The endpoint the service delivers events to (created and owned by us; its handle
|
||||||
/// was handed to the service as a capability at subscribe time).
|
/// was handed to the service as a capability at subscribe time).
|
||||||
endpoint: ipc.Handle,
|
endpoint: ipc.Handle,
|
||||||
receive: [input_protocol.event_size]u8 = undefined,
|
/// A pushed packet is the folded header plus one typed event, so the buffer is
|
||||||
|
/// the push floor rather than any one event's size.
|
||||||
|
receive: [envelope.post_maximum]u8 = undefined,
|
||||||
|
|
||||||
/// Block until the next event is pushed, and return it. Events arrive as asynchronous
|
/// Block until the next event is pushed, and return it. Events arrive as asynchronous
|
||||||
/// buffered messages (`ipc_send` from the service), so nothing is owed in reply — the
|
/// buffered messages (`ipc_send` from the service), so nothing is owed in reply — the
|
||||||
/// empty reply this issues is a harmless no-op. Returns null for any non-event wake-up
|
/// empty reply this issues is a harmless no-op. Returns null for any non-event wake-up
|
||||||
/// (there should be none), so callers can loop.
|
/// (there should be none), so callers can loop.
|
||||||
|
///
|
||||||
|
/// The device class is the packet's operation, so it is read from the header and
|
||||||
|
/// re-tagged into an `InputEvent` here — one decoded type for a caller that took
|
||||||
|
/// several classes on one stream.
|
||||||
pub fn next(self: *Subscriber) ?InputEvent {
|
pub fn next(self: *Subscriber) ?InputEvent {
|
||||||
const got = ipc.replyWait(self.endpoint, &.{}, &self.receive, null);
|
const got = ipc.replyWait(self.endpoint, &.{}, &self.receive, null);
|
||||||
if (!got.isMessage() or got.len < input_protocol.event_size) return null;
|
if (!got.isMessage()) return null;
|
||||||
return std.mem.bytesToValue(InputEvent, self.receive[0..input_protocol.event_size]);
|
const packet = self.receive[0..@min(got.len, self.receive.len)];
|
||||||
|
return switch (Protocol.eventOf(packet) orelse return null) {
|
||||||
|
.keyboard => InputEvent.fromKeyboard(Protocol.decodeEvent(.keyboard, packet) orelse return null),
|
||||||
|
.mouse => InputEvent.fromMouse(Protocol.decodeEvent(.mouse, packet) orelse return null),
|
||||||
|
.joystick => InputEvent.fromJoystick(Protocol.decodeEvent(.joystick, packet) orelse return null),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Subscribe to the input classes named in `device_mask` (an OR of `device_*`, or
|
/// Subscribe to the input classes named in `device_mask` (an OR of `device_*`, or
|
||||||
/// `device_all`). Creates an endpoint for the service to push to and hands it over as a
|
/// `device_all`). Creates an endpoint for the service to push to and hands it over as a
|
||||||
/// capability. Returns a `Subscriber` to loop `next` on, or null on failure.
|
/// capability — the envelope's reserved `subscribe`, whose shape this is exactly. Returns
|
||||||
|
/// a `Subscriber` to loop `next` on, or null on failure.
|
||||||
pub fn subscribe(device_mask: u32) ?Subscriber {
|
pub fn subscribe(device_mask: u32) ?Subscriber {
|
||||||
const service = lookupService() orelse return null;
|
const service = lookupService() orelse return null;
|
||||||
const endpoint = ipc.createIpcEndpoint() orelse return null;
|
const endpoint = ipc.createIpcEndpoint() orelse return null;
|
||||||
|
|
||||||
var request = input_protocol.Request{ .operation = @intFromEnum(input_protocol.Operation.subscribe), .device_mask = device_mask };
|
var packet: [input_protocol.message_maximum]u8 = undefined;
|
||||||
var reply: [input_protocol.reply_size]u8 = undefined;
|
const framed = input_protocol.encodeSubscribe(device_mask, &packet) orelse return null;
|
||||||
const result = ipc.callCap(service, std.mem.asBytes(&request), &reply, endpoint) catch return null;
|
var reply: [input_protocol.message_maximum]u8 = undefined;
|
||||||
if (result.len < input_protocol.reply_size) return null;
|
const result = ipc.callCap(service, framed, &reply, endpoint) catch return null;
|
||||||
if (std.mem.bytesToValue(input_protocol.Reply, reply[0..input_protocol.reply_size]).status != 0) return null;
|
const status = envelope.statusOf(reply[0..result.len]) orelse return null;
|
||||||
|
if (status.status != 0) return null;
|
||||||
return .{ .endpoint = endpoint };
|
return .{ .endpoint = endpoint };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -149,11 +164,12 @@ pub const Publisher = struct {
|
||||||
service: ipc.Handle,
|
service: ipc.Handle,
|
||||||
|
|
||||||
fn publish(self: Publisher, event: InputEvent) bool {
|
fn publish(self: Publisher, event: InputEvent) bool {
|
||||||
var request = input_protocol.Request{ .operation = @intFromEnum(input_protocol.Operation.publish), .event = event };
|
var packet: [input_protocol.message_maximum]u8 = undefined;
|
||||||
var reply: [input_protocol.reply_size]u8 = undefined;
|
const framed = Protocol.encodeRequest(.publish, 0, event, &.{}, &packet) orelse return false;
|
||||||
const len = ipc.call(self.service, std.mem.asBytes(&request), &reply) catch return false;
|
var reply: [input_protocol.message_maximum]u8 = undefined;
|
||||||
if (len < input_protocol.reply_size) return false;
|
const len = ipc.call(self.service, framed, &reply) catch return false;
|
||||||
return std.mem.bytesToValue(input_protocol.Reply, reply[0..input_protocol.reply_size]).status == 0;
|
const status = envelope.statusOf(reply[0..len]) orelse return false;
|
||||||
|
return status.status == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Broadcast a keyboard event to every subscriber that took keyboard events.
|
/// Broadcast a keyboard event to every subscriber that took keyboard events.
|
||||||
|
|
|
||||||
|
|
@ -7,12 +7,14 @@
|
||||||
//! `runtime.dma.alloc`), so whole sectors move without crossing the IPC size
|
//! `runtime.dma.alloc`), so whole sectors move without crossing the IPC size
|
||||||
//! limit — the same handoff usb-storage uses toward the controller.
|
//! limit — the same handoff usb-storage uses toward the controller.
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
const channel = @import("channel");
|
const channel = @import("channel");
|
||||||
|
const envelope = @import("envelope");
|
||||||
const ipc = @import("ipc");
|
const ipc = @import("ipc");
|
||||||
const time = @import("time");
|
const time = @import("time");
|
||||||
const block_protocol = @import("block-protocol");
|
const block_protocol = @import("block-protocol");
|
||||||
|
|
||||||
|
const Protocol = block_protocol.Protocol;
|
||||||
|
|
||||||
pub const Geometry = struct { block_size: u32, block_count: u64 };
|
pub const Geometry = struct { block_size: u32, block_count: u64 };
|
||||||
|
|
||||||
pub const Device = struct {
|
pub const Device = struct {
|
||||||
|
|
@ -20,12 +22,9 @@ pub const Device = struct {
|
||||||
|
|
||||||
/// The device's block size and total block count.
|
/// The device's block size and total block count.
|
||||||
pub fn geometry(self: Device) ?Geometry {
|
pub fn geometry(self: Device) ?Geometry {
|
||||||
var request = block_protocol.Request{ .operation = @intFromEnum(block_protocol.Operation.geometry), .lba = 0, .count = 0, .physical = 0 };
|
var reply: [block_protocol.message_maximum]u8 = undefined;
|
||||||
var reply: [block_protocol.reply_size]u8 = undefined;
|
const answered = self.call(.geometry, {}, null, &reply) orelse return null;
|
||||||
const n = ipc.call(self.endpoint, std.mem.asBytes(&request), &reply) catch return null;
|
const result = Protocol.decodeReply(.geometry, answered) orelse return null;
|
||||||
if (n < block_protocol.reply_size) return null;
|
|
||||||
const result = std.mem.bytesToValue(block_protocol.Reply, reply[0..block_protocol.reply_size]);
|
|
||||||
if (result.status != 0) return null;
|
|
||||||
return .{ .block_size = result.block_size, .block_count = result.block_count };
|
return .{ .block_size = result.block_size, .block_count = result.block_count };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -34,36 +33,45 @@ pub const Device = struct {
|
||||||
/// addresses become reachable by the device. Call once per buffer before naming it
|
/// addresses become reachable by the device. Call once per buffer before naming it
|
||||||
/// in `read`/`write`. Harmless success when no IOMMU is enforcing.
|
/// in `read`/`write`. Harmless success when no IOMMU is enforcing.
|
||||||
pub fn attach(self: Device, handle: ipc.Handle) bool {
|
pub fn attach(self: Device, handle: ipc.Handle) bool {
|
||||||
var request = block_protocol.Request{ .operation = @intFromEnum(block_protocol.Operation.attach), .lba = 0, .count = 0, .physical = 0 };
|
var reply: [block_protocol.message_maximum]u8 = undefined;
|
||||||
var reply: [block_protocol.reply_size]u8 = undefined;
|
return self.call(.attach, {}, handle, &reply) != null;
|
||||||
const result = ipc.callCap(self.endpoint, std.mem.asBytes(&request), &reply, handle) catch return false;
|
|
||||||
if (result.len < block_protocol.reply_size) return false;
|
|
||||||
return std.mem.bytesToValue(block_protocol.Reply, reply[0..block_protocol.reply_size]).status == 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Read `count` blocks starting at `lba` into the DMA buffer at `physical`.
|
/// Read `count` blocks starting at `lba` into the DMA buffer at `physical`.
|
||||||
pub fn read(self: Device, lba: u64, count: u32, physical: u64) bool {
|
pub fn read(self: Device, lba: u64, count: u32, physical: u64) bool {
|
||||||
return self.transfer(.read, lba, count, physical);
|
var reply: [block_protocol.message_maximum]u8 = undefined;
|
||||||
|
return self.call(.read, .{ .lba = lba, .count = count, .physical = physical }, null, &reply) != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Write `count` blocks starting at `lba` from the DMA buffer at `physical`.
|
/// Write `count` blocks starting at `lba` from the DMA buffer at `physical`.
|
||||||
pub fn write(self: Device, lba: u64, count: u32, physical: u64) bool {
|
pub fn write(self: Device, lba: u64, count: u32, physical: u64) bool {
|
||||||
return self.transfer(.write, lba, count, physical);
|
var reply: [block_protocol.message_maximum]u8 = undefined;
|
||||||
|
return self.call(.write, .{ .lba = lba, .count = count, .physical = physical }, null, &reply) != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Commit any device write cache to stable media (SCSI SYNCHRONIZE CACHE), so
|
/// Commit any device write cache to stable media (SCSI SYNCHRONIZE CACHE), so
|
||||||
/// prior writes survive a power-off. A filesystem calls this before the machine
|
/// prior writes survive a power-off. A filesystem calls this before the machine
|
||||||
/// goes down; no data transfer, so the buffer arguments are unused.
|
/// goes down; no data transfer, so the buffer arguments are unused.
|
||||||
pub fn flush(self: Device) bool {
|
pub fn flush(self: Device) bool {
|
||||||
return self.transfer(.flush, 0, 0, 0);
|
var reply: [block_protocol.message_maximum]u8 = undefined;
|
||||||
|
return self.call(.flush, {}, null, &reply) != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn transfer(self: Device, operation: block_protocol.Operation, lba: u64, count: u32, physical: u64) bool {
|
/// One request at the driver. `target` is always 0: one endpoint per device, so
|
||||||
var request = block_protocol.Request{ .operation = @intFromEnum(operation), .lba = lba, .count = count, .physical = physical };
|
/// there is no object within the peer to address.
|
||||||
var reply: [block_protocol.reply_size]u8 = undefined;
|
fn call(
|
||||||
const n = ipc.call(self.endpoint, std.mem.asBytes(&request), &reply) catch return false;
|
self: Device,
|
||||||
if (n < block_protocol.reply_size) return false;
|
comptime operation: Protocol.Operation,
|
||||||
return std.mem.bytesToValue(block_protocol.Reply, reply[0..block_protocol.reply_size]).status == 0;
|
request: Protocol.RequestOf(operation),
|
||||||
|
capability: ?ipc.Handle,
|
||||||
|
reply: []u8,
|
||||||
|
) ?[]u8 {
|
||||||
|
var packet: [block_protocol.message_maximum]u8 = undefined;
|
||||||
|
const framed = Protocol.encodeRequest(operation, 0, request, &.{}, &packet) orelse return null;
|
||||||
|
const answer = ipc.callCap(self.endpoint, 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];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,7 @@ pub fn build(b: *std.Build) void {
|
||||||
.{ .name = "abi", .module = abi },
|
.{ .name = "abi", .module = abi },
|
||||||
.{ .name = "channel", .module = channel },
|
.{ .name = "channel", .module = channel },
|
||||||
.{ .name = "device-abi", .module = device_abi },
|
.{ .name = "device-abi", .module = device_abi },
|
||||||
|
.{ .name = "envelope", .module = protocol.module("envelope") },
|
||||||
.{ .name = "system-call", .module = system_call },
|
.{ .name = "system-call", .module = system_call },
|
||||||
.{ .name = "ipc", .module = ipc },
|
.{ .name = "ipc", .module = ipc },
|
||||||
.{ .name = "time", .module = time },
|
.{ .name = "time", .module = time },
|
||||||
|
|
@ -87,6 +88,7 @@ pub fn build(b: *std.Build) void {
|
||||||
.root_source_file = b.path("usb/usb.zig"),
|
.root_source_file = b.path("usb/usb.zig"),
|
||||||
.imports = &.{
|
.imports = &.{
|
||||||
.{ .name = "channel", .module = channel },
|
.{ .name = "channel", .module = channel },
|
||||||
|
.{ .name = "envelope", .module = protocol.module("envelope") },
|
||||||
.{ .name = "ipc", .module = ipc },
|
.{ .name = "ipc", .module = ipc },
|
||||||
.{ .name = "time", .module = time },
|
.{ .name = "time", .module = time },
|
||||||
.{ .name = "usb-transfer-protocol", .module = protocol.module("usb-transfer-protocol") },
|
.{ .name = "usb-transfer-protocol", .module = protocol.module("usb-transfer-protocol") },
|
||||||
|
|
@ -99,6 +101,7 @@ pub fn build(b: *std.Build) void {
|
||||||
.root_source_file = b.path("block/block.zig"),
|
.root_source_file = b.path("block/block.zig"),
|
||||||
.imports = &.{
|
.imports = &.{
|
||||||
.{ .name = "channel", .module = channel },
|
.{ .name = "channel", .module = channel },
|
||||||
|
.{ .name = "envelope", .module = protocol.module("envelope") },
|
||||||
.{ .name = "ipc", .module = ipc },
|
.{ .name = "ipc", .module = ipc },
|
||||||
.{ .name = "time", .module = time },
|
.{ .name = "time", .module = time },
|
||||||
.{ .name = "block-protocol", .module = protocol.module("block-protocol") },
|
.{ .name = "block-protocol", .module = protocol.module("block-protocol") },
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ const abi = @import("abi");
|
||||||
const device_abi = @import("device-abi");
|
const device_abi = @import("device-abi");
|
||||||
const sc = @import("system-call");
|
const sc = @import("system-call");
|
||||||
const channel = @import("channel");
|
const channel = @import("channel");
|
||||||
|
const envelope = @import("envelope");
|
||||||
const ipc = @import("ipc");
|
const ipc = @import("ipc");
|
||||||
const time = @import("time");
|
const time = @import("time");
|
||||||
const device_manager_protocol = @import("device-manager-protocol");
|
const device_manager_protocol = @import("device-manager-protocol");
|
||||||
|
|
@ -168,6 +169,9 @@ const lookup_pause_ms: u64 = 20;
|
||||||
/// (best-effort standalone bring-up) or it refused the handshake. Bus drivers keep the handle
|
/// (best-effort standalone bring-up) or it refused the handshake. Bus drivers keep the handle
|
||||||
/// to report children through; a driver that runs fine unsupervised discards it with `_ =`,
|
/// to report children through; a driver that runs fine unsupervised discards it with `_ =`,
|
||||||
/// and one that requires supervision bails on null. Logs the outcome itself.
|
/// and one that requires supervision bails on null. Logs the outcome itself.
|
||||||
|
///
|
||||||
|
/// The device this driver was assigned is the packet's `Header.target` — the manager's
|
||||||
|
/// object addressing, so `no_device` here is a driver that serves none.
|
||||||
pub fn hello(role: Role, device_id: u64) ?ipc.Handle {
|
pub fn hello(role: Role, device_id: u64) ?ipc.Handle {
|
||||||
var attempts: u32 = 0;
|
var attempts: u32 = 0;
|
||||||
const manager = while (attempts < lookup_attempts) : (attempts += 1) {
|
const manager = while (attempts < lookup_attempts) : (attempts += 1) {
|
||||||
|
|
@ -178,15 +182,25 @@ pub fn hello(role: Role, device_id: u64) ?ipc.Handle {
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const message = device_manager_protocol.Hello{ .role = @intFromEnum(role), .device_id = device_id };
|
var packet: [device_manager_protocol.message_maximum]u8 = undefined;
|
||||||
var reply: [device_manager_protocol.reply_size]u8 = undefined;
|
const framed = device_manager_protocol.Protocol.encodeRequest(
|
||||||
const length = ipc.call(manager, std.mem.asBytes(&message), &reply) catch {
|
.hello,
|
||||||
|
device_id,
|
||||||
|
.{ .role = @intFromEnum(role) },
|
||||||
|
&.{},
|
||||||
|
&packet,
|
||||||
|
) orelse return null;
|
||||||
|
|
||||||
|
var reply: [device_manager_protocol.message_maximum]u8 = undefined;
|
||||||
|
const length = ipc.call(manager, framed, &reply) catch {
|
||||||
std.log.info("hello call failed", .{});
|
std.log.info("hello call failed", .{});
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
if (length < device_manager_protocol.reply_size or
|
const status = envelope.statusOf(reply[0..length]) orelse {
|
||||||
std.mem.bytesToValue(device_manager_protocol.HelloReply, reply[0..device_manager_protocol.reply_size]).status != 0)
|
std.log.info("hello answered nothing readable", .{});
|
||||||
{
|
return null;
|
||||||
|
};
|
||||||
|
if (status.status != 0) {
|
||||||
std.log.info("hello refused", .{});
|
std.log.info("hello refused", .{});
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,16 +11,24 @@
|
||||||
//! _ = device.subscribeInterrupt(address, length); // reports arrive asynchronously
|
//! _ = device.subscribeInterrupt(address, length); // reports arrive asynchronously
|
||||||
//! while (true) { ... ipc.replyWait(device.endpoint, ...) ... } // its own loop
|
//! while (true) { ... ipc.replyWait(device.endpoint, ...) ... } // its own loop
|
||||||
//!
|
//!
|
||||||
//! Reports are delivered to `device.endpoint` as asynchronous `InterruptReport`
|
//! Reports are delivered to `device.endpoint` as asynchronous `interrupt_report`
|
||||||
//! messages (the class driver runs a bare `replyWait` loop to read them, because
|
//! event packets, decoded with `reportOf` (the class driver runs a bare `replyWait`
|
||||||
//! the service harness drops buffered-message payloads — see service.zig).
|
//! loop to read them, because the service harness drops buffered-message payloads
|
||||||
|
//! — see service.zig).
|
||||||
|
//!
|
||||||
|
//! Every packet this file lays down is an envelope packet: the verb and the
|
||||||
|
//! device token in the folded `Header`, the transfer's own fields after it, and
|
||||||
|
//! a control transfer's data stage in the tail.
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const channel = @import("channel");
|
const channel = @import("channel");
|
||||||
|
const envelope = @import("envelope");
|
||||||
const ipc = @import("ipc");
|
const ipc = @import("ipc");
|
||||||
const time = @import("time");
|
const time = @import("time");
|
||||||
const usb_transfer_protocol = @import("usb-transfer-protocol");
|
const usb_transfer_protocol = @import("usb-transfer-protocol");
|
||||||
|
|
||||||
|
const Protocol = usb_transfer_protocol.Protocol;
|
||||||
|
|
||||||
/// The USB chapter-9 wire ABI and the class taxonomy, re-exported so a class driver reaches
|
/// The USB chapter-9 wire ABI and the class taxonomy, re-exported so a class driver reaches
|
||||||
/// the whole USB domain through its one `usb` import (`usb.abi.getDescriptor`, `usb.ids.Class`).
|
/// the whole USB domain through its one `usb` import (`usb.abi.getDescriptor`, `usb.ids.Class`).
|
||||||
pub const abi = @import("usb-abi");
|
pub const abi = @import("usb-abi");
|
||||||
|
|
@ -57,21 +65,41 @@ pub const Device = struct {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// One request at the bus driver, addressing this device by its token — the
|
||||||
|
/// packet's `Header.target`, so no request body ever names the device again.
|
||||||
|
/// Null covers both a failed transport and a refusal: a class driver has the
|
||||||
|
/// same recourse either way.
|
||||||
|
fn call(
|
||||||
|
self: *Device,
|
||||||
|
comptime operation: Protocol.Operation,
|
||||||
|
request: Protocol.RequestOf(operation),
|
||||||
|
tail: []const u8,
|
||||||
|
capability: ?ipc.Handle,
|
||||||
|
reply: []u8,
|
||||||
|
) ?[]u8 {
|
||||||
|
var packet: [usb_transfer_protocol.message_maximum]u8 = undefined;
|
||||||
|
const framed = Protocol.encodeRequest(operation, self.token, request, tail, &packet) orelse return null;
|
||||||
|
const answer = ipc.callCap(self.bus, framed, reply, capability) catch return null;
|
||||||
|
const status = envelope.statusOf(reply[0..answer.len]) orelse return null;
|
||||||
|
if (status.status != 0) return null;
|
||||||
|
return reply[0..answer.len];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The data stage rides the tail in both directions, so the answer's length
|
||||||
|
/// *is* the transferred length — `Status.len`, which the envelope stamps.
|
||||||
fn controlTransfer(self: *Device, setup: [8]u8, direction_in: bool, data: []u8) ?usize {
|
fn controlTransfer(self: *Device, setup: [8]u8, direction_in: bool, data: []u8) ?usize {
|
||||||
var request = usb_transfer_protocol.ControlRequest{
|
if (data.len > usb_transfer_protocol.max_inline_data) return null;
|
||||||
.device_token = self.token,
|
const outgoing: []const u8 = if (direction_in) &.{} else data;
|
||||||
|
var reply: [usb_transfer_protocol.message_maximum]u8 = undefined;
|
||||||
|
const answered = self.call(.control, .{
|
||||||
.setup = setup,
|
.setup = setup,
|
||||||
.direction_in = @intFromBool(direction_in),
|
.direction_in = @intFromBool(direction_in),
|
||||||
.data_length = @intCast(data.len),
|
.data_length = @intCast(data.len),
|
||||||
};
|
}, outgoing, null, &reply) orelse return null;
|
||||||
if (!direction_in and data.len > 0) @memcpy(request.data[0..data.len], data);
|
|
||||||
var reply: [@sizeOf(usb_transfer_protocol.ControlReply)]u8 = undefined;
|
const returned = Protocol.replyTail(.control, answered);
|
||||||
const length = ipc.call(self.bus, std.mem.asBytes(&request), &reply) catch return null;
|
const actual = @min(returned.len, data.len);
|
||||||
if (length < @sizeOf(usb_transfer_protocol.ControlReply)) return null;
|
if (direction_in and actual > 0) @memcpy(data[0..actual], returned[0..actual]);
|
||||||
const control_reply = std.mem.bytesToValue(usb_transfer_protocol.ControlReply, reply[0..@sizeOf(usb_transfer_protocol.ControlReply)]);
|
|
||||||
if (control_reply.status != 0) return null;
|
|
||||||
const actual = @min(control_reply.actual_length, data.len);
|
|
||||||
if (direction_in and actual > 0) @memcpy(data[0..actual], control_reply.data[0..actual]);
|
|
||||||
return actual;
|
return actual;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -89,15 +117,11 @@ pub const Device = struct {
|
||||||
/// Begin periodic IN polling of an interrupt endpoint; reports flow back to
|
/// Begin periodic IN polling of an interrupt endpoint; reports flow back to
|
||||||
/// `self.endpoint` as asynchronous `InterruptReport` messages.
|
/// `self.endpoint` as asynchronous `InterruptReport` messages.
|
||||||
pub fn subscribeInterrupt(self: *Device, endpoint_address: u8, max_length: u16) bool {
|
pub fn subscribeInterrupt(self: *Device, endpoint_address: u8, max_length: u16) bool {
|
||||||
var request = usb_transfer_protocol.InterruptSubscribeRequest{
|
var reply: [usb_transfer_protocol.message_maximum]u8 = undefined;
|
||||||
.device_token = self.token,
|
return self.call(.interrupt_subscribe, .{
|
||||||
.endpoint_address = endpoint_address,
|
.endpoint_address = endpoint_address,
|
||||||
.max_length = max_length,
|
.max_length = max_length,
|
||||||
};
|
}, &.{}, null, &reply) != null;
|
||||||
var reply: [@sizeOf(usb_transfer_protocol.InterruptSubscribeReply)]u8 = undefined;
|
|
||||||
const length = ipc.call(self.bus, std.mem.asBytes(&request), &reply) catch return false;
|
|
||||||
if (length < @sizeOf(usb_transfer_protocol.InterruptSubscribeReply)) return false;
|
|
||||||
return std.mem.bytesToValue(usb_transfer_protocol.InterruptSubscribeReply, reply[0..@sizeOf(usb_transfer_protocol.InterruptSubscribeReply)]).status == 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Hand the controller a DMA-region capability (`handle` — from a `shareable`
|
/// Hand the controller a DMA-region capability (`handle` — from a `shareable`
|
||||||
|
|
@ -106,31 +130,33 @@ pub const Device = struct {
|
||||||
/// will name in a `bulk` transfer, before the transfer. Harmless (and a no-op
|
/// will name in a `bulk` transfer, before the transfer. Harmless (and a no-op
|
||||||
/// success) when no IOMMU is enforcing. Returns false on failure.
|
/// success) when no IOMMU is enforcing. Returns false on failure.
|
||||||
pub fn attachDma(self: *Device, handle: ipc.Handle) bool {
|
pub fn attachDma(self: *Device, handle: ipc.Handle) bool {
|
||||||
var request = usb_transfer_protocol.DmaAttachRequest{ .device_token = self.token };
|
var reply: [usb_transfer_protocol.message_maximum]u8 = undefined;
|
||||||
var reply: [@sizeOf(usb_transfer_protocol.DmaAttachReply)]u8 = undefined;
|
return self.call(.dma_attach, {}, &.{}, handle, &reply) != null;
|
||||||
const result = ipc.callCap(self.bus, std.mem.asBytes(&request), &reply, handle) catch return false;
|
|
||||||
if (result.len < @sizeOf(usb_transfer_protocol.DmaAttachReply)) return false;
|
|
||||||
return std.mem.bytesToValue(usb_transfer_protocol.DmaAttachReply, reply[0..@sizeOf(usb_transfer_protocol.DmaAttachReply)]).status == 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One bulk transfer (IN or OUT per `endpoint_address`'s direction bit) to or
|
/// One bulk transfer (IN or OUT per `endpoint_address`'s direction bit) to or
|
||||||
/// from the caller's own DMA buffer at `physical`. Returns the bytes moved.
|
/// from the caller's own DMA buffer at `physical`. Returns the bytes moved.
|
||||||
pub fn bulk(self: *Device, endpoint_address: u8, physical: u64, length: u32) ?u32 {
|
pub fn bulk(self: *Device, endpoint_address: u8, physical: u64, length: u32) ?u32 {
|
||||||
var request = usb_transfer_protocol.BulkRequest{
|
var reply: [usb_transfer_protocol.message_maximum]u8 = undefined;
|
||||||
.device_token = self.token,
|
const answered = self.call(.bulk, .{
|
||||||
.physical_address = physical,
|
.physical_address = physical,
|
||||||
.length = length,
|
.length = length,
|
||||||
.endpoint_address = endpoint_address,
|
.endpoint_address = endpoint_address,
|
||||||
};
|
}, &.{}, null, &reply) orelse return null;
|
||||||
var reply: [@sizeOf(usb_transfer_protocol.BulkReply)]u8 = undefined;
|
return (Protocol.decodeReply(.bulk, answered) orelse return null).actual_length;
|
||||||
const replied = ipc.call(self.bus, std.mem.asBytes(&request), &reply) catch return null;
|
|
||||||
if (replied < @sizeOf(usb_transfer_protocol.BulkReply)) return null;
|
|
||||||
const bulk_reply = std.mem.bytesToValue(usb_transfer_protocol.BulkReply, reply[0..@sizeOf(usb_transfer_protocol.BulkReply)]);
|
|
||||||
if (bulk_reply.status != 0) return null;
|
|
||||||
return bulk_reply.actual_length;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Decode one asynchronous interrupt report out of a packet that arrived on the
|
||||||
|
/// class driver's own endpoint. Null when it is not one — a stray message, or a
|
||||||
|
/// packet too short to carry the report it names. The device it came from is the
|
||||||
|
/// packet's `Header.target`, which a single-device class driver never has to read.
|
||||||
|
pub fn reportOf(packet: []const u8) ?InterruptReport {
|
||||||
|
const event = Protocol.eventOf(packet) orelse return null;
|
||||||
|
if (event != .interrupt_report) return null;
|
||||||
|
return Protocol.decodeEvent(.interrupt_report, packet);
|
||||||
|
}
|
||||||
|
|
||||||
/// Open `/protocol/usb-transfer` and, on that channel, open the device with the
|
/// Open `/protocol/usb-transfer` and, on that channel, open the device with the
|
||||||
/// assigned id, handing over a freshly created endpoint for asynchronous interrupt
|
/// assigned id, handing over a freshly created endpoint for asynchronous interrupt
|
||||||
/// reports. Retries while the bus is still coming up (a class driver races the bus
|
/// reports. Retries while the bus is still coming up (a class driver races the bus
|
||||||
|
|
@ -144,12 +170,17 @@ pub fn open(device_id: u64) ?Device {
|
||||||
} else return null;
|
} else return null;
|
||||||
|
|
||||||
const endpoint = ipc.createIpcEndpoint() orelse return null;
|
const endpoint = ipc.createIpcEndpoint() orelse return null;
|
||||||
var request = usb_transfer_protocol.OpenRequest{ .device_id = device_id };
|
// The assigned device id is the target: it is what the caller has before a
|
||||||
var reply: [@sizeOf(usb_transfer_protocol.OpenReply)]u8 = undefined;
|
// token exists, and the token the reply hands back addresses every packet
|
||||||
const result = ipc.callCap(bus, std.mem.asBytes(&request), &reply, endpoint) catch return null;
|
// after this one.
|
||||||
if (result.len < @sizeOf(usb_transfer_protocol.OpenReply)) return null;
|
var packet: [usb_transfer_protocol.message_maximum]u8 = undefined;
|
||||||
const open_reply = std.mem.bytesToValue(usb_transfer_protocol.OpenReply, reply[0..@sizeOf(usb_transfer_protocol.OpenReply)]);
|
const framed = Protocol.encodeRequest(.open, device_id, {}, &.{}, &packet) orelse return null;
|
||||||
if (open_reply.status != 0) return null;
|
var reply: [usb_transfer_protocol.message_maximum]u8 = undefined;
|
||||||
|
const result = ipc.callCap(bus, framed, &reply, endpoint) catch return null;
|
||||||
|
const answered = reply[0..result.len];
|
||||||
|
const status = envelope.statusOf(answered) orelse return null;
|
||||||
|
if (status.status != 0) return null;
|
||||||
|
const open_reply = Protocol.decodeReply(.open, answered) orelse return null;
|
||||||
|
|
||||||
var device = Device{
|
var device = Device{
|
||||||
.bus = bus,
|
.bus = bus,
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,7 @@ pub fn build(b: *std.Build) void {
|
||||||
.{ .name = "system-call", .module = system_call },
|
.{ .name = "system-call", .module = system_call },
|
||||||
.{ .name = "ipc", .module = ipc },
|
.{ .name = "ipc", .module = ipc },
|
||||||
.{ .name = "vfs-protocol", .module = protocol.module("vfs-protocol") },
|
.{ .name = "vfs-protocol", .module = protocol.module("vfs-protocol") },
|
||||||
|
.{ .name = "envelope", .module = protocol.module("envelope") },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
// The channel is the L1 concept made concrete (docs/os-development/communication.md):
|
// The channel is the L1 concept made concrete (docs/os-development/communication.md):
|
||||||
|
|
@ -86,11 +87,14 @@ pub fn build(b: *std.Build) void {
|
||||||
});
|
});
|
||||||
// The harness binds the service's contract name at startup, which is a
|
// The harness binds the service's contract name at startup, which is a
|
||||||
// conversation with the registry — hence channel (and time, for the patience
|
// conversation with the registry — hence channel (and time, for the patience
|
||||||
// a provider that beat init to the mount needs).
|
// a provider that beat init to the mount needs). It also owns the subscriber
|
||||||
|
// table and the fan-out, which are expressed in the envelope's vocabulary
|
||||||
|
// (the reserved subscribe verb, the push floor) — hence envelope.
|
||||||
_ = b.addModule("service", .{
|
_ = b.addModule("service", .{
|
||||||
.root_source_file = b.path("service.zig"),
|
.root_source_file = b.path("service.zig"),
|
||||||
.imports = &.{
|
.imports = &.{
|
||||||
.{ .name = "channel", .module = channel },
|
.{ .name = "channel", .module = channel },
|
||||||
|
.{ .name = "envelope", .module = protocol.module("envelope") },
|
||||||
.{ .name = "ipc", .module = ipc },
|
.{ .name = "ipc", .module = ipc },
|
||||||
.{ .name = "process", .module = process },
|
.{ .name = "process", .module = process },
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -192,41 +192,37 @@ fn reach(path: []const u8) ?Registry {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One vfs-protocol round trip at a backend: fixed header, inline payload, and
|
/// One vfs-protocol round trip at a backend: the folded header, the verb's own
|
||||||
/// an optional capability in each direction.
|
/// fixed part, the name as the packet's tail, and an optional capability in each
|
||||||
|
/// direction. Both verbs this file sends address the backend itself (target 0) —
|
||||||
|
/// the name in the tail is what they are about.
|
||||||
fn transact(
|
fn transact(
|
||||||
|
comptime operation: vfs_protocol.Operation,
|
||||||
handle: ipc.Handle,
|
handle: ipc.Handle,
|
||||||
operation: vfs_protocol.Operation,
|
request: vfs_protocol.Protocol.RequestOf(operation),
|
||||||
payload: []const u8,
|
name: []const u8,
|
||||||
send_capability: ?ipc.Handle,
|
send_capability: ?ipc.Handle,
|
||||||
) ?struct { reply: vfs_protocol.Reply, capability: ?ipc.Handle } {
|
) ?struct { status: envelope.Status, capability: ?ipc.Handle } {
|
||||||
var request: [vfs_protocol.message_maximum]u8 = undefined;
|
var packet: [vfs_protocol.message_maximum]u8 = undefined;
|
||||||
if (vfs_protocol.request_size + payload.len > request.len) return null;
|
const framed = vfs_protocol.Protocol.encodeRequest(operation, 0, request, name, &packet) orelse return null;
|
||||||
const header = vfs_protocol.Request{
|
|
||||||
.operation = operation,
|
|
||||||
.node = 0,
|
|
||||||
.offset = 0,
|
|
||||||
.len = @intCast(payload.len),
|
|
||||||
.flags = 0,
|
|
||||||
};
|
|
||||||
@memcpy(request[0..vfs_protocol.request_size], std.mem.asBytes(&header));
|
|
||||||
@memcpy(request[vfs_protocol.request_size..][0..payload.len], payload);
|
|
||||||
|
|
||||||
var reply: [vfs_protocol.message_maximum]u8 = undefined;
|
var reply: [vfs_protocol.message_maximum]u8 = undefined;
|
||||||
const answer = ipc.callCap(handle, request[0 .. vfs_protocol.request_size + payload.len], &reply, send_capability) catch return null;
|
const answer = ipc.callCap(handle, framed, &reply, send_capability) catch return null;
|
||||||
if (answer.len < vfs_protocol.reply_size) return null;
|
const status = envelope.statusOf(reply[0..answer.len]) orelse return null;
|
||||||
return .{
|
return .{ .status = status, .capability = answer.cap };
|
||||||
.reply = std.mem.bytesToValue(vfs_protocol.Reply, reply[0..vfs_protocol.reply_size]),
|
|
||||||
.capability = answer.cap,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolve an absolute `/protocol/...` path and take the provider's endpoint out
|
/// Resolve an absolute `/protocol/...` path and take the provider's endpoint out
|
||||||
/// of the open reply's capability.
|
/// of the open reply's capability.
|
||||||
fn openPath(path: []const u8) ?ipc.Handle {
|
fn openPath(path: []const u8) ?ipc.Handle {
|
||||||
const registry = reach(path) orelse return null;
|
const registry = reach(path) orelse return null;
|
||||||
const answered = transact(registry.handle, .open, registry.path(), null) orelse return null;
|
const answered = transact(.open, registry.handle, .{ .flags = 0 }, registry.path(), null) orelse return null;
|
||||||
if (answered.reply.status != 0) return null;
|
if (answered.status.status != 0) {
|
||||||
|
// A refusal carries no channel; anything that arrived anyway would be a
|
||||||
|
// handle-table slot spent for nothing.
|
||||||
|
if (answered.capability) |handle| _ = ipc.close(handle);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
// The capability *is* the channel — an open that succeeds without one was
|
// The capability *is* the channel — an open that succeeds without one was
|
||||||
// answered by a file backend, which does not speak protocols.
|
// answered by a file backend, which does not speak protocols.
|
||||||
return answered.capability;
|
return answered.capability;
|
||||||
|
|
@ -257,8 +253,8 @@ pub fn openEndpoint(name: []const u8) ?ipc.Handle {
|
||||||
/// `-EBUSY` a live provider already holds it.
|
/// `-EBUSY` a live provider already holds it.
|
||||||
pub fn bind(name: []const u8, endpoint: ipc.Handle) ?i32 {
|
pub fn bind(name: []const u8, endpoint: ipc.Handle) ?i32 {
|
||||||
const registry = reach(root) orelse return null;
|
const registry = reach(root) orelse return null;
|
||||||
const answered = transact(registry.handle, .bind, name, endpoint) orelse return null;
|
const answered = transact(.bind, registry.handle, {}, name, endpoint) orelse return null;
|
||||||
return answered.reply.status;
|
return answered.status.status;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// How long a provider keeps offering itself before giving up. The registry is
|
/// How long a provider keeps offering itself before giving up. The registry is
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,13 @@ const std = @import("std");
|
||||||
const abi = @import("abi");
|
const abi = @import("abi");
|
||||||
const sc = @import("system-call");
|
const sc = @import("system-call");
|
||||||
const ipc = @import("ipc");
|
const ipc = @import("ipc");
|
||||||
|
const envelope = @import("envelope");
|
||||||
const vfs_protocol = @import("vfs-protocol");
|
const vfs_protocol = @import("vfs-protocol");
|
||||||
|
|
||||||
|
/// The generated vfs contract: encode/decode for every verb, with the node id
|
||||||
|
/// carried in the packet header's `target`.
|
||||||
|
const Protocol = vfs_protocol.Protocol;
|
||||||
|
|
||||||
/// The kind of a filesystem node — re-exported so a caller need not import the
|
/// The kind of a filesystem node — re-exported so a caller need not import the
|
||||||
/// wire protocol.
|
/// wire protocol.
|
||||||
pub const Kind = vfs_protocol.NodeKind;
|
pub const Kind = vfs_protocol.NodeKind;
|
||||||
|
|
@ -89,23 +94,24 @@ fn resolve(path: []const u8, flags: usize) ?Route {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const Result = struct { reply: vfs_protocol.Reply, payload: []u8 };
|
// One request/reply round trip: frame `[Header][request][tail]`, send it, and
|
||||||
|
// hand back the whole reply packet for the caller to decode with the generated
|
||||||
// One request/reply round trip: [Request header][send payload] -> backend ->
|
// helpers. A backend that refused (a negative status) reads as null, which is
|
||||||
// [Reply header][receive payload]. The receive payload lands in `out`.
|
// what every caller here did with it anyway.
|
||||||
fn transact(h: ipc.Handle, request: vfs_protocol.Request, send: []const u8, out: []u8) ?Result {
|
fn transact(
|
||||||
var message: [vfs_protocol.message_maximum]u8 = undefined;
|
comptime operation: Protocol.Operation,
|
||||||
@memcpy(message[0..vfs_protocol.request_size], std.mem.asBytes(&request));
|
handle: ipc.Handle,
|
||||||
const slen = @min(send.len, vfs_protocol.maximum_payload);
|
target: u64,
|
||||||
@memcpy(message[vfs_protocol.request_size..][0..slen], send[0..slen]);
|
request: Protocol.RequestOf(operation),
|
||||||
|
tail: []const u8,
|
||||||
var rbuf: [vfs_protocol.message_maximum]u8 = undefined;
|
reply: []u8,
|
||||||
const n = ipc.call(h, message[0 .. vfs_protocol.request_size + slen], &rbuf) catch return null;
|
) ?[]u8 {
|
||||||
if (n < vfs_protocol.reply_size) return null;
|
var packet: [vfs_protocol.message_maximum]u8 = undefined;
|
||||||
const reply = std.mem.bytesToValue(vfs_protocol.Reply, rbuf[0..vfs_protocol.reply_size]);
|
const framed = Protocol.encodeRequest(operation, target, request, tail, &packet) orelse return null;
|
||||||
const rpl = @min(n - vfs_protocol.reply_size, out.len);
|
const n = ipc.call(handle, framed, reply) catch return null;
|
||||||
@memcpy(out[0..rpl], rbuf[vfs_protocol.reply_size..][0..rpl]);
|
const status = envelope.statusOf(reply[0..n]) orelse return null;
|
||||||
return .{ .reply = reply, .payload = out[0..rpl] };
|
if (status.status != 0) return null;
|
||||||
|
return reply[0..n];
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An open file: a VFS node plus a byte cursor. Read and write advance the cursor.
|
/// An open file: a VFS node plus a byte cursor. Read and write advance the cursor.
|
||||||
|
|
@ -125,11 +131,13 @@ pub const File = struct {
|
||||||
return n;
|
return n;
|
||||||
};
|
};
|
||||||
const want: u32 = @intCast(@min(buffer.len, vfs_protocol.maximum_payload));
|
const want: u32 = @intCast(@min(buffer.len, vfs_protocol.maximum_payload));
|
||||||
const request = vfs_protocol.Request{ .operation = .read, .node = self.node, .offset = self.offset, .len = want, .flags = 0 };
|
var reply: [vfs_protocol.message_maximum]u8 = undefined;
|
||||||
const r = transact(h, request, &.{}, buffer) orelse return null;
|
const answered = transact(.read, h, self.node, .{ .offset = self.offset, .len = want }, &.{}, &reply) orelse return null;
|
||||||
if (r.reply.status != 0) return null;
|
const bytes = Protocol.replyTail(.read, answered);
|
||||||
self.offset += r.reply.len;
|
const n = @min(bytes.len, buffer.len);
|
||||||
return r.reply.len;
|
@memcpy(buffer[0..n], bytes[0..n]);
|
||||||
|
self.offset += n;
|
||||||
|
return n;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Write `data` at the current offset; returns the count written. A single
|
/// Write `data` at the current offset; returns the count written. A single
|
||||||
|
|
@ -139,11 +147,11 @@ pub const File = struct {
|
||||||
pub fn write(self: *File, data: []const u8) ?usize {
|
pub fn write(self: *File, data: []const u8) ?usize {
|
||||||
const h = self.backend orelse return null;
|
const h = self.backend orelse return null;
|
||||||
const want: u32 = @intCast(@min(data.len, vfs_protocol.maximum_payload));
|
const want: u32 = @intCast(@min(data.len, vfs_protocol.maximum_payload));
|
||||||
const request = vfs_protocol.Request{ .operation = .write, .node = self.node, .offset = self.offset, .len = want, .flags = 0 };
|
var reply: [vfs_protocol.message_maximum]u8 = undefined;
|
||||||
const r = transact(h, request, data[0..want], &.{}) orelse return null;
|
const answered = transact(.write, h, self.node, .{ .offset = self.offset, .len = want }, data[0..want], &reply) orelse return null;
|
||||||
if (r.reply.status != 0) return null;
|
const written = Protocol.decodeReply(.write, answered) orelse return null;
|
||||||
self.offset += r.reply.len;
|
self.offset += written.count;
|
||||||
return r.reply.len;
|
return written.count;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Write all of `data`, looping past the per-call payload cap. Returns the
|
/// Write all of `data`, looping past the per-call payload cap. Returns the
|
||||||
|
|
@ -169,11 +177,9 @@ pub const File = struct {
|
||||||
const a = fsNodeStatus(self.node) orelse return null;
|
const a = fsNodeStatus(self.node) orelse return null;
|
||||||
return .{ .size = a.size, .kind = if (a.kind == file_kind_directory) .directory else .regular, .mtime = a.mtime };
|
return .{ .size = a.size, .kind = if (a.kind == file_kind_directory) .directory else .regular, .mtime = a.mtime };
|
||||||
};
|
};
|
||||||
const request = vfs_protocol.Request{ .operation = .status, .node = self.node, .offset = 0, .len = 0, .flags = 0 };
|
var reply: [vfs_protocol.message_maximum]u8 = undefined;
|
||||||
var buffer: [@sizeOf(vfs_protocol.FileStatus)]u8 = undefined;
|
const answered = transact(.status, h, self.node, {}, &.{}, &reply) orelse return null;
|
||||||
const r = transact(h, request, &.{}, &buffer) orelse return null;
|
const status = Protocol.decodeReply(.status, answered) orelse return null;
|
||||||
if (r.reply.status != 0 or r.payload.len < @sizeOf(vfs_protocol.FileStatus)) return null;
|
|
||||||
const status = std.mem.bytesToValue(vfs_protocol.FileStatus, buffer[0..@sizeOf(vfs_protocol.FileStatus)]);
|
|
||||||
return .{ .size = status.size, .kind = kindFromWire(status.kind), .mtime = status.mtime };
|
return .{ .size = status.size, .kind = kindFromWire(status.kind), .mtime = status.mtime };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -181,8 +187,8 @@ pub const File = struct {
|
||||||
/// tokens are permanent — nothing to release.
|
/// tokens are permanent — nothing to release.
|
||||||
pub fn close(self: *File) void {
|
pub fn close(self: *File) void {
|
||||||
const h = self.backend orelse return;
|
const h = self.backend orelse return;
|
||||||
const request = vfs_protocol.Request{ .operation = .close, .node = self.node, .offset = 0, .len = 0, .flags = 0 };
|
var reply: [vfs_protocol.message_maximum]u8 = undefined;
|
||||||
_ = transact(h, request, &.{}, &.{});
|
_ = transact(.close, h, self.node, {}, &.{}, &reply);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -193,10 +199,10 @@ pub fn open(path: []const u8, options: OpenOptions) ?File {
|
||||||
.kernel => |token| return .{ .node = token, .backend = null },
|
.kernel => |token| return .{ .node = token, .backend = null },
|
||||||
.backend => |b| {
|
.backend => |b| {
|
||||||
const relative = route.backendPath();
|
const relative = route.backendPath();
|
||||||
const request = vfs_protocol.Request{ .operation = .open, .node = 0, .offset = 0, .len = @intCast(relative.len), .flags = options.wireFlags() };
|
var reply: [vfs_protocol.message_maximum]u8 = undefined;
|
||||||
const r = transact(b.handle, request, relative, &.{}) orelse return null;
|
const answered = transact(.open, b.handle, 0, .{ .flags = options.wireFlags() }, relative, &reply) orelse return null;
|
||||||
if (r.reply.status != 0) return null;
|
const opened = Protocol.decodeReply(.open, answered) orelse return null;
|
||||||
return .{ .node = r.reply.node, .backend = b.handle };
|
return .{ .node = opened.node, .backend = b.handle };
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -248,15 +254,13 @@ pub const Directory = struct {
|
||||||
self.cursor += 1;
|
self.cursor += 1;
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
const request = vfs_protocol.Request{ .operation = .readdir, .node = self.node, .offset = self.cursor, .len = 0, .flags = 0 };
|
var reply: [vfs_protocol.message_maximum]u8 = undefined;
|
||||||
var buffer: [vfs_protocol.message_maximum]u8 = undefined;
|
const answered = transact(.readdir, h, self.node, .{ .cursor = self.cursor }, &.{}, &reply) orelse return false;
|
||||||
const r = transact(h, request, &.{}, &buffer) orelse return false;
|
const header = Protocol.decodeReply(.readdir, answered) orelse return false;
|
||||||
if (r.reply.status != 0 or r.reply.len == 0) return false; // error or EOF
|
if (header.name_len == 0) return false; // end of directory
|
||||||
if (r.payload.len < vfs_protocol.directory_entry_size) return false;
|
|
||||||
const header = std.mem.bytesToValue(vfs_protocol.DirectoryEntry, r.payload[0..vfs_protocol.directory_entry_size]);
|
|
||||||
entry.kind = kindFromWire(header.kind);
|
entry.kind = kindFromWire(header.kind);
|
||||||
entry.size = header.size;
|
entry.size = header.size;
|
||||||
const source = r.payload[vfs_protocol.directory_entry_size..];
|
const source = Protocol.replyTail(.readdir, answered);
|
||||||
const nlen = @min(@min(@as(usize, header.name_len), source.len), entry.name_buffer.len);
|
const nlen = @min(@min(@as(usize, header.name_len), source.len), entry.name_buffer.len);
|
||||||
@memcpy(entry.name_buffer[0..nlen], source[0..nlen]);
|
@memcpy(entry.name_buffer[0..nlen], source[0..nlen]);
|
||||||
entry.name_len = nlen;
|
entry.name_len = nlen;
|
||||||
|
|
@ -280,13 +284,11 @@ pub fn openDirectory(path: []const u8) ?Directory {
|
||||||
// A path-based request that returns only a status (mkdir, unlink). Kernel-served
|
// A path-based request that returns only a status (mkdir, unlink). Kernel-served
|
||||||
// paths (the read-only /system) refuse mutation by construction: the resolve
|
// paths (the read-only /system) refuse mutation by construction: the resolve
|
||||||
// must land on a backend.
|
// must land on a backend.
|
||||||
fn pathOperation(operation: vfs_protocol.Operation, path: []const u8) bool {
|
fn pathOperation(comptime operation: Protocol.Operation, path: []const u8) bool {
|
||||||
const route = resolve(path, 0) orelse return false;
|
const route = resolve(path, 0) orelse return false;
|
||||||
if (route != .backend) return false;
|
if (route != .backend) return false;
|
||||||
const relative = route.backendPath();
|
var reply: [vfs_protocol.message_maximum]u8 = undefined;
|
||||||
const request = vfs_protocol.Request{ .operation = operation, .node = 0, .offset = 0, .len = @intCast(relative.len), .flags = 0 };
|
return transact(operation, route.backend.handle, 0, {}, route.backendPath(), &reply) != null;
|
||||||
const r = transact(route.backend.handle, request, relative, &.{}) orelse return false;
|
|
||||||
return r.reply.status == 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a directory at `path` (its parent must already exist). Returns true on
|
/// Create a directory at `path` (its parent must already exist). Returns true on
|
||||||
|
|
@ -336,9 +338,8 @@ pub fn rename(old_path: []const u8, new_path: []const u8) bool {
|
||||||
@memcpy(payload[0..old_relative.len], old_relative);
|
@memcpy(payload[0..old_relative.len], old_relative);
|
||||||
payload[old_relative.len] = 0;
|
payload[old_relative.len] = 0;
|
||||||
@memcpy(payload[old_relative.len + 1 ..][0..new_relative.len], new_relative);
|
@memcpy(payload[old_relative.len + 1 ..][0..new_relative.len], new_relative);
|
||||||
const request = vfs_protocol.Request{ .operation = .rename, .node = 0, .offset = 0, .len = @intCast(total), .flags = 0 };
|
var reply: [vfs_protocol.message_maximum]u8 = undefined;
|
||||||
const r = transact(old_route.backend.handle, request, payload[0..total], &.{}) orelse return false;
|
return transact(.rename, old_route.backend.handle, 0, {}, payload[0..total], &reply) != null;
|
||||||
return r.reply.status == 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Mount a filesystem backend (its server endpoint) at absolute path `target`;
|
/// Mount a filesystem backend (its server endpoint) at absolute path `target`;
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,12 @@
|
||||||
//! therefore safe, and keeping is explicit; the opposite arrangement quietly
|
//! therefore safe, and keeping is explicit; the opposite arrangement quietly
|
||||||
//! spends a handle-table slot per request.
|
//! spends a handle-table slot per request.
|
||||||
//!
|
//!
|
||||||
|
//! The harness also owns the **subscriber side** of a protocol that declares
|
||||||
|
//! `.events` — see `Subscribers`. The table, the reserved subscribe/unsubscribe
|
||||||
|
//! verbs, the fan-out, and the dead-subscriber sweep live here rather than in
|
||||||
|
//! each provider, so every event stream in the system has identical semantics
|
||||||
|
//! (docs/os-development/protocol-namespace.md, "Wiring").
|
||||||
|
//!
|
||||||
//! The liveness probe: a **zero-length request is the universal ping**, answered
|
//! The liveness probe: a **zero-length request is the universal ping**, answered
|
||||||
//! with a zero-length reply by the harness itself. No protocol's requests start
|
//! with a zero-length reply by the harness itself. No protocol's requests start
|
||||||
//! at length zero, so the encoding cannot collide, and there is nothing for a
|
//! at length zero, so the encoding cannot collide, and there is nothing for a
|
||||||
|
|
@ -19,9 +25,23 @@
|
||||||
//! is the diagnosis (see docs/ipc.md).
|
//! is the diagnosis (see docs/ipc.md).
|
||||||
|
|
||||||
const channel = @import("channel");
|
const channel = @import("channel");
|
||||||
|
const envelope = @import("envelope");
|
||||||
const ipc = @import("ipc");
|
const ipc = @import("ipc");
|
||||||
const process = @import("process");
|
const process = @import("process");
|
||||||
|
|
||||||
|
/// The harness's handle on a provider's subscriber table, type-erased because
|
||||||
|
/// `run` is not generic over the protocol while `Subscribers` is. A service names
|
||||||
|
/// its table once, as `Callbacks.subscribers`, and the loop does the rest: it
|
||||||
|
/// subscribes to published process exits at startup and drops a dead task's
|
||||||
|
/// subscriptions before the service's own notification callback ever sees the
|
||||||
|
/// badge.
|
||||||
|
pub const SubscriberHooks = struct {
|
||||||
|
/// Ask the kernel for published exit events on this service's endpoint.
|
||||||
|
watch: *const fn (endpoint: ipc.Handle) void,
|
||||||
|
/// Drop everything task `dead` had subscribed.
|
||||||
|
forget: *const fn (dead: u32) void,
|
||||||
|
};
|
||||||
|
|
||||||
pub const Callbacks = struct {
|
pub const Callbacks = struct {
|
||||||
/// Called once with the service's endpoint before the loop starts — the
|
/// Called once with the service's endpoint before the loop starts — the
|
||||||
/// place to subscribe to exit events, bind IRQs, or announce readiness.
|
/// place to subscribe to exit events, bind IRQs, or announce readiness.
|
||||||
|
|
@ -60,8 +80,216 @@ pub const Callbacks = struct {
|
||||||
/// `init` runs, so the service is reachable the moment it serves. A refusal
|
/// `init` runs, so the service is reachable the moment it serves. A refusal
|
||||||
/// (not granted, or a live provider already holds the name) aborts startup.
|
/// (not granted, or a live provider already holds the name) aborts startup.
|
||||||
service: ?[]const u8 = null,
|
service: ?[]const u8 = null,
|
||||||
|
/// This provider's subscriber table — `Subscribers(Protocol, Context).hooks`
|
||||||
|
/// — for a protocol that declares `.events`. Naming it here is what buys the
|
||||||
|
/// exit-notification sweep: the loop subscribes to published deaths at
|
||||||
|
/// startup and releases a dead subscriber's slot (and the endpoint capability
|
||||||
|
/// in it) when one lands.
|
||||||
|
subscribers: ?SubscriberHooks = null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// How many subscribers one provider fans out to. Bounded like every table in
|
||||||
|
/// this system; a subscribe past the end is refused with `-ENOSPC` rather than
|
||||||
|
/// silently forgetting an earlier one.
|
||||||
|
pub const subscriber_capacity = 8;
|
||||||
|
|
||||||
|
/// The interest mask that means "every event of this protocol" — what a
|
||||||
|
/// subscriber which named no class gets, and what a provider passes when the
|
||||||
|
/// event it is publishing belongs to no class.
|
||||||
|
pub const every_event: u32 = 0;
|
||||||
|
|
||||||
|
/// The subscriber side of a protocol, for a provider whose contract declares
|
||||||
|
/// `.events` (docs/os-development/protocol-namespace.md: *the harness owns the
|
||||||
|
/// machinery — the subscriber table, the dead-subscriber sweep, and the fan-out
|
||||||
|
/// loop*). Three services hand-rolled this, with three different ideas of when a
|
||||||
|
/// dead subscriber goes away — a poll of the process list on subscribe, a drop on
|
||||||
|
/// a failed send, and nothing at all. This is the one idiom.
|
||||||
|
///
|
||||||
|
/// ```zig
|
||||||
|
/// const Subscriptions = service.Subscribers(power_protocol.Protocol, void);
|
||||||
|
/// ...
|
||||||
|
/// fn onMessage(message: []const u8, reply: []u8, sender: u32, arrived: *ipc.Arrival) usize {
|
||||||
|
/// return Subscriptions.dispatch({}, handlers, message, sender, arrived, reply);
|
||||||
|
/// }
|
||||||
|
/// pub fn main() void {
|
||||||
|
/// service.run(power_protocol.message_maximum, .{
|
||||||
|
/// .service = "power",
|
||||||
|
/// .on_message = onMessage,
|
||||||
|
/// .subscribers = Subscriptions.hooks,
|
||||||
|
/// });
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// What the provider still writes is its own events — `publish(.power_button, 0,
|
||||||
|
/// .{})`. Everything else happens here: registering the caller's endpoint on the
|
||||||
|
/// reserved `subscribe` verb, taking that capability out of the turn, dropping it
|
||||||
|
/// on `unsubscribe` or on the subscriber's death, and framing one packet for the
|
||||||
|
/// whole fan-out.
|
||||||
|
///
|
||||||
|
/// The table is per instantiation (a container-level `var` inside the generic
|
||||||
|
/// type), so a process providing two contracts gets two tables and neither can
|
||||||
|
/// see the other's subscribers.
|
||||||
|
pub fn Subscribers(comptime Protocol: type, comptime Context: type) type {
|
||||||
|
return struct {
|
||||||
|
/// The generated dispatch this provider answers with.
|
||||||
|
pub const Provider = Protocol.Provider(Context);
|
||||||
|
pub const Handlers = Provider.Handlers;
|
||||||
|
|
||||||
|
/// One registered subscriber: the endpoint events are pushed to (the
|
||||||
|
/// capability it handed over at subscribe time, which this slot owns),
|
||||||
|
/// the task that handed it over — the kernel-stamped badge, the only
|
||||||
|
/// source identity there is — and which classes of event it asked for.
|
||||||
|
const Slot = struct {
|
||||||
|
used: bool = false,
|
||||||
|
endpoint: ipc.Handle = 0,
|
||||||
|
task: u32 = 0,
|
||||||
|
interest: u32 = every_event,
|
||||||
|
};
|
||||||
|
|
||||||
|
var slots: [subscriber_capacity]Slot = .{Slot{}} ** subscriber_capacity;
|
||||||
|
|
||||||
|
/// Set when a slot has taken the capability the turn carried, and read
|
||||||
|
/// back in `dispatch`, which is 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* has to travel back out this way. One
|
||||||
|
/// turn, one handler, one thread: there is nothing here to race.
|
||||||
|
var claimed = false;
|
||||||
|
|
||||||
|
/// What `Callbacks.subscribers` is given.
|
||||||
|
pub const hooks: SubscriberHooks = .{ .watch = watchExits, .forget = forget };
|
||||||
|
|
||||||
|
fn watchExits(endpoint: ipc.Handle) void {
|
||||||
|
// Published exits, not a poll of the process list: a service must
|
||||||
|
// never depend on clients cleaning up after themselves, and it must
|
||||||
|
// not have to walk the whole table on every subscribe to find out
|
||||||
|
// either (docs/process-lifecycle.md, "Who learns of a death").
|
||||||
|
_ = process.subscribeExits(endpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Release everything task `dead` had subscribed. The slot owns the
|
||||||
|
/// endpoint capability, so reclaiming the slot closes it — otherwise a
|
||||||
|
/// process that subscribes and dies costs a handle-table slot that never
|
||||||
|
/// comes back.
|
||||||
|
pub fn forget(dead: u32) void {
|
||||||
|
for (&slots) |*slot| {
|
||||||
|
if (slot.used and slot.task == dead) {
|
||||||
|
_ = ipc.close(slot.endpoint);
|
||||||
|
slot.* = .{};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether `task` is a subscriber — the gate for an operation a provider
|
||||||
|
/// honours from its subscribers and nobody else. The power service's
|
||||||
|
/// shutdown is the one: the badge is kernel-stamped, so nothing in a
|
||||||
|
/// packet can claim to be the subscriber that already ran the stop
|
||||||
|
/// sequence.
|
||||||
|
pub fn has(task: u32) bool {
|
||||||
|
for (&slots) |*slot| {
|
||||||
|
if (slot.used and slot.task == task) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Answer one received packet, with the reserved `subscribe` and
|
||||||
|
/// `unsubscribe` verbs already wired — a provider that leaves those two
|
||||||
|
/// handlers null (every provider should) gets the harness's. The turn's
|
||||||
|
/// capability is peeked, never taken, unless a slot actually kept it.
|
||||||
|
pub fn dispatch(
|
||||||
|
context: Context,
|
||||||
|
handlers: Handlers,
|
||||||
|
packet: []const u8,
|
||||||
|
sender: u32,
|
||||||
|
arrived: *ipc.Arrival,
|
||||||
|
reply: []u8,
|
||||||
|
) usize {
|
||||||
|
var wired = handlers;
|
||||||
|
if (wired.subscribe == null) wired.subscribe = onSubscribe;
|
||||||
|
if (wired.unsubscribe == null) wired.unsubscribe = onUnsubscribe;
|
||||||
|
claimed = false;
|
||||||
|
const written = Provider.dispatch(context, wired, packet, sender, arrived.peek(), reply);
|
||||||
|
if (claimed) _ = arrived.take();
|
||||||
|
return written;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Push one event to every subscriber.
|
||||||
|
pub fn publish(
|
||||||
|
comptime event: Protocol.Event,
|
||||||
|
target: u64,
|
||||||
|
payload: Protocol.PayloadOf(event),
|
||||||
|
) void {
|
||||||
|
publishClass(event, target, payload, every_event);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Push one event to the subscribers whose interest mask includes
|
||||||
|
/// `class` (a subscriber that named no class takes everything). The
|
||||||
|
/// packet is framed **once**, outside the loop, so every subscriber of a
|
||||||
|
/// class receives identical bytes; and delivery is `ipc.send`, which
|
||||||
|
/// never blocks, so one slow or dead subscriber can never stall the rest
|
||||||
|
/// — the whole reason broadcast is a provider pattern and not a kernel
|
||||||
|
/// primitive.
|
||||||
|
pub fn publishClass(
|
||||||
|
comptime event: Protocol.Event,
|
||||||
|
target: u64,
|
||||||
|
payload: Protocol.PayloadOf(event),
|
||||||
|
class: u32,
|
||||||
|
) void {
|
||||||
|
var packet: [envelope.post_maximum]u8 = undefined;
|
||||||
|
const framed = Protocol.encodeEvent(event, target, payload, &packet) orelse return;
|
||||||
|
for (&slots) |*slot| {
|
||||||
|
if (!slot.used) continue;
|
||||||
|
if (!wants(slot.*, class)) continue;
|
||||||
|
// The sweep is what normally reclaims a dead subscriber, promptly
|
||||||
|
// and with its capability closed. This is the backstop for a
|
||||||
|
// notification that never arrived: an endpoint's notify ring is
|
||||||
|
// bounded, so a burst of deaths can drop one, and a send to an
|
||||||
|
// endpoint whose owner is gone fails rather than blocking.
|
||||||
|
if (!ipc.send(slot.endpoint, framed)) {
|
||||||
|
_ = ipc.close(slot.endpoint);
|
||||||
|
slot.* = .{};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wants(slot: Slot, class: u32) bool {
|
||||||
|
if (class == every_event) return true; // the event belongs to no class
|
||||||
|
if (slot.interest == every_event) return true; // the subscriber named none
|
||||||
|
return slot.interest & class != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The reserved `subscribe` verb: register the caller's endpoint (the
|
||||||
|
/// call's capability) for the classes its tail names. A refusal simply
|
||||||
|
/// returns and the turn closes what arrived — the harness's ownership
|
||||||
|
/// rule (`ipc.Arrival`), which is why a subscribe storm against a full
|
||||||
|
/// table cannot spend the handle table.
|
||||||
|
fn onSubscribe(_: Context, invocation: envelope.Invocation(void), _: envelope.Answer(void)) isize {
|
||||||
|
const endpoint = invocation.capability orelse return -envelope.EPROTO; // no endpoint passed
|
||||||
|
const interest = envelope.decodeSubscribe(invocation.tail).interest;
|
||||||
|
for (&slots) |*slot| {
|
||||||
|
if (slot.used) continue;
|
||||||
|
// Appended, not replaced: one task may hold several subscriptions
|
||||||
|
// on different endpoints (a client taking keyboard and mouse as
|
||||||
|
// two streams), and each is its own conversation.
|
||||||
|
slot.* = .{ .used = true, .endpoint = endpoint, .task = invocation.sender, .interest = interest };
|
||||||
|
claimed = true; // the table holds it until that task dies
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return -envelope.ENOSPC; // table full
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The reserved `unsubscribe` verb: every subscription the calling task
|
||||||
|
/// holds here goes, which is exactly what its death would do. It names no
|
||||||
|
/// endpoint because the badge already names the only subscriber a caller
|
||||||
|
/// can speak for — its own.
|
||||||
|
fn onUnsubscribe(_: Context, invocation: envelope.Invocation(void), _: envelope.Answer(void)) isize {
|
||||||
|
if (!has(invocation.sender)) return -envelope.ENOENT;
|
||||||
|
forget(invocation.sender);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/// Run the service: create the endpoint, bind it under the service's contract
|
/// Run the service: create the endpoint, bind it under the service's contract
|
||||||
/// name (if it has one), bind signals to it, call `init`, then serve until
|
/// name (if it has one), bind signals to it, call `init`, then serve until
|
||||||
/// `terminate` arrives — at which point the loop returns and main's return is
|
/// `terminate` arrives — at which point the loop returns and main's return is
|
||||||
|
|
@ -74,6 +302,9 @@ pub fn run(comptime maximum_message: usize, callbacks: Callbacks) void {
|
||||||
if (!channel.bindPatiently(name, endpoint)) return;
|
if (!channel.bindPatiently(name, endpoint)) return;
|
||||||
}
|
}
|
||||||
_ = process.bindSignals(endpoint);
|
_ = process.bindSignals(endpoint);
|
||||||
|
// Before `init`, so a subscriber that arrives the instant the name is bound
|
||||||
|
// is already covered by the sweep that will release it.
|
||||||
|
if (callbacks.subscribers) |subscribers| subscribers.watch(endpoint);
|
||||||
if (callbacks.init) |initialise| {
|
if (callbacks.init) |initialise| {
|
||||||
if (!initialise(endpoint)) return;
|
if (!initialise(endpoint)) return;
|
||||||
}
|
}
|
||||||
|
|
@ -105,6 +336,13 @@ pub fn run(comptime maximum_message: usize, callbacks: Callbacks) void {
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
// A death sweeps the subscriber table first, then still reaches the
|
||||||
|
// service: a provider often has its own per-client state to release
|
||||||
|
// (open file handles, device tokens, layers) and the same badge is
|
||||||
|
// the notice for both.
|
||||||
|
if (got.isChildExit()) {
|
||||||
|
if (callbacks.subscribers) |subscribers| subscribers.forget(got.childProcessId());
|
||||||
|
}
|
||||||
if (callbacks.on_notification) |onNotification| onNotification(got.badge);
|
if (callbacks.on_notification) |onNotification| onNotification(got.badge);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,51 +1,61 @@
|
||||||
//! The block-device wire protocol — what a filesystem (the FAT server) says to a
|
//! The block-device wire protocol — what a filesystem (the FAT server) says to a
|
||||||
//! block driver (usb-storage) over its well-known `.block` endpoint. A protocol
|
//! block driver (usb-storage) over `/protocol/block`. Defined through the
|
||||||
//! module like vfs-protocol / usb-transfer-protocol: extern-struct messages, an
|
//! envelope, so every packet begins with the folded `Header`.
|
||||||
//! `Operation` tag, everything in one IPC message.
|
//!
|
||||||
|
//! **`Header.target` is always 0 here**: a block driver instance serves exactly
|
||||||
|
//! one device over its own endpoint, so there is no object within the peer to
|
||||||
|
//! address. A driver that later fronts several volumes gives them target ids and
|
||||||
|
//! `enumerate` lists them; nothing else about the protocol changes.
|
||||||
//!
|
//!
|
||||||
//! Data path: read and write move whole blocks to or from a **caller-owned DMA
|
//! Data path: read and write move whole blocks to or from a **caller-owned DMA
|
||||||
//! buffer**, named by its physical address — the same physical-address handoff
|
//! buffer**, named by its physical address — the same physical-address handoff
|
||||||
//! usb-storage already uses toward the controller, one layer up. So a 512-byte
|
//! usb-storage already uses toward the controller, one layer up. So a 512-byte
|
||||||
//! sector never has to cross the 256-byte IPC boundary; only the small request /
|
//! sector never has to cross the packet floor; only the small request / reply
|
||||||
//! reply headers do. Under an enforcing IOMMU the buffer's physical addresses are
|
//! parts do. Under an enforcing IOMMU the buffer's physical addresses are only
|
||||||
//! only reachable by the device once the filesystem has `attach`ed the buffer's
|
//! reachable by the device once the filesystem has `attach`ed the buffer's
|
||||||
//! capability (the block server forwards it to the controller); see docs/driver-model.md.
|
//! capability (the block server forwards it to the controller); see
|
||||||
|
//! docs/driver-model.md.
|
||||||
|
|
||||||
pub const Operation = enum(u32) {
|
const envelope = @import("envelope");
|
||||||
/// geometry() -> { block_size, block_count }
|
|
||||||
geometry = 0,
|
/// The answer to `geometry()`.
|
||||||
/// read(lba, count, physical): read `count` blocks from `lba` into the buffer
|
pub const Geometry = extern struct {
|
||||||
read = 1,
|
block_size: u32, // bytes per block (512)
|
||||||
/// write(lba, count, physical): write `count` blocks at `lba` from the buffer
|
_padding: u32 = 0,
|
||||||
write = 2,
|
block_count: u64, // total blocks
|
||||||
/// flush(): commit any device write cache to stable media (no data transfer).
|
|
||||||
/// A filesystem calls this to make prior writes durable — e.g. before power-off,
|
|
||||||
/// so a shutdown-time write isn't lost in the USB flash controller's cache.
|
|
||||||
flush = 3,
|
|
||||||
/// attach(): the caller's DMA-region capability rides the call's cap slot; the
|
|
||||||
/// block server forwards it to the controller so the buffer's physical addresses
|
|
||||||
/// (named in later read/write) are reachable by the device under an enforcing
|
|
||||||
/// IOMMU. Call once per buffer before using it in a transfer.
|
|
||||||
attach = 4,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const Request = extern struct {
|
/// `read(lba, count, physical)` / `write(...)`: move `count` blocks between the
|
||||||
operation: u32,
|
/// device and the caller's DMA buffer at `physical`.
|
||||||
reserved: u32 = 0,
|
pub const Transfer = extern struct {
|
||||||
lba: u64,
|
lba: u64,
|
||||||
count: u32, // number of blocks (read/write)
|
count: u32,
|
||||||
reserved2: u32 = 0,
|
_padding: u32 = 0,
|
||||||
physical: u64, // caller's DMA buffer physical address (read/write)
|
physical: u64, // caller's DMA buffer physical address
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const Reply = extern struct {
|
/// How many blocks a transfer actually moved.
|
||||||
status: i32, // 0 on success, negative on failure
|
pub const Transferred = extern struct { count: u32 };
|
||||||
reserved: u32 = 0,
|
|
||||||
block_size: u32, // geometry: bytes per block (512)
|
|
||||||
reserved2: u32 = 0,
|
|
||||||
block_count: u64, // geometry: total blocks; read/write: blocks moved
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const message_maximum: usize = 256;
|
pub const Protocol = envelope.Define(.{
|
||||||
pub const request_size: usize = @sizeOf(Request);
|
.name = "block",
|
||||||
pub const reply_size: usize = @sizeOf(Reply);
|
.version = 1,
|
||||||
|
.operations = &.{
|
||||||
|
.{ .name = "geometry", .reply = Geometry },
|
||||||
|
.{ .name = "read", .request = Transfer, .reply = Transferred },
|
||||||
|
.{ .name = "write", .request = Transfer, .reply = Transferred },
|
||||||
|
// flush(): commit any device write cache to stable media (no data
|
||||||
|
// transfer). A filesystem calls this to make prior writes durable —
|
||||||
|
// before power-off, so a shutdown-time write isn't lost in the USB flash
|
||||||
|
// controller's cache.
|
||||||
|
.{ .name = "flush" },
|
||||||
|
// attach(): the caller's DMA-region capability rides the call's cap
|
||||||
|
// slot; the block server forwards it to the controller so the buffer's
|
||||||
|
// physical addresses (named in later read/write) are reachable by the
|
||||||
|
// device under an enforcing IOMMU. Call once per buffer before using it.
|
||||||
|
.{ .name = "attach" },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
pub const Operation = Protocol.Operation;
|
||||||
|
pub const message_maximum: usize = Protocol.message_maximum;
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,8 @@
|
||||||
//! every conversation depend on the contract by name; neither reaches into the
|
//! every conversation depend on the contract by name; neither reaches into the
|
||||||
//! other's files. Pure flat wire types: no protocol module imports anything.
|
//! other's files. Pure flat wire types: no protocol module imports anything.
|
||||||
//!
|
//!
|
||||||
//! One module here is not a protocol but the shape the others are written in:
|
//! One module here is not a protocol but the shape the others are written in,
|
||||||
|
//! and therefore the one module every other one imports:
|
||||||
//!
|
//!
|
||||||
//! envelope : the packet prefix + comptime Define (docs/os-development/protocol-namespace.md)
|
//! envelope : the packet prefix + comptime Define (docs/os-development/protocol-namespace.md)
|
||||||
//!
|
//!
|
||||||
|
|
@ -19,10 +20,12 @@
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
|
||||||
pub fn build(b: *std.Build) void {
|
pub fn build(b: *std.Build) void {
|
||||||
|
// Not a protocol, hence not `-protocol`: the envelope is what a protocol is
|
||||||
|
// defined *through*, so it is built first and handed to every protocol
|
||||||
|
// below as their one import.
|
||||||
|
const envelope = b.addModule("envelope", .{ .root_source_file = b.path("envelope/envelope.zig") });
|
||||||
|
|
||||||
for ([_]struct { name: []const u8, root: []const u8 }{
|
for ([_]struct { name: []const u8, root: []const u8 }{
|
||||||
// Not a protocol, hence not `-protocol`: the envelope is what a
|
|
||||||
// protocol is defined *through*.
|
|
||||||
.{ .name = "envelope", .root = "envelope/envelope.zig" },
|
|
||||||
.{ .name = "vfs-protocol", .root = "vfs/vfs-protocol.zig" },
|
.{ .name = "vfs-protocol", .root = "vfs/vfs-protocol.zig" },
|
||||||
.{ .name = "input-protocol", .root = "input/input-protocol.zig" },
|
.{ .name = "input-protocol", .root = "input/input-protocol.zig" },
|
||||||
.{ .name = "block-protocol", .root = "block/block-protocol.zig" },
|
.{ .name = "block-protocol", .root = "block/block-protocol.zig" },
|
||||||
|
|
@ -32,21 +35,38 @@ pub fn build(b: *std.Build) void {
|
||||||
.{ .name = "scanout-protocol", .root = "scanout/scanout-protocol.zig" },
|
.{ .name = "scanout-protocol", .root = "scanout/scanout-protocol.zig" },
|
||||||
.{ .name = "power-protocol", .root = "power/power-protocol.zig" },
|
.{ .name = "power-protocol", .root = "power/power-protocol.zig" },
|
||||||
}) |protocol| {
|
}) |protocol| {
|
||||||
_ = b.addModule(protocol.name, .{ .root_source_file = b.path(protocol.root) });
|
_ = b.addModule(protocol.name, .{
|
||||||
|
.root_source_file = b.path(protocol.root),
|
||||||
|
.imports = &.{.{ .name = "envelope", .module = envelope }},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Standalone `zig build test` for this domain alone; the root build keeps
|
// Standalone `zig build test` for this domain alone; the root build keeps
|
||||||
// its aggregate test step.
|
// its aggregate test step.
|
||||||
const test_step = b.step("test", "Run the protocol unit tests");
|
const test_step = b.step("test", "Run the protocol unit tests");
|
||||||
|
// The envelope tests itself with no import of its own — everything else
|
||||||
|
// imports it, so it is built separately rather than importing itself.
|
||||||
|
const envelope_tests = b.addTest(.{
|
||||||
|
.root_module = b.createModule(.{
|
||||||
|
.root_source_file = b.path("envelope/envelope.zig"), // framing round trips, verb numbering, dispatch, the floors
|
||||||
|
.target = b.resolveTargetQuery(.{}),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
test_step.dependOn(&b.addRunArtifact(envelope_tests).step);
|
||||||
|
|
||||||
for ([_][]const u8{
|
for ([_][]const u8{
|
||||||
"envelope/envelope.zig", // framing round trips, verb numbering, dispatch, the floors
|
|
||||||
"vfs/vfs-protocol.zig", // NodeKind / DirectoryEntry sizes + op values
|
"vfs/vfs-protocol.zig", // NodeKind / DirectoryEntry sizes + op values
|
||||||
|
"input/input-protocol.zig", // event numbering + the push-floor budget
|
||||||
"display/display-protocol.zig", // pack(): native pixel encoding per format
|
"display/display-protocol.zig", // pack(): native pixel encoding per format
|
||||||
|
"device-manager/device-manager-protocol.zig", // the dual-use report, exactly on the push floor
|
||||||
|
"power/power-protocol.zig", // the event kind as the packet's verb
|
||||||
|
"usb-transfer/usb-transfer-protocol.zig", // the tail-carried control stage + the trimmed report
|
||||||
}) |root| {
|
}) |root| {
|
||||||
const protocol_tests = b.addTest(.{
|
const protocol_tests = b.addTest(.{
|
||||||
.root_module = b.createModule(.{
|
.root_module = b.createModule(.{
|
||||||
.root_source_file = b.path(root),
|
.root_source_file = b.path(root),
|
||||||
.target = b.resolveTargetQuery(.{}),
|
.target = b.resolveTargetQuery(.{}),
|
||||||
|
.imports = &.{.{ .name = "envelope", .module = envelope }},
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
test_step.dependOn(&b.addRunArtifact(protocol_tests).step);
|
test_step.dependOn(&b.addRunArtifact(protocol_tests).step);
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,48 @@
|
||||||
//! The device-manager protocol (docs/device-manager.md): what drivers and
|
//! The device-manager protocol (docs/device-driver-development/device-manager.md):
|
||||||
//! applications say to the device manager over its well-known endpoint. The
|
//! what drivers and applications say to the device manager over
|
||||||
//! vfs-protocol pattern — extern-struct messages, a version in the handshake,
|
//! `/protocol/device-manager`. Defined through the envelope
|
||||||
//! reserved fields — so both sides depend on the contract by name. Deliberately
|
//! (docs/os-development/protocol-namespace.md), so every packet — request, reply,
|
||||||
//! contains nothing lifecycle-shaped: stopping, liveness (the zero-length ping),
|
//! and pushed event alike — begins with the folded `Header`.
|
||||||
//! and exit reasons are the universal vocabulary of
|
//!
|
||||||
|
//! **`Header.target` is the device id.** It was the `device_id` field of three
|
||||||
|
//! different messages; folding it into the header is what made the packed
|
||||||
|
//! leading operation byte disappear along with it. `no_device` addresses a
|
||||||
|
//! driver that serves no enumerated device.
|
||||||
|
//!
|
||||||
|
//! Two of the manager's four old operations were the reserved verbs under
|
||||||
|
//! another name and are gone from this protocol's own numbering: `enumerate`
|
||||||
|
//! (the tree, one `ChildEntry` per record in the reply tail) and `subscribe`
|
||||||
|
//! (the watcher's endpoint rides as the call's capability). What is left is the
|
||||||
|
//! driver-facing half — the handshake and the two tree reports.
|
||||||
|
//!
|
||||||
|
//! **`ChildAdded` travels in both directions, and says so twice.** A bus driver
|
||||||
|
//! *calls* `child_added` to report a device; the manager then *pushes* the same
|
||||||
|
//! struct to every subscriber as the `child_added` event. Operations and events
|
||||||
|
//! are numbered in separate spaces, so one struct under two numbers is exactly
|
||||||
|
//! how the envelope spells "one encoding, both directions" — and the direction
|
||||||
|
//! (call vs. send) already tells them apart.
|
||||||
|
//!
|
||||||
|
//! Deliberately contains nothing lifecycle-shaped: stopping, liveness (the
|
||||||
|
//! zero-length ping), and exit reasons are the universal vocabulary of
|
||||||
//! docs/process-lifecycle.md, not this protocol.
|
//! docs/process-lifecycle.md, not this protocol.
|
||||||
|
|
||||||
/// The protocol version a driver states in its hello. A manager that cannot
|
const std = @import("std");
|
||||||
/// serve a driver's version refuses the hello, and the mismatch is loud at
|
const envelope = @import("envelope");
|
||||||
/// startup instead of quiet corruption later.
|
|
||||||
pub const version: u16 = 1;
|
/// The protocol version a driver states in its hello, and the version this
|
||||||
|
/// contract answers `describe` with. A manager that cannot serve a driver's
|
||||||
|
/// version refuses the hello, and the mismatch is loud at startup instead of
|
||||||
|
/// quiet corruption later.
|
||||||
|
///
|
||||||
|
/// `describe` publishes the same number, but it cannot replace this: it tells a
|
||||||
|
/// *client* what the provider is, and here it is the **provider** that has to
|
||||||
|
/// learn what the client was built against in order to refuse it.
|
||||||
|
pub const version = 1;
|
||||||
|
|
||||||
|
/// `Header.target` for a driver that serves no enumerated device (a test
|
||||||
|
/// fixture, a synthetic source), and `ChildAdded`'s answer for a leaf that was
|
||||||
|
/// never `device_register`ed.
|
||||||
|
pub const no_device: u64 = ~@as(u64, 0);
|
||||||
|
|
||||||
/// Which bus a `child_added` came from — stated by the reporting bus driver so
|
/// Which bus a `child_added` came from — stated by the reporting bus driver so
|
||||||
/// the manager's /system/configuration/devices.csv matcher knows how to read the report's identity
|
/// the manager's /system/configuration/devices.csv matcher knows how to read the report's identity
|
||||||
|
|
@ -24,7 +57,7 @@ pub const BusKind = enum(u8) {
|
||||||
acpi = 3,
|
acpi = 3,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// What kind of driver is talking (docs/driver-model.md's shapes).
|
/// What kind of driver is talking (docs/device-driver-development/driver-model.md's shapes).
|
||||||
pub const Role = enum(u8) {
|
pub const Role = enum(u8) {
|
||||||
/// Owns a controller and reports the devices behind it (`child_added`).
|
/// Owns a controller and reports the devices behind it (`child_added`).
|
||||||
bus = 1,
|
bus = 1,
|
||||||
|
|
@ -32,58 +65,45 @@ pub const Role = enum(u8) {
|
||||||
device = 2,
|
device = 2,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// The message kinds.
|
// --- the per-operation request parts ----------------------------------------
|
||||||
pub const Operation = enum(u8) {
|
//
|
||||||
hello = 1,
|
// Each names the bytes AFTER the prefix. Nothing here carries an operation or a
|
||||||
child_added = 2,
|
// device id: those are the packet header's, folded in once. No reply part
|
||||||
child_removed = 3,
|
// carries a status either — that is the `Status` every reply already begins
|
||||||
enumerate = 4,
|
// with, so the manager's old three `{status, reserved}` reply structs are gone.
|
||||||
subscribe = 5,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// `Hello.device_id` for a driver that serves no enumerated device (a test
|
|
||||||
/// fixture, a synthetic source).
|
|
||||||
pub const no_device: u64 = ~@as(u64, 0);
|
|
||||||
|
|
||||||
/// The handshake, sent once by every driver the manager spawns — the manager's
|
/// The handshake, sent once by every driver the manager spawns — the manager's
|
||||||
/// one self-enforced deadline: spawned and silent past it means wrong binary,
|
/// one self-enforced deadline: spawned and silent past it means wrong binary,
|
||||||
/// wrong version, or wedged before main, and the stop sequence follows.
|
/// wrong version, or wedged before main, and the stop sequence follows. The
|
||||||
|
/// device this driver was assigned (its argv[1]) is `Header.target`.
|
||||||
pub const Hello = extern struct {
|
pub const Hello = extern struct {
|
||||||
operation: u8 = @intFromEnum(Operation.hello),
|
/// A `Role` value.
|
||||||
/// A Role value.
|
|
||||||
role: u8,
|
role: u8,
|
||||||
|
_padding: u8 = 0,
|
||||||
/// The protocol version this driver was built against (`version`).
|
/// The protocol version this driver was built against (`version`).
|
||||||
version: u16 = version,
|
version: u16 = version,
|
||||||
reserved: u32 = 0,
|
|
||||||
/// The device this driver was assigned (its argv[1]), or `no_device`.
|
|
||||||
device_id: u64,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const hello_size = @sizeOf(Hello);
|
|
||||||
|
|
||||||
/// The manager's answer to a hello. Nonzero status = refused (version mismatch,
|
|
||||||
/// unknown sender); a refused driver should exit cleanly.
|
|
||||||
pub const HelloReply = extern struct {
|
|
||||||
status: i32,
|
|
||||||
reserved: u32 = 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const reply_size = @sizeOf(HelloReply);
|
|
||||||
|
|
||||||
/// A bus driver reporting one device it discovered behind its controller
|
/// A bus driver reporting one device it discovered behind its controller
|
||||||
/// (docs/device-manager.md "the tree"). Identity is the bus's native language —
|
/// (docs/device-driver-development/device-manager.md "the tree"), and the payload
|
||||||
/// for USB a port-speed class; the (class, subclass, protocol) triple joins it
|
/// the manager pushes to its subscribers for the same event. Identity is the
|
||||||
/// once control transfers exist (the USB track). The manager mirrors the child
|
/// bus's native language — for USB a port-speed class, for PCI the class triple.
|
||||||
/// into its tree; when the reporting driver dies, the manager prunes everything
|
/// The manager mirrors the child into its tree; when the reporting driver dies,
|
||||||
/// it reported (the children describe protocol state that died with it) and the
|
/// the manager prunes everything it reported (the children describe protocol
|
||||||
/// restarted instance rediscovers and re-reports.
|
/// state that died with it) and the restarted instance rediscovers and
|
||||||
|
/// re-reports.
|
||||||
|
///
|
||||||
|
/// `Header.target` is the kernel device id this child was `device_register`ed
|
||||||
|
/// as — what the manager hands a matched driver as its argv assignment — or
|
||||||
|
/// `no_device` for an unregistered leaf (a USB port before the descriptor
|
||||||
|
/// track). That is the field that used to sit at the end of this struct.
|
||||||
|
///
|
||||||
|
/// **The field order is the size budget.** An event packet is the header plus
|
||||||
|
/// this, within 64 bytes, and three `u64`s round the whole struct up to a
|
||||||
|
/// multiple of eight whatever order they sit in — so the small fields are
|
||||||
|
/// packed tail-first into the space the rounding pays for anyway. `Define`
|
||||||
|
/// checks the result; this comment is why there is no slack in it.
|
||||||
pub const ChildAdded = extern struct {
|
pub const ChildAdded = extern struct {
|
||||||
operation: u8 = @intFromEnum(Operation.child_added),
|
|
||||||
/// A `BusKind` value: which bus reported this child, so the manager reads the
|
|
||||||
/// identity in the right namespace and matches against the right `bus` column.
|
|
||||||
bus: u8 = @intFromEnum(BusKind.unknown),
|
|
||||||
reserved1: u16 = 0,
|
|
||||||
reserved2: u32 = 0,
|
|
||||||
/// The reporting driver's own device (the controller) — the child's parent.
|
/// The reporting driver's own device (the controller) — the child's parent.
|
||||||
parent: u64,
|
parent: u64,
|
||||||
/// Where on the bus (for USB: the root port number, 1-based).
|
/// Where on the bus (for USB: the root port number, 1-based).
|
||||||
|
|
@ -91,10 +111,10 @@ pub const ChildAdded = extern struct {
|
||||||
/// Bus-specific identity (for USB: the PORTSC port-speed class; for PCI:
|
/// Bus-specific identity (for USB: the PORTSC port-speed class; for PCI:
|
||||||
/// the class triple; for ACPI devices, 0 — identity is the hid below).
|
/// the class triple; for ACPI devices, 0 — identity is the hid below).
|
||||||
identity: u64,
|
identity: u64,
|
||||||
/// The kernel device id this child was `device_register`ed as — what the
|
/// The PCI subsystem id, packed `(subsystem_vendor << 16) | subsystem_device`
|
||||||
/// manager hands a matched driver as its argv assignment — or `no_device`
|
/// (so it reads vendor-first, matching the CSV's `ssvid:ssid`), or 0 when the
|
||||||
/// for an unregistered leaf (a USB port before the descriptor track).
|
/// device has no subsystem id (a bridge, or a non-PCI bus).
|
||||||
device_id: u64 = no_device,
|
subsystem: u32 = 0,
|
||||||
/// The vendor id (PCI vendor / USB idVendor), or 0 when the bus has no such
|
/// The vendor id (PCI vendor / USB idVendor), or 0 when the bus has no such
|
||||||
/// concept (ACPI). Carried so the manager's /system/configuration/devices.csv matcher can bind
|
/// concept (ACPI). Carried so the manager's /system/configuration/devices.csv matcher can bind
|
||||||
/// on vendor — a level the bus-native `identity` (a class triple) cannot express.
|
/// on vendor — a level the bus-native `identity` (a class triple) cannot express.
|
||||||
|
|
@ -103,73 +123,118 @@ pub const ChildAdded = extern struct {
|
||||||
/// level: this is what lets one virtio-gpu (1AF4:1050) be told from any other
|
/// level: this is what lets one virtio-gpu (1AF4:1050) be told from any other
|
||||||
/// virtio display function without the driver re-confirming after it is spawned.
|
/// virtio display function without the driver re-confirming after it is spawned.
|
||||||
device: u16 = 0,
|
device: u16 = 0,
|
||||||
/// The PCI subsystem id, packed `(subsystem_vendor << 16) | subsystem_device`
|
|
||||||
/// (so it reads vendor-first, matching the CSV's `ssvid:ssid`), or 0 when the
|
|
||||||
/// device has no subsystem id (a bridge, or a non-PCI bus).
|
|
||||||
subsystem: u32 = 0,
|
|
||||||
/// The ACPI hardware id (`_HID`), EISA-decoded (e.g. "PNP0303"), for devices
|
/// The ACPI hardware id (`_HID`), EISA-decoded (e.g. "PNP0303"), for devices
|
||||||
/// discovered by firmware string rather than a numeric bus identity. Empty
|
/// discovered by firmware string rather than a numeric bus identity. Empty
|
||||||
/// (all zero) otherwise. Widens for FDT `compatible` strings later.
|
/// (all zero) otherwise. Widens for FDT `compatible` strings later.
|
||||||
hid: [8]u8 = .{0} ** 8,
|
hid: [8]u8 = .{0} ** 8,
|
||||||
|
/// A `BusKind` value: which bus reported this child, so the manager reads the
|
||||||
|
/// identity in the right namespace and matches against the right `bus` column.
|
||||||
|
bus: u8 = @intFromEnum(BusKind.unknown),
|
||||||
|
_padding: [7]u8 = .{0} ** 7,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const child_added_size = @sizeOf(ChildAdded);
|
/// A bus driver reporting a device gone (hot-unplug), and the payload pushed to
|
||||||
|
/// subscribers for it.
|
||||||
/// A bus driver reporting a device gone (hot-unplug). Not yet sent by any
|
///
|
||||||
/// driver — the port scan has no unplug interrupt — but the manager handles it;
|
/// **This is the one message whose target stays 0.** A removal is addressed by
|
||||||
/// death-pruning covers removal until hotplug lands.
|
/// the composite (parent, bus address) — the reporter knows where the device
|
||||||
|
/// *was*, not necessarily what id it had been registered under — and a single
|
||||||
|
/// `u64` cannot carry a pair. So the address stays in the payload, where it
|
||||||
|
/// always was, and the header addresses the provider itself.
|
||||||
pub const ChildRemoved = extern struct {
|
pub const ChildRemoved = extern struct {
|
||||||
operation: u8 = @intFromEnum(Operation.child_removed),
|
|
||||||
reserved0: u8 = 0,
|
|
||||||
reserved1: u16 = 0,
|
|
||||||
reserved2: u32 = 0,
|
|
||||||
parent: u64,
|
parent: u64,
|
||||||
bus_address: u64,
|
bus_address: u64,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const child_removed_size = @sizeOf(ChildRemoved);
|
/// One record of the reserved `enumerate` reply: the manager's mirror, one
|
||||||
|
/// entry per known child, packed into the reply tail. The count is
|
||||||
/// The manager's answer to a tree report.
|
/// `Status.len / @sizeOf(ChildEntry)` — the envelope's reply length says how
|
||||||
pub const ReportReply = extern struct {
|
/// many arrived, so no count header is spent on saying it twice.
|
||||||
status: i32,
|
|
||||||
reserved: u32 = 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// An application asking for the tree (M18.3): the reply is an EnumerateReply
|
|
||||||
/// header followed by `count` ChildEntry records.
|
|
||||||
pub const Enumerate = extern struct {
|
|
||||||
operation: u8 = @intFromEnum(Operation.enumerate),
|
|
||||||
reserved0: u8 = 0,
|
|
||||||
reserved1: u16 = 0,
|
|
||||||
reserved2: u32 = 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const EnumerateReply = extern struct {
|
|
||||||
status: i32,
|
|
||||||
/// ChildEntry records following this header.
|
|
||||||
count: u32,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const ChildEntry = extern struct {
|
pub const ChildEntry = extern struct {
|
||||||
parent: u64,
|
parent: u64,
|
||||||
bus_address: u64,
|
bus_address: u64,
|
||||||
identity: u64,
|
identity: u64,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// An application subscribing to published add/remove events (the input-service
|
/// How many `ChildEntry` records one `enumerate` reply can carry. Paging joins
|
||||||
/// pattern): the subscriber's endpoint rides as the call's **capability**, and
|
/// the protocol if a tree ever outgrows one packet.
|
||||||
/// events arrive on it as buffered messages whose payload is the same
|
pub const entries_per_reply: usize = (envelope.packet_maximum - envelope.prefix_size) / @sizeOf(ChildEntry);
|
||||||
/// ChildAdded / ChildRemoved struct the bus drivers send — one encoding, both
|
|
||||||
/// directions.
|
|
||||||
pub const Subscribe = extern struct {
|
|
||||||
operation: u8 = @intFromEnum(Operation.subscribe),
|
|
||||||
reserved0: u8 = 0,
|
|
||||||
reserved1: u16 = 0,
|
|
||||||
reserved2: u32 = 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Upper bound on any message in this protocol — sizes the endpoint buffers.
|
pub const Protocol = envelope.Define(.{
|
||||||
/// Capped by the kernel's IPC MESSAGE_MAXIMUM (256): an EnumerateReply carries
|
.name = "device-manager",
|
||||||
/// up to ten ChildEntry records per call, plenty for the mirror's current
|
.version = version,
|
||||||
/// bounds; paging joins the protocol if a tree ever outgrows one message.
|
.operations = &.{
|
||||||
pub const message_maximum = 256;
|
// 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,93 +1,137 @@
|
||||||
//! The display wire protocol — what a client says to the display service over its
|
//! The display wire protocol — what a client says to the display service over
|
||||||
//! well-known `.display` endpoint. extern-struct messages with an `Operation` tag, the
|
//! `/protocol/display`. The compositor owns the framebuffer and an ordered stack of
|
||||||
//! same shape as block/vfs/input protocols. The compositor owns the framebuffer and an
|
//! **layers**; a client creates layers, draws into them with these operations, marks damage,
|
||||||
//! ordered stack of **layers**; a client creates layers, draws into them with these
|
//! and asks for a `present`. v1 surfaces are server-owned (a client draws by command);
|
||||||
//! operations, marks damage, and asks for a `present`. v1 surfaces are server-owned (a
|
//! shared-memory surfaces are a later milestone (docs/display.md).
|
||||||
//! client draws by command); shared-memory surfaces are a later milestone (docs/display.md).
|
//!
|
||||||
|
//! **`Header.target` is the layer** on every verb that names one — the field that used to be
|
||||||
|
//! `Request.layer`. `info`, `present`, `set_mode`, `get_modes` and `attach_scanout` address
|
||||||
|
//! the compositor itself, so they leave it 0.
|
||||||
|
//!
|
||||||
|
//! Every verb carries its own request type. The single overloaded 40-byte request this
|
||||||
|
//! protocol used to have is gone, and with it the field abuse it invited: `attach_scanout`
|
||||||
|
//! spent `x` on a stride, `y` on a refresh rate and `colour` on a pixel format, which no
|
||||||
|
//! reader could have guessed and no compiler could have caught.
|
||||||
|
|
||||||
|
const envelope = @import("envelope");
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
|
||||||
pub const Operation = enum(u32) {
|
/// The answer to `info()`: the display's current mode.
|
||||||
/// info() -> { width, height, pitch, format }: the display's current mode.
|
pub const Info = extern struct {
|
||||||
info = 0,
|
|
||||||
/// create_layer(x, y, width, height, z) -> { layer }: a new server-owned surface.
|
|
||||||
create_layer = 1,
|
|
||||||
/// configure_layer(layer, x, y, z, visible): move, restack, show, or hide a layer.
|
|
||||||
configure_layer = 2,
|
|
||||||
/// destroy_layer(layer): release a layer.
|
|
||||||
destroy_layer = 3,
|
|
||||||
/// fill_rect(layer, x, y, width, height, colour): fill a rectangle of a layer.
|
|
||||||
fill_rect = 4,
|
|
||||||
/// blit_tile(layer, x, y, width, height, <inline pixels>): copy a small pixel tile in.
|
|
||||||
blit_tile = 5,
|
|
||||||
/// damage(layer, x, y, width, height): mark a region dirty for the next present.
|
|
||||||
damage = 6,
|
|
||||||
/// present(): composite the dirty layers and flush to the screen.
|
|
||||||
present = 7,
|
|
||||||
/// attach_scanout(x=stride, y=refresh_hz, width, height, colour=format) + <surface
|
|
||||||
/// capability>: a native scanout driver announces itself, handing over the shared scanout
|
|
||||||
/// surface as an `ipc_call` send_cap. The compositor maps it, looks up the driver's
|
|
||||||
/// `.scanout` present channel, and upgrades off the GOP floor (docs/display-v2.md V4).
|
|
||||||
/// `x` is the surface's row stride in pixels, `y` the panel refresh rate from the
|
|
||||||
/// driver's EDID read (0 = unknown; paces the compositor's frame clock), `colour` the
|
|
||||||
/// DisplayFormat.
|
|
||||||
attach_scanout = 8,
|
|
||||||
/// set_mode(width, height): change the display resolution — only a native backend that
|
|
||||||
/// reports `canModeSet` honours it; on the GOP floor it fails (docs/display-v2.md V5).
|
|
||||||
set_mode = 9,
|
|
||||||
/// get_modes() -> ModesReply: the resolutions the display can switch to (empty on GOP).
|
|
||||||
get_modes = 10,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// The fixed request header. A `blit_tile`'s pixel payload (width*height 32-bit pixels)
|
|
||||||
/// follows this header inline in the same message, up to `maximum_payload`.
|
|
||||||
pub const Request = extern struct {
|
|
||||||
operation: u32,
|
|
||||||
layer: u32 = 0, // create/configure/destroy/fill/blit/damage: the target layer
|
|
||||||
x: u32 = 0,
|
|
||||||
y: u32 = 0,
|
|
||||||
width: u32 = 0,
|
width: u32 = 0,
|
||||||
height: u32 = 0,
|
height: u32 = 0,
|
||||||
z: u32 = 0, // create_layer / configure_layer: stacking order (higher = in front)
|
pitch: u32 = 0, // bytes per row (may exceed width*4)
|
||||||
colour: u32 = 0, // fill_rect: the fill colour (native pixel value)
|
|
||||||
visible: u32 = 1, // configure_layer: 0 hides the layer
|
|
||||||
reserved: u32 = 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const Reply = extern struct {
|
|
||||||
status: i32, // 0 on success, negative on failure
|
|
||||||
reserved: u32 = 0,
|
|
||||||
// info():
|
|
||||||
width: u32 = 0,
|
|
||||||
height: u32 = 0,
|
|
||||||
pitch: u32 = 0,
|
|
||||||
format: u32 = 0, // a device-abi DisplayFormat value (0 = rgbx, 1 = bgrx)
|
format: u32 = 0, // a device-abi DisplayFormat value (0 = rgbx, 1 = bgrx)
|
||||||
// create_layer():
|
|
||||||
layer: u32 = 0,
|
|
||||||
reserved2: u32 = 0,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// `create_layer(...)`: a new server-owned surface. Coordinates are signed — a layer may sit
|
||||||
|
/// partly off-screen.
|
||||||
|
pub const CreateLayer = extern struct {
|
||||||
|
x: i32,
|
||||||
|
y: i32,
|
||||||
|
width: u32,
|
||||||
|
height: u32,
|
||||||
|
z: u32 = 0, // stacking order (higher = nearer the front)
|
||||||
|
visible: u32 = 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// The layer a `create_layer` established — the integer later packets put in `Header.target`.
|
||||||
|
pub const Created = extern struct { layer: u32 };
|
||||||
|
|
||||||
|
/// `configure_layer(...)` on `Header.target`: move, restack, show, or hide it.
|
||||||
|
pub const ConfigureLayer = extern struct {
|
||||||
|
x: i32,
|
||||||
|
y: i32,
|
||||||
|
z: u32 = 0,
|
||||||
|
visible: u32 = 1, // 0 hides the layer
|
||||||
|
};
|
||||||
|
|
||||||
|
/// `fill_rect(...)` on `Header.target`: fill a layer-local rectangle with a native pixel value.
|
||||||
|
pub const FillRect = extern struct {
|
||||||
|
x: i32,
|
||||||
|
y: i32,
|
||||||
|
width: u32,
|
||||||
|
height: u32,
|
||||||
|
colour: u32,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// `blit_tile(...)` on `Header.target`: copy a `width`×`height` tile of native pixels
|
||||||
|
/// (row-major, little-endian) into the layer. The pixels ride inline as the packet's tail,
|
||||||
|
/// up to `maximum_payload`.
|
||||||
|
pub const BlitTile = extern struct {
|
||||||
|
x: i32,
|
||||||
|
y: i32,
|
||||||
|
width: u32,
|
||||||
|
height: u32,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// `damage(...)` on `Header.target`: mark a layer-local region dirty for the next present.
|
||||||
|
pub const Damage = extern struct {
|
||||||
|
x: i32,
|
||||||
|
y: i32,
|
||||||
|
width: u32,
|
||||||
|
height: u32,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// `attach_scanout(...)` + the shared surface as the call's capability: a native scanout
|
||||||
|
/// driver announces itself. The compositor maps the surface, opens the driver's
|
||||||
|
/// `/protocol/scanout` present channel, and upgrades off the GOP floor (docs/display-v2.md
|
||||||
|
/// V4). Each field says what it is, which the old shared request could not.
|
||||||
|
pub const AttachScanout = extern struct {
|
||||||
|
/// The surface's row stride in pixels (it is sized to the driver's largest mode).
|
||||||
|
stride: u32,
|
||||||
|
/// The active mode within that surface.
|
||||||
|
width: u32,
|
||||||
|
height: u32,
|
||||||
|
/// A device-abi DisplayFormat value.
|
||||||
|
format: u32,
|
||||||
|
/// The panel refresh rate from the driver's EDID read (0 = unknown); it paces the
|
||||||
|
/// compositor's frame clock.
|
||||||
|
refresh_hz: u32 = 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// `set_mode(width, height)`: change the display resolution — only a native backend that
|
||||||
|
/// reports `canModeSet` honours it; on the GOP floor it fails (docs/display-v2.md V5).
|
||||||
|
pub const SetMode = extern struct { width: u32, height: u32 };
|
||||||
|
|
||||||
/// One selectable display mode.
|
/// One selectable display mode.
|
||||||
pub const Mode = extern struct { width: u32, height: u32 };
|
pub const Mode = extern struct { width: u32, height: u32 };
|
||||||
pub const max_modes = 4;
|
pub const max_modes = 4;
|
||||||
|
|
||||||
/// The reply to `get_modes`: a small fixed list of resolutions the display can switch to.
|
/// The answer to `get_modes`: the resolutions the display can switch to (empty on GOP).
|
||||||
pub const ModesReply = extern struct {
|
pub const Modes = extern struct {
|
||||||
status: i32,
|
count: u32 = 0,
|
||||||
count: u32,
|
_padding: u32 = 0,
|
||||||
modes: [max_modes]Mode,
|
modes: [max_modes]Mode = @splat(.{ .width = 0, .height = 0 }),
|
||||||
};
|
};
|
||||||
pub const modes_reply_size: usize = @sizeOf(ModesReply);
|
|
||||||
|
|
||||||
/// The IPC message size — the kernel caps every message at `MESSAGE_MAXIMUM` (256 bytes,
|
pub const Protocol = envelope.Define(.{
|
||||||
/// system/kernel/ipc-synchronous.zig), so this matches it (a larger receive/reply buffer
|
.name = "display",
|
||||||
/// is rejected with -E2BIG). A `blit_tile` therefore carries only a *small* tile inline —
|
.version = 1,
|
||||||
/// `maximum_payload` bytes = up to 54 pixels, enough for a cursor or small sprite; larger
|
.operations = &.{
|
||||||
/// bitmaps are the deferred shared-memory surface path (docs/display.md).
|
.{ .name = "info", .reply = Info },
|
||||||
pub const message_maximum: usize = 256;
|
.{ .name = "create_layer", .request = CreateLayer, .reply = Created },
|
||||||
pub const request_size: usize = @sizeOf(Request);
|
.{ .name = "configure_layer", .request = ConfigureLayer },
|
||||||
pub const reply_size: usize = @sizeOf(Reply);
|
.{ .name = "destroy_layer" },
|
||||||
pub const maximum_payload: usize = message_maximum - request_size;
|
.{ .name = "fill_rect", .request = FillRect },
|
||||||
|
.{ .name = "blit_tile", .request = BlitTile },
|
||||||
|
.{ .name = "damage", .request = Damage },
|
||||||
|
.{ .name = "present" },
|
||||||
|
.{ .name = "attach_scanout", .request = AttachScanout },
|
||||||
|
.{ .name = "set_mode", .request = SetMode },
|
||||||
|
.{ .name = "get_modes", .reply = Modes },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
pub const Operation = Protocol.Operation;
|
||||||
|
pub const message_maximum: usize = Protocol.message_maximum;
|
||||||
|
|
||||||
|
/// The largest inline pixel tile a `blit_tile` may carry: the call floor less the header and
|
||||||
|
/// this verb's own fixed part — 224 bytes, up to 56 pixels, enough for a cursor or a small
|
||||||
|
/// sprite. Larger bitmaps are the deferred shared-memory surface path (docs/display.md).
|
||||||
|
/// Per-verb rather than protocol-wide, because with per-operation requests there is no
|
||||||
|
/// single "request size" to subtract any more.
|
||||||
|
pub const maximum_payload: usize = envelope.packet_maximum - envelope.prefix_size - @sizeOf(BlitTile);
|
||||||
|
|
||||||
/// Pack an 8-bit-per-channel colour into the display's native 32-bit pixel for `format`
|
/// Pack an 8-bit-per-channel colour into the display's native 32-bit pixel for `format`
|
||||||
/// (a device-abi `DisplayFormat`: 0 = rgbx, 1 = bgrx). Shared so a `colour` in a
|
/// (a device-abi `DisplayFormat`: 0 = rgbx, 1 = bgrx). Shared so a `colour` in a
|
||||||
|
|
@ -113,3 +157,15 @@ test "pack encodes native byte order for rgbx and bgrx" {
|
||||||
try std.testing.expectEqual(@as(u32, 0x00AA_0000), pack(1, 0xAA, 0, 0));
|
try std.testing.expectEqual(@as(u32, 0x00AA_0000), pack(1, 0xAA, 0, 0));
|
||||||
try std.testing.expectEqual(@as(u32, 0x0000_3020), pack(0, 0x20, 0x30, 0)); // green in byte 1
|
try std.testing.expectEqual(@as(u32, 0x0000_3020), pack(0, 0x20, 0x30, 0)); // green in byte 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "the layer rides the header, and the blit tile grew with the split" {
|
||||||
|
var buffer: [message_maximum]u8 = undefined;
|
||||||
|
const pixels = [_]u8{0xFF} ** 16;
|
||||||
|
const packet = Protocol.encodeRequest(.blit_tile, 3, .{ .x = 1, .y = 2, .width = 2, .height = 2 }, &pixels, &buffer).?;
|
||||||
|
try std.testing.expectEqual(@as(u64, 3), envelope.headerOf(packet).?.target);
|
||||||
|
try std.testing.expectEqual(@as(i32, 1), Protocol.decodeRequest(.blit_tile, packet).?.x);
|
||||||
|
try std.testing.expectEqual(@as(usize, 16), Protocol.requestTail(.blit_tile, packet).len);
|
||||||
|
// 216 bytes under the old 40-byte shared request; the header plus this
|
||||||
|
// verb's own four fields is 32.
|
||||||
|
try std.testing.expectEqual(@as(usize, 224), maximum_payload);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,41 @@ pub const operation_subscribe: u32 = 2; // capability = the subscriber's endpoin
|
||||||
pub const operation_unsubscribe: u32 = 3;
|
pub const operation_unsubscribe: u32 = 3;
|
||||||
pub const first_protocol_operation: u32 = 16;
|
pub const first_protocol_operation: u32 = 16;
|
||||||
|
|
||||||
|
/// The optional body of a reserved `subscribe`: **which** of a provider's events
|
||||||
|
/// the subscriber wants, as a bit mask whose meaning the protocol defines (the
|
||||||
|
/// input service's device classes are the model). A reserved verb carries no
|
||||||
|
/// typed request, so this rides the packet's tail — and zero, which is also what
|
||||||
|
/// a subscribe that sent no body at all reads as, means *every* event.
|
||||||
|
///
|
||||||
|
/// The mask lives here rather than in each protocol because the subscriber
|
||||||
|
/// machinery is the service harness's (library/kernel/service.zig): the harness
|
||||||
|
/// records the number, the protocol decides what its bits mean, and neither has
|
||||||
|
/// to know the other.
|
||||||
|
pub const Subscription = extern struct { interest: u32 = 0 };
|
||||||
|
|
||||||
|
/// Frame a `subscribe` request. The subscriber's own endpoint travels as the
|
||||||
|
/// call's *capability*, never in the packet — that is what makes the reverse
|
||||||
|
/// path unforgeable.
|
||||||
|
pub fn encodeSubscribe(interest: u32, buffer: []u8) ?[]u8 {
|
||||||
|
const header = Header{ .operation = operation_subscribe };
|
||||||
|
const body = Subscription{ .interest = interest };
|
||||||
|
return frame(std.mem.asBytes(&header), std.mem.asBytes(&body), &.{}, buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Frame a bare `unsubscribe`: it names no event and no endpoint, because it
|
||||||
|
/// means "every subscription this task holds here" (one task, one voice).
|
||||||
|
pub fn encodeUnsubscribe(buffer: []u8) ?[]u8 {
|
||||||
|
const header = Header{ .operation = operation_unsubscribe };
|
||||||
|
return frame(std.mem.asBytes(&header), &.{}, &.{}, buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The interest mask out of a `subscribe` packet's tail, on the provider's side.
|
||||||
|
/// A caller that sent no mask reads as the every-event mask.
|
||||||
|
pub fn decodeSubscribe(tail: []const u8) Subscription {
|
||||||
|
if (tail.len < @sizeOf(Subscription)) return .{};
|
||||||
|
return std.mem.bytesToValue(Subscription, tail[0..@sizeOf(Subscription)]);
|
||||||
|
}
|
||||||
|
|
||||||
/// The `describe` reply's fixed part, followed inline by `name_len` bytes of the
|
/// The `describe` reply's fixed part, followed inline by `name_len` bytes of the
|
||||||
/// protocol's name. This is the version handshake: the version is asked for
|
/// protocol's name. This is the version handshake: the version is asked for
|
||||||
/// once, at connect time, rather than re-carried by every packet out of a
|
/// once, at connect time, rather than re-carried by every packet out of a
|
||||||
|
|
@ -876,6 +911,21 @@ test "a truncated packet answers -EPROTO" {
|
||||||
// operation with `.request = extern struct { bytes: [241]u8 }`, or an event with
|
// operation with `.request = extern struct { bytes: [241]u8 }`, or an event with
|
||||||
// `.payload = extern struct { bytes: [49]u8 }`, fails to compile with the
|
// `.payload = extern struct { bytes: [49]u8 }`, fails to compile with the
|
||||||
// protocol, the verb, and the two numbers named in the message.
|
// protocol, the verb, and the two numbers named in the message.
|
||||||
|
test "a subscribe carries its interest mask in the reserved verb's tail" {
|
||||||
|
var buffer: [packet_maximum]u8 = undefined;
|
||||||
|
const packet = encodeSubscribe(0b101, &buffer).?;
|
||||||
|
try testing.expectEqual(operation_subscribe, headerOf(packet).?.operation);
|
||||||
|
try testing.expectEqual(@as(u32, 0b101), decodeSubscribe(packet[prefix_size..]).interest);
|
||||||
|
// No body at all — and a body too short to be one — read as "every event",
|
||||||
|
// which is what a subscriber that named nothing wants.
|
||||||
|
try testing.expectEqual(@as(u32, 0), decodeSubscribe(&.{}).interest);
|
||||||
|
try testing.expectEqual(@as(u32, 0), decodeSubscribe(&.{ 1, 2 }).interest);
|
||||||
|
|
||||||
|
const bare = encodeUnsubscribe(&buffer).?;
|
||||||
|
try testing.expectEqual(operation_unsubscribe, headerOf(bare).?.operation);
|
||||||
|
try testing.expectEqual(prefix_size, bare.len);
|
||||||
|
}
|
||||||
|
|
||||||
test "the floor counts the header once, and the boundary is exact" {
|
test "the floor counts the header once, and the boundary is exact" {
|
||||||
try testing.expect(fitsPacket(extern struct { bytes: [240]u8 }));
|
try testing.expect(fitsPacket(extern struct { bytes: [240]u8 }));
|
||||||
try testing.expect(!fitsPacket(extern struct { bytes: [241]u8 }));
|
try testing.expect(!fitsPacket(extern struct { bytes: [241]u8 }));
|
||||||
|
|
|
||||||
|
|
@ -4,25 +4,30 @@
|
||||||
//! **subscriber** (any program) that subscribes and is then pushed each event.
|
//! **subscriber** (any program) that subscribes and is then pushed each event.
|
||||||
//!
|
//!
|
||||||
//! The service handles several device classes over one endpoint. Each class has its own
|
//! The service handles several device classes over one endpoint. Each class has its own
|
||||||
//! typed event (`KeyEvent`, `MouseEvent`, `JoystickEvent`); they all travel in a common
|
//! typed event (`KeyEvent`, `MouseEvent`, `JoystickEvent`); a subscriber declares which
|
||||||
//! `InputEvent` envelope tagged with a `DeviceKind`, so the fan-out path is one code path
|
//! classes it wants with a `device_mask`, and the service routes accordingly.
|
||||||
//! and a subscriber can take a mix of devices on a single stream. A subscriber declares
|
|
||||||
//! which classes it wants with a `device_mask`, and the service routes accordingly.
|
|
||||||
//!
|
//!
|
||||||
//! Two message shapes ride over the endpoint, tagged by `Operation`, like the
|
//! Three shapes ride over the channel, and the envelope names all three
|
||||||
//! [VFS protocol](../vfs/protocol.zig):
|
//! (docs/os-development/protocol-namespace.md):
|
||||||
//!
|
//!
|
||||||
//! - **subscribe / publish**: a synchronous `ipc_call` carrying a `Request`. `subscribe`
|
//! - **subscribe** is the *reserved* verb, not one of this protocol's own: its shape — a
|
||||||
//! hands the service the subscriber's own endpoint as a capability (`send_cap`) and a
|
//! synchronous call whose attached capability is the subscriber's endpoint — is exactly
|
||||||
//! `device_mask`; `publish` carries an `InputEvent`. The reply is a `Reply`.
|
//! what `envelope.operation_subscribe` means everywhere. The interest mask travels as the
|
||||||
//! - **delivery**: the service pushes each `InputEvent` to every interested subscriber with
|
//! packet's tail (`envelope.Subscription`), because a reserved verb carries no typed
|
||||||
//! the asynchronous `ipc_send` — no reply owed, and a dead subscriber can never stall the
|
//! request; what this protocol supplies is the *meaning* of its bits — the device classes.
|
||||||
//! broadcast. Received in the subscriber's buffer with `Received.isMessage()` set.
|
//! - **publish** is this protocol's one verb: a source sends one `InputEvent` and the
|
||||||
|
//! service answers at once, so publishing never blocks on a slow subscriber.
|
||||||
|
//! - **delivery** is an event push: the service `ipc_send`s each event to every interested
|
||||||
|
//! subscriber — no reply owed, so a dead subscriber can never stall the broadcast. The
|
||||||
|
//! packet is the folded header plus the typed event, and **the device class is the
|
||||||
|
//! header's operation**: one event per class, so a subscriber reads the kind from the
|
||||||
|
//! packet rather than from a tag inside the payload.
|
||||||
//!
|
//!
|
||||||
//! This is a danos-native contract, shared by the input service, the `runtime.input`
|
//! `Header.target` is unused (0) in both directions: the service is the only object either
|
||||||
//! client helpers, and every source/subscriber. Everything fits one IPC message.
|
//! side addresses.
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
const envelope = @import("envelope");
|
||||||
|
|
||||||
/// The classes of input device the service fans out. Each names a typed event and a bit in
|
/// The classes of input device the service fans out. Each names a typed event and a bit in
|
||||||
/// the subscription mask.
|
/// the subscription mask.
|
||||||
|
|
@ -242,14 +247,17 @@ pub const JoystickEvent = extern struct {
|
||||||
buttons: u32, // current pressed-button bitmask
|
buttons: u32, // current pressed-button bitmask
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- the common envelope ----------------------------------------------------
|
// --- the tagged union of the three ------------------------------------------
|
||||||
|
|
||||||
/// The largest per-device event, so `InputEvent` can hold any of them inline.
|
/// The largest per-device event, so `InputEvent` can hold any of them inline.
|
||||||
pub const max_event_size: usize = @max(@sizeOf(KeyEvent), @max(@sizeOf(MouseEvent), @sizeOf(JoystickEvent)));
|
pub const max_event_size: usize = @max(@sizeOf(KeyEvent), @max(@sizeOf(MouseEvent), @sizeOf(JoystickEvent)));
|
||||||
|
|
||||||
/// The tagged envelope broadcast to subscribers: a `DeviceKind` plus the raw bytes of the
|
/// One event of any class: a `DeviceKind` plus the raw bytes of the matching per-device
|
||||||
/// matching per-device event. Decode it with `asKeyboard`/`asMouse`/`asJoystick` (each
|
/// event. This is what a source `publish`es (one verb for all three classes) and what a
|
||||||
/// returns null unless `device` matches), or build one with the `from*` constructors.
|
/// subscriber's helper hands back after decoding a delivery — on the *delivery* wire the
|
||||||
|
/// class is the packet header's operation instead, so this tag never travels there. Decode
|
||||||
|
/// it with `asKeyboard`/`asMouse`/`asJoystick` (each returns null unless `device` matches),
|
||||||
|
/// or build one with the `from*` constructors.
|
||||||
pub const InputEvent = extern struct {
|
pub const InputEvent = extern struct {
|
||||||
device: u32, // a DeviceKind
|
device: u32, // a DeviceKind
|
||||||
_padding: u32 = 0,
|
_padding: u32 = 0,
|
||||||
|
|
@ -285,35 +293,88 @@ pub const InputEvent = extern struct {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- request / reply --------------------------------------------------------
|
// --- the contract -----------------------------------------------------------
|
||||||
|
|
||||||
/// Which side of a request this is.
|
pub const Protocol = envelope.Define(.{
|
||||||
pub const Operation = enum(u32) {
|
.name = "input",
|
||||||
subscribe = 0, // register the caller's endpoint (send_cap) for the classes in device_mask
|
.version = 1,
|
||||||
publish = 1, // a source submits `event` to broadcast to interested subscribers
|
.operations = &.{
|
||||||
};
|
// A source submits one event; the service broadcasts it to whoever wants that class.
|
||||||
|
.{ .name = "publish", .request = InputEvent },
|
||||||
|
},
|
||||||
|
.events = &.{
|
||||||
|
// One per device class: the class is the packet's operation, the typed event its
|
||||||
|
// payload. The push floor is 64 bytes and the header spends 16 of them, so the
|
||||||
|
// widest of these — the 28-byte mouse event — leaves the budget with room to spare.
|
||||||
|
.{ .name = "keyboard", .payload = KeyEvent },
|
||||||
|
.{ .name = "mouse", .payload = MouseEvent },
|
||||||
|
.{ .name = "joystick", .payload = JoystickEvent },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
/// Request header. For `subscribe`, `device_mask` is the OR of `device_*` bits the caller
|
pub const Operation = Protocol.Operation;
|
||||||
/// wants (0 means all) and the caller's receive endpoint travels as the call's capability;
|
pub const Event = Protocol.Event;
|
||||||
/// `event` is ignored. For `publish`, `event` is the event to broadcast.
|
pub const message_maximum: usize = Protocol.message_maximum;
|
||||||
pub const Request = extern struct {
|
|
||||||
operation: u32, // an Operation
|
|
||||||
device_mask: u32 = 0, // subscribe: interested device classes (0 => all)
|
|
||||||
event: InputEvent = .{ .device = 0 },
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Reply header. `status` is 0 on success or a negative errno.
|
|
||||||
pub const Reply = extern struct {
|
|
||||||
status: i32,
|
|
||||||
_padding: u32 = 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const request_size: usize = @sizeOf(Request);
|
|
||||||
pub const reply_size: usize = @sizeOf(Reply);
|
|
||||||
pub const event_size: usize = @sizeOf(InputEvent);
|
pub const event_size: usize = @sizeOf(InputEvent);
|
||||||
|
|
||||||
comptime {
|
/// The event class a `DeviceKind` value (as it appears in `InputEvent.device`) is delivered
|
||||||
// The delivery path posts a bare InputEvent through ipc_send, so it must fit an
|
/// as. Null for a value no class claims, which is delivered to nobody.
|
||||||
// endpoint's async payload slot (POST_MAXIMUM is 64).
|
pub fn eventOfDevice(device: u32) ?Event {
|
||||||
if (event_size > 64) @compileError("InputEvent must fit the ipc_send payload (POST_MAXIMUM)");
|
return switch (device) {
|
||||||
|
@intFromEnum(DeviceKind.keyboard) => .keyboard,
|
||||||
|
@intFromEnum(DeviceKind.mouse) => .mouse,
|
||||||
|
@intFromEnum(DeviceKind.joystick) => .joystick,
|
||||||
|
else => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Frame a `subscribe` request: the reserved verb's header, then the interest mask. Null if
|
||||||
|
/// the buffer is too small. The mask itself is the envelope's `Subscription` — the interest
|
||||||
|
/// a reserved subscribe carries is universal, and the *meaning* of its bits (here: the
|
||||||
|
/// device classes above) is what each protocol supplies. Kept as a named helper because
|
||||||
|
/// `device_mask` is what an input caller calls it.
|
||||||
|
pub fn encodeSubscribe(device_mask: u32, buffer: []u8) ?[]u8 {
|
||||||
|
return envelope.encodeSubscribe(device_mask, buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "an event of every class fits the push floor, header included" {
|
||||||
|
// What the hand-rolled comptime assert used to say about `InputEvent`, now
|
||||||
|
// said by `Define` about each typed event — and counting the header, which
|
||||||
|
// the old check did not.
|
||||||
|
try std.testing.expectEqual(envelope.prefix_size + @sizeOf(MouseEvent), Protocol.event_maximum);
|
||||||
|
try std.testing.expect(Protocol.event_maximum <= envelope.post_maximum);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "the verb numbering, and the class an event carries" {
|
||||||
|
try std.testing.expectEqual(@as(u32, 16), @intFromEnum(Operation.publish));
|
||||||
|
// Events number in their own space, so the three classes start at 16 too.
|
||||||
|
try std.testing.expectEqual(@as(u32, 16), @intFromEnum(Event.keyboard));
|
||||||
|
try std.testing.expectEqual(@as(u32, 17), @intFromEnum(Event.mouse));
|
||||||
|
try std.testing.expectEqual(@as(u32, 18), @intFromEnum(Event.joystick));
|
||||||
|
// subscribe is the RESERVED verb, below the protocol range entirely.
|
||||||
|
try std.testing.expectEqual(@as(u32, 2), envelope.operation_subscribe);
|
||||||
|
|
||||||
|
var buffer: [envelope.post_maximum]u8 = undefined;
|
||||||
|
const packet = Protocol.encodeEvent(.mouse, 0, .{
|
||||||
|
.kind = @intFromEnum(MouseEventKind.motion),
|
||||||
|
.button = 0,
|
||||||
|
.dx = 3,
|
||||||
|
.dy = -4,
|
||||||
|
.scroll_x = 0,
|
||||||
|
.scroll_y = 0,
|
||||||
|
.buttons = 0,
|
||||||
|
}, &buffer).?;
|
||||||
|
try std.testing.expectEqual(Event.mouse, Protocol.eventOf(packet).?);
|
||||||
|
try std.testing.expectEqual(@as(i32, -4), Protocol.decodeEvent(.mouse, packet).?.dy);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "a subscribe carries its mask in the tail of the reserved verb" {
|
||||||
|
var buffer: [envelope.packet_maximum]u8 = undefined;
|
||||||
|
const packet = encodeSubscribe(device_mouse, &buffer).?;
|
||||||
|
try std.testing.expectEqual(envelope.operation_subscribe, envelope.headerOf(packet).?.operation);
|
||||||
|
// The provider side of this is the service harness's, which reads the same
|
||||||
|
// interest mask out of the tail for every protocol.
|
||||||
|
try std.testing.expectEqual(device_mouse, envelope.decodeSubscribe(packet[envelope.prefix_size..]).interest);
|
||||||
|
// A caller that sent nothing at all reads as the every-class mask.
|
||||||
|
try std.testing.expectEqual(@as(u32, 0), envelope.decodeSubscribe(&.{}).interest);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,70 +1,104 @@
|
||||||
//! The power protocol (docs/power.md): system power's domain-named surface,
|
//! The power protocol (docs/os-development/power.md): system power's
|
||||||
//! bound at `/protocol/power`. On x86 the acpi service provides it; on ARM a
|
//! domain-named surface, bound at `/protocol/power`. On x86 the acpi service
|
||||||
//! PSCI/mailbox service will bind the same name — subscribers never learn which
|
//! provides it; on ARM a PSCI/mailbox service will bind the same name —
|
||||||
//! firmware they are on (docs/discovery.md — firmware neutrality), which is the
|
//! subscribers never learn which firmware they are on (docs/discovery.md —
|
||||||
//! whole point of naming the contract rather than the provider
|
//! firmware neutrality), which is the whole point of naming the contract rather
|
||||||
//! (docs/os-development/protocol-namespace.md).
|
//! than the provider (docs/os-development/protocol-namespace.md).
|
||||||
//! The vfs-protocol pattern: extern-struct messages, a version, reserved fields.
|
//!
|
||||||
|
//! Defined through the envelope, so every packet begins with the folded
|
||||||
|
//! `Header`. Three shapes ride the channel, and the envelope names all three:
|
||||||
|
//!
|
||||||
|
//! - **subscribe** is the *reserved* verb, not one of this protocol's own: a
|
||||||
|
//! synchronous call whose attached capability is the subscriber's endpoint is
|
||||||
|
//! exactly what `envelope.operation_subscribe` means everywhere.
|
||||||
|
//! - **shutdown** is this protocol's one verb — the only operation that *does*
|
||||||
|
//! something irreversible, and the reason the provider gates it by badge.
|
||||||
|
//! - **the events** are pushes: the service `ipc_send`s each one to every
|
||||||
|
//! subscriber, no reply owed, so a slow or dead subscriber can never wedge the
|
||||||
|
//! source. **The kind is the packet's operation** — one declared event per
|
||||||
|
//! named kind, exactly as the input protocol delivers one per device class —
|
||||||
|
//! so a subscriber reads *what happened* out of the header instead of a tag
|
||||||
|
//! inside the payload. That is what the old `EventMessage`'s two leading bytes
|
||||||
|
//! (an operation byte saying "this is an event", then the kind) fold into.
|
||||||
|
//!
|
||||||
|
//! `Header.target` is unused (0) in both directions: the provider is the only
|
||||||
|
//! object either side addresses. And no packet carries a version any more — the
|
||||||
|
//! reserved `describe` verb is the version handshake, asked once at connect time
|
||||||
|
//! rather than re-carried out of every packet's budget.
|
||||||
|
|
||||||
/// The protocol version a client states nowhere yet — reserved for the day a
|
const std = @import("std");
|
||||||
/// handshake needs it; requests carry it so a mismatch can be refused loudly.
|
const envelope = @import("envelope");
|
||||||
pub const version: u16 = 1;
|
|
||||||
|
|
||||||
pub const Operation = enum(u8) {
|
/// What a published event carries beyond its kind. The kind is the packet's
|
||||||
/// Subscribe to power events: the subscriber's endpoint rides as the
|
/// operation, so nothing here repeats it; `power_button`, `lid`, `ac` and
|
||||||
/// call's capability (the input/device-manager pattern); events arrive on
|
/// `battery` leave both fields zero and are fully described by the verb alone.
|
||||||
/// it as buffered messages carrying an `EventMessage`.
|
pub const Notice = extern struct {
|
||||||
subscribe = 1,
|
/// The device notification code (ACPI `Notify`'s second argument), or 0.
|
||||||
/// Orderly shutdown's last step: enter S5. Accepted only from PID 1
|
|
||||||
/// (init) — the process that has already run the stop sequence over
|
|
||||||
/// everything else.
|
|
||||||
shutdown = 2,
|
|
||||||
/// The published event payload (never sent *to* the service).
|
|
||||||
event = 3,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// What happened. The vocabulary is hardware-neutral: a lid is a lid whether
|
|
||||||
/// ACPI or a PSCI mailbox reported it.
|
|
||||||
pub const Event = enum(u8) {
|
|
||||||
power_button = 1,
|
|
||||||
lid = 2,
|
|
||||||
ac = 3,
|
|
||||||
battery = 4,
|
|
||||||
/// A device notification that maps to none of the named events — the
|
|
||||||
/// `code` and `hid` fields say which device and what code.
|
|
||||||
notify = 5,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const Subscribe = extern struct {
|
|
||||||
operation: u8 = @intFromEnum(Operation.subscribe),
|
|
||||||
reserved0: u8 = 0,
|
|
||||||
version: u16 = version,
|
|
||||||
reserved1: u32 = 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const Shutdown = extern struct {
|
|
||||||
operation: u8 = @intFromEnum(Operation.shutdown),
|
|
||||||
reserved0: u8 = 0,
|
|
||||||
version: u16 = version,
|
|
||||||
reserved1: u32 = 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// A published event, as the buffered-message payload subscribers receive.
|
|
||||||
pub const EventMessage = extern struct {
|
|
||||||
operation: u8 = @intFromEnum(Operation.event),
|
|
||||||
/// An Event value.
|
|
||||||
event: u8,
|
|
||||||
reserved0: u16 = 0,
|
|
||||||
/// The device notification code (Notify's second argument), or 0.
|
|
||||||
code: u32 = 0,
|
code: u32 = 0,
|
||||||
/// The notifying device's hardware id (EISA-decoded), or all zero.
|
/// The notifying device's hardware id (EISA-decoded), or all zero.
|
||||||
hid: [8]u8 = .{0} ** 8,
|
hid: [8]u8 = .{0} ** 8,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const Reply = extern struct {
|
pub const Protocol = envelope.Define(.{
|
||||||
status: i32,
|
.name = "power",
|
||||||
reserved: u32 = 0,
|
.version = 1,
|
||||||
};
|
.operations = &.{
|
||||||
|
// Orderly shutdown's last step: enter S5. Honored only from a
|
||||||
|
// subscriber — init, the process that has already run the stop sequence
|
||||||
|
// over everything else (docs/os-development/power.md, "authority, not
|
||||||
|
// information"). Nothing to say and nothing to answer, so the verb and
|
||||||
|
// the reply's `Status` are the whole exchange.
|
||||||
|
.{ .name = "shutdown" },
|
||||||
|
},
|
||||||
|
.events = &.{
|
||||||
|
// The vocabulary is hardware-neutral: a lid is a lid whether ACPI or a
|
||||||
|
// PSCI mailbox reported it. One event per kind, each carrying the same
|
||||||
|
// `Notice`, because what differs between them is which thing happened —
|
||||||
|
// and that is the header's job now.
|
||||||
|
.{ .name = "power_button", .payload = Notice },
|
||||||
|
.{ .name = "lid", .payload = Notice },
|
||||||
|
.{ .name = "ac", .payload = Notice },
|
||||||
|
.{ .name = "battery", .payload = Notice },
|
||||||
|
// A device notification that maps to none of the named events — the
|
||||||
|
// `code` and `hid` say which device and what happened.
|
||||||
|
.{ .name = "notify", .payload = Notice },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
/// Upper bound on any message in this protocol — sizes endpoint buffers.
|
pub const Operation = Protocol.Operation;
|
||||||
pub const message_maximum = 64;
|
|
||||||
|
/// What happened. The event *is* the kind: this is the generated event
|
||||||
|
/// enumeration, re-exported under the name this protocol has always called its
|
||||||
|
/// vocabulary, with the same members it has always had.
|
||||||
|
pub const Event = Protocol.Event;
|
||||||
|
|
||||||
|
/// What a provider and a subscriber size their buffers to. This module used to
|
||||||
|
/// declare 64 — the *push* floor — which was simply wrong for a protocol whose
|
||||||
|
/// requests ride `ipc_call`: a provider sizing its receive buffer to 64 refuses
|
||||||
|
/// any caller that sends up to the floor it is entitled to.
|
||||||
|
pub const message_maximum: usize = Protocol.message_maximum;
|
||||||
|
|
||||||
|
test "the kind is the verb, and an event fits the push floor" {
|
||||||
|
try std.testing.expectEqual(@as(u32, 16), @intFromEnum(Operation.shutdown));
|
||||||
|
try std.testing.expectEqual(@as(u32, 16), @intFromEnum(Event.power_button));
|
||||||
|
try std.testing.expectEqual(@as(u32, 17), @intFromEnum(Event.lid));
|
||||||
|
try std.testing.expectEqual(@as(u32, 18), @intFromEnum(Event.ac));
|
||||||
|
try std.testing.expectEqual(@as(u32, 19), @intFromEnum(Event.battery));
|
||||||
|
try std.testing.expectEqual(@as(u32, 20), @intFromEnum(Event.notify));
|
||||||
|
// subscribe is the RESERVED verb, below the protocol range entirely.
|
||||||
|
try std.testing.expectEqual(@as(u32, 2), envelope.operation_subscribe);
|
||||||
|
try std.testing.expectEqual(envelope.prefix_size + @sizeOf(Notice), Protocol.event_maximum);
|
||||||
|
try std.testing.expect(Protocol.event_maximum <= envelope.post_maximum);
|
||||||
|
// The call floor, not the push floor: `shutdown` is a synchronous call.
|
||||||
|
try std.testing.expectEqual(envelope.packet_maximum, message_maximum);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "a pushed event names its kind in the header" {
|
||||||
|
var buffer: [envelope.post_maximum]u8 = undefined;
|
||||||
|
const packet = Protocol.encodeEvent(.power_button, 0, .{}, &buffer).?;
|
||||||
|
try std.testing.expectEqual(Event.power_button, Protocol.eventOf(packet).?);
|
||||||
|
|
||||||
|
const notified = Protocol.encodeEvent(.notify, 0, .{ .code = 0x80, .hid = "PNP0C0A\x00".* }, &buffer).?;
|
||||||
|
try std.testing.expectEqual(Event.notify, Protocol.eventOf(notified).?);
|
||||||
|
try std.testing.expectEqual(@as(u32, 0x80), Protocol.decodeEvent(.notify, notified).?.code);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,49 +1,55 @@
|
||||||
//! The scanout wire protocol — what the compositor says to a native scanout driver (e.g.
|
//! The scanout wire protocol — what the compositor says to a native scanout driver (e.g.
|
||||||
//! virtio-gpu) over its well-known `.scanout` endpoint to put a composited frame on screen.
|
//! virtio-gpu) over `/protocol/scanout` to put a composited frame on screen. The driver owns
|
||||||
//! The driver owns the panel and the shared scanout surface it handed the compositor (via the
|
//! the panel and the shared scanout surface it handed the compositor (via the display
|
||||||
//! display service's `attach_scanout`); the compositor composites into that surface, then asks
|
//! service's `attach_scanout`); the compositor composites into that surface, then asks the
|
||||||
//! the driver to present a damaged rectangle. Tiny by design — one present request. Separate
|
//! driver to present a damaged rectangle. Tiny by design — one present request. Separate from
|
||||||
//! from the display protocol because the directions differ: clients call the compositor over
|
//! the display protocol because the directions differ: clients call the compositor over
|
||||||
//! `.display`; the compositor calls the driver over `.scanout`. See docs/display-v2.md.
|
//! `/protocol/display`; the compositor calls the driver over `/protocol/scanout`. See
|
||||||
|
//! docs/display-v2.md.
|
||||||
|
//!
|
||||||
|
//! One scanout per driver instance, so `Header.target` is always 0.
|
||||||
|
|
||||||
const std = @import("std");
|
const envelope = @import("envelope");
|
||||||
|
|
||||||
pub const Operation = enum(u32) {
|
/// `present(rect)`: put the given rectangle of the shared scanout surface on the panel (on
|
||||||
/// present(x, y, width, height): put the given rectangle of the shared scanout surface on
|
/// virtio-gpu: transfer-to-host of the region, then a fenced resource flush).
|
||||||
/// the panel (on virtio-gpu: transfer-to-host of the region, then a fenced resource flush).
|
pub const Present = extern struct {
|
||||||
present = 0,
|
|
||||||
/// get_modes() -> ModesReply: the display modes this scanout can switch to (V5).
|
|
||||||
get_modes = 1,
|
|
||||||
/// set_mode(width, height): change the scanout resolution — the shared surface is sized to
|
|
||||||
/// the largest mode, so this just re-points the scanout rectangle; the surface is unchanged.
|
|
||||||
set_mode = 2,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const Request = extern struct {
|
|
||||||
operation: u32,
|
|
||||||
x: u32 = 0,
|
x: u32 = 0,
|
||||||
y: u32 = 0,
|
y: u32 = 0,
|
||||||
width: u32 = 0,
|
width: u32 = 0,
|
||||||
height: u32 = 0,
|
height: u32 = 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const Reply = extern struct {
|
/// `set_mode(width, height)`: change the scanout resolution — the shared surface is sized to
|
||||||
status: i32, // 0 on success, negative on failure
|
/// the largest mode, so this just re-points the scanout rectangle; the surface is unchanged.
|
||||||
reserved: u32 = 0,
|
pub const SetMode = extern struct { width: u32, height: u32 };
|
||||||
};
|
|
||||||
|
|
||||||
/// One offered display mode.
|
/// One offered display mode.
|
||||||
pub const Mode = extern struct { width: u32, height: u32 };
|
pub const Mode = extern struct { width: u32, height: u32 };
|
||||||
pub const max_modes = 4;
|
pub const max_modes = 4;
|
||||||
|
|
||||||
/// The reply to `get_modes`: a small fixed list of modes.
|
/// The answer to `get_modes`: a small fixed list of modes. The success/failure verdict is
|
||||||
pub const ModesReply = extern struct {
|
/// the reply's `Status`, so this carries only the modes.
|
||||||
status: i32,
|
pub const Modes = extern struct {
|
||||||
count: u32,
|
count: u32 = 0,
|
||||||
modes: [max_modes]Mode,
|
_padding: u32 = 0,
|
||||||
|
modes: [max_modes]Mode = @splat(.{ .width = 0, .height = 0 }),
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const message_maximum: usize = 64;
|
pub const Protocol = envelope.Define(.{
|
||||||
pub const request_size: usize = @sizeOf(Request);
|
.name = "scanout",
|
||||||
pub const reply_size: usize = @sizeOf(Reply);
|
.version = 1,
|
||||||
pub const modes_reply_size: usize = @sizeOf(ModesReply);
|
.operations = &.{
|
||||||
|
.{ .name = "present", .request = Present },
|
||||||
|
.{ .name = "get_modes", .reply = Modes },
|
||||||
|
.{ .name = "set_mode", .request = SetMode },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
pub const Operation = Protocol.Operation;
|
||||||
|
|
||||||
|
/// The call floor, like every synchronous protocol. This module used to declare
|
||||||
|
/// 64 — the *push* floor — which was simply wrong: nothing here is pushed, and a
|
||||||
|
/// provider sizing its receive buffer to 64 refuses (`-E2BIG`) any caller that
|
||||||
|
/// sends up to the floor it is entitled to.
|
||||||
|
pub const message_maximum: usize = Protocol.message_maximum;
|
||||||
|
|
|
||||||
|
|
@ -1,57 +1,66 @@
|
||||||
//! The USB transfer protocol: what a USB class driver (a keyboard, mouse, or
|
//! The USB transfer protocol: what a USB class driver (a keyboard, mouse, or
|
||||||
//! mass-storage driver) says to the xHCI bus driver over its well-known
|
//! mass-storage driver) says to the xHCI bus driver over `/protocol/usb-transfer`
|
||||||
//! `.usb_bus` endpoint to drive its device. The class driver owns no hardware —
|
//! to drive its device. The class driver owns no hardware — it reaches its device
|
||||||
//! it reaches its device entirely through these messages, the way a PS/2 keyboard
|
//! entirely through these packets, the way a PS/2 keyboard driver reaches the
|
||||||
//! driver reaches the 8042 through the ps2-bus. Extern-struct messages tagged by
|
//! 8042 through the ps2-bus.
|
||||||
//! `Operation`, the vfs-protocol / device-manager-protocol pattern.
|
//!
|
||||||
|
//! Defined through the envelope (docs/os-development/protocol-namespace.md), so
|
||||||
|
//! every packet begins with the folded `Header`. **`Header.target` is the device
|
||||||
|
//! token** — the per-open handle the bus driver hands back, which every request
|
||||||
|
//! but `open` addressed through a `device_token` field of its own before the
|
||||||
|
//! rebase. `open` itself addresses the *assigned device id*, because that is what
|
||||||
|
//! the caller has before there is a token.
|
||||||
//!
|
//!
|
||||||
//! The shape:
|
//! The shape:
|
||||||
//! - **open** (a capability-passing `ipc.callCap`): the class driver hands over
|
//! - **open** (a capability-passing call): the class driver hands over its own
|
||||||
//! its own endpoint (for asynchronous interrupt reports) and its assigned
|
//! endpoint (for asynchronous interrupt reports); the target is its assigned
|
||||||
//! device id, and receives a `device_token` plus its interface's endpoints.
|
//! device id, and the reply carries a `device_token` plus its interface's
|
||||||
//! - **control / bulk** (synchronous `ipc.call`): one transfer, answered when
|
//! endpoints.
|
||||||
//! it completes. Control data travels inline (descriptors, HID/MSC class
|
//! - **control / bulk** (synchronous calls): one transfer, answered when it
|
||||||
//! requests are all small); bulk data travels by **physical address** — the
|
//! completes. Control data travels **in the packet's tail** in both
|
||||||
//! class driver's own `dma_alloc`'d buffer — so a 512-byte sector never has
|
//! directions (descriptors, HID/MSC class requests are all small), so the
|
||||||
//! to cross the 256-byte IPC boundary.
|
//! fixed parts stay tiny and `Status.len` is the transferred length — the
|
||||||
|
//! envelope's own field for "how many bytes follow", which is precisely what
|
||||||
|
//! the old `actual_length` said. Bulk data travels by **physical address** —
|
||||||
|
//! the class driver's own `dma_alloc`'d buffer — so a 512-byte sector never
|
||||||
|
//! has to cross the packet floor.
|
||||||
//! - **interrupt_subscribe** (synchronous): arm periodic IN polling of an
|
//! - **interrupt_subscribe** (synchronous): arm periodic IN polling of an
|
||||||
//! interrupt endpoint; each report the device produces is then pushed to the
|
//! interrupt endpoint; each report the device produces is then pushed to the
|
||||||
//! class driver's endpoint as an asynchronous `InterruptReport` (`ipc.send`),
|
//! class driver's endpoint as an asynchronous `interrupt_report` event.
|
||||||
//! exactly how the input service delivers events.
|
//! It stays one of **this protocol's own verbs**, not the reserved
|
||||||
|
//! `subscribe`: the reserved verb means "push me this provider's events" and
|
||||||
|
//! carries the subscriber's endpoint, while this names one endpoint address
|
||||||
|
//! on one device and a poll length, and the endpoint it pushes to was handed
|
||||||
|
//! over at `open`. Same word, different contract.
|
||||||
|
//! - **dma_attach**: a class driver hands the controller a DMA-region
|
||||||
|
//! capability (riding the call's cap slot) so the controller binds that
|
||||||
|
//! buffer into its IOMMU domain and may then DMA to the physical addresses
|
||||||
|
//! inside it. Needed once per buffer the class driver will name in a `bulk`
|
||||||
|
//! transfer (its own, or one forwarded to it).
|
||||||
//!
|
//!
|
||||||
//! Single controller assumption: one `.usb_bus` singleton serves QEMU's one xHCI.
|
//! Single controller assumption: one provider serves QEMU's one xHCI. A
|
||||||
//! A multi-controller machine would need a per-controller endpoint (the device
|
//! multi-controller machine would need the controller in the target (or the
|
||||||
//! manager handing each class driver the right one); noted, not built.
|
//! spawner wiring each class driver its own channel); noted, not built.
|
||||||
|
|
||||||
/// Fits one synchronous IPC message (kernel MESSAGE_MAXIMUM).
|
const std = @import("std");
|
||||||
pub const message_maximum: usize = 256;
|
const envelope = @import("envelope");
|
||||||
|
|
||||||
/// The largest inline control-transfer payload. Sized so a whole message
|
/// The largest control-transfer data stage. It rides the packet's tail, so the
|
||||||
/// (header + data) stays under `message_maximum`: descriptors and HID/MSC class
|
/// bound is the call floor less the header and the fixed request part — derived
|
||||||
/// requests are all far smaller.
|
/// rather than declared, which is what keeps it honest when a field moves.
|
||||||
pub const max_inline_data: usize = 200;
|
pub const max_inline_data: usize = envelope.packet_maximum - envelope.prefix_size - @sizeOf(Control);
|
||||||
|
|
||||||
/// The largest interrupt report pushed asynchronously. Sized so `InterruptReport`
|
/// The largest interrupt report pushed asynchronously. An event packet is the
|
||||||
/// fits an `ipc_send` payload slot (POST_MAXIMUM = 64): boot keyboard reports are
|
/// header plus the payload within 64 bytes, so this is what is left after the
|
||||||
/// 8 bytes, boot mouse reports 3–4.
|
/// report's own four bytes of framing: boot keyboard reports are 8 bytes, boot
|
||||||
pub const max_report_data: usize = 48;
|
/// 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
|
/// Endpoints per interface reported back in an open reply (a boot HID interface
|
||||||
/// has one interrupt endpoint, a mass-storage interface two bulk endpoints).
|
/// has one interrupt endpoint, a mass-storage interface two bulk endpoints).
|
||||||
pub const max_reported_endpoints: usize = 4;
|
pub const max_reported_endpoints: usize = 4;
|
||||||
|
|
||||||
pub const Operation = enum(u32) {
|
|
||||||
open = 0,
|
|
||||||
control = 1,
|
|
||||||
interrupt_subscribe = 2,
|
|
||||||
bulk = 3,
|
|
||||||
/// dma_attach: a class driver hands the controller a DMA-region capability (riding
|
|
||||||
/// the call's cap slot) so the controller binds that buffer into its IOMMU domain
|
|
||||||
/// and may then DMA to the physical addresses inside it. Needed once per buffer the
|
|
||||||
/// class driver will name in a `bulk` transfer (its own, or one forwarded to it).
|
|
||||||
dma_attach = 4,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// The endpoint facts a class driver needs, lifted from the endpoint descriptor
|
/// The endpoint facts a class driver needs, lifted from the endpoint descriptor
|
||||||
/// the bus driver already parsed during enumeration.
|
/// the bus driver already parsed during enumeration.
|
||||||
pub const Endpoint = extern struct {
|
pub const Endpoint = extern struct {
|
||||||
|
|
@ -64,114 +73,146 @@ pub const Endpoint = extern struct {
|
||||||
reserved: [3]u8 = .{ 0, 0, 0 },
|
reserved: [3]u8 = .{ 0, 0, 0 },
|
||||||
};
|
};
|
||||||
|
|
||||||
/// open: the class driver's receive endpoint rides as the call's capability, and
|
// --- the per-operation request and reply parts ------------------------------
|
||||||
/// `device_id` is the interface's assigned id (its argv[1]).
|
//
|
||||||
pub const OpenRequest = extern struct {
|
// Each names the bytes AFTER the prefix. Nothing here carries an operation or a
|
||||||
operation: u32 = @intFromEnum(Operation.open),
|
// device token: those are the packet header's, folded in once. No reply carries
|
||||||
reserved: u32 = 0,
|
// a status either — that is the `Status` every reply begins with.
|
||||||
device_id: u64,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// The answer to open: a token scoping every later request to this device, the
|
/// The answer to `open`: the token every later packet puts in `Header.target`,
|
||||||
/// interface's class triple (a sanity check), and its endpoints.
|
/// the interface's class triple (a sanity check), and its endpoints.
|
||||||
pub const OpenReply = extern struct {
|
pub const Opened = extern struct {
|
||||||
status: i32,
|
|
||||||
endpoint_count: u32,
|
|
||||||
device_token: u64,
|
device_token: u64,
|
||||||
|
endpoint_count: u32,
|
||||||
interface_class: u8,
|
interface_class: u8,
|
||||||
interface_subclass: u8,
|
interface_subclass: u8,
|
||||||
interface_protocol: u8,
|
interface_protocol: u8,
|
||||||
interface_number: u8,
|
interface_number: u8,
|
||||||
reserved2: u32 = 0,
|
|
||||||
endpoints: [max_reported_endpoints]Endpoint = [_]Endpoint{.{ .address = 0, .transfer_type = 0, .max_packet_size = 0, .interval = 0 }} ** max_reported_endpoints,
|
endpoints: [max_reported_endpoints]Endpoint = [_]Endpoint{.{ .address = 0, .transfer_type = 0, .max_packet_size = 0, .interval = 0 }} ** max_reported_endpoints,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// control: one EP0 control transfer. `setup` is a bit-cast `usb_abi.Request`.
|
/// `control`: one EP0 control transfer on `Header.target`. `setup` is a bit-cast
|
||||||
/// For an OUT transfer `data[0..data_length]` is sent; for an IN transfer the
|
/// `usb_abi.Request`. For an OUT transfer the data stage is the request's tail;
|
||||||
/// reply carries up to `data_length` bytes back.
|
/// for an IN transfer it comes back as the reply's tail, and `Status.len` is how
|
||||||
pub const ControlRequest = extern struct {
|
/// much of it arrived.
|
||||||
operation: u32 = @intFromEnum(Operation.control),
|
pub const Control = extern struct {
|
||||||
reserved: u32 = 0,
|
|
||||||
device_token: u64,
|
|
||||||
setup: [8]u8,
|
setup: [8]u8,
|
||||||
direction_in: u8, // 1 = device-to-host (IN), 0 = host-to-device (OUT)
|
/// 1 = device-to-host (IN), 0 = host-to-device (OUT).
|
||||||
reserved2: u8 = 0,
|
direction_in: u8,
|
||||||
|
_padding: u8 = 0,
|
||||||
|
/// Bytes of data stage: what an IN transfer asks for, and what an OUT
|
||||||
|
/// transfer's tail carries.
|
||||||
data_length: u16,
|
data_length: u16,
|
||||||
reserved3: u32 = 0,
|
_padding2: u32 = 0,
|
||||||
data: [max_inline_data]u8 = [_]u8{0} ** max_inline_data,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const ControlReply = extern struct {
|
/// `interrupt_subscribe`: begin periodic IN polling of an interrupt endpoint of
|
||||||
status: i32, // 0 success, negative on failure/stall
|
/// `Header.target`. Each report the device returns is pushed to the endpoint the
|
||||||
actual_length: u32,
|
/// caller handed over at `open`, as an `interrupt_report` event.
|
||||||
data: [max_inline_data]u8 = [_]u8{0} ** max_inline_data,
|
pub const InterruptSubscribe = extern struct {
|
||||||
};
|
|
||||||
|
|
||||||
/// interrupt_subscribe: begin periodic IN polling of an interrupt endpoint. Each
|
|
||||||
/// report the device returns is pushed to the caller's endpoint (handed over at
|
|
||||||
/// open) as an asynchronous `InterruptReport`.
|
|
||||||
pub const InterruptSubscribeRequest = extern struct {
|
|
||||||
operation: u32 = @intFromEnum(Operation.interrupt_subscribe),
|
|
||||||
reserved: u32 = 0,
|
|
||||||
device_token: u64,
|
|
||||||
endpoint_address: u8,
|
endpoint_address: u8,
|
||||||
reserved2: u8 = 0,
|
_padding: u8 = 0,
|
||||||
max_length: u16, // bytes to request per poll (the endpoint's max packet size)
|
/// Bytes to request per poll (the endpoint's max packet size).
|
||||||
|
max_length: u16,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const InterruptSubscribeReply = extern struct {
|
/// `bulk`: one bulk IN or OUT transfer on `Header.target`. `physical_address` is
|
||||||
status: i32,
|
/// the class driver's own `dma_alloc`'d buffer — the controller DMAs straight
|
||||||
reserved: u32 = 0,
|
/// to/from it, so the bulk data never crosses IPC. `endpoint_address`'s bit 7
|
||||||
};
|
/// selects IN vs OUT.
|
||||||
|
pub const Bulk = extern struct {
|
||||||
/// bulk: one bulk IN or OUT transfer. `physical_address` is the class driver's own
|
|
||||||
/// `dma_alloc`'d buffer — the controller DMAs straight to/from it, so the bulk
|
|
||||||
/// data never crosses IPC. `endpoint_address`'s bit 7 selects IN vs OUT.
|
|
||||||
pub const BulkRequest = extern struct {
|
|
||||||
operation: u32 = @intFromEnum(Operation.bulk),
|
|
||||||
reserved: u32 = 0,
|
|
||||||
device_token: u64,
|
|
||||||
physical_address: u64,
|
physical_address: u64,
|
||||||
length: u32,
|
length: u32,
|
||||||
endpoint_address: u8,
|
endpoint_address: u8,
|
||||||
reserved2: u8 = 0,
|
_padding: u8 = 0,
|
||||||
reserved3: u16 = 0,
|
_padding2: u16 = 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const BulkReply = extern struct {
|
/// How many bytes a bulk transfer actually moved. It cannot ride `Status.len`
|
||||||
status: i32,
|
/// the way a control transfer's does: nothing follows a bulk reply, because the
|
||||||
actual_length: u32,
|
/// data went to the caller's DMA buffer rather than into the packet.
|
||||||
};
|
pub const Transferred = extern struct { actual_length: u32 };
|
||||||
|
|
||||||
/// dma_attach: the region capability rides the call's cap slot; the body only carries
|
/// One asynchronous interrupt report, pushed to the endpoint the class driver
|
||||||
/// the device token (scoping) so the controller knows which caller is attaching.
|
/// handed over at `open`. The device it came from is `Header.target`.
|
||||||
pub const DmaAttachRequest = extern struct {
|
|
||||||
operation: u32 = @intFromEnum(Operation.dma_attach),
|
|
||||||
reserved: u32 = 0,
|
|
||||||
device_token: u64,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const DmaAttachReply = extern struct {
|
|
||||||
status: i32,
|
|
||||||
reserved: u32 = 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// An asynchronous interrupt report, pushed with `ipc.send` to a subscriber's
|
|
||||||
/// endpoint. `Received.isMessage()` is set; there is no reply owed.
|
|
||||||
pub const InterruptReport = extern struct {
|
pub const InterruptReport = extern struct {
|
||||||
device_token: u64,
|
|
||||||
endpoint_address: u8,
|
endpoint_address: u8,
|
||||||
length: u8,
|
length: u8,
|
||||||
reserved: u16 = 0,
|
_padding: u16 = 0,
|
||||||
data: [max_report_data]u8 = [_]u8{0} ** max_report_data,
|
data: [max_report_data]u8 = [_]u8{0} ** max_report_data,
|
||||||
};
|
};
|
||||||
|
|
||||||
comptime {
|
pub const Protocol = envelope.Define(.{
|
||||||
const std = @import("std");
|
.name = "usb-transfer",
|
||||||
// Every synchronous message must fit one IPC message; the async report must
|
.version = 1,
|
||||||
// fit an ipc_send payload slot.
|
.operations = &.{
|
||||||
std.debug.assert(@sizeOf(ControlRequest) <= message_maximum);
|
// open: the target is the interface's assigned device id (its argv[1]),
|
||||||
std.debug.assert(@sizeOf(ControlReply) <= message_maximum);
|
// and the class driver's receive endpoint rides as the capability.
|
||||||
std.debug.assert(@sizeOf(OpenReply) <= message_maximum);
|
.{ .name = "open", .reply = Opened },
|
||||||
std.debug.assert(@sizeOf(InterruptReport) <= 64);
|
.{ .name = "control", .request = Control },
|
||||||
|
.{ .name = "interrupt_subscribe", .request = InterruptSubscribe },
|
||||||
|
.{ .name = "bulk", .request = Bulk, .reply = Transferred },
|
||||||
|
// dma_attach: the region capability rides the call's cap slot; the
|
||||||
|
// target says which caller's device is attaching, so there is nothing
|
||||||
|
// left for a body to carry.
|
||||||
|
.{ .name = "dma_attach" },
|
||||||
|
},
|
||||||
|
.events = &.{
|
||||||
|
.{ .name = "interrupt_report", .payload = InterruptReport },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
pub const Operation = Protocol.Operation;
|
||||||
|
pub const Event = Protocol.Event;
|
||||||
|
|
||||||
|
/// What both sides size their buffers to — the call floor, as every protocol does.
|
||||||
|
pub const message_maximum: usize = Protocol.message_maximum;
|
||||||
|
|
||||||
|
test "the budgets, re-verified by Define rather than by hand" {
|
||||||
|
// What the hand-rolled comptime asserts used to say, now said by `Define`
|
||||||
|
// — and counting the header, which the old checks did not.
|
||||||
|
try std.testing.expectEqual(@as(usize, 224), max_inline_data);
|
||||||
|
try std.testing.expect(Protocol.request_maximum <= envelope.packet_maximum);
|
||||||
|
try std.testing.expect(Protocol.reply_maximum <= envelope.packet_maximum);
|
||||||
|
// The report was 48 bytes of data in a 64-byte struct that had no room left
|
||||||
|
// for a header. Folding the device token into the target and trimming the
|
||||||
|
// data to 40 leaves the whole packet at 60 of the 64-byte push floor.
|
||||||
|
try std.testing.expectEqual(@as(usize, 44), @sizeOf(InterruptReport));
|
||||||
|
try std.testing.expectEqual(@as(usize, 60), Protocol.event_maximum);
|
||||||
|
try std.testing.expect(Protocol.event_maximum <= envelope.post_maximum);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "the verb numbering, and the device token in the header" {
|
||||||
|
try std.testing.expectEqual(@as(u32, 16), @intFromEnum(Operation.open));
|
||||||
|
try std.testing.expectEqual(@as(u32, 17), @intFromEnum(Operation.control));
|
||||||
|
try std.testing.expectEqual(@as(u32, 18), @intFromEnum(Operation.interrupt_subscribe));
|
||||||
|
try std.testing.expectEqual(@as(u32, 19), @intFromEnum(Operation.bulk));
|
||||||
|
try std.testing.expectEqual(@as(u32, 20), @intFromEnum(Operation.dma_attach));
|
||||||
|
try std.testing.expectEqual(@as(u32, 16), @intFromEnum(Event.interrupt_report));
|
||||||
|
|
||||||
|
var buffer: [message_maximum]u8 = undefined;
|
||||||
|
const packet = Protocol.encodeRequest(.control, 9, .{
|
||||||
|
.setup = .{ 0, 6, 0, 1, 0, 0, 18, 0 },
|
||||||
|
.direction_in = 1,
|
||||||
|
.data_length = 18,
|
||||||
|
}, &.{}, &buffer).?;
|
||||||
|
try std.testing.expectEqual(@as(u64, 9), envelope.headerOf(packet).?.target);
|
||||||
|
try std.testing.expectEqual(@as(u16, 18), Protocol.decodeRequest(.control, packet).?.data_length);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "a control OUT carries its data stage as the packet's tail" {
|
||||||
|
var buffer: [message_maximum]u8 = undefined;
|
||||||
|
const payload = [_]u8{ 1, 2, 3, 4 };
|
||||||
|
const packet = Protocol.encodeRequest(.control, 5, .{
|
||||||
|
.setup = .{ 0x21, 11, 0, 0, 0, 0, 4, 0 },
|
||||||
|
.direction_in = 0,
|
||||||
|
.data_length = payload.len,
|
||||||
|
}, &payload, &buffer).?;
|
||||||
|
try std.testing.expectEqualSlices(u8, &payload, Protocol.requestTail(.control, packet));
|
||||||
|
|
||||||
|
// And the answer to an IN: the bytes follow the (empty) fixed reply part,
|
||||||
|
// with `Status.len` counting exactly them.
|
||||||
|
const answered = Protocol.encodeReply(.control, 0, {}, &payload, &buffer).?;
|
||||||
|
try std.testing.expectEqual(@as(u32, payload.len), envelope.statusOf(answered).?.len);
|
||||||
|
try std.testing.expectEqualSlices(u8, &payload, Protocol.replyTail(.control, answered));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,52 +1,27 @@
|
||||||
//! The VFS wire protocol — the message format spoken between a client (via the file
|
//! The VFS wire protocol — what a client (through the file API,
|
||||||
//! API) and the user-space VFS server over IPC. A request is a fixed `Request` header
|
//! library/kernel/file-system.zig) says to a filesystem backend over IPC. Defined
|
||||||
//! followed by an inline payload (a path, or write bytes); a reply is a fixed `Reply`
|
//! through the envelope (docs/os-development/protocol-namespace.md), so every
|
||||||
//! header followed by an inline payload (read bytes, or a FileStatus). Everything fits
|
//! packet begins with the folded `Header`: the verb in `Header.operation`, and
|
||||||
//! in one IPC message (<= ipc MESSAGE_MAXIMUM = 256 bytes).
|
//! **the open node id in `Header.target`** — the field that used to be
|
||||||
|
//! `Request.node`. A path appears in the conversation once, at `open`; every
|
||||||
|
//! packet after it addresses that integer.
|
||||||
//!
|
//!
|
||||||
//! This is a danos-native contract, so it uses danos names throughout. The client
|
//! This is a danos-native contract, so it uses danos names throughout. It is
|
||||||
//! side is `runtime.fs` (library/runtime/fs.zig), which programs use directly.
|
//! user-space only — the kernel knows nothing of files or paths; it only routes
|
||||||
|
//! (`fs_resolve`) and moves the bytes. The backends that serve it today are the
|
||||||
|
//! FAT server (system/services/fat/) and the protocol registry inside PID 1
|
||||||
|
//! (system/services/init/), which is a *synthetic* backend: `/protocol` holds
|
||||||
|
//! contracts rather than files.
|
||||||
//!
|
//!
|
||||||
//! This is user-space only — the kernel knows nothing of files or paths; it only moves the bytes.
|
//! **An `open` reply may carry a capability.** The `open` request rides
|
||||||
//! Shared by library/runtime/fs.zig (the client) and the mount backends that serve it (today
|
|
||||||
//! the fat server, system/services/fat/). The standalone user-space VFS server it was first
|
|
||||||
//! written against has retired — path routing moved into the kernel (system/kernel/vfs.zig,
|
|
||||||
//! fs_resolve) — but the protocol module outlived it.
|
|
||||||
|
|
||||||
//! **An `open` reply may carry a capability.** The vfs `open` request rides
|
|
||||||
//! `ipc_call`, and the reply direction of a call can hand back an endpoint
|
//! `ipc_call`, and the reply direction of a call can hand back an endpoint
|
||||||
//! (`ipc.callCap`'s `Reply.cap`). A file backend never uses it — FAT answers
|
//! (`ipc.callCap`'s `Reply.cap`). A file backend never uses it — FAT answers with
|
||||||
//! with a node id and nothing else — but a *synthetic* backend does: opening a
|
//! a node id and nothing else — but the registry does: opening a
|
||||||
//! `NodeKind.protocol` node under `/protocol` returns the provider's endpoint,
|
//! `NodeKind.protocol` node under `/protocol` returns the provider's endpoint,
|
||||||
//! which is the channel (docs/os-development/protocol-namespace.md). The
|
//! which is the channel. The convention is per-backend, not per-operation: a
|
||||||
//! convention is per-backend, not per-operation: a client that did not ask a
|
//! client that did not ask a synthetic backend simply gets no capability back.
|
||||||
//! synthetic backend simply gets no capability back, exactly as today.
|
|
||||||
|
|
||||||
pub const Operation = enum(u32) {
|
const envelope = @import("envelope");
|
||||||
open, // open(path) -> node id (a synthetic backend may reply with a capability instead)
|
|
||||||
close, // close(node)
|
|
||||||
read, // read(node, offset, len) -> bytes
|
|
||||||
write, // write(node, offset, bytes) -> count
|
|
||||||
status, // status(node) -> FileStatus
|
|
||||||
// Appended for the mount router (M5). Values stay stable, so existing clients
|
|
||||||
// and the flat-ramfs tests are unaffected.
|
|
||||||
readdir, // readdir(dir_node, cursor=offset) -> one DirectoryEntry (len==0 => EOF)
|
|
||||||
mount, // mount(prefix payload, capability = backend endpoint)
|
|
||||||
unmount, // unmount(prefix payload)
|
|
||||||
// Appended for filesystem mutation (Phase 2). Path-based (the path is the
|
|
||||||
// payload); a mounted backend handles them, the flat ramfs refuses them.
|
|
||||||
mkdir, // mkdir(path payload) -> status
|
|
||||||
unlink, // unlink(path payload) -> status
|
|
||||||
// rename: the payload is the old path, a single 0x00 separator, then the new
|
|
||||||
// path. Same-directory rename only (the router requires both under one mount).
|
|
||||||
rename, // rename(old\0new payload) -> status
|
|
||||||
// Appended for the protocol namespace (P2). The registry is a synthetic
|
|
||||||
// backend mounted at /protocol: `open` establishes a channel and `readdir`
|
|
||||||
// lists the bound names like any directory, so those two verbs need nothing
|
|
||||||
// new — but *claiming* a name does. A file backend refuses it, alongside the
|
|
||||||
// router verbs it does not implement either; only the registry implements it.
|
|
||||||
bind, // bind(name payload, capability = the provider's endpoint) -> status
|
|
||||||
};
|
|
||||||
|
|
||||||
/// The type of a filesystem node, aligned to the node-kind table
|
/// The type of a filesystem node, aligned to the node-kind table
|
||||||
/// (docs/file-system-development/file-system-hierarchy.md). Fills `FileStatus.kind` and
|
/// (docs/file-system-development/file-system-hierarchy.md). Fills `FileStatus.kind` and
|
||||||
|
|
@ -67,38 +42,18 @@ pub const NodeKind = enum(u32) {
|
||||||
protocol = 7,
|
protocol = 7,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// One directory entry, returned by `readdir`: a fixed header followed inline in
|
/// One directory entry: the fixed part of a `readdir` reply, followed inline by
|
||||||
/// the reply payload by `name_len` bytes of name. A zero-length reply is EOF.
|
/// `name_len` bytes of name. **A zero `name_len` is end of directory** — the
|
||||||
|
/// reply's own length cannot say so any more, because the envelope always sends
|
||||||
|
/// the fixed part.
|
||||||
pub const DirectoryEntry = extern struct {
|
pub const DirectoryEntry = extern struct {
|
||||||
kind: u32, // a NodeKind
|
kind: u32 = 0, // a NodeKind
|
||||||
name_len: u32,
|
name_len: u32 = 0,
|
||||||
size: u64,
|
size: u64 = 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const directory_entry_size: usize = @sizeOf(DirectoryEntry);
|
pub const directory_entry_size: usize = @sizeOf(DirectoryEntry);
|
||||||
|
|
||||||
/// Request header. `node` is the server-side open-file id (from a prior open);
|
|
||||||
/// for `open` the path is the payload and `len` is its length. `offset`/`len`
|
|
||||||
/// carry the read/write position and count.
|
|
||||||
pub const Request = extern struct {
|
|
||||||
operation: Operation,
|
|
||||||
node: u64,
|
|
||||||
offset: u64,
|
|
||||||
len: u32,
|
|
||||||
flags: u32,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Reply header. `status` is 0 on success or a negative errno; `node` is the new
|
|
||||||
/// open-file id (for `open`); `len` is the payload length (bytes read, or the
|
|
||||||
/// FileStatus size).
|
|
||||||
pub const Reply = extern struct {
|
|
||||||
status: i32,
|
|
||||||
_padding: u32 = 0,
|
|
||||||
node: u64 = 0,
|
|
||||||
len: u32 = 0,
|
|
||||||
_padding2: u32 = 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// A file's metadata (the danos-native answer to a `status` request). The POSIX
|
/// A file's metadata (the danos-native answer to a `status` request). The POSIX
|
||||||
/// layer maps this onto `struct stat`.
|
/// layer maps this onto `struct stat`.
|
||||||
pub const FileStatus = extern struct {
|
pub const FileStatus = extern struct {
|
||||||
|
|
@ -110,13 +65,84 @@ pub const FileStatus = extern struct {
|
||||||
mtime: u64 = 0,
|
mtime: u64 = 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const message_maximum: usize = 256;
|
// --- the per-operation request and reply parts ------------------------------
|
||||||
pub const request_size: usize = @sizeOf(Request);
|
//
|
||||||
pub const reply_size: usize = @sizeOf(Reply);
|
// Each names the bytes AFTER the prefix. Nothing here carries an operation or a
|
||||||
/// Largest inline payload that still fits one IPC message alongside a header.
|
// node id: those are the packet header's, folded in once.
|
||||||
pub const maximum_payload: usize = message_maximum - request_size;
|
|
||||||
|
|
||||||
/// Open flags (danos-native; `runtime.fs.OpenOptions` maps its booleans onto these).
|
/// `open(flags)` with the path as the packet's tail. The one verb that spends a
|
||||||
|
/// path; everything after it addresses the node id this returns.
|
||||||
|
pub const Open = extern struct { flags: u32 = 0 };
|
||||||
|
|
||||||
|
/// The node id an `open` established — the integer every later packet puts in
|
||||||
|
/// `Header.target`. Meaningful only between this client and this backend.
|
||||||
|
pub const Opened = extern struct { node: u64 };
|
||||||
|
|
||||||
|
/// `read(offset, len)` on `Header.target`; the bytes come back as the reply tail.
|
||||||
|
pub const Read = extern struct {
|
||||||
|
offset: u64,
|
||||||
|
len: u32,
|
||||||
|
_padding: u32 = 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// `write(offset, len)` on `Header.target`, with the data as the packet's tail.
|
||||||
|
pub const Write = extern struct {
|
||||||
|
offset: u64,
|
||||||
|
len: u32,
|
||||||
|
_padding: u32 = 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// How many bytes a `write` actually took — it may be short.
|
||||||
|
pub const Written = extern struct { count: u32 };
|
||||||
|
|
||||||
|
/// `readdir(cursor)` on `Header.target`: one entry per call, cursor-advanced.
|
||||||
|
pub const Readdir = extern struct { cursor: u64 };
|
||||||
|
|
||||||
|
/// The contract, whole. Verbs number from `envelope.first_protocol_operation`
|
||||||
|
/// (16) in this order; the reserved verbs below it mean what they mean
|
||||||
|
/// everywhere. `readdir` stays a protocol verb rather than folding into the
|
||||||
|
/// reserved `enumerate`: it enumerates the children of one *node*, where
|
||||||
|
/// `enumerate` names a provider's targets.
|
||||||
|
pub const Protocol = envelope.Define(.{
|
||||||
|
.name = "vfs",
|
||||||
|
.version = 1,
|
||||||
|
.operations = &.{
|
||||||
|
.{ .name = "open", .request = Open, .reply = Opened },
|
||||||
|
.{ .name = "close" },
|
||||||
|
.{ .name = "read", .request = Read },
|
||||||
|
.{ .name = "write", .request = Write, .reply = Written },
|
||||||
|
.{ .name = "status", .reply = FileStatus },
|
||||||
|
.{ .name = "readdir", .request = Readdir, .reply = DirectoryEntry },
|
||||||
|
// The mount router's two verbs. Path routing lives in the kernel now
|
||||||
|
// (system/kernel/vfs.zig), so no backend implements either; they keep
|
||||||
|
// their numbers so the vocabulary stays the one docs/vfs-protocol.md
|
||||||
|
// describes.
|
||||||
|
.{ .name = "mount" }, // tail = the prefix, capability = the backend's endpoint
|
||||||
|
.{ .name = "unmount" }, // tail = the prefix
|
||||||
|
// Filesystem mutation, path-based: the path is the packet's tail.
|
||||||
|
.{ .name = "mkdir" },
|
||||||
|
.{ .name = "unlink" },
|
||||||
|
// rename: the tail is the old path, a single 0x00 separator, then the
|
||||||
|
// new path. Same-directory rename only.
|
||||||
|
.{ .name = "rename" },
|
||||||
|
// The registry's claim verb (P2): the name is the tail and the
|
||||||
|
// provider's endpoint rides the call as its capability. A file backend
|
||||||
|
// refuses it; only init implements it.
|
||||||
|
.{ .name = "bind" },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
pub const Operation = Protocol.Operation;
|
||||||
|
|
||||||
|
/// What a backend sizes its buffers to — the call floor, as every protocol does.
|
||||||
|
pub const message_maximum: usize = Protocol.message_maximum;
|
||||||
|
|
||||||
|
/// The most inline payload any request may carry: the floor less the header and
|
||||||
|
/// the widest fixed request part, so one bound serves every verb (a path, write
|
||||||
|
/// data, a read's answer).
|
||||||
|
pub const maximum_payload: usize = envelope.packet_maximum - Protocol.request_maximum;
|
||||||
|
|
||||||
|
/// Open flags (danos-native; `file_system.OpenOptions` maps its booleans onto these).
|
||||||
pub const create: u32 = 1;
|
pub const create: u32 = 1;
|
||||||
/// Open a directory (for readdir) rather than a file. A mounted backend uses
|
/// Open a directory (for readdir) rather than a file. A mounted backend uses
|
||||||
/// this to open a directory node; the flat ramfs ignores it.
|
/// this to open a directory node; the flat ramfs ignores it.
|
||||||
|
|
@ -126,7 +152,7 @@ pub const directory: u32 = 2;
|
||||||
/// backend frees the old cluster chain; the flat ramfs ignores it.
|
/// backend frees the old cluster chain; the flat ramfs ignores it.
|
||||||
pub const truncate: u32 = 4;
|
pub const truncate: u32 = 4;
|
||||||
|
|
||||||
test "protocol struct sizes and node kinds" {
|
test "the stable wire values: node kinds, entry layout, and the verb numbering" {
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
try std.testing.expectEqual(@as(u32, 0), @intFromEnum(NodeKind.regular));
|
try std.testing.expectEqual(@as(u32, 0), @intFromEnum(NodeKind.regular));
|
||||||
try std.testing.expectEqual(@as(u32, 1), @intFromEnum(NodeKind.directory));
|
try std.testing.expectEqual(@as(u32, 1), @intFromEnum(NodeKind.directory));
|
||||||
|
|
@ -134,11 +160,34 @@ test "protocol struct sizes and node kinds" {
|
||||||
try std.testing.expectEqual(@as(u32, 6), @intFromEnum(NodeKind.socket));
|
try std.testing.expectEqual(@as(u32, 6), @intFromEnum(NodeKind.socket));
|
||||||
try std.testing.expectEqual(@as(u32, 7), @intFromEnum(NodeKind.protocol));
|
try std.testing.expectEqual(@as(u32, 7), @intFromEnum(NodeKind.protocol));
|
||||||
try std.testing.expectEqual(@as(usize, 16), @sizeOf(DirectoryEntry));
|
try std.testing.expectEqual(@as(usize, 16), @sizeOf(DirectoryEntry));
|
||||||
// The appended operations keep the original values.
|
|
||||||
try std.testing.expectEqual(@as(u32, 0), @intFromEnum(Operation.open));
|
// The numbering the envelope gives this protocol. These are NEW values: the
|
||||||
try std.testing.expectEqual(@as(u32, 4), @intFromEnum(Operation.status));
|
// rebase moved every verb above the reserved range, so the old 0..11 are
|
||||||
try std.testing.expectEqual(@as(u32, 5), @intFromEnum(Operation.readdir));
|
// gone and 16..27 are what the wire carries. Pinned because both sides of a
|
||||||
try std.testing.expectEqual(@as(u32, 10), @intFromEnum(Operation.rename));
|
// flag-day have to agree on them, not because they may never change again.
|
||||||
// The registry's claim verb, appended last with the protocol namespace.
|
try std.testing.expectEqual(@as(u32, 16), @intFromEnum(Operation.open));
|
||||||
try std.testing.expectEqual(@as(u32, 11), @intFromEnum(Operation.bind));
|
try std.testing.expectEqual(@as(u32, 17), @intFromEnum(Operation.close));
|
||||||
|
try std.testing.expectEqual(@as(u32, 18), @intFromEnum(Operation.read));
|
||||||
|
try std.testing.expectEqual(@as(u32, 19), @intFromEnum(Operation.write));
|
||||||
|
try std.testing.expectEqual(@as(u32, 20), @intFromEnum(Operation.status));
|
||||||
|
try std.testing.expectEqual(@as(u32, 21), @intFromEnum(Operation.readdir));
|
||||||
|
try std.testing.expectEqual(@as(u32, 26), @intFromEnum(Operation.rename));
|
||||||
|
try std.testing.expectEqual(@as(u32, 27), @intFromEnum(Operation.bind));
|
||||||
|
// The payload bound is what it always was, arrived at the other way round:
|
||||||
|
// the header plus the widest fixed request part is 32 bytes of the floor.
|
||||||
|
try std.testing.expectEqual(@as(usize, 224), maximum_payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "the node id rides the header, and a path rides the tail" {
|
||||||
|
const std = @import("std");
|
||||||
|
var buffer: [message_maximum]u8 = undefined;
|
||||||
|
|
||||||
|
const opening = Protocol.encodeRequest(.open, 0, .{ .flags = create }, "/a/b", &buffer).?;
|
||||||
|
try std.testing.expectEqual(@as(u32, create), Protocol.decodeRequest(.open, opening).?.flags);
|
||||||
|
try std.testing.expectEqualStrings("/a/b", Protocol.requestTail(.open, opening));
|
||||||
|
try std.testing.expectEqual(@as(u64, 0), envelope.headerOf(opening).?.target);
|
||||||
|
|
||||||
|
const reading = Protocol.encodeRequest(.read, 7, .{ .offset = 512, .len = 64 }, &.{}, &buffer).?;
|
||||||
|
try std.testing.expectEqual(@as(u64, 7), envelope.headerOf(reading).?.target);
|
||||||
|
try std.testing.expectEqual(@as(u64, 512), Protocol.decodeRequest(.read, reading).?.offset);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -252,8 +252,11 @@ pub const klog_maximum_message: usize = 256;
|
||||||
pub const fs_route_kernel: u64 = 0; // rdx = node token; serve via fs_node
|
pub const fs_route_kernel: u64 = 0; // rdx = node token; serve via fs_node
|
||||||
pub const fs_route_backend: u64 = 1; // rdx = endpoint handle; speak vfs-protocol
|
pub const fs_route_backend: u64 = 1; // rdx = endpoint handle; speak vfs-protocol
|
||||||
|
|
||||||
/// fs_node operations — the same numbers as the vfs-protocol Operation enum, so
|
/// fs_node operations. These were once the vfs-protocol Operation numbers; the
|
||||||
/// client code shares one vocabulary.
|
/// rebase onto the envelope moved every protocol verb above the reserved range
|
||||||
|
/// (16 and up), and these did not follow — they are a *syscall* selector, not a
|
||||||
|
/// packet's verb, and renumbering a kernel ABI to track a wire format would be
|
||||||
|
/// coupling in the wrong direction. The two vocabularies are simply separate now.
|
||||||
pub const fs_node_read: u64 = 2;
|
pub const fs_node_read: u64 = 2;
|
||||||
pub const fs_node_status: u64 = 4;
|
pub const fs_node_status: u64 = 4;
|
||||||
pub const fs_node_readdir: u64 = 5;
|
pub const fs_node_readdir: u64 = 5;
|
||||||
|
|
|
||||||
|
|
@ -148,6 +148,26 @@
|
||||||
/test/system/services/input-source, kernel, open, input
|
/test/system/services/input-source, kernel, open, input
|
||||||
/test/system/services/input-test, kernel, open, input
|
/test/system/services/input-test, kernel, open, input
|
||||||
|
|
||||||
|
# The guessable-id probe (test/system/services/badge-scope-test) runs as two
|
||||||
|
# processes of one binary: the owner, which the scenario spawns, and the intruder,
|
||||||
|
# which the owner spawns with the ids it holds. Both reach the compositor — the
|
||||||
|
# owner to create the layer, the intruder to be refused it — so the binary is
|
||||||
|
# named twice, once per supervisor. The second row needs no 'supervise'
|
||||||
|
# delegation: the owner was spawned by the KERNEL, which is a chain init can
|
||||||
|
# vouch for on its own.
|
||||||
|
/test/system/services/badge-scope-test, kernel, open, display
|
||||||
|
/test/system/services/badge-scope-test, /test/*, open, display
|
||||||
|
|
||||||
|
# The conformance probe (test/system/services/protocol-conformance-test) asks
|
||||||
|
# every provider its boot bound for the envelope's reserved verbs. It reaches
|
||||||
|
# ONLY the two contracts its own scenario boots a provider for, named one at a
|
||||||
|
# time exactly like the two rows above — no subtree, no wildcard. Everything else
|
||||||
|
# under /protocol stays absent for it, which is the point: the fixture walks the
|
||||||
|
# namespace listing and reports what it could not open rather than being handed
|
||||||
|
# the tree to make the test look broad.
|
||||||
|
/test/system/services/protocol-conformance-test, kernel, open, input
|
||||||
|
/test/system/services/protocol-conformance-test, kernel, open, display
|
||||||
|
|
||||||
# The laundering-deputy probe (test/system/services/protocol-registry-test) runs
|
# The laundering-deputy probe (test/system/services/protocol-registry-test) runs
|
||||||
# a grandchild whose supervisor is a fixture nobody authorized — that is the
|
# a grandchild whose supervisor is a fixture nobody authorized — that is the
|
||||||
# point of it, and its bind must stay refused. It still has to report the verdict
|
# point of it, and its bind must stay refused. It still has to report the verdict
|
||||||
|
|
|
||||||
|
Can't render this file because it contains an unexpected character in line 12 and column 15.
|
|
|
@ -229,18 +229,21 @@ fn registerAndReport(bus: u64, dev: u64, function: u64, class_triple: u32) void
|
||||||
std.log.info("register refused for {d}:{d}.{d}", .{ bus, dev, function });
|
std.log.info("register refused for {d}:{d}.{d}", .{ bus, dev, function });
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
const report = device_manager_protocol.ChildAdded{
|
// The registered device id is the packet's target — the manager's object
|
||||||
|
// addressing — so the report body carries only where on the bus it sits and
|
||||||
|
// what it is.
|
||||||
|
var packet: [device_manager_protocol.message_maximum]u8 = undefined;
|
||||||
|
const framed = device_manager_protocol.Protocol.encodeRequest(.child_added, registered, .{
|
||||||
.bus = @intFromEnum(device_manager_protocol.BusKind.pci),
|
.bus = @intFromEnum(device_manager_protocol.BusKind.pci),
|
||||||
.parent = bridge_id,
|
.parent = bridge_id,
|
||||||
.bus_address = (bus << 8) | (dev << 3) | function,
|
.bus_address = (bus << 8) | (dev << 3) | function,
|
||||||
.identity = class_triple,
|
.identity = class_triple,
|
||||||
.device_id = registered,
|
|
||||||
.vendor = descriptor.vendor,
|
.vendor = descriptor.vendor,
|
||||||
.device = descriptor.device,
|
.device = descriptor.device,
|
||||||
.subsystem = descriptor.subsystem,
|
.subsystem = descriptor.subsystem,
|
||||||
};
|
}, &.{}, &packet) orelse return;
|
||||||
var reply: [device_manager_protocol.message_maximum]u8 = undefined;
|
var reply: [device_manager_protocol.message_maximum]u8 = undefined;
|
||||||
_ = ipc.call(manager_handle, std.mem.asBytes(&report), &reply) catch {
|
_ = ipc.call(manager_handle, framed, &reply) catch {
|
||||||
std.log.info("child report for {d}:{d}.{d} failed", .{ bus, dev, function });
|
std.log.info("child report for {d}:{d}.{d} failed", .{ bus, dev, function });
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -112,9 +112,10 @@ pub fn main(init: process.Init) void {
|
||||||
if (signals.has(.terminate)) return;
|
if (signals.has(.terminate)) return;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!got.isMessage() or got.len < @sizeOf(usb.InterruptReport)) continue;
|
if (!got.isMessage()) continue;
|
||||||
|
// An `interrupt_report` event packet: the verb in its folded header, the
|
||||||
const message = std.mem.bytesToValue(usb.InterruptReport, receive[0..@sizeOf(usb.InterruptReport)]);
|
// report after it. Anything else on this endpoint is not ours.
|
||||||
|
const message = usb.reportOf(receive[0..got.len]) orelse continue;
|
||||||
if (message.length < @sizeOf(hid.KeyboardReport)) continue;
|
if (message.length < @sizeOf(hid.KeyboardReport)) continue;
|
||||||
const report = std.mem.bytesToValue(hid.KeyboardReport, message.data[0..@sizeOf(hid.KeyboardReport)]);
|
const report = std.mem.bytesToValue(hid.KeyboardReport, message.data[0..@sizeOf(hid.KeyboardReport)]);
|
||||||
const transitions = decoder.feed(report);
|
const transitions = decoder.feed(report);
|
||||||
|
|
|
||||||
|
|
@ -74,9 +74,10 @@ pub fn main(init: process.Init) void {
|
||||||
if (signals.has(.terminate)) return;
|
if (signals.has(.terminate)) return;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!got.isMessage() or got.len < @sizeOf(usb.InterruptReport)) continue;
|
if (!got.isMessage()) continue;
|
||||||
|
// An `interrupt_report` event packet: the verb in its folded header, the
|
||||||
const message = std.mem.bytesToValue(usb.InterruptReport, receive[0..@sizeOf(usb.InterruptReport)]);
|
// report after it. Anything else on this endpoint is not ours.
|
||||||
|
const message = usb.reportOf(receive[0..got.len]) orelse continue;
|
||||||
const length = @min(message.length, message.data.len);
|
const length = @min(message.length, message.data.len);
|
||||||
const report = hid.parseMouse(message.data[0..length]) orelse continue;
|
const report = hid.parseMouse(message.data[0..length]) orelse continue;
|
||||||
const mask = buttonMask(report.buttons);
|
const mask = buttonMask(report.buttons);
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,8 @@ pub fn build(b: *std.Build) void {
|
||||||
.name = "usb-storage",
|
.name = "usb-storage",
|
||||||
.root_source_file = b.path("usb-storage.zig"),
|
.root_source_file = b.path("usb-storage.zig"),
|
||||||
.imports = &.{
|
.imports = &.{
|
||||||
"block-protocol", "driver", "ipc", "logging", "memory", "process", "service",
|
"block-protocol", "driver", "envelope", "ipc", "logging", "memory", "process",
|
||||||
"time", "usb",
|
"service", "time", "usb",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
b.installArtifact(exe);
|
b.installArtifact(exe);
|
||||||
|
|
|
||||||
|
|
@ -22,8 +22,16 @@ const logging = @import("logging");
|
||||||
const usb = @import("usb");
|
const usb = @import("usb");
|
||||||
const scsi = @import("scsi.zig");
|
const scsi = @import("scsi.zig");
|
||||||
const bot = @import("bulk-only-transport.zig");
|
const bot = @import("bulk-only-transport.zig");
|
||||||
|
const envelope = @import("envelope");
|
||||||
const block_protocol = @import("block-protocol");
|
const block_protocol = @import("block-protocol");
|
||||||
|
|
||||||
|
/// The generated block dispatch. One device per process, so the handler context
|
||||||
|
/// is empty and the geometry stays in this file's globals.
|
||||||
|
const Serve = block_protocol.Protocol.Provider(void);
|
||||||
|
|
||||||
|
const Invocation = envelope.Invocation;
|
||||||
|
const Answer = envelope.Answer;
|
||||||
|
|
||||||
var device_id: u64 = 0;
|
var device_id: u64 = 0;
|
||||||
var device: usb.Device = undefined;
|
var device: usb.Device = undefined;
|
||||||
var bulk_in: usb.Endpoint = undefined;
|
var bulk_in: usb.Endpoint = undefined;
|
||||||
|
|
@ -144,53 +152,65 @@ fn initialise(endpoint: ipc.Handle) bool {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Serve the block protocol: geometry, and whole-block read/write to/from the
|
// --- serving the block protocol ---------------------------------------------
|
||||||
/// caller's DMA buffer (named by physical address).
|
//
|
||||||
fn onMessage(message: []const u8, reply: []u8, sender: u32, arrived: *ipc.Arrival) usize {
|
// Geometry, and whole-block read/write to and from the caller's DMA buffer
|
||||||
_ = sender;
|
// (named by physical address). One device per process, so `Header.target` is
|
||||||
if (message.len < block_protocol.request_size) return 0;
|
// always 0 and no handler reads it.
|
||||||
const request = std.mem.bytesToValue(block_protocol.Request, message[0..block_protocol.request_size]);
|
|
||||||
switch (request.operation) {
|
/// A transfer the device refused. Every failure here is the same one — the SCSI
|
||||||
@intFromEnum(block_protocol.Operation.attach) => {
|
/// command did not complete — so there is one errno for all of them.
|
||||||
// The filesystem's DMA buffer: forward its capability to the controller
|
const refused: isize = -envelope.ENOENT;
|
||||||
// so the device can reach it. Never claimed — the binding holds its own
|
|
||||||
// reference, so our copy is the turn's to close, on this path and on the
|
fn onGeometry(_: void, _: Invocation(void), answer: Answer(block_protocol.Geometry)) isize {
|
||||||
// refusal above it alike.
|
answer.set(.{ .block_size = block_size, .block_count = block_count });
|
||||||
const handle = arrived.peek() orelse return writeReply(reply, .{ .status = -1, .block_size = 0, .block_count = 0 });
|
return 0;
|
||||||
const ok = device.attachDma(handle);
|
|
||||||
return writeReply(reply, .{ .status = if (ok) 0 else -1, .block_size = 0, .block_count = 0 });
|
|
||||||
},
|
|
||||||
@intFromEnum(block_protocol.Operation.geometry) => {
|
|
||||||
return writeReply(reply, .{ .status = 0, .block_size = block_size, .block_count = block_count });
|
|
||||||
},
|
|
||||||
@intFromEnum(block_protocol.Operation.read) => {
|
|
||||||
const count: u16 = @intCast(request.count);
|
|
||||||
const cdb = scsi.read10(@intCast(request.lba), count);
|
|
||||||
const ok = transact(&cdb, true, request.physical, request.count * block_size);
|
|
||||||
return writeReply(reply, .{ .status = if (ok) 0 else -1, .block_size = block_size, .block_count = if (ok) request.count else 0 });
|
|
||||||
},
|
|
||||||
@intFromEnum(block_protocol.Operation.write) => {
|
|
||||||
const count: u16 = @intCast(request.count);
|
|
||||||
const cdb = scsi.write10(@intCast(request.lba), count);
|
|
||||||
const ok = transact(&cdb, false, request.physical, request.count * block_size);
|
|
||||||
return writeReply(reply, .{ .status = if (ok) 0 else -1, .block_size = block_size, .block_count = if (ok) request.count else 0 });
|
|
||||||
},
|
|
||||||
@intFromEnum(block_protocol.Operation.flush) => {
|
|
||||||
// SYNCHRONIZE CACHE: commit the device's write cache to flash. No data
|
|
||||||
// stage. Makes prior writes durable before a caller (init at shutdown)
|
|
||||||
// cuts power. A device without a volatile cache reports success anyway.
|
|
||||||
const cdb = scsi.synchronizeCache10();
|
|
||||||
const ok = transact(&cdb, false, 0, 0);
|
|
||||||
return writeReply(reply, .{ .status = if (ok) 0 else -1, .block_size = block_size, .block_count = 0 });
|
|
||||||
},
|
|
||||||
else => return 0,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn writeReply(reply: []u8, value: block_protocol.Reply) usize {
|
fn onRead(_: void, invocation: Invocation(block_protocol.Transfer), answer: Answer(block_protocol.Transferred)) isize {
|
||||||
const bytes = std.mem.asBytes(&value);
|
const request = invocation.request;
|
||||||
@memcpy(reply[0..bytes.len], bytes);
|
const cdb = scsi.read10(@intCast(request.lba), @intCast(request.count));
|
||||||
return bytes.len;
|
if (!transact(&cdb, true, request.physical, request.count * block_size)) return refused;
|
||||||
|
answer.set(.{ .count = request.count });
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn onWrite(_: void, invocation: Invocation(block_protocol.Transfer), answer: Answer(block_protocol.Transferred)) isize {
|
||||||
|
const request = invocation.request;
|
||||||
|
const cdb = scsi.write10(@intCast(request.lba), @intCast(request.count));
|
||||||
|
if (!transact(&cdb, false, request.physical, request.count * block_size)) return refused;
|
||||||
|
answer.set(.{ .count = request.count });
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// SYNCHRONIZE CACHE: commit the device's write cache to flash. No data stage.
|
||||||
|
/// Makes prior writes durable before a caller (init at shutdown) cuts power. A
|
||||||
|
/// device without a volatile cache reports success anyway.
|
||||||
|
fn onFlush(_: void, _: Invocation(void), _: Answer(void)) isize {
|
||||||
|
const cdb = scsi.synchronizeCache10();
|
||||||
|
return if (transact(&cdb, false, 0, 0)) 0 else refused;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The filesystem's DMA buffer: forward its capability to the controller so the
|
||||||
|
/// device can reach it. Never claimed — the binding holds its own reference, so
|
||||||
|
/// our copy is the turn's to close, on this path and on the refusal alike.
|
||||||
|
fn onAttach(_: void, invocation: Invocation(void), _: Answer(void)) isize {
|
||||||
|
const handle = invocation.capability orelse return -envelope.EPROTO;
|
||||||
|
return if (device.attachDma(handle)) 0 else refused;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlers = Serve.Handlers{
|
||||||
|
.geometry = onGeometry,
|
||||||
|
.read = onRead,
|
||||||
|
.write = onWrite,
|
||||||
|
.flush = onFlush,
|
||||||
|
.attach = onAttach,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn onMessage(message: []const u8, reply: []u8, sender: u32, arrived: *ipc.Arrival) usize {
|
||||||
|
// Peeked, never taken: `attach` forwards the capability and the controller's
|
||||||
|
// binding takes its own reference, so this copy stays the turn's to close.
|
||||||
|
return Serve.dispatch({}, handlers, message, sender, arrived.peek(), reply);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn main(init: process.Init) void {
|
pub fn main(init: process.Init) void {
|
||||||
|
|
|
||||||
|
|
@ -10,10 +10,10 @@ pub fn build(b: *std.Build) void {
|
||||||
.name = "usb-xhci-bus",
|
.name = "usb-xhci-bus",
|
||||||
.root_source_file = b.path("usb-xhci-bus.zig"),
|
.root_source_file = b.path("usb-xhci-bus.zig"),
|
||||||
.imports = &.{
|
.imports = &.{
|
||||||
"channel", "device-manager-protocol", "driver", "input-client",
|
"channel", "device-manager-protocol", "driver", "envelope",
|
||||||
"ipc", "logging", "memory", "mmio",
|
"input-client", "ipc", "logging", "memory",
|
||||||
"pci", "process", "service", "time",
|
"mmio", "pci", "process", "service",
|
||||||
"usb-abi", "usb-ids", "usb-transfer-protocol",
|
"time", "usb-abi", "usb-ids", "usb-transfer-protocol",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
b.installArtifact(exe);
|
b.installArtifact(exe);
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ const device_manager = @import("driver");
|
||||||
const memory = @import("memory");
|
const memory = @import("memory");
|
||||||
const logging = @import("logging");
|
const logging = @import("logging");
|
||||||
const device_manager_protocol = @import("device-manager-protocol");
|
const device_manager_protocol = @import("device-manager-protocol");
|
||||||
|
const envelope = @import("envelope");
|
||||||
const usb_ids = @import("usb-ids");
|
const usb_ids = @import("usb-ids");
|
||||||
const usb_abi = @import("usb-abi");
|
const usb_abi = @import("usb-abi");
|
||||||
const usb_transfer_protocol = @import("usb-transfer-protocol");
|
const usb_transfer_protocol = @import("usb-transfer-protocol");
|
||||||
|
|
@ -60,22 +61,33 @@ fn timerInterval() u64 {
|
||||||
return if (msi_vector != null) reconcile_interval_ms else poll_interval_ms;
|
return if (msi_vector != null) reconcile_interval_ms else poll_interval_ms;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The class driver endpoints that opened each device, so interrupt reports can
|
/// The class driver that opened each device: the endpoint interrupt reports are
|
||||||
/// be pushed back to them. Keyed by the device token (the interface's device id).
|
/// pushed back to, and **the task that opened it** — the kernel-stamped badge, so
|
||||||
|
/// a device token is scoped to the client that was given it. Keyed by the device
|
||||||
|
/// token (the interface's device id).
|
||||||
|
///
|
||||||
|
/// The scoping is the point (docs/os-development/protocol-namespace.md: handles
|
||||||
|
/// are validated against the badge). A token is a small registered-device id any
|
||||||
|
/// process could name, and every packet that carries one used to be honoured from
|
||||||
|
/// anyone: a stranger could run control transfers on another driver's device, arm
|
||||||
|
/// interrupt polling on it, or redirect its reports.
|
||||||
const Open = struct {
|
const Open = struct {
|
||||||
used: bool = false,
|
used: bool = false,
|
||||||
device_token: u64 = 0,
|
device_token: u64 = 0,
|
||||||
|
owner: u32 = 0,
|
||||||
report_endpoint: usize = 0,
|
report_endpoint: usize = 0,
|
||||||
};
|
};
|
||||||
var opens = [_]Open{.{}} ** 16;
|
var opens = [_]Open{.{}} ** 16;
|
||||||
|
|
||||||
/// Remember (or replace) the endpoint that reports for `device_token`. Returns
|
/// Remember (or replace) the endpoint task `owner` receives reports for
|
||||||
/// whether the table kept the handle — false means the caller still owns it and
|
/// `device_token` on. Returns whether the table kept the handle — false means the
|
||||||
/// must dispose of it. A re-open supersedes the previous endpoint, and the one
|
/// caller still owns it and must dispose of it. A re-open by the **same** client
|
||||||
/// it displaced is closed here: the table holds exactly one reference per slot.
|
/// supersedes its previous endpoint, and the one it displaced is closed here: the
|
||||||
fn recordOpen(device_token: u64, report_endpoint: usize) bool {
|
/// table holds exactly one reference per slot.
|
||||||
|
fn recordOpen(device_token: u64, owner: u32, report_endpoint: usize) bool {
|
||||||
for (&opens) |*open| {
|
for (&opens) |*open| {
|
||||||
if (open.used and open.device_token == device_token) {
|
if (open.used and open.device_token == device_token) {
|
||||||
|
if (open.owner != owner) return false; // someone else's device; nothing kept
|
||||||
if (open.report_endpoint != report_endpoint) _ = ipc.close(open.report_endpoint);
|
if (open.report_endpoint != report_endpoint) _ = ipc.close(open.report_endpoint);
|
||||||
open.report_endpoint = report_endpoint;
|
open.report_endpoint = report_endpoint;
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -83,13 +95,33 @@ fn recordOpen(device_token: u64, report_endpoint: usize) bool {
|
||||||
}
|
}
|
||||||
for (&opens) |*open| {
|
for (&opens) |*open| {
|
||||||
if (!open.used) {
|
if (!open.used) {
|
||||||
open.* = .{ .used = true, .device_token = device_token, .report_endpoint = report_endpoint };
|
open.* = .{ .used = true, .device_token = device_token, .owner = owner, .report_endpoint = report_endpoint };
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false; // table full: not kept
|
return false; // table full: not kept
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether `device_token` is open to task `owner`. An open device belonging to
|
||||||
|
/// someone else answers exactly as one that was never opened, so a prober cannot
|
||||||
|
/// tell another driver's device from an absent one.
|
||||||
|
fn openedBy(device_token: u64, owner: u32) bool {
|
||||||
|
for (&opens) |*open| {
|
||||||
|
if (open.used and open.device_token == device_token) return open.owner == owner;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether `device_token` is open at all. Asked only after `openedBy` has said
|
||||||
|
/// the caller is not the holder, so an answer of true means *someone else* holds
|
||||||
|
/// it — one device, one class driver.
|
||||||
|
fn heldByAnother(device_token: u64) bool {
|
||||||
|
for (&opens) |*open| {
|
||||||
|
if (open.used and open.device_token == device_token) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
fn reportEndpointFor(device_token: u64) ?usize {
|
fn reportEndpointFor(device_token: u64) ?usize {
|
||||||
for (&opens) |*open| {
|
for (&opens) |*open| {
|
||||||
if (open.used and open.device_token == device_token) return open.report_endpoint;
|
if (open.used and open.device_token == device_token) return open.report_endpoint;
|
||||||
|
|
@ -97,6 +129,23 @@ fn reportEndpointFor(device_token: u64) ?usize {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Release every device a dead client held: its slot, and the report endpoint
|
||||||
|
/// capability in it. Driven by published process exits — the same sweep idiom the
|
||||||
|
/// FAT server uses for open files and the harness uses for subscribers — which is
|
||||||
|
/// also what lets a restarted class driver re-open the device its predecessor had.
|
||||||
|
fn releaseOpensOf(dead: u32) void {
|
||||||
|
for (&opens) |*open| {
|
||||||
|
if (open.used and open.owner == dead) {
|
||||||
|
// The engine first: it holds this endpoint's handle number per
|
||||||
|
// subscription, and the close below frees that number for reuse.
|
||||||
|
if (controller) |*engine| engine.releaseSubscriptions(open.device_token);
|
||||||
|
_ = ipc.close(open.report_endpoint);
|
||||||
|
std.log.info("released device {d} for dead client {d}", .{ open.device_token, dead });
|
||||||
|
open.* = .{};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var controller_id: u64 = device_manager_protocol.no_device;
|
var controller_id: u64 = device_manager_protocol.no_device;
|
||||||
|
|
||||||
/// Claim the assigned controller, find its register window, and hello the
|
/// Claim the assigned controller, find its register window, and hello the
|
||||||
|
|
@ -104,6 +153,10 @@ var controller_id: u64 = device_manager_protocol.no_device;
|
||||||
/// manager reads as "meant to stop" — a missing assignment is not a crash loop.
|
/// manager reads as "meant to stop" — a missing assignment is not a crash loop.
|
||||||
fn initialise(endpoint: ipc.Handle) bool {
|
fn initialise(endpoint: ipc.Handle) bool {
|
||||||
service_endpoint = endpoint;
|
service_endpoint = endpoint;
|
||||||
|
// Device tokens are per-client state, so this driver needs deaths for the
|
||||||
|
// same reason the FAT server does: a class driver that crashes must not keep
|
||||||
|
// its device open, or its restarted instance could never claim it back.
|
||||||
|
_ = process.subscribeExits(endpoint);
|
||||||
|
|
||||||
// The transfer contract, bound by hand rather than through the harness's
|
// The transfer contract, bound by hand rather than through the harness's
|
||||||
// `.service`, because **losing it is not fatal here**. One machine can carry
|
// `.service`, because **losing it is not fatal here**. One machine can carry
|
||||||
|
|
@ -386,6 +439,23 @@ fn bringUpBehindHub(manager: ipc.Handle, engine: *library.Controller, hub: *libr
|
||||||
if (deviceIsHub(usb_device)) _ = engine.setupHub(usb_device);
|
if (deviceIsHub(usb_device)) _ = engine.setupHub(usb_device);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Report one interface gone. A removal is addressed by the composite (parent,
|
||||||
|
/// bus address) — a pair no single `Header.target` can carry — so that stays the
|
||||||
|
/// packet's body and the target addresses the manager itself. `where` names the
|
||||||
|
/// port and interface for the log line, or null where the caller stays quiet
|
||||||
|
/// (a hub subtree collapsing reports a great many at once).
|
||||||
|
fn reportRemoved(manager: ipc.Handle, bus_address: u64, where: ?struct { port: u32, interface: u8 }) void {
|
||||||
|
var packet: [device_manager_protocol.message_maximum]u8 = undefined;
|
||||||
|
const framed = device_manager_protocol.Protocol.encodeRequest(.child_removed, 0, .{
|
||||||
|
.parent = controller_id,
|
||||||
|
.bus_address = bus_address,
|
||||||
|
}, &.{}, &packet) orelse return;
|
||||||
|
var reply: [device_manager_protocol.message_maximum]u8 = undefined;
|
||||||
|
_ = ipc.call(manager, framed, &reply) catch {
|
||||||
|
if (where) |place| std.log.info("child-removed report for port {d} interface {d} failed", .{ place.port, place.interface });
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/// Tear down a device that disconnected from a hub: recursively tear down its
|
/// Tear down a device that disconnected from a hub: recursively tear down its
|
||||||
/// own downstream devices first if it is a hub, report each interface removed,
|
/// own downstream devices first if it is a hub, report each interface removed,
|
||||||
/// then Disable Slot. Mirrors tearDownPort for a hub-attached device.
|
/// then Disable Slot. Mirrors tearDownPort for a hub-attached device.
|
||||||
|
|
@ -398,12 +468,7 @@ fn tearDownHubDevice(manager: ipc.Handle, engine: *library.Controller, dev: *lib
|
||||||
const key = hubPortKey(dev.parent_slot, dev.parent_port);
|
const key = hubPortKey(dev.parent_slot, dev.parent_port);
|
||||||
for (dev.interfaces[0..dev.interface_count]) |*interface| {
|
for (dev.interfaces[0..dev.interface_count]) |*interface| {
|
||||||
if (interface.registered_device_id == 0) continue;
|
if (interface.registered_device_id == 0) continue;
|
||||||
const event = device_manager_protocol.ChildRemoved{
|
reportRemoved(manager, (@as(u64, key) << 8) | interface.number, null);
|
||||||
.parent = controller_id,
|
|
||||||
.bus_address = (@as(u64, key) << 8) | interface.number,
|
|
||||||
};
|
|
||||||
var reply: [device_manager_protocol.message_maximum]u8 = undefined;
|
|
||||||
_ = ipc.call(manager, std.mem.asBytes(&event), &reply) catch {};
|
|
||||||
interface.registered_device_id = 0;
|
interface.registered_device_id = 0;
|
||||||
}
|
}
|
||||||
engine.tearDownDevice(dev);
|
engine.tearDownDevice(dev);
|
||||||
|
|
@ -428,14 +493,7 @@ fn tearDownPort(manager: ipc.Handle, engine: *library.Controller, port: u32) voi
|
||||||
std.log.info("port {d} disconnected", .{port});
|
std.log.info("port {d} disconnected", .{port});
|
||||||
for (usb_device.interfaces[0..usb_device.interface_count]) |*interface| {
|
for (usb_device.interfaces[0..usb_device.interface_count]) |*interface| {
|
||||||
if (interface.registered_device_id == 0) continue;
|
if (interface.registered_device_id == 0) continue;
|
||||||
const event = device_manager_protocol.ChildRemoved{
|
reportRemoved(manager, (@as(u64, port) << 8) | interface.number, .{ .port = port, .interface = interface.number });
|
||||||
.parent = controller_id,
|
|
||||||
.bus_address = (@as(u64, port) << 8) | interface.number,
|
|
||||||
};
|
|
||||||
var reply: [device_manager_protocol.message_maximum]u8 = undefined;
|
|
||||||
_ = ipc.call(manager, std.mem.asBytes(&event), &reply) catch {
|
|
||||||
std.log.info("child-removed report for port {d} interface {d} failed", .{ port, interface.number });
|
|
||||||
};
|
|
||||||
interface.registered_device_id = 0;
|
interface.registered_device_id = 0;
|
||||||
}
|
}
|
||||||
engine.tearDownDevice(usb_device);
|
engine.tearDownDevice(usb_device);
|
||||||
|
|
@ -469,15 +527,17 @@ fn reportInterface(manager: ipc.Handle, port: u32, interface: library.InterfaceI
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const report = device_manager_protocol.ChildAdded{
|
// The registered device id is the packet's target — the manager's object
|
||||||
|
// addressing — so the report body carries only where on the bus it sits.
|
||||||
|
var packet: [device_manager_protocol.message_maximum]u8 = undefined;
|
||||||
|
const framed = device_manager_protocol.Protocol.encodeRequest(.child_added, registered, .{
|
||||||
.bus = @intFromEnum(device_manager_protocol.BusKind.usb),
|
.bus = @intFromEnum(device_manager_protocol.BusKind.usb),
|
||||||
.parent = controller_id,
|
.parent = controller_id,
|
||||||
.bus_address = (@as(u64, port) << 8) | interface.number,
|
.bus_address = (@as(u64, port) << 8) | interface.number,
|
||||||
.identity = identity,
|
.identity = identity,
|
||||||
.device_id = registered,
|
}, &.{}, &packet) orelse return null;
|
||||||
};
|
|
||||||
var reply: [device_manager_protocol.message_maximum]u8 = undefined;
|
var reply: [device_manager_protocol.message_maximum]u8 = undefined;
|
||||||
_ = ipc.call(manager, std.mem.asBytes(&report), &reply) catch {
|
_ = ipc.call(manager, framed, &reply) catch {
|
||||||
std.log.info("child report for port {d} interface {d} failed", .{ port, interface.number });
|
std.log.info("child report for port {d} interface {d} failed", .{ port, interface.number });
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
@ -496,59 +556,60 @@ fn reportInterface(manager: ipc.Handle, port: u32, interface: library.InterfaceI
|
||||||
return registered;
|
return registered;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Serve the USB transfer protocol: a class driver opens its device, then issues
|
/// The generated transfer dispatch. One controller per process, so the handler
|
||||||
/// control / interrupt-subscribe / bulk requests against it.
|
/// context is empty and the open table stays in this file's globals.
|
||||||
|
const Serve = usb_transfer_protocol.Protocol.Provider(void);
|
||||||
|
|
||||||
|
const Invocation = envelope.Invocation;
|
||||||
|
const Answer = envelope.Answer;
|
||||||
|
|
||||||
|
/// Every refusal here is the same one — this controller does not (or no longer)
|
||||||
|
/// serve the device the packet addressed — so there is one errno for all of them.
|
||||||
|
const refused: isize = -envelope.ENOENT;
|
||||||
|
|
||||||
|
/// Set by `onOpen` when the open table has taken ownership of the endpoint the
|
||||||
|
/// call carried, and read by `onMessage`, where the turn's `Arrival` lives. The
|
||||||
|
/// generated dispatch hands a handler the raw handle rather than the `Arrival`,
|
||||||
|
/// so the *claim* travels back out this way. One turn, one handler, one thread.
|
||||||
|
var capability_claimed = false;
|
||||||
|
|
||||||
fn onMessage(message: []const u8, reply: []u8, sender: u32, arrived: *ipc.Arrival) usize {
|
fn onMessage(message: []const u8, reply: []u8, sender: u32, arrived: *ipc.Arrival) usize {
|
||||||
_ = sender;
|
capability_claimed = false;
|
||||||
if (message.len < 4) return 0;
|
const written = Serve.dispatch({}, handlers, message, sender, arrived.peek(), reply);
|
||||||
const operation = std.mem.readInt(u32, message[0..4], .little);
|
if (capability_claimed) _ = arrived.take();
|
||||||
return switch (operation) {
|
return written;
|
||||||
@intFromEnum(usb_transfer_protocol.Operation.open) => handleOpen(message, reply, arrived),
|
|
||||||
@intFromEnum(usb_transfer_protocol.Operation.control) => handleControl(message, reply),
|
|
||||||
@intFromEnum(usb_transfer_protocol.Operation.interrupt_subscribe) => handleSubscribe(message, reply),
|
|
||||||
@intFromEnum(usb_transfer_protocol.Operation.bulk) => handleBulk(message, reply),
|
|
||||||
@intFromEnum(usb_transfer_protocol.Operation.dma_attach) => handleDmaAttach(message, reply, arrived),
|
|
||||||
else => 0,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// dma_attach: bind the class driver's DMA-region capability into the controller's IOMMU
|
const handlers = Serve.Handlers{
|
||||||
/// domain, so the controller may DMA to the physical addresses inside that buffer. The
|
.open = onOpen,
|
||||||
/// binding holds its own kernel reference, so this never claims the arriving handle —
|
.control = onControl,
|
||||||
/// the turn's `defer` in the harness is the close, on the failure paths as well as this
|
.interrupt_subscribe = onInterruptSubscribe,
|
||||||
/// one.
|
.bulk = onBulk,
|
||||||
fn handleDmaAttach(message: []const u8, reply: []u8, arrived: *ipc.Arrival) usize {
|
.dma_attach = onDmaAttach,
|
||||||
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 });
|
|
||||||
}
|
|
||||||
|
|
||||||
fn writeReply(reply: []u8, value: anytype) usize {
|
/// open: the target is the class driver's assigned device id. Resolve it to an
|
||||||
const bytes = std.mem.asBytes(&value);
|
/// interface, remember the caller's endpoint (for interrupt reports), and answer
|
||||||
@memcpy(reply[0..bytes.len], bytes);
|
/// with a device token — the target of every later packet — plus the interface's
|
||||||
return bytes.len;
|
/// 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;
|
||||||
/// open: resolve the assigned device id to an interface, remember the caller's
|
const found = engine.findInterface(invocation.target) orelse return refused;
|
||||||
/// endpoint (for interrupt reports), and answer with a device token + the
|
// A device another live client holds is refused exactly as an absent one: one
|
||||||
/// interface's endpoints so the class driver need not re-read the config.
|
// device, one class driver. The predecessor's slot is released by the exit
|
||||||
fn handleOpen(message: []const u8, reply: []u8, arrived: *ipc.Arrival) usize {
|
// sweep, and notifications are delivered ahead of requests, so a *restarted*
|
||||||
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 });
|
// driver's open always finds the device free.
|
||||||
const request = std.mem.bytesToValue(usb_transfer_protocol.OpenRequest, message[0..@sizeOf(usb_transfer_protocol.OpenRequest)]);
|
if (!openedBy(invocation.target, invocation.sender) and heldByAnother(invocation.target)) return refused;
|
||||||
const engine = if (controller) |*c| c else return writeReply(reply, usb_transfer_protocol.OpenReply{ .status = -1, .endpoint_count = 0, .device_token = 0, .interface_class = 0, .interface_subclass = 0, .interface_protocol = 0, .interface_number = 0 });
|
|
||||||
const found = engine.findInterface(request.device_id) orelse return writeReply(reply, usb_transfer_protocol.OpenReply{ .status = -1, .endpoint_count = 0, .device_token = 0, .interface_class = 0, .interface_subclass = 0, .interface_protocol = 0, .interface_number = 0 });
|
|
||||||
|
|
||||||
// The report endpoint is claimed only if the open table actually keeps it;
|
// The report endpoint is claimed only if the open table actually keeps it;
|
||||||
// a full table leaves it to the turn to close.
|
// a full table leaves it to the turn to close.
|
||||||
if (arrived.peek()) |endpoint| {
|
if (invocation.capability) |endpoint| {
|
||||||
if (recordOpen(request.device_id, endpoint)) _ = arrived.take();
|
if (recordOpen(invocation.target, invocation.sender, endpoint)) capability_claimed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
var open_reply = usb_transfer_protocol.OpenReply{
|
var opened = usb_transfer_protocol.Opened{
|
||||||
.status = 0,
|
.device_token = invocation.target,
|
||||||
.endpoint_count = found.interface.endpoint_count,
|
.endpoint_count = found.interface.endpoint_count,
|
||||||
.device_token = request.device_id,
|
|
||||||
.interface_class = found.interface.class,
|
.interface_class = found.interface.class,
|
||||||
.interface_subclass = found.interface.subclass,
|
.interface_subclass = found.interface.subclass,
|
||||||
.interface_protocol = found.interface.protocol,
|
.interface_protocol = found.interface.protocol,
|
||||||
|
|
@ -556,57 +617,74 @@ fn handleOpen(message: []const u8, reply: []u8, arrived: *ipc.Arrival) usize {
|
||||||
};
|
};
|
||||||
const count = @min(found.interface.endpoint_count, usb_transfer_protocol.max_reported_endpoints);
|
const count = @min(found.interface.endpoint_count, usb_transfer_protocol.max_reported_endpoints);
|
||||||
for (found.interface.endpoints[0..count], 0..) |endpoint, index| {
|
for (found.interface.endpoints[0..count], 0..) |endpoint, index| {
|
||||||
open_reply.endpoints[index] = .{
|
opened.endpoints[index] = .{
|
||||||
.address = endpoint.address,
|
.address = endpoint.address,
|
||||||
.transfer_type = endpoint.transfer_type,
|
.transfer_type = endpoint.transfer_type,
|
||||||
.max_packet_size = endpoint.max_packet_size,
|
.max_packet_size = endpoint.max_packet_size,
|
||||||
.interval = endpoint.interval,
|
.interval = endpoint.interval,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return writeReply(reply, open_reply);
|
answer.set(opened);
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// control: one EP0 control transfer, small data inline both ways.
|
/// control: one EP0 control transfer. The data stage rides the tail in both
|
||||||
fn handleControl(message: []const u8, reply: []u8) usize {
|
/// directions, so an IN transfer's answer is simply however many bytes were
|
||||||
if (message.len < @sizeOf(usb_transfer_protocol.ControlRequest)) return writeReply(reply, usb_transfer_protocol.ControlReply{ .status = -1, .actual_length = 0 });
|
/// written into `answer.tail()` — which is what `Status.len` then reports.
|
||||||
const request = std.mem.bytesToValue(usb_transfer_protocol.ControlRequest, message[0..@sizeOf(usb_transfer_protocol.ControlRequest)]);
|
fn onControl(_: void, invocation: Invocation(usb_transfer_protocol.Control), answer: Answer(void)) isize {
|
||||||
const engine = if (controller) |*c| c else return writeReply(reply, usb_transfer_protocol.ControlReply{ .status = -1, .actual_length = 0 });
|
const engine = if (controller) |*c| c else return refused;
|
||||||
const found = engine.findInterface(request.device_token) orelse return writeReply(reply, usb_transfer_protocol.ControlReply{ .status = -1, .actual_length = 0 });
|
if (!openedBy(invocation.target, invocation.sender)) 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;
|
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);
|
if (!engine.controlTransfer(found.device, setup, data[0..data_length], direction_in)) return refused;
|
||||||
var control_reply = usb_transfer_protocol.ControlReply{ .status = if (ok) 0 else -1, .actual_length = if (ok) data_length else 0 };
|
if (!direction_in) return 0; // nothing follows an OUT: the status is the whole answer
|
||||||
if (ok and direction_in) @memcpy(control_reply.data[0..data_length], data[0..data_length]);
|
@memcpy(answer.tail()[0..data_length], data[0..data_length]);
|
||||||
return writeReply(reply, control_reply);
|
return @intCast(data_length);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// interrupt_subscribe: arm periodic IN polling; reports flow back asynchronously.
|
/// interrupt_subscribe: arm periodic IN polling; reports flow back asynchronously
|
||||||
fn handleSubscribe(message: []const u8, reply: []u8) usize {
|
/// to the endpoint this device's `open` handed over.
|
||||||
if (message.len < @sizeOf(usb_transfer_protocol.InterruptSubscribeRequest)) return writeReply(reply, usb_transfer_protocol.InterruptSubscribeReply{ .status = -1 });
|
fn onInterruptSubscribe(_: void, invocation: Invocation(usb_transfer_protocol.InterruptSubscribe), _: Answer(void)) isize {
|
||||||
const request = std.mem.bytesToValue(usb_transfer_protocol.InterruptSubscribeRequest, message[0..@sizeOf(usb_transfer_protocol.InterruptSubscribeRequest)]);
|
const engine = if (controller) |*c| c else return refused;
|
||||||
const engine = if (controller) |*c| c else return writeReply(reply, usb_transfer_protocol.InterruptSubscribeReply{ .status = -1 });
|
if (!openedBy(invocation.target, invocation.sender)) return refused;
|
||||||
const found = engine.findInterface(request.device_token) orelse return writeReply(reply, usb_transfer_protocol.InterruptSubscribeReply{ .status = -1 });
|
const found = engine.findInterface(invocation.target) orelse return refused;
|
||||||
const endpoint = library.Controller.endpointForAddress(found.interface, request.endpoint_address) orelse return writeReply(reply, usb_transfer_protocol.InterruptSubscribeReply{ .status = -1 });
|
const endpoint = library.Controller.endpointForAddress(found.interface, invocation.request.endpoint_address) orelse return refused;
|
||||||
const report_endpoint = reportEndpointFor(request.device_token) orelse return writeReply(reply, usb_transfer_protocol.InterruptSubscribeReply{ .status = -1 });
|
const report_endpoint = reportEndpointFor(invocation.target) orelse return refused;
|
||||||
const ok = engine.subscribeInterrupt(found.device, endpoint, request.device_token, report_endpoint);
|
return if (engine.subscribeInterrupt(found.device, endpoint, invocation.target, report_endpoint)) 0 else refused;
|
||||||
return writeReply(reply, usb_transfer_protocol.InterruptSubscribeReply{ .status = if (ok) 0 else -1 });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// bulk: one bulk transfer to/from the class driver's own DMA buffer (by physical
|
/// bulk: one bulk transfer to/from the class driver's own DMA buffer (by physical
|
||||||
/// address), so sector-sized data never crosses IPC.
|
/// address), so sector-sized data never crosses IPC.
|
||||||
fn handleBulk(message: []const u8, reply: []u8) usize {
|
fn onBulk(_: void, invocation: Invocation(usb_transfer_protocol.Bulk), answer: Answer(usb_transfer_protocol.Transferred)) isize {
|
||||||
if (message.len < @sizeOf(usb_transfer_protocol.BulkRequest)) return writeReply(reply, usb_transfer_protocol.BulkReply{ .status = -1, .actual_length = 0 });
|
const engine = if (controller) |*c| c else return refused;
|
||||||
const request = std.mem.bytesToValue(usb_transfer_protocol.BulkRequest, message[0..@sizeOf(usb_transfer_protocol.BulkRequest)]);
|
if (!openedBy(invocation.target, invocation.sender)) return refused;
|
||||||
const engine = if (controller) |*c| c else return writeReply(reply, usb_transfer_protocol.BulkReply{ .status = -1, .actual_length = 0 });
|
const found = engine.findInterface(invocation.target) orelse return refused;
|
||||||
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, invocation.request.endpoint_address) orelse return refused;
|
||||||
const endpoint = library.Controller.endpointForAddress(found.interface, request.endpoint_address) orelse return writeReply(reply, usb_transfer_protocol.BulkReply{ .status = -1, .actual_length = 0 });
|
const transferred = engine.bulkTransfer(found.device, endpoint, invocation.request.physical_address, invocation.request.length) orelse return refused;
|
||||||
const transferred = engine.bulkTransfer(found.device, endpoint, request.physical_address, request.length);
|
answer.set(.{ .actual_length = transferred });
|
||||||
return writeReply(reply, usb_transfer_protocol.BulkReply{ .status = if (transferred != null) 0 else -1, .actual_length = transferred orelse 0 });
|
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.
|
/// A timer tick or an MSI landed: drain the event ring, reconcile ports, and fan out.
|
||||||
|
|
@ -615,6 +693,12 @@ fn handleBulk(message: []const u8, reply: []u8) usize {
|
||||||
/// arriving after the drain takes IP 0→1 and fires a fresh edge instead of being
|
/// arriving after the drain takes IP 0→1 and fires a fresh edge instead of being
|
||||||
/// swallowed until the reconcile tick.
|
/// swallowed until the reconcile tick.
|
||||||
fn onNotification(badge: u64) void {
|
fn onNotification(badge: u64) void {
|
||||||
|
if (badge & ipc.notify_exit_bit != 0) {
|
||||||
|
// A class driver died: release the devices it held, so its successor can
|
||||||
|
// open them and no report is aimed at an endpoint that is gone.
|
||||||
|
releaseOpensOf(@intCast(badge & ~(ipc.notify_badge_bit | ipc.notify_exit_bit)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (badge & ipc.notify_timer_bit != 0) {
|
if (badge & ipc.notify_timer_bit != 0) {
|
||||||
serviceController();
|
serviceController();
|
||||||
_ = time.timerOnce(service_endpoint, timerInterval());
|
_ = time.timerOnce(service_endpoint, timerInterval());
|
||||||
|
|
@ -679,14 +763,18 @@ fn serviceController() void {
|
||||||
if (serviced >= 32) break;
|
if (serviced >= 32) break;
|
||||||
}
|
}
|
||||||
while (engine.takeReport()) |report| {
|
while (engine.takeReport()) |report| {
|
||||||
var message = usb_transfer_protocol.InterruptReport{
|
// One `interrupt_report` event packet: the device token in the folded
|
||||||
.device_token = report.device_token,
|
// header, the report after it. A device that produced more than the
|
||||||
.endpoint_address = report.endpoint_address,
|
// push floor admits has its report truncated here, never split.
|
||||||
.length = @intCast(@min(report.length, usb_transfer_protocol.max_report_data)),
|
|
||||||
};
|
|
||||||
const n = @min(report.length, usb_transfer_protocol.max_report_data);
|
const n = @min(report.length, usb_transfer_protocol.max_report_data);
|
||||||
@memcpy(message.data[0..n], report.data[0..n]);
|
var payload = usb_transfer_protocol.InterruptReport{
|
||||||
_ = ipc.send(report.report_endpoint, std.mem.asBytes(&message));
|
.endpoint_address = report.endpoint_address,
|
||||||
|
.length = @intCast(n),
|
||||||
|
};
|
||||||
|
@memcpy(payload.data[0..n], report.data[0..n]);
|
||||||
|
var packet: [envelope.post_maximum]u8 = undefined;
|
||||||
|
const framed = usb_transfer_protocol.Protocol.encodeEvent(.interrupt_report, report.device_token, payload, &packet) orelse continue;
|
||||||
|
_ = ipc.send(report.report_endpoint, framed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1498,6 +1498,24 @@ pub const Controller = struct {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Stop reporting for `device_token`: deactivate every subscription tagged
|
||||||
|
/// with it, so no further report is queued and the slot can be reused. Called
|
||||||
|
/// when the class driver that owned the token dies — its endpoint handle is
|
||||||
|
/// closed with it, and a queued report would then be aimed at a handle number
|
||||||
|
/// the bus driver has since given to something else. In-process hub
|
||||||
|
/// subscriptions carry no token and are never touched.
|
||||||
|
///
|
||||||
|
/// One completion may already be in flight; it finds no active subscription
|
||||||
|
/// and is dropped as a foreign transfer event, which is exactly what it is.
|
||||||
|
pub fn releaseSubscriptions(self: *Controller, device_token: u64) void {
|
||||||
|
for (&self.subscriptions) |*subscription| {
|
||||||
|
if (!subscription.active or subscription.hub != null) continue;
|
||||||
|
if (subscription.device_token != device_token) continue;
|
||||||
|
subscription.active = false;
|
||||||
|
subscription.report_endpoint = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Arm (or re-arm) a subscription's endpoint with a Normal TRB pointing at its
|
// Arm (or re-arm) a subscription's endpoint with a Normal TRB pointing at its
|
||||||
// report buffer, and ring the endpoint's doorbell so the controller polls it.
|
// report buffer, and ring the endpoint's doorbell so the controller polls it.
|
||||||
fn armInterrupt(self: *Controller, subscription: *Subscription) void {
|
fn armInterrupt(self: *Controller, subscription: *Subscription) void {
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,8 @@ pub fn build(b: *std.Build) void {
|
||||||
.name = "virtio-gpu",
|
.name = "virtio-gpu",
|
||||||
.root_source_file = b.path("virtio-gpu.zig"),
|
.root_source_file = b.path("virtio-gpu.zig"),
|
||||||
.imports = &.{
|
.imports = &.{
|
||||||
"channel", "display-protocol", "driver", "ipc", "logging", "memory", "mmio", "pci",
|
"channel", "display-protocol", "driver", "envelope", "ipc", "logging", "memory",
|
||||||
"process", "scanout-protocol", "service", "time",
|
"mmio", "pci", "process", "scanout-protocol", "service", "time",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
b.installArtifact(exe);
|
b.installArtifact(exe);
|
||||||
|
|
|
||||||
|
|
@ -25,11 +25,19 @@ const memory = @import("memory");
|
||||||
const logging = @import("logging");
|
const logging = @import("logging");
|
||||||
const mmio = @import("mmio");
|
const mmio = @import("mmio");
|
||||||
const pci = @import("pci");
|
const pci = @import("pci");
|
||||||
|
const envelope = @import("envelope");
|
||||||
const display_protocol = @import("display-protocol");
|
const display_protocol = @import("display-protocol");
|
||||||
const scanout_protocol = @import("scanout-protocol");
|
const scanout_protocol = @import("scanout-protocol");
|
||||||
const vp = @import("virtio-pci.zig");
|
const vp = @import("virtio-pci.zig");
|
||||||
const vg = @import("virtio-gpu-protocol.zig");
|
const vg = @import("virtio-gpu-protocol.zig");
|
||||||
|
|
||||||
|
/// The generated scanout dispatch. One scanout per driver instance, so the
|
||||||
|
/// handler context is empty and the mode stays in this file's globals.
|
||||||
|
const Serve = scanout_protocol.Protocol.Provider(void);
|
||||||
|
|
||||||
|
const Invocation = envelope.Invocation;
|
||||||
|
const Answer = envelope.Answer;
|
||||||
|
|
||||||
/// The DisplayFormat (device-abi) our B8G8R8X8 scanout resource presents: bgrx = 1. Handed to
|
/// The DisplayFormat (device-abi) our B8G8R8X8 scanout resource presents: bgrx = 1. Handed to
|
||||||
/// the compositor in the announce so it packs colours in the surface's byte order.
|
/// the compositor in the announce so it packs colours in the surface's byte order.
|
||||||
const display_format_bgrx: u32 = 1;
|
const display_format_bgrx: u32 = 1;
|
||||||
|
|
@ -482,60 +490,60 @@ fn announce() void {
|
||||||
std.log.info("no display service to announce to (scanout-only)", .{});
|
std.log.info("no display service to announce to (scanout-only)", .{});
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
var request = display_protocol.Request{
|
var packet: [display_protocol.message_maximum]u8 = undefined;
|
||||||
.operation = @intFromEnum(display_protocol.Operation.attach_scanout),
|
const framed = display_protocol.Protocol.encodeRequest(.attach_scanout, 0, .{
|
||||||
.x = max_width, // the shared surface's row stride in pixels (it is sized to the max mode)
|
.stride = max_width, // the shared surface's row stride in pixels (it is sized to the max mode)
|
||||||
.y = edid_refresh_hz, // the panel refresh from EDID (0 = unknown) — the frame-clock seed
|
|
||||||
.width = current_width,
|
.width = current_width,
|
||||||
.height = current_height,
|
.height = current_height,
|
||||||
.colour = display_format_bgrx,
|
.format = display_format_bgrx,
|
||||||
};
|
.refresh_hz = edid_refresh_hz, // from EDID (0 = unknown) — the frame-clock seed
|
||||||
var reply: [display_protocol.reply_size]u8 = undefined;
|
}, &.{}, &packet) orelse return;
|
||||||
_ = ipc.callCap(display, std.mem.asBytes(&request), &reply, surface.handle) catch {
|
var reply: [display_protocol.message_maximum]u8 = undefined;
|
||||||
|
_ = ipc.callCap(display, framed, &reply, surface.handle) catch {
|
||||||
std.log.info("announce to display failed", .{});
|
std.log.info("announce to display failed", .{});
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
std.log.info("announced scanout to display", .{});
|
std.log.info("announced scanout to display", .{});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A `scanout_protocol.Reply{status}` written into `reply`.
|
// --- serving the scanout protocol -------------------------------------------
|
||||||
fn scanoutStatus(reply: []u8, ok: bool) usize {
|
//
|
||||||
const response = scanout_protocol.Reply{ .status = if (ok) 0 else -1 };
|
// The compositor drives present / mode queries here. The pixels are already in
|
||||||
@memcpy(reply[0..scanout_protocol.reply_size], std.mem.asBytes(&response));
|
// the shared surface, so a present is a transfer-to-host + fenced flush; a mode
|
||||||
return scanout_protocol.reply_size;
|
// change just re-points the scanout rectangle (the surface is sized to the
|
||||||
|
// largest mode). One scanout, so `Header.target` is always 0.
|
||||||
|
|
||||||
|
/// The device did not take the frame, or the mode asked for is not one this
|
||||||
|
/// scanout offers.
|
||||||
|
const refused: isize = -envelope.ENOENT;
|
||||||
|
|
||||||
|
fn onPresent(_: void, _: Invocation(scanout_protocol.Present), _: Answer(void)) isize {
|
||||||
|
return if (presentFull()) 0 else refused;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The `.scanout` service: the compositor drives present / mode queries here. The pixels are
|
fn onGetModes(_: void, _: Invocation(void), answer: Answer(scanout_protocol.Modes)) isize {
|
||||||
/// already in the shared surface, so a present is a transfer-to-host + fenced flush; a mode
|
var offered = scanout_protocol.Modes{ .count = offered_modes.len };
|
||||||
/// change just re-points the scanout rectangle (the surface is sized to the largest mode).
|
for (0..@min(offered_modes.len, scanout_protocol.max_modes)) |i| {
|
||||||
fn onMessage(message: []const u8, reply: []u8, sender: u32, arrived: *ipc.Arrival) usize {
|
offered.modes[i] = .{ .width = offered_modes[i].width, .height = offered_modes[i].height };
|
||||||
_ = sender;
|
|
||||||
_ = arrived; // nothing here takes a capability: the harness closes what arrives
|
|
||||||
if (message.len < scanout_protocol.request_size) return 0;
|
|
||||||
const request = std.mem.bytesToValue(scanout_protocol.Request, message[0..scanout_protocol.request_size]);
|
|
||||||
switch (request.operation) {
|
|
||||||
@intFromEnum(scanout_protocol.Operation.present) => return scanoutStatus(reply, presentFull()),
|
|
||||||
@intFromEnum(scanout_protocol.Operation.get_modes) => {
|
|
||||||
var response = scanout_protocol.ModesReply{ .status = 0, .count = offered_modes.len, .modes = undefined };
|
|
||||||
for (0..scanout_protocol.max_modes) |i| {
|
|
||||||
response.modes[i] = if (i < offered_modes.len)
|
|
||||||
.{ .width = offered_modes[i].width, .height = offered_modes[i].height }
|
|
||||||
else
|
|
||||||
.{ .width = 0, .height = 0 };
|
|
||||||
}
|
|
||||||
@memcpy(reply[0..scanout_protocol.modes_reply_size], std.mem.asBytes(&response));
|
|
||||||
return scanout_protocol.modes_reply_size;
|
|
||||||
},
|
|
||||||
@intFromEnum(scanout_protocol.Operation.set_mode) => {
|
|
||||||
const w = request.width;
|
|
||||||
const h = request.height;
|
|
||||||
if (w == 0 or h == 0 or w > max_width or h > max_height) return scanoutStatus(reply, false);
|
|
||||||
current_width = w;
|
|
||||||
current_height = h;
|
|
||||||
return scanoutStatus(reply, setScanoutRect());
|
|
||||||
},
|
|
||||||
else => return 0,
|
|
||||||
}
|
}
|
||||||
|
answer.set(offered);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn onSetMode(_: void, invocation: Invocation(scanout_protocol.SetMode), _: Answer(void)) isize {
|
||||||
|
const w = invocation.request.width;
|
||||||
|
const h = invocation.request.height;
|
||||||
|
if (w == 0 or h == 0 or w > max_width or h > max_height) return refused;
|
||||||
|
current_width = w;
|
||||||
|
current_height = h;
|
||||||
|
return if (setScanoutRect()) 0 else refused;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlers = Serve.Handlers{ .present = onPresent, .get_modes = onGetModes, .set_mode = onSetMode };
|
||||||
|
|
||||||
|
fn onMessage(message: []const u8, reply: []u8, sender: u32, arrived: *ipc.Arrival) usize {
|
||||||
|
_ = arrived; // nothing here takes a capability: the harness closes what arrives
|
||||||
|
return Serve.dispatch({}, handlers, message, sender, null, reply);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn main(init: process.Init) void {
|
pub fn main(init: process.Init) void {
|
||||||
|
|
@ -547,7 +555,7 @@ pub fn main(init: process.Init) void {
|
||||||
std.log.info("malformed device id '{s}'", .{argument});
|
std.log.info("malformed device id '{s}'", .{argument});
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
service.run(256, .{
|
service.run(scanout_protocol.message_maximum, .{
|
||||||
.service = "scanout",
|
.service = "scanout",
|
||||||
.init = initialise,
|
.init = initialise,
|
||||||
.on_message = onMessage,
|
.on_message = onMessage,
|
||||||
|
|
|
||||||
|
|
@ -1520,7 +1520,15 @@ pub fn exitReasonOf(caller_id: u32, target_id: u32) i64 {
|
||||||
/// subscriptions — that must release what a dead client held and cannot learn it
|
/// subscriptions — that must release what a dead client held and cannot learn it
|
||||||
/// any other way (a client that simply never calls again looks like silence).
|
/// any other way (a client that simply never calls again looks like silence).
|
||||||
/// Bounded like every kernel table; each entry holds its own endpoint reference.
|
/// Bounded like every kernel table; each entry holds its own endpoint reference.
|
||||||
const exit_subscriber_capacity = 8;
|
///
|
||||||
|
/// Sixteen, not eight: a subscription is now what *every* provider with
|
||||||
|
/// per-client state uses to release it — the FAT server's open files, the input,
|
||||||
|
/// power and device-manager subscriber tables (the service harness subscribes for
|
||||||
|
/// them), the compositor's layers, and each USB controller driver's device tokens.
|
||||||
|
/// A single boot already fields six, and a machine with several xHCI controllers
|
||||||
|
/// fields one per controller, so the old ceiling was within two of a service
|
||||||
|
/// silently losing its sweep.
|
||||||
|
const exit_subscriber_capacity = 16;
|
||||||
const ExitSubscriber = struct { endpoint: *ipc.Endpoint, owner: u32 };
|
const ExitSubscriber = struct { endpoint: *ipc.Endpoint, owner: u32 };
|
||||||
var exit_subscribers: [exit_subscriber_capacity]?ExitSubscriber = .{null} ** exit_subscriber_capacity;
|
var exit_subscribers: [exit_subscriber_capacity]?ExitSubscriber = .{null} ** exit_subscriber_capacity;
|
||||||
|
|
||||||
|
|
@ -1538,16 +1546,27 @@ fn systemProcessSubscribe(state: *architecture.CpuState) void {
|
||||||
if (t.address_space == 0) return fail(state);
|
if (t.address_space == 0) return fail(state);
|
||||||
const endpoint = ipc.resolveHandle(t, architecture.systemCallArg(state, 0)) orelse return failErr(state, ipc.EBADF);
|
const endpoint = ipc.resolveHandle(t, architecture.systemCallArg(state, 0)) orelse return failErr(state, ipc.EBADF);
|
||||||
if (!ipc.ownedBy(endpoint, t)) return failErr(state, ipc.EPERM);
|
if (!ipc.ownedBy(endpoint, t)) return failErr(state, ipc.EPERM);
|
||||||
|
const result = subscribeExits(endpoint, t.id);
|
||||||
|
if (result < 0) return failErr(state, @intCast(-result));
|
||||||
|
architecture.setSystemCallResult(state, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Take a slot in the published-exit table for `endpoint`, owned by task `owner`.
|
||||||
|
/// The body of `process_subscribe` minus the authorization, so a kernel test can
|
||||||
|
/// exercise the fan-out (several subscribers, one death, every one notified) that
|
||||||
|
/// every provider's release-what-the-dead-client-held sweep is built on. Returns
|
||||||
|
/// 0, or -ENOSPC.
|
||||||
|
pub fn subscribeExits(endpoint: *ipc.Endpoint, owner: u32) i64 {
|
||||||
const flags = sync.enter();
|
const flags = sync.enter();
|
||||||
defer sync.leave(flags);
|
defer sync.leave(flags);
|
||||||
for (&exit_subscribers) |*slot| {
|
for (&exit_subscribers) |*slot| {
|
||||||
if (slot.* == null) {
|
if (slot.* == null) {
|
||||||
endpoint.refcount += 1; // the slot's own reference, dropped on unsubscribe-by-death
|
endpoint.refcount += 1; // the slot's own reference, dropped on unsubscribe-by-death
|
||||||
slot.* = .{ .endpoint = endpoint, .owner = t.id };
|
slot.* = .{ .endpoint = endpoint, .owner = owner };
|
||||||
return architecture.setSystemCallResult(state, 0);
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
failErr(state, ipc.ENOSPC);
|
return -ipc.ENOSPC;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// signal_bind(endpoint): nominate where this process's signals arrive — the
|
/// signal_bind(endpoint): nominate where this process's signals arrive — the
|
||||||
|
|
|
||||||
|
|
@ -250,6 +250,8 @@ pub fn run(case: []const u8, boot_information: *const BootInformation) void {
|
||||||
protocolRegistryTest(boot_information);
|
protocolRegistryTest(boot_information);
|
||||||
} else if (eql(case, "protocol-denied")) {
|
} else if (eql(case, "protocol-denied")) {
|
||||||
protocolDeniedTest(boot_information);
|
protocolDeniedTest(boot_information);
|
||||||
|
} else if (eql(case, "protocol-conformance")) {
|
||||||
|
protocolConformanceTest(boot_information);
|
||||||
} else if (eql(case, "reboot")) {
|
} else if (eql(case, "reboot")) {
|
||||||
rebootTest();
|
rebootTest();
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -2246,9 +2248,67 @@ fn processKillTest(boot_information: *const BootInformation) void {
|
||||||
}
|
}
|
||||||
check("neither victim is listed after its kill", !still_listed);
|
check("neither victim is listed after its kill", !still_listed);
|
||||||
check("a killed id stays dead (-ESRCH on a second kill)", process.killProcess(me, sleeper) == -ipcsync.ESRCH);
|
check("a killed id stays dead (-ESRCH on a second kill)", process.killProcess(me, sleeper) == -ipcsync.ESRCH);
|
||||||
|
|
||||||
|
publishedExitChecks(rd, me);
|
||||||
result();
|
result();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The mechanism every provider's release-what-a-dead-client-held sweep is built
|
||||||
|
/// on (docs/process-lifecycle.md, "Who learns of a death"): a **published** exit,
|
||||||
|
/// fanned out to every subscriber rather than only to the supervisor. The service
|
||||||
|
/// harness's subscriber sweep, the FAT server's open files, the compositor's
|
||||||
|
/// layers and the xHCI driver's device tokens all release on exactly this, and
|
||||||
|
/// several of them are subscribed at once in a normal boot — so what is checked
|
||||||
|
/// here is the fan-out: three independent subscribers, one death, three
|
||||||
|
/// notifications carrying the same badge, none of them the supervisor's.
|
||||||
|
///
|
||||||
|
/// Run at the end of the process-kill case, because a subscription is for every
|
||||||
|
/// death from then on and the checks above spawn victims of their own.
|
||||||
|
fn publishedExitChecks(rd: initial_ramdisk.Reader, me: u32) void {
|
||||||
|
var subscribers: [3]*ipcsync.Endpoint = undefined;
|
||||||
|
var subscribed: usize = 0;
|
||||||
|
while (subscribed < subscribers.len) : (subscribed += 1) {
|
||||||
|
subscribers[subscribed] = ipcsync.createIpcEndpoint() orelse break;
|
||||||
|
if (process.subscribeExits(subscribers[subscribed], me) != 0) break;
|
||||||
|
}
|
||||||
|
check("three endpoints subscribed to published exits", subscribed == subscribers.len);
|
||||||
|
if (subscribed != subscribers.len) return;
|
||||||
|
|
||||||
|
// A supervised child so the supervisor notification remains distinguishable:
|
||||||
|
// it lands on `endpoint`, the published ones on the three above.
|
||||||
|
const supervisor_endpoint = ipcsync.createIpcEndpoint() orelse {
|
||||||
|
check("supervisor endpoint allocated", false);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
var child: u32 = 0;
|
||||||
|
var i: u32 = 0;
|
||||||
|
while (i < rd.count) : (i += 1) {
|
||||||
|
const item = rd.entry(i) orelse continue;
|
||||||
|
if (!eql(initial_ramdisk.basename(item.name), "args-echo")) continue;
|
||||||
|
child = process.spawnProcessSupervised(item.blob, 4, &.{ "args-echo", "published-exit" }, me, supervisor_endpoint) catch 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
check("a clean-exit child spawned for the published exit", child != 0);
|
||||||
|
if (child == 0) return;
|
||||||
|
|
||||||
|
var badge: u64 = 0;
|
||||||
|
var received_cap: u64 = 0;
|
||||||
|
_ = ipcsync.replyWait(supervisor_endpoint, 0, 0, 0, 0, abi.no_cap, &badge, &received_cap);
|
||||||
|
check("the supervisor heard the child end", badge == abi.notify_badge_bit | abi.notify_exit_bit | child);
|
||||||
|
|
||||||
|
// The publication happens in the same locked section as the supervisor's
|
||||||
|
// notification and before it, so all three are already queued: a subscriber
|
||||||
|
// that had not heard would block here and time the harness out rather than
|
||||||
|
// pass vacuously.
|
||||||
|
var heard: usize = 0;
|
||||||
|
for (subscribers) |subscriber| {
|
||||||
|
badge = 0;
|
||||||
|
_ = ipcsync.replyWait(subscriber, 0, 0, 0, 0, abi.no_cap, &badge, &received_cap);
|
||||||
|
if (badge == abi.notify_badge_bit | abi.notify_exit_bit | child) heard += 1;
|
||||||
|
}
|
||||||
|
check("every subscriber heard the same death, not just the supervisor", heard == subscribers.len);
|
||||||
|
}
|
||||||
|
|
||||||
/// M17.1: a dead process's device claims are released by the reap, so a restarted
|
/// M17.1: a dead process's device claims are released by the reap, so a restarted
|
||||||
/// driver can claim its hardware again (docs/process-lifecycle.md iron rule 1).
|
/// driver can claim its hardware again (docs/process-lifecycle.md iron rule 1).
|
||||||
/// First the broker release in isolation — two owners, one released, the other's
|
/// First the broker release in isolation — two owners, one released, the other's
|
||||||
|
|
@ -2737,6 +2797,10 @@ fn fatMountTest(boot_information: *const BootInformation) void {
|
||||||
const init_ok = if (process.spawnBundled("/system/services/init")) true else |_| false;
|
const init_ok = if (process.spawnBundled("/system/services/init")) true else |_| false;
|
||||||
check("init spawned (boots the tree, incl. the fat server)", init_ok);
|
check("init spawned (boots the tree, incl. the fat server)", init_ok);
|
||||||
check("fat-test client spawned", spawnNamed(rd, "fat-test"));
|
check("fat-test client spawned", spawnNamed(rd, "fat-test"));
|
||||||
|
// The badge-scoping probe rides the same boot: it needs the fat server for a
|
||||||
|
// node id and the compositor for a layer id, and init starts both. It spawns
|
||||||
|
// its own second process — the intruder — with the ids it holds (P4c).
|
||||||
|
check("badge-scope-test owner spawned", spawnNamed(rd, "badge-scope-test"));
|
||||||
result();
|
result();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3898,6 +3962,68 @@ fn protocolDeniedTest(boot_information: *const BootInformation) void {
|
||||||
result();
|
result();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// P4a — the reserved verbs, asked of live providers
|
||||||
|
/// (docs/security-track-plan.md P4a; docs/os-development/protocol-namespace.md).
|
||||||
|
/// Every protocol rebased onto `envelope.Define` gets `describe` answered from its
|
||||||
|
/// specification and `-ENOSYS` for a verb it does not define, without its provider
|
||||||
|
/// implementing either — this case is where that stops being a host unit test of
|
||||||
|
/// the generated dispatch and becomes an observation of real providers over real
|
||||||
|
/// IPC.
|
||||||
|
///
|
||||||
|
/// The scenario is the assertion's scaffolding, the same shape `protocol-denied`
|
||||||
|
/// uses: `/protocol` (init in its registry role) plus the providers the fixture is
|
||||||
|
/// granted to reach — the **input service** and the **compositor**, two protocols
|
||||||
|
/// of different sizes and different verb counts, so "uniform" means something. The
|
||||||
|
/// fixture reads `/protocol`'s own listing rather than a list compiled into it, so
|
||||||
|
/// what it checks is what this boot actually bound; the three other P4a protocols
|
||||||
|
/// (vfs, block, scanout) sit behind hardware chains this scenario deliberately does
|
||||||
|
/// not boot, and the fixture names them on serial as unchecked rather than passing
|
||||||
|
/// over them.
|
||||||
|
///
|
||||||
|
/// The fixture's `protocol-conformance: ok` is the marker; each contract it checks
|
||||||
|
/// prints its own line, which the harness's ordered regex reads.
|
||||||
|
fn protocolConformanceTest(boot_information: *const BootInformation) void {
|
||||||
|
log("DANOS-TEST-BEGIN: protocol-conformance\n", .{});
|
||||||
|
if (boot_information.initial_ramdisk_len == 0) {
|
||||||
|
check("bootloader handed over an initial_ramdisk", false);
|
||||||
|
result();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.initial_ramdisk_base)))[0..boot_information.initial_ramdisk_len];
|
||||||
|
const rd = initial_ramdisk.Reader.init(image) orelse {
|
||||||
|
check("initial_ramdisk image is valid", false);
|
||||||
|
result();
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
process.setInitialRamdisk(image);
|
||||||
|
check("registry (init) spawned", spawnRegistry(rd));
|
||||||
|
// The two providers under test. Neither needs hardware beyond the framebuffer
|
||||||
|
// the kernel already seeded: input binds /protocol/input and waits for
|
||||||
|
// subscribers, and the compositor binds /protocol/display and composes into
|
||||||
|
// that framebuffer (the display-service scenario boots it exactly this way).
|
||||||
|
check("input service spawned", spawnNamed(rd, "input"));
|
||||||
|
check("display service spawned", spawnNamed(rd, "display"));
|
||||||
|
check("protocol-conformance-test spawned", spawnNamedWithArg(rd, "protocol-conformance-test", "run"));
|
||||||
|
|
||||||
|
const pass_marker = "protocol-conformance: ok";
|
||||||
|
const fail_marker = "protocol-conformance: FAIL";
|
||||||
|
scheduler.setPriority(1);
|
||||||
|
const deadline = architecture.millis() + 20000;
|
||||||
|
var saw_pass = false;
|
||||||
|
var saw_fail = false;
|
||||||
|
while (architecture.millis() < deadline and !saw_pass and !saw_fail) {
|
||||||
|
if (bufferHas(pass_marker)) saw_pass = true;
|
||||||
|
if (bufferHas(fail_marker)) saw_fail = true;
|
||||||
|
scheduler.yield();
|
||||||
|
}
|
||||||
|
scheduler.setPriority(4);
|
||||||
|
|
||||||
|
check("no provider failed the reserved-verb contract", !saw_fail);
|
||||||
|
check("the fixture conformance-checked every provider its scenario boots", saw_pass);
|
||||||
|
result();
|
||||||
|
}
|
||||||
|
|
||||||
fn deviceManagerTest(boot_information: *const BootInformation) void {
|
fn deviceManagerTest(boot_information: *const BootInformation) void {
|
||||||
log("DANOS-TEST-BEGIN: device-manager\n", .{});
|
log("DANOS-TEST-BEGIN: device-manager\n", .{});
|
||||||
if (boot_information.initial_ramdisk_len == 0) {
|
if (boot_information.initial_ramdisk_len == 0) {
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ const logging = @import("logging");
|
||||||
const aml = @import("aml");
|
const aml = @import("aml");
|
||||||
const acpi_ids = @import("acpi-ids");
|
const acpi_ids = @import("acpi-ids");
|
||||||
const device_manager_protocol = @import("device-manager-protocol");
|
const device_manager_protocol = @import("device-manager-protocol");
|
||||||
|
const envelope = @import("envelope");
|
||||||
const power_protocol = @import("power-protocol");
|
const power_protocol = @import("power-protocol");
|
||||||
/// AML opcode/prefix bytes by name (`zero_opcode`, `byte_prefix`, …) — so the `_HID`
|
/// AML opcode/prefix bytes by name (`zero_opcode`, `byte_prefix`, …) — so the `_HID`
|
||||||
/// integer decode names the opcodes instead of bare 0x0A/0x0B/… (docs/coding-standards.md).
|
/// integer decode names the opcodes instead of bare 0x0A/0x0B/… (docs/coding-standards.md).
|
||||||
|
|
@ -59,15 +60,14 @@ const pwrbtn_bit: u16 = 1 << 8;
|
||||||
const sci_en_bit: u32 = 1 << 0;
|
const sci_en_bit: u32 = 1 << 0;
|
||||||
const slp_en: u32 = 1 << 13;
|
const slp_en: u32 = 1 << 13;
|
||||||
|
|
||||||
// The `.power` subscribers: endpoints handed over as capabilities, each
|
/// The `.power` subscribers, kept by the service harness (P4c): endpoints handed
|
||||||
// receiving events as buffered messages. Dropped on a failed send. The
|
/// over as capabilities, each receiving events as buffered messages, each swept
|
||||||
// subscriber's task id is kept too — a shutdown request is honored only from a
|
/// when its task dies. The table remembers which task subscribed, which is what
|
||||||
// subscriber (init subscribes; a stray process does not), the soft gate that
|
/// the shutdown gate below asks — a shutdown request is honored only from a
|
||||||
// stands in for "only the system supervisor may power off" without hardcoding
|
/// subscriber (init subscribes; a stray process does not), the soft gate that
|
||||||
// a pid the kernel's idle tasks would have taken.
|
/// stands in for "only the system supervisor may power off" without hardcoding a
|
||||||
const maximum_subscribers = 8;
|
/// pid the kernel's idle tasks would have taken.
|
||||||
var subscribers: [maximum_subscribers]?ipc.Handle = .{null} ** maximum_subscribers;
|
const Subscriptions = service.Subscribers(power_protocol.Protocol, void);
|
||||||
var subscriber_tasks: [maximum_subscribers]u32 = .{0} ** maximum_subscribers;
|
|
||||||
|
|
||||||
// Pass-1 registration record (see main): what pass 2 reports.
|
// Pass-1 registration record (see main): what pass 2 reports.
|
||||||
const Registered = struct { hid: [8]u8 = .{0} ** 8, hid_len: usize = 0, device_id: u64 = 0, resource_count: u64 = 0 };
|
const Registered = struct { hid: [8]u8 = .{0} ** 8, hid_len: usize = 0, device_id: u64 = 0, resource_count: u64 = 0 };
|
||||||
|
|
@ -200,6 +200,7 @@ pub fn main(init: process.Init) void {
|
||||||
.init = onInit,
|
.init = onInit,
|
||||||
.on_message = onMessage,
|
.on_message = onMessage,
|
||||||
.on_notification = onNotification,
|
.on_notification = onNotification,
|
||||||
|
.subscribers = Subscriptions.hooks,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -241,10 +242,20 @@ fn onInit(endpoint: ipc.Handle) bool {
|
||||||
else
|
else
|
||||||
std.log.info("device {d} bus=acpi hid={s} ({d} resources)", .{ entry.device_id, hid, entry.resource_count });
|
std.log.info("device {d} bus=acpi hid={s} ({d} resources)", .{ entry.device_id, hid, entry.resource_count });
|
||||||
if (manager) |h| {
|
if (manager) |h| {
|
||||||
var report = device_manager_protocol.ChildAdded{ .bus = @intFromEnum(device_manager_protocol.BusKind.acpi), .parent = node_id, .bus_address = entry.device_id, .identity = 0, .device_id = entry.device_id };
|
// The registered device id is the packet's target, so the body only
|
||||||
|
// says where on the firmware tree the node sits and what it is.
|
||||||
|
var report = device_manager_protocol.ChildAdded{
|
||||||
|
.bus = @intFromEnum(device_manager_protocol.BusKind.acpi),
|
||||||
|
.parent = node_id,
|
||||||
|
.bus_address = entry.device_id,
|
||||||
|
.identity = 0,
|
||||||
|
};
|
||||||
@memcpy(report.hid[0..entry.hid_len], entry.hid[0..entry.hid_len]);
|
@memcpy(report.hid[0..entry.hid_len], entry.hid[0..entry.hid_len]);
|
||||||
var reply: [device_manager_protocol.message_maximum]u8 = undefined;
|
var packet: [device_manager_protocol.message_maximum]u8 = undefined;
|
||||||
_ = ipc.call(h, std.mem.asBytes(&report), &reply) catch {};
|
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});
|
std.log.info("reported {d} device(s) to the manager", .{registered_count});
|
||||||
|
|
@ -389,13 +400,21 @@ fn dispatchGpe(n: u32) void {
|
||||||
|
|
||||||
fn publishNotify(node: *aml.Node, code: u64) void {
|
fn publishNotify(node: *aml.Node, code: u64) void {
|
||||||
// Map the notified device's _HID to a domain event where we recognize it.
|
// Map the notified device's _HID to a domain event where we recognize it.
|
||||||
|
// The kind IS the packet's verb, so the mapping picks which event to frame
|
||||||
|
// rather than which tag to put in a payload.
|
||||||
var hid: [8]u8 = .{0} ** 8;
|
var hid: [8]u8 = .{0} ** 8;
|
||||||
if (readHid(node, &global_interpreter)) |h| hid = h;
|
if (readHid(node, &global_interpreter)) |h| hid = h;
|
||||||
const which: power_protocol.Event = if (std.mem.eql(u8, hid[0..7], "PNP0C0A")) .battery else if (std.mem.eql(u8, hid[0..7], "ACPI0003")) .ac else if (std.mem.eql(u8, hid[0..7], "PNP0C0D")) .lid else .notify;
|
const notice = power_protocol.Notice{ .code = @truncate(code), .hid = hid };
|
||||||
var event = power_protocol.EventMessage{ .event = @intFromEnum(which), .code = @truncate(code) };
|
|
||||||
event.hid = hid;
|
|
||||||
std.log.info("power: notify {s} code {d}", .{ hid[0..7], code });
|
std.log.info("power: notify {s} code {d}", .{ hid[0..7], code });
|
||||||
publishEvent(std.mem.asBytes(&event));
|
if (std.mem.eql(u8, hid[0..7], "PNP0C0A")) {
|
||||||
|
Subscriptions.publish(.battery, 0, notice);
|
||||||
|
} else if (std.mem.eql(u8, hid[0..7], "ACPI0003")) {
|
||||||
|
Subscriptions.publish(.ac, 0, notice);
|
||||||
|
} else if (std.mem.eql(u8, hid[0..7], "PNP0C0D")) {
|
||||||
|
Subscriptions.publish(.lid, 0, notice);
|
||||||
|
} else {
|
||||||
|
Subscriptions.publish(.notify, 0, notice);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Two lowercase hex digits of `n` into `out[0..2]`.
|
/// Two lowercase hex digits of `n` into `out[0..2]`.
|
||||||
|
|
@ -406,23 +425,10 @@ fn writeHex2(out: []u8, n: u32) void {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn publishButton() void {
|
fn publishButton() void {
|
||||||
const event = power_protocol.EventMessage{ .event = @intFromEnum(power_protocol.Event.power_button) };
|
// The kind is the packet's operation, so the harness frames it once and pushes
|
||||||
publishEvent(std.mem.asBytes(&event));
|
// the same bytes to every subscriber. There is no class here: a power event
|
||||||
}
|
// goes to everyone who asked for power events.
|
||||||
|
Subscriptions.publish(.power_button, 0, .{});
|
||||||
fn publishEvent(bytes: []const u8) void {
|
|
||||||
for (&subscribers) |*slot| {
|
|
||||||
if (slot.*) |handle| {
|
|
||||||
if (!ipc.send(handle, bytes)) slot.* = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn isSubscriber(task: u32) bool {
|
|
||||||
for (&subscribers, 0..) |*slot, si| {
|
|
||||||
if (slot.* != null and subscriber_tasks[si] == task) return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Enter S5 (soft off): write SLP_TYP|SLP_EN to the PM1 control register(s).
|
/// Enter S5 (soft off): write SLP_TYP|SLP_EN to the PM1 control register(s).
|
||||||
|
|
@ -444,48 +450,39 @@ fn enterS5() void {
|
||||||
// --- harness callbacks --------------------------------------------------------
|
// --- harness callbacks --------------------------------------------------------
|
||||||
|
|
||||||
fn onNotification(badge: u64) void {
|
fn onNotification(badge: u64) void {
|
||||||
// The only notification the service binds is the SCI (an IRQ badge).
|
// Two kinds of notification reach this loop now. The SCI is the one this
|
||||||
_ = badge;
|
// service binds; the published process exits are the harness's, which it has
|
||||||
|
// already used to sweep the subscriber table before calling here. Everything
|
||||||
|
// that is not a bare IRQ badge must therefore be ignored — treating a death
|
||||||
|
// as an interrupt would clear PM1 status the firmware never set.
|
||||||
|
if (badge & (ipc.notify_exit_bit | ipc.notify_timer_bit | ipc.notify_message_bit | ipc.notify_signal_bit) != 0) return;
|
||||||
onSci();
|
onSci();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The `.power` protocol: subscribe (endpoint as the call's capability),
|
const Invocation = envelope.Invocation;
|
||||||
/// shutdown (PID 1 only). Device discovery uses a different endpoint (the
|
const Answer = envelope.Answer;
|
||||||
/// device manager's), so nothing here handles ChildAdded.
|
|
||||||
|
/// The power contract: the reserved `subscribe` (the subscriber's endpoint as
|
||||||
|
/// the call's capability, answered by the harness) and `shutdown` (subscribers
|
||||||
|
/// only). Device discovery uses a different endpoint — the device manager's — so
|
||||||
|
/// nothing here handles a tree report.
|
||||||
fn onMessage(message: []const u8, reply: []u8, sender: u32, arrived: *ipc.Arrival) usize {
|
fn onMessage(message: []const u8, reply: []u8, sender: u32, arrived: *ipc.Arrival) usize {
|
||||||
if (message.len < 1) return 0;
|
return Subscriptions.dispatch({}, handlers, message, sender, arrived, reply);
|
||||||
switch (message[0]) {
|
}
|
||||||
@intFromEnum(power_protocol.Operation.subscribe) => {
|
|
||||||
// The subscriber's endpoint is claimed only when a slot takes it;
|
/// `subscribe` and `unsubscribe` are absent on purpose: the harness answers both.
|
||||||
// a full table refuses and the turn closes what arrived.
|
const handlers = Subscriptions.Handlers{ .shutdown = onShutdown };
|
||||||
var status: i32 = -1;
|
|
||||||
if (arrived.peek() != null) {
|
/// Honored only from a power subscriber — init, which has already run the stop
|
||||||
for (&subscribers, 0..) |*slot, si| {
|
/// sequence over everything else. The power service is mechanism (write S5);
|
||||||
if (slot.* == null) {
|
/// deciding *when* to shut down and stopping the rest of the system first is
|
||||||
slot.* = arrived.take();
|
/// init's policy. The badge is the whole gate: it is kernel-stamped, so nothing
|
||||||
subscriber_tasks[si] = sender;
|
/// in the packet can claim to be init. The subscriber table moved into the
|
||||||
status = 0;
|
/// harness; the question it answers has not changed.
|
||||||
break;
|
fn onShutdown(_: void, invocation: Invocation(void), _: Answer(void)) isize {
|
||||||
}
|
if (!Subscriptions.has(invocation.sender)) return -envelope.EPERM;
|
||||||
}
|
enterS5();
|
||||||
}
|
return 0;
|
||||||
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,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Depth-first walk: register + report each present device with a _HID, then
|
/// 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",
|
.name = "discovery",
|
||||||
.root_source_file = b.path("acpi.zig"),
|
.root_source_file = b.path("acpi.zig"),
|
||||||
.imports = &.{
|
.imports = &.{
|
||||||
"acpi-ids", "aml", "channel", "device-manager-protocol", "driver", "ipc", "logging",
|
"acpi-ids", "aml", "channel", "device-manager-protocol", "driver", "envelope",
|
||||||
"memory", "power-protocol", "process", "service", "time",
|
"ipc", "logging", "memory", "power-protocol", "process", "service", "time",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
b.installArtifact(exe);
|
b.installArtifact(exe);
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,8 @@ pub fn build(b: *std.Build) void {
|
||||||
.name = "device-manager",
|
.name = "device-manager",
|
||||||
.root_source_file = b.path("device-manager.zig"),
|
.root_source_file = b.path("device-manager.zig"),
|
||||||
.imports = &.{
|
.imports = &.{
|
||||||
"device-manager-protocol", "device-registry", "driver", "file-system", "ipc",
|
"device-manager-protocol", "device-registry", "driver", "envelope", "file-system",
|
||||||
"logging", "memory", "process", "service", "time",
|
"ipc", "logging", "memory", "process", "service", "time",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
b.installArtifact(exe);
|
b.installArtifact(exe);
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,17 @@ const time = @import("time");
|
||||||
const memory = @import("memory");
|
const memory = @import("memory");
|
||||||
const logging = @import("logging");
|
const logging = @import("logging");
|
||||||
const device_manager_protocol = @import("device-manager-protocol");
|
const device_manager_protocol = @import("device-manager-protocol");
|
||||||
|
const envelope = @import("envelope");
|
||||||
const registry = @import("device-registry");
|
const registry = @import("device-registry");
|
||||||
|
|
||||||
|
/// The generated device-manager dispatch, plus the subscriber machinery the
|
||||||
|
/// harness owns (P4c): the watcher table, the reserved `subscribe` verb, the
|
||||||
|
/// exit sweep, and the fan-out. One manager per system, so the handler context is
|
||||||
|
/// empty and the tables stay in this file's globals.
|
||||||
|
const Serve = service.Subscribers(device_manager_protocol.Protocol, void);
|
||||||
|
|
||||||
|
const Invocation = envelope.Invocation;
|
||||||
|
const Answer = envelope.Answer;
|
||||||
const fs = @import("file-system");
|
const fs = @import("file-system");
|
||||||
|
|
||||||
// --- the device registry ------------------------------------------------------
|
// --- the device registry ------------------------------------------------------
|
||||||
|
|
@ -148,23 +158,6 @@ var test_scanout_killed = false;
|
||||||
var test_kill_pid: u32 = 0;
|
var test_kill_pid: u32 = 0;
|
||||||
var test_kill_due_ns: u64 = 0;
|
var test_kill_due_ns: u64 = 0;
|
||||||
|
|
||||||
/// The application subscribers (M18.3, the input-service pattern): endpoints
|
|
||||||
/// handed over as capabilities, each receiving every child add/remove as a
|
|
||||||
/// buffered message. A subscriber whose endpoint stops accepting (it died) is
|
|
||||||
/// dropped on the failed send.
|
|
||||||
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 {
|
|
||||||
for (&subscribers) |*slot| {
|
|
||||||
if (slot.*) |handle| {
|
|
||||||
if (!ipc.send(handle, event)) slot.* = null; // dead subscriber
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The manager's mirror of what bus drivers report (docs/device-manager.md "the
|
/// The manager's mirror of what bus drivers report (docs/device-manager.md "the
|
||||||
/// tree"): the children, keyed by (parent, bus address), each remembering which
|
/// tree"): the children, keyed by (parent, bus address), each remembering which
|
||||||
/// driver instance reported it — that is what death-pruning sweeps by.
|
/// driver instance reported it — that is what death-pruning sweeps by.
|
||||||
|
|
@ -209,8 +202,7 @@ fn pruneChildrenOf(reporter: u32) void {
|
||||||
if (child.used and child.reporter == reporter) {
|
if (child.used and child.reporter == reporter) {
|
||||||
std.log.info("child removed (device {d} port {d})", .{ child.parent, child.bus_address });
|
std.log.info("child removed (device {d} port {d})", .{ child.parent, child.bus_address });
|
||||||
child.used = false;
|
child.used = false;
|
||||||
const event = device_manager_protocol.ChildRemoved{ .parent = child.parent, .bus_address = child.bus_address };
|
Serve.publish(.child_removed, 0, .{ .parent = child.parent, .bus_address = child.bus_address });
|
||||||
publishEvent(std.mem.asBytes(&event));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -225,7 +217,11 @@ fn childCountOf(reporter: u32) u32 {
|
||||||
return n;
|
return n;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The driver entry a live process id belongs to. Zero is not a process id here:
|
||||||
|
/// it is what `onDriverExit` writes back to retire an id it has already acted on,
|
||||||
|
/// so a second notification for the same death matches nothing.
|
||||||
fn driverByProcess(process_id: u32) ?*Driver {
|
fn driverByProcess(process_id: u32) ?*Driver {
|
||||||
|
if (process_id == 0) return null;
|
||||||
for (&drivers) |*driver| {
|
for (&drivers) |*driver| {
|
||||||
if (driver.used and driver.process_id == process_id) return driver;
|
if (driver.used and driver.process_id == process_id) return driver;
|
||||||
}
|
}
|
||||||
|
|
@ -293,8 +289,17 @@ fn spawnDriver(driver: *Driver) void {
|
||||||
/// is the whole restart decision: a clean exit meant to stop; anything else
|
/// is the whole restart decision: a clean exit meant to stop; anything else
|
||||||
/// restarts with backoff until the crash-loop cap.
|
/// restarts with backoff until the crash-loop cap.
|
||||||
fn onDriverExit(driver: *Driver) void {
|
fn onDriverExit(driver: *Driver) void {
|
||||||
pruneChildrenOf(driver.process_id);
|
const dead = driver.process_id;
|
||||||
const reason = process.exitReason(driver.process_id) orelse .fault;
|
// One death, two notifications: the manager is this driver's supervisor (its
|
||||||
|
// spawn named this endpoint) *and*, since P4c put the watcher table in the
|
||||||
|
// harness, a subscriber to published exits. Both badges carry the same id, and
|
||||||
|
// the ring delivers them separately — so the id is retired here, before any
|
||||||
|
// decision is taken, and the second notification finds no driver to act on.
|
||||||
|
// Without this the backoff would count one death twice and the crash-loop cap
|
||||||
|
// would fire at half the deaths it names.
|
||||||
|
driver.process_id = 0;
|
||||||
|
pruneChildrenOf(dead);
|
||||||
|
const reason = process.exitReason(dead) orelse .fault;
|
||||||
if (reason == .exited) {
|
if (reason == .exited) {
|
||||||
driver.state = .stopped;
|
driver.state = .stopped;
|
||||||
std.log.info("{s} exited cleanly; not restarting", .{driver.name()});
|
std.log.info("{s} exited cleanly; not restarting", .{driver.name()});
|
||||||
|
|
@ -396,58 +401,61 @@ fn initialise(endpoint: ipc.Handle) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn onMessage(message: []const u8, reply: []u8, sender: u32, arrived: *ipc.Arrival) usize {
|
fn onMessage(message: []const u8, reply: []u8, sender: u32, arrived: *ipc.Arrival) usize {
|
||||||
if (message.len < 1) return 0;
|
return Serve.dispatch({}, handlers, message, sender, arrived, reply);
|
||||||
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]);
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A bus driver reported a discovered device: mirror it, and in
|
/// `subscribe` and `unsubscribe` are absent on purpose: the harness answers both,
|
||||||
/// test-usb-restart mode kill the reporter once after its second child — the
|
/// and its table is what `publish` fans out over.
|
||||||
|
const handlers = Serve.Handlers{
|
||||||
|
.hello = onHello,
|
||||||
|
.child_added = onChildAdded,
|
||||||
|
.child_removed = onChildRemoved,
|
||||||
|
.enumerate = onEnumerate,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// 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.
|
/// deterministic trigger for prune -> backoff -> respawn -> re-report.
|
||||||
fn onChildAdded(message: []const u8, reply: []u8, sender: u32) usize {
|
fn onChildAdded(_: void, invocation: Invocation(device_manager_protocol.ChildAdded), _: Answer(void)) isize {
|
||||||
if (message.len < device_manager_protocol.child_added_size) return 0;
|
const report = invocation.request;
|
||||||
const report = std.mem.bytesToValue(device_manager_protocol.ChildAdded, message[0..device_manager_protocol.child_added_size]);
|
const sender = invocation.sender;
|
||||||
var status: i32 = 0;
|
// The registered kernel device id is the packet's target, not a field: what
|
||||||
|
// the manager hands a matched driver as its argv assignment.
|
||||||
|
const device_id = invocation.target;
|
||||||
|
|
||||||
|
var status: isize = 0;
|
||||||
if (driverByProcess(sender)) |driver| {
|
if (driverByProcess(sender)) |driver| {
|
||||||
if (!addChild(report.parent, report.bus_address, report.identity, report.device_id, sender)) status = -1;
|
if (!addChild(report.parent, report.bus_address, report.identity, device_id, sender)) status = -envelope.ENOSPC;
|
||||||
std.log.info("child added (device {d} port {d}, identity {d}) by {s}", .{ report.parent, report.bus_address, report.identity, driver.name() });
|
std.log.info("child added (device {d} port {d}, identity {d}) by {s}", .{ report.parent, report.bus_address, report.identity, driver.name() });
|
||||||
if (status == 0) publishEvent(message[0..device_manager_protocol.child_added_size]);
|
if (status == 0) Serve.publish(.child_added, device_id, report);
|
||||||
// Matching from reports (M19.3), now data-driven via the /system/configuration/devices.csv
|
// Matching from reports (M19.3), now data-driven via the /system/configuration/devices.csv
|
||||||
// registry: a registered child gets the most-specific driver its identity
|
// registry: a registered child gets the most-specific driver its identity
|
||||||
// matches, once — re-reports after a bus restart dedupe on the registered
|
// matches, once — re-reports after a bus restart dedupe on the registered
|
||||||
// id, exactly like the registrations do.
|
// id, exactly like the registrations do.
|
||||||
if (status == 0 and report.device_id != device_manager_protocol.no_device) {
|
if (status == 0 and device_id != device_manager_protocol.no_device) {
|
||||||
const id = identityFromReport(report);
|
const id = identityFromReport(report);
|
||||||
if (registry.matchDriver(registry_rules[0..registry_count], id)) |match| {
|
if (registry.matchDriver(registry_rules[0..registry_count], id)) |match| {
|
||||||
if (match.ambiguous)
|
if (match.ambiguous)
|
||||||
|
|
@ -458,15 +466,13 @@ fn onChildAdded(message: []const u8, reply: []u8, sender: u32) usize {
|
||||||
if (!alreadySupervised(match.driver)) addDriver(match.driver, device_manager_protocol.no_device, false);
|
if (!alreadySupervised(match.driver)) addDriver(match.driver, device_manager_protocol.no_device, false);
|
||||||
} else {
|
} else {
|
||||||
// A per-device driver: one instance, the registered id as argv[1].
|
// A per-device driver: one instance, the registered id as argv[1].
|
||||||
if (!driverForDevice(report.device_id)) addDriver(match.driver, report.device_id, true);
|
if (!driverForDevice(device_id)) addDriver(match.driver, device_id, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
status = -1;
|
status = -envelope.EPERM;
|
||||||
}
|
}
|
||||||
const report_reply = device_manager_protocol.ReportReply{ .status = status };
|
|
||||||
@memcpy(reply[0..@sizeOf(device_manager_protocol.ReportReply)], std.mem.asBytes(&report_reply));
|
|
||||||
if (test_pci_restart_mode and !test_usb_killed) {
|
if (test_pci_restart_mode and !test_usb_killed) {
|
||||||
if (driverByProcess(sender)) |driver| {
|
if (driverByProcess(sender)) |driver| {
|
||||||
if (std.mem.eql(u8, driver.name(), "pci-bus") and childCountOf(sender) >= 3) {
|
if (std.mem.eql(u8, driver.name(), "pci-bus") and childCountOf(sender) >= 3) {
|
||||||
|
|
@ -494,66 +500,47 @@ fn onChildAdded(message: []const u8, reply: []u8, sender: u32) usize {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return @sizeOf(device_manager_protocol.ReportReply);
|
return status;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A bus driver reported a device gone (hot-unplug; no sender exists yet, but
|
/// A bus driver reported a device gone (hot-unplug). Addressed by the composite
|
||||||
/// the handler is protocol-complete — death-pruning covers removal until then).
|
/// (parent, bus address) the reporter knows, which is why that pair is the
|
||||||
fn onChildRemoved(message: []const u8, reply: []u8, sender: u32) usize {
|
/// packet's body rather than its target.
|
||||||
if (message.len < device_manager_protocol.child_removed_size) return 0;
|
fn onChildRemoved(_: void, invocation: Invocation(device_manager_protocol.ChildRemoved), _: Answer(void)) isize {
|
||||||
const report = std.mem.bytesToValue(device_manager_protocol.ChildRemoved, message[0..device_manager_protocol.child_removed_size]);
|
const report = invocation.request;
|
||||||
var status: i32 = -1;
|
var status: isize = -envelope.ENOENT;
|
||||||
for (&children) |*child| {
|
for (&children) |*child| {
|
||||||
if (child.used and child.parent == report.parent and child.bus_address == report.bus_address and child.reporter == sender) {
|
if (child.used and child.parent == report.parent and child.bus_address == report.bus_address and child.reporter == invocation.sender) {
|
||||||
std.log.info("child removed (device {d} port {d})", .{ child.parent, child.bus_address });
|
std.log.info("child removed (device {d} port {d})", .{ child.parent, child.bus_address });
|
||||||
child.used = false;
|
child.used = false;
|
||||||
status = 0;
|
status = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const report_reply = device_manager_protocol.ReportReply{ .status = status };
|
return status;
|
||||||
@memcpy(reply[0..@sizeOf(device_manager_protocol.ReportReply)], std.mem.asBytes(&report_reply));
|
|
||||||
return @sizeOf(device_manager_protocol.ReportReply);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An application asked for the tree: the mirror, as a header plus entries.
|
/// The reserved `enumerate` verb: the mirror, one `ChildEntry` per known child,
|
||||||
fn onEnumerate(reply: []u8) usize {
|
/// packed into the reply's tail. How many arrived is the reply's own length —
|
||||||
var count: u32 = 0;
|
/// `Status.len` — so no count header is spent saying it twice.
|
||||||
var offset: usize = @sizeOf(device_manager_protocol.EnumerateReply);
|
fn onEnumerate(_: void, _: Invocation(void), answer: Answer(void)) isize {
|
||||||
|
const entry_size = @sizeOf(device_manager_protocol.ChildEntry);
|
||||||
|
const tail = answer.tail();
|
||||||
|
var written: usize = 0;
|
||||||
for (&children) |*child| {
|
for (&children) |*child| {
|
||||||
if (!child.used) continue;
|
if (!child.used) continue;
|
||||||
if (offset + @sizeOf(device_manager_protocol.ChildEntry) > reply.len) break;
|
if (written + entry_size > tail.len) break;
|
||||||
const entry = device_manager_protocol.ChildEntry{ .parent = child.parent, .bus_address = child.bus_address, .identity = child.identity };
|
const entry = device_manager_protocol.ChildEntry{ .parent = child.parent, .bus_address = child.bus_address, .identity = child.identity };
|
||||||
@memcpy(reply[offset..][0..@sizeOf(device_manager_protocol.ChildEntry)], std.mem.asBytes(&entry));
|
@memcpy(tail[written..][0..entry_size], std.mem.asBytes(&entry));
|
||||||
offset += @sizeOf(device_manager_protocol.ChildEntry);
|
written += entry_size;
|
||||||
count += 1;
|
|
||||||
}
|
}
|
||||||
const header = device_manager_protocol.EnumerateReply{ .status = 0, .count = count };
|
return @intCast(written);
|
||||||
@memcpy(reply[0..@sizeOf(device_manager_protocol.EnumerateReply)], std.mem.asBytes(&header));
|
|
||||||
return offset;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// An application subscribed: its endpoint arrived as the call's capability. The
|
|
||||||
/// 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const report_reply = device_manager_protocol.ReportReply{ .status = status };
|
|
||||||
@memcpy(reply[0..@sizeOf(device_manager_protocol.ReportReply)], std.mem.asBytes(&report_reply));
|
|
||||||
return @sizeOf(device_manager_protocol.ReportReply);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn onNotification(badge: u64) void {
|
fn onNotification(badge: u64) void {
|
||||||
if (badge & ipc.notify_exit_bit != 0) {
|
if (badge & ipc.notify_exit_bit != 0) {
|
||||||
const dead: u32 = @intCast(badge & ~(ipc.notify_badge_bit | ipc.notify_exit_bit));
|
const dead: u32 = @intCast(badge & ~(ipc.notify_badge_bit | ipc.notify_exit_bit));
|
||||||
|
// The harness has already swept the watcher table for this death; what is
|
||||||
|
// left is the manager's own concern, its supervised drivers.
|
||||||
if (driverByProcess(dead)) |driver| onDriverExit(driver);
|
if (driverByProcess(dead)) |driver| onDriverExit(driver);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -572,5 +559,6 @@ pub fn main(init: process.Init) void {
|
||||||
.init = initialise,
|
.init = initialise,
|
||||||
.on_message = onMessage,
|
.on_message = onMessage,
|
||||||
.on_notification = onNotification,
|
.on_notification = onNotification,
|
||||||
|
.subscribers = Serve.hooks,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ const memory = @import("memory");
|
||||||
const logging = @import("logging");
|
const logging = @import("logging");
|
||||||
const compositor = @import("compositor.zig");
|
const compositor = @import("compositor.zig");
|
||||||
|
|
||||||
|
const envelope = @import("envelope");
|
||||||
const scanout_protocol = @import("scanout-protocol");
|
const scanout_protocol = @import("scanout-protocol");
|
||||||
const Rect = compositor.Rect;
|
const Rect = compositor.Rect;
|
||||||
const Surface = compositor.Surface;
|
const Surface = compositor.Surface;
|
||||||
|
|
@ -188,45 +189,53 @@ pub const VirtioGpu = struct {
|
||||||
/// accumulated; the driver transfers + fenced-flushes the whole frame.
|
/// accumulated; the driver transfers + fenced-flushes the whole frame.
|
||||||
pub fn present(self: *const VirtioGpu, damage: []const Rect) void {
|
pub fn present(self: *const VirtioGpu, damage: []const Rect) void {
|
||||||
_ = damage;
|
_ = damage;
|
||||||
var request = scanout_protocol.Request{
|
_ = call(self.scanout, .present, .{ .width = self.width, .height = self.height });
|
||||||
.operation = @intFromEnum(scanout_protocol.Operation.present),
|
|
||||||
.width = self.width,
|
|
||||||
.height = self.height,
|
|
||||||
};
|
|
||||||
var reply: [scanout_protocol.reply_size]u8 = undefined;
|
|
||||||
_ = ipc.call(self.scanout, std.mem.asBytes(&request), &reply) catch {};
|
|
||||||
}
|
}
|
||||||
/// Fill `out` with the driver's offered modes; returns how many were written.
|
/// Fill `out` with the driver's offered modes; returns how many were written.
|
||||||
pub fn modes(self: *const VirtioGpu, out: []Mode) usize {
|
pub fn modes(self: *const VirtioGpu, out: []Mode) usize {
|
||||||
var request = scanout_protocol.Request{ .operation = @intFromEnum(scanout_protocol.Operation.get_modes) };
|
const answered = call(self.scanout, .get_modes, {}) orelse return 0;
|
||||||
var reply: [scanout_protocol.modes_reply_size]u8 = undefined;
|
const offered = Scanout.decodeReply(.get_modes, answered.packet[0..answered.len]) orelse return 0;
|
||||||
const n = ipc.call(self.scanout, std.mem.asBytes(&request), &reply) catch return 0;
|
const count = @min(@min(offered.count, scanout_protocol.max_modes), out.len);
|
||||||
if (n < scanout_protocol.modes_reply_size) return 0;
|
for (0..count) |i| out[i] = offered.modes[i];
|
||||||
const answer = std.mem.bytesToValue(scanout_protocol.ModesReply, reply[0..scanout_protocol.modes_reply_size]);
|
|
||||||
if (answer.status != 0) return 0;
|
|
||||||
const count = @min(@min(answer.count, scanout_protocol.max_modes), out.len);
|
|
||||||
for (0..count) |i| out[i] = answer.modes[i];
|
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
/// Change the scanout resolution. On success the active `width`/`height` update (the shared
|
/// Change the scanout resolution. On success the active `width`/`height` update (the shared
|
||||||
/// surface — sized to the max mode — is unchanged, so `stride` stays put).
|
/// surface — sized to the max mode — is unchanged, so `stride` stays put).
|
||||||
pub fn setMode(self: *VirtioGpu, w: u32, h: u32) bool {
|
pub fn setMode(self: *VirtioGpu, w: u32, h: u32) bool {
|
||||||
if (w == 0 or h == 0 or w > self.stride) return false;
|
if (w == 0 or h == 0 or w > self.stride) return false;
|
||||||
var request = scanout_protocol.Request{
|
_ = call(self.scanout, .set_mode, .{ .width = w, .height = h }) orelse return false;
|
||||||
.operation = @intFromEnum(scanout_protocol.Operation.set_mode),
|
|
||||||
.width = w,
|
|
||||||
.height = h,
|
|
||||||
};
|
|
||||||
var reply: [scanout_protocol.reply_size]u8 = undefined;
|
|
||||||
const n = ipc.call(self.scanout, std.mem.asBytes(&request), &reply) catch return false;
|
|
||||||
if (n < scanout_protocol.reply_size) return false;
|
|
||||||
if (std.mem.bytesToValue(scanout_protocol.Reply, reply[0..scanout_protocol.reply_size]).status != 0) return false;
|
|
||||||
self.width = w;
|
self.width = w;
|
||||||
self.height = h;
|
self.height = h;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const Scanout = scanout_protocol.Protocol;
|
||||||
|
|
||||||
|
/// A reply the driver answered with, kept whole so the caller can decode the
|
||||||
|
/// verb's own fixed part out of it.
|
||||||
|
const Answered = struct {
|
||||||
|
packet: [scanout_protocol.message_maximum]u8,
|
||||||
|
len: usize,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// One request at the scanout driver. Null covers both a transport failure and a
|
||||||
|
/// driver that refused — a present that did not happen is a present that did not
|
||||||
|
/// happen, and this backend has nothing to do about either but skip the frame.
|
||||||
|
fn call(
|
||||||
|
scanout: ipc.Handle,
|
||||||
|
comptime operation: Scanout.Operation,
|
||||||
|
request: Scanout.RequestOf(operation),
|
||||||
|
) ?Answered {
|
||||||
|
var packet: [scanout_protocol.message_maximum]u8 = undefined;
|
||||||
|
const framed = Scanout.encodeRequest(operation, 0, request, &.{}, &packet) orelse return null;
|
||||||
|
var answered: Answered = .{ .packet = undefined, .len = 0 };
|
||||||
|
answered.len = ipc.call(scanout, framed, &answered.packet) catch return null;
|
||||||
|
const status = envelope.statusOf(answered.packet[0..answered.len]) orelse return null;
|
||||||
|
if (status.status != 0) return null;
|
||||||
|
return answered;
|
||||||
|
}
|
||||||
|
|
||||||
/// The pluggable scanout backend. A tagged union so the compositor holds one value and
|
/// The pluggable scanout backend. A tagged union so the compositor holds one value and
|
||||||
/// dispatches without caring which is active; the `virtio` native backend joins `gop` at V4.
|
/// dispatches without caring which is active; the `virtio` native backend joins `gop` at V4.
|
||||||
pub const Backend = union(enum) {
|
pub const Backend = union(enum) {
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,9 @@ pub fn build(b: *std.Build) void {
|
||||||
.name = "display",
|
.name = "display",
|
||||||
.root_source_file = b.path("display.zig"),
|
.root_source_file = b.path("display.zig"),
|
||||||
.imports = &.{
|
.imports = &.{
|
||||||
"channel", "display-client", "display-protocol", "driver", "input-client", "ipc",
|
"channel", "display-client", "display-protocol", "driver", "envelope", "input-client",
|
||||||
"logging", "memory", "scanout-protocol", "service", "thread", "time",
|
"ipc", "logging", "memory", "process", "scanout-protocol",
|
||||||
|
"service", "thread", "time",
|
||||||
},
|
},
|
||||||
.threaded = true, // real atomics/TLS (docs/threading.md)
|
.threaded = true, // real atomics/TLS (docs/threading.md)
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ const std = @import("std");
|
||||||
const channel = @import("channel");
|
const channel = @import("channel");
|
||||||
const ipc = @import("ipc");
|
const ipc = @import("ipc");
|
||||||
const input = @import("input-client");
|
const input = @import("input-client");
|
||||||
|
const process = @import("process");
|
||||||
const Thread = @import("thread").Thread;
|
const Thread = @import("thread").Thread;
|
||||||
const service = @import("service");
|
const service = @import("service");
|
||||||
const time = @import("time");
|
const time = @import("time");
|
||||||
|
|
@ -28,10 +29,22 @@ const logging = @import("logging");
|
||||||
const compositor = @import("compositor.zig");
|
const compositor = @import("compositor.zig");
|
||||||
const backend_mod = @import("backend.zig");
|
const backend_mod = @import("backend.zig");
|
||||||
|
|
||||||
|
const envelope = @import("envelope");
|
||||||
const display_protocol = @import("display-protocol");
|
const display_protocol = @import("display-protocol");
|
||||||
const Rect = compositor.Rect;
|
const Rect = compositor.Rect;
|
||||||
const Surface = compositor.Surface;
|
const Surface = compositor.Surface;
|
||||||
|
|
||||||
|
/// The generated display dispatch. One compositor per process, so the handler
|
||||||
|
/// context is empty and the layer stack stays in this file's globals.
|
||||||
|
const Serve = display_protocol.Protocol.Provider(void);
|
||||||
|
|
||||||
|
const Invocation = envelope.Invocation;
|
||||||
|
const Answer = envelope.Answer;
|
||||||
|
|
||||||
|
/// What a handler returns when the layer named in `Header.target` is not one of
|
||||||
|
/// ours, or a mode was refused.
|
||||||
|
const refused: isize = -envelope.ENOENT;
|
||||||
|
|
||||||
/// The active scanout backend — the GOP framebuffer at boot, upgraded to a native driver
|
/// The active scanout backend — the GOP framebuffer at boot, upgraded to a native driver
|
||||||
/// (virtio-gpu) when one announces itself (V4).
|
/// (virtio-gpu) when one announces itself (V4).
|
||||||
var backend: backend_mod.Backend = undefined;
|
var backend: backend_mod.Backend = undefined;
|
||||||
|
|
@ -62,8 +75,22 @@ var background: u32 = 0;
|
||||||
/// small copies rather than one huge bounding box (see compositor.DamageList).
|
/// small copies rather than one huge bounding box (see compositor.DamageList).
|
||||||
const maximum_layers = 16;
|
const maximum_layers = 16;
|
||||||
|
|
||||||
|
/// A layer belongs to the client that created it. `owner` is the kernel-stamped
|
||||||
|
/// badge of that task, and `service_owned` (0, an id no task wears) marks the
|
||||||
|
/// compositor's own layers — the cursor sprite and the self-check pair — which
|
||||||
|
/// this file creates by direct call rather than over the protocol.
|
||||||
|
///
|
||||||
|
/// Layer ids are slots in a sixteen-entry table: small, dense, and guessable, so
|
||||||
|
/// before this field any client could configure, draw into, or destroy any
|
||||||
|
/// other's layer — including the cursor. The check lives in the protocol handlers
|
||||||
|
/// (docs/os-development/protocol-namespace.md: handles validated against the
|
||||||
|
/// badge); the internal helpers stay unscoped precisely so the compositor can
|
||||||
|
/// still drive its own.
|
||||||
|
const service_owned: u32 = 0;
|
||||||
|
|
||||||
const Layer = struct {
|
const Layer = struct {
|
||||||
used: bool = false,
|
used: bool = false,
|
||||||
|
owner: u32 = service_owned,
|
||||||
x: i32 = 0,
|
x: i32 = 0,
|
||||||
y: i32 = 0,
|
y: i32 = 0,
|
||||||
z: u32 = 0,
|
z: u32 = 0,
|
||||||
|
|
@ -177,7 +204,8 @@ fn layerAt(id: u32) ?*Layer {
|
||||||
return &layers[id];
|
return &layers[id];
|
||||||
}
|
}
|
||||||
|
|
||||||
fn createLayer(x: i32, y: i32, w: u32, h: u32, z: u32, visible: bool) ?u32 {
|
/// Create a layer for `owner` — `service_owned` for the compositor's own.
|
||||||
|
fn createLayer(owner: u32, x: i32, y: i32, w: u32, h: u32, z: u32, visible: bool) ?u32 {
|
||||||
if (w == 0 or h == 0) return null;
|
if (w == 0 or h == 0) return null;
|
||||||
const slot = freeLayer() orelse return null;
|
const slot = freeLayer() orelse return null;
|
||||||
const len = @as(usize, w) * h * 4;
|
const len = @as(usize, w) * h * 4;
|
||||||
|
|
@ -185,6 +213,7 @@ fn createLayer(x: i32, y: i32, w: u32, h: u32, z: u32, visible: bool) ?u32 {
|
||||||
if (memory.mmapFailed(base)) return null;
|
if (memory.mmapFailed(base)) return null;
|
||||||
layers[slot] = .{
|
layers[slot] = .{
|
||||||
.used = true,
|
.used = true,
|
||||||
|
.owner = owner,
|
||||||
.x = x,
|
.x = x,
|
||||||
.y = y,
|
.y = y,
|
||||||
.z = z,
|
.z = z,
|
||||||
|
|
@ -314,11 +343,18 @@ fn verifyNativePresent() void {
|
||||||
/// `systemSharedMemoryMap`) — the pixels stay ours after the handle naming them
|
/// `systemSharedMemoryMap`) — the pixels stay ours after the handle naming them
|
||||||
/// goes, and a driver that dies and re-announces no longer costs a handle slot
|
/// goes, and a driver that dies and re-announces no longer costs a handle slot
|
||||||
/// per restart.
|
/// per restart.
|
||||||
fn attachScanout(stride: u32, width: u32, height: u32, format: u32, refresh_hz: u32, arrived: *ipc.Arrival, reply: []u8) usize {
|
fn onAttachScanout(_: void, invocation: Invocation(display_protocol.AttachScanout), _: Answer(void)) isize {
|
||||||
const cap = arrived.peek() orelse return fail(reply);
|
const announce = invocation.request;
|
||||||
if (width == 0 or height == 0 or stride < width) return fail(reply);
|
const stride = announce.stride;
|
||||||
const mapped = memory.sharedMap(cap) orelse return fail(reply);
|
const width = announce.width;
|
||||||
const scanout = channel.openEndpoint("scanout") orelse return fail(reply);
|
const height = announce.height;
|
||||||
|
const format = announce.format;
|
||||||
|
const refresh_hz = announce.refresh_hz;
|
||||||
|
|
||||||
|
const cap = invocation.capability orelse return refused;
|
||||||
|
if (width == 0 or height == 0 or stride < width) return refused;
|
||||||
|
const mapped = memory.sharedMap(cap) orelse return refused;
|
||||||
|
const scanout = channel.openEndpoint("scanout") orelse return refused;
|
||||||
// A second announce means the driver died and was restarted (V6): re-attach to its fresh
|
// A second announce means the driver died and was restarted (V6): re-attach to its fresh
|
||||||
// scanout. (The previous shared mapping leaks — there is no shared_memory_unmap syscall yet — but the
|
// scanout. (The previous shared mapping leaks — there is no shared_memory_unmap syscall yet — but the
|
||||||
// frames are the dead driver's, reclaimed on its exit; a handful across a crash is benign.)
|
// frames are the dead driver's, reclaimed on its exit; a handful across a crash is benign.)
|
||||||
|
|
@ -346,7 +382,7 @@ fn attachScanout(stride: u32, width: u32, height: u32, format: u32, refresh_hz:
|
||||||
"display: scanout re-attached\n"
|
"display: scanout re-attached\n"
|
||||||
else
|
else
|
||||||
"display: scanout upgraded to virtio-gpu\n");
|
"display: scanout upgraded to virtio-gpu\n");
|
||||||
return ok(reply);
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// After the native upgrade is verified, prove the runtime-resolution-change and fenced-present
|
/// After the native upgrade is verified, prove the runtime-resolution-change and fenced-present
|
||||||
|
|
@ -401,8 +437,8 @@ fn selfCheck() void {
|
||||||
const format = backend.info().format;
|
const format = backend.info().format;
|
||||||
const red = display_protocol.pack(format, 0xC0, 0x20, 0x20);
|
const red = display_protocol.pack(format, 0xC0, 0x20, 0x20);
|
||||||
const green = display_protocol.pack(format, 0x20, 0xC0, 0x20);
|
const green = display_protocol.pack(format, 0x20, 0xC0, 0x20);
|
||||||
const bottom = createLayer(100, 100, 80, 80, 0, true) orelse return fail_check("create");
|
const bottom = createLayer(service_owned, 100, 100, 80, 80, 0, true) orelse return fail_check("create");
|
||||||
const top = createLayer(140, 140, 80, 80, 1, true) orelse return fail_check("create");
|
const top = createLayer(service_owned, 140, 140, 80, 80, 1, true) orelse return fail_check("create");
|
||||||
_ = fillLayer(bottom, Rect.init(0, 0, 80, 80), red);
|
_ = fillLayer(bottom, Rect.init(0, 0, 80, 80), red);
|
||||||
_ = fillLayer(top, Rect.init(0, 0, 80, 80), green);
|
_ = fillLayer(top, Rect.init(0, 0, 80, 80), green);
|
||||||
present();
|
present();
|
||||||
|
|
@ -567,7 +603,7 @@ fn startCursorTracking() void {
|
||||||
const mode = backend.info();
|
const mode = backend.info();
|
||||||
cursor_origin_x = @divTrunc(@as(i32, @intCast(mode.width)), 2);
|
cursor_origin_x = @divTrunc(@as(i32, @intCast(mode.width)), 2);
|
||||||
cursor_origin_y = @divTrunc(@as(i32, @intCast(mode.height)), 2);
|
cursor_origin_y = @divTrunc(@as(i32, @intCast(mode.height)), 2);
|
||||||
const id = createLayer(cursor_origin_x, cursor_origin_y, cursor_size, cursor_size, cursor_z, true) orelse {
|
const id = createLayer(service_owned, cursor_origin_x, cursor_origin_y, cursor_size, cursor_size, cursor_z, true) orelse {
|
||||||
_ = logging.write("display: could not create cursor layer\n");
|
_ = logging.write("display: could not create cursor layer\n");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
@ -584,6 +620,9 @@ fn startCursorTracking() void {
|
||||||
|
|
||||||
fn initialise(endpoint: ipc.Handle) bool {
|
fn initialise(endpoint: ipc.Handle) bool {
|
||||||
service_endpoint = endpoint;
|
service_endpoint = endpoint;
|
||||||
|
// Layers are per-client state, so the compositor needs deaths: a client that
|
||||||
|
// crashes leaves its surfaces on screen and its slots spent otherwise.
|
||||||
|
_ = process.subscribeExits(endpoint);
|
||||||
|
|
||||||
// Pick the scanout backend (GOP today). It logs the reason on failure.
|
// Pick the scanout backend (GOP today). It logs the reason on failure.
|
||||||
backend = backend_mod.select() orelse return false;
|
backend = backend_mod.select() orelse return false;
|
||||||
|
|
@ -609,97 +648,149 @@ fn initialise(endpoint: ipc.Handle) bool {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn writeReply(reply: []u8, value: display_protocol.Reply) usize {
|
// --- the protocol handlers --------------------------------------------------
|
||||||
const bytes = std.mem.asBytes(&value);
|
//
|
||||||
@memcpy(reply[0..bytes.len], bytes);
|
// A layer id is `Header.target` on every verb that names one, so no handler
|
||||||
return bytes.len;
|
// reads a layer out of its own request any more. `target` is a u64 and a layer
|
||||||
|
// id a u32: a value that does not fit is not a layer of ours, and `layerAt`
|
||||||
|
// refuses it the same way an out-of-range one is refused.
|
||||||
|
|
||||||
|
/// The layer a packet addresses, **for the task that sent it**: null unless the
|
||||||
|
/// target names a used slot this sender created. A layer that is somebody else's
|
||||||
|
/// is refused exactly as one that never existed, so a client cannot use the
|
||||||
|
/// refusal to learn which ids are live (P3's refusal-equals-absence, applied to
|
||||||
|
/// ids rather than names).
|
||||||
|
fn targetLayer(target: u64, sender: u32) ?u32 {
|
||||||
|
if (target > std.math.maxInt(u32)) return null; // a layer id is a u32
|
||||||
|
const id: u32 = @intCast(target);
|
||||||
|
const layer = layerAt(id) orelse return null;
|
||||||
|
if (layer.owner != sender) return null;
|
||||||
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ok(reply: []u8) usize {
|
/// Destroy every layer a dead client left behind — its surface is pages nobody
|
||||||
return writeReply(reply, .{ .status = 0 });
|
/// will ever draw into again, and its slot is one of sixteen. The published
|
||||||
}
|
/// exit events are the notice, the same sweep idiom the FAT server uses for open
|
||||||
|
/// files and the harness uses for subscribers.
|
||||||
fn fail(reply: []u8) usize {
|
fn releaseLayersOf(dead: u32) void {
|
||||||
return writeReply(reply, .{ .status = -1 });
|
var released: u32 = 0;
|
||||||
}
|
for (&layers, 0..) |*layer, id| {
|
||||||
|
if (layer.used and layer.owner == dead) {
|
||||||
fn onMessage(message: []const u8, reply: []u8, sender: u32, arrived: *ipc.Arrival) usize {
|
_ = destroyLayer(@intCast(id));
|
||||||
_ = sender;
|
released += 1;
|
||||||
if (message.len < display_protocol.request_size) return fail(reply);
|
}
|
||||||
const request = std.mem.bytesToValue(display_protocol.Request, message[0..display_protocol.request_size]);
|
}
|
||||||
const payload = message[display_protocol.request_size..];
|
if (released != 0) {
|
||||||
// Switch on the raw operation value — an out-of-range one must fail cleanly, not
|
std.log.info("released {d} layer(s) for dead client {d}", .{ released, dead });
|
||||||
// panic an `@enumFromInt`.
|
schedulePresent(); // the screen still shows what they painted
|
||||||
switch (request.operation) {
|
|
||||||
@intFromEnum(display_protocol.Operation.info) => {
|
|
||||||
const m = backend.info();
|
|
||||||
return writeReply(reply, .{ .status = 0, .width = m.width, .height = m.height, .pitch = m.pitch, .format = m.format });
|
|
||||||
},
|
|
||||||
@intFromEnum(display_protocol.Operation.create_layer) => {
|
|
||||||
// x/y are signed coordinates carried in the u32 wire fields — reinterpret the
|
|
||||||
// bits (@bitCast), don't range-check (@intCast) which a negative would fail.
|
|
||||||
const slot = createLayer(@bitCast(request.x), @bitCast(request.y), request.width, request.height, request.z, request.visible != 0) orelse return fail(reply);
|
|
||||||
return writeReply(reply, .{ .status = 0, .layer = slot });
|
|
||||||
},
|
|
||||||
@intFromEnum(display_protocol.Operation.configure_layer) => {
|
|
||||||
return if (configureLayer(request.layer, @bitCast(request.x), @bitCast(request.y), request.z, request.visible != 0)) ok(reply) else fail(reply);
|
|
||||||
},
|
|
||||||
@intFromEnum(display_protocol.Operation.destroy_layer) => {
|
|
||||||
return if (destroyLayer(request.layer)) ok(reply) else fail(reply);
|
|
||||||
},
|
|
||||||
@intFromEnum(display_protocol.Operation.fill_rect) => {
|
|
||||||
const local = Rect.init(@bitCast(request.x), @bitCast(request.y), @intCast(request.width), @intCast(request.height));
|
|
||||||
return if (fillLayer(request.layer, local, request.colour)) ok(reply) else fail(reply);
|
|
||||||
},
|
|
||||||
@intFromEnum(display_protocol.Operation.blit_tile) => {
|
|
||||||
return if (blitLayer(request.layer, @bitCast(request.x), @bitCast(request.y), request.width, request.height, payload)) ok(reply) else fail(reply);
|
|
||||||
},
|
|
||||||
@intFromEnum(display_protocol.Operation.damage) => {
|
|
||||||
const l = layerAt(request.layer) orelse return fail(reply);
|
|
||||||
const screen = Rect{ .x = l.x + @as(i32, @bitCast(request.x)), .y = l.y + @as(i32, @bitCast(request.y)), .w = @intCast(request.width), .h = @intCast(request.height) };
|
|
||||||
addDamage(screen.intersect(layerScreenRect(l)));
|
|
||||||
return ok(reply);
|
|
||||||
},
|
|
||||||
@intFromEnum(display_protocol.Operation.present) => {
|
|
||||||
// Scheduled, not immediate: the frame clock composites the accumulated damage
|
|
||||||
// at the next tick, so back-to-back client presents coalesce into one frame.
|
|
||||||
schedulePresent();
|
|
||||||
return ok(reply);
|
|
||||||
},
|
|
||||||
@intFromEnum(display_protocol.Operation.attach_scanout) => {
|
|
||||||
return attachScanout(request.x, request.width, request.height, request.colour, request.y, arrived, reply);
|
|
||||||
},
|
|
||||||
@intFromEnum(display_protocol.Operation.set_mode) => {
|
|
||||||
if (!backend.setMode(request.width, request.height)) return fail(reply);
|
|
||||||
addDamage(screenRect()); // repaint the whole screen at the new resolution
|
|
||||||
present();
|
|
||||||
return ok(reply);
|
|
||||||
},
|
|
||||||
@intFromEnum(display_protocol.Operation.get_modes) => {
|
|
||||||
var list: [4]backend_mod.Mode = undefined;
|
|
||||||
const count = backend.modes(&list);
|
|
||||||
var response = display_protocol.ModesReply{ .status = 0, .count = @intCast(count), .modes = undefined };
|
|
||||||
for (0..display_protocol.max_modes) |i| {
|
|
||||||
response.modes[i] = if (i < count)
|
|
||||||
.{ .width = list[i].width, .height = list[i].height }
|
|
||||||
else
|
|
||||||
.{ .width = 0, .height = 0 };
|
|
||||||
}
|
|
||||||
const bytes = std.mem.asBytes(&response);
|
|
||||||
@memcpy(reply[0..bytes.len], bytes);
|
|
||||||
return bytes.len;
|
|
||||||
},
|
|
||||||
else => return fail(reply),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Two notification sources reach the compositor, and one coalesced badge can carry
|
fn onInfo(_: void, _: Invocation(void), answer: Answer(display_protocol.Info)) isize {
|
||||||
/// both, so each bit is handled independently. A **message-notification** is a poke from
|
const mode = backend.info();
|
||||||
/// the mouse-listener thread (a buffered self-`ipc.send`, `notify_message_bit`): fold the
|
answer.set(.{ .width = mode.width, .height = mode.height, .pitch = mode.pitch, .format = mode.format });
|
||||||
/// newest cursor position into the scene. A **timer** (`notify_timer_bit`) is the frame
|
return 0;
|
||||||
/// clock — or the deferred first native present after `attach_scanout` — either way,
|
}
|
||||||
/// present the accumulated damage.
|
|
||||||
|
fn onCreateLayer(_: void, invocation: Invocation(display_protocol.CreateLayer), answer: Answer(display_protocol.Created)) isize {
|
||||||
|
const request = invocation.request;
|
||||||
|
const slot = createLayer(invocation.sender, request.x, request.y, request.width, request.height, request.z, request.visible != 0) orelse return refused;
|
||||||
|
answer.set(.{ .layer = slot });
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn onConfigureLayer(_: void, invocation: Invocation(display_protocol.ConfigureLayer), _: Answer(void)) isize {
|
||||||
|
const id = targetLayer(invocation.target, invocation.sender) orelse return refused;
|
||||||
|
const request = invocation.request;
|
||||||
|
return if (configureLayer(id, request.x, request.y, request.z, request.visible != 0)) 0 else refused;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn onDestroyLayer(_: void, invocation: Invocation(void), _: Answer(void)) isize {
|
||||||
|
const id = targetLayer(invocation.target, invocation.sender) orelse return refused;
|
||||||
|
return if (destroyLayer(id)) 0 else refused;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn onFillRect(_: void, invocation: Invocation(display_protocol.FillRect), _: Answer(void)) isize {
|
||||||
|
const id = targetLayer(invocation.target, invocation.sender) orelse return refused;
|
||||||
|
const request = invocation.request;
|
||||||
|
const local = Rect.init(request.x, request.y, @intCast(request.width), @intCast(request.height));
|
||||||
|
return if (fillLayer(id, local, request.colour)) 0 else refused;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn onBlitTile(_: void, invocation: Invocation(display_protocol.BlitTile), _: Answer(void)) isize {
|
||||||
|
const id = targetLayer(invocation.target, invocation.sender) orelse return refused;
|
||||||
|
const request = invocation.request;
|
||||||
|
return if (blitLayer(id, request.x, request.y, request.width, request.height, invocation.tail)) 0 else refused;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn onDamage(_: void, invocation: Invocation(display_protocol.Damage), _: Answer(void)) isize {
|
||||||
|
const id = targetLayer(invocation.target, invocation.sender) orelse return refused;
|
||||||
|
const l = layerAt(id) orelse return refused;
|
||||||
|
const request = invocation.request;
|
||||||
|
const screen = Rect{ .x = l.x + request.x, .y = l.y + request.y, .w = @intCast(request.width), .h = @intCast(request.height) };
|
||||||
|
addDamage(screen.intersect(layerScreenRect(l)));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn onPresent(_: void, _: Invocation(void), _: Answer(void)) isize {
|
||||||
|
// Scheduled, not immediate: the frame clock composites the accumulated damage
|
||||||
|
// at the next tick, so back-to-back client presents coalesce into one frame.
|
||||||
|
schedulePresent();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn onSetMode(_: void, invocation: Invocation(display_protocol.SetMode), _: Answer(void)) isize {
|
||||||
|
if (!backend.setMode(invocation.request.width, invocation.request.height)) return refused;
|
||||||
|
addDamage(screenRect()); // repaint the whole screen at the new resolution
|
||||||
|
present();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn onGetModes(_: void, _: Invocation(void), answer: Answer(display_protocol.Modes)) isize {
|
||||||
|
var list: [4]backend_mod.Mode = undefined;
|
||||||
|
const count = backend.modes(&list);
|
||||||
|
var modes = display_protocol.Modes{ .count = @intCast(count) };
|
||||||
|
for (0..@min(count, display_protocol.max_modes)) |i| {
|
||||||
|
modes.modes[i] = .{ .width = list[i].width, .height = list[i].height };
|
||||||
|
}
|
||||||
|
answer.set(modes);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlers = Serve.Handlers{
|
||||||
|
.info = onInfo,
|
||||||
|
.create_layer = onCreateLayer,
|
||||||
|
.configure_layer = onConfigureLayer,
|
||||||
|
.destroy_layer = onDestroyLayer,
|
||||||
|
.fill_rect = onFillRect,
|
||||||
|
.blit_tile = onBlitTile,
|
||||||
|
.damage = onDamage,
|
||||||
|
.present = onPresent,
|
||||||
|
.attach_scanout = onAttachScanout,
|
||||||
|
.set_mode = onSetMode,
|
||||||
|
.get_modes = onGetModes,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn onMessage(message: []const u8, reply: []u8, sender: u32, arrived: *ipc.Arrival) usize {
|
||||||
|
// The one capability this service is ever handed is the scanout driver's
|
||||||
|
// shared surface, and `attachScanout` deliberately does not claim it (the
|
||||||
|
// mapping holds its own reference) — so the capability is peeked, never
|
||||||
|
// taken, and the turn closes it.
|
||||||
|
return Serve.dispatch({}, handlers, message, sender, arrived.peek(), reply);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Three notification sources reach the compositor, and one coalesced badge can carry
|
||||||
|
/// more than one, so each bit is handled independently. A **message-notification** is a
|
||||||
|
/// poke from the mouse-listener thread (a buffered self-`ipc.send`, `notify_message_bit`):
|
||||||
|
/// fold the newest cursor position into the scene. A **timer** (`notify_timer_bit`) is the
|
||||||
|
/// frame clock — or the deferred first native present after `attach_scanout` — either way,
|
||||||
|
/// present the accumulated damage. A **published exit** (`notify_exit_bit`) is a client
|
||||||
|
/// gone: release the layers it left.
|
||||||
fn onNotification(badge: u64) void {
|
fn onNotification(badge: u64) void {
|
||||||
|
if (badge & ipc.notify_exit_bit != 0) {
|
||||||
|
releaseLayersOf(@intCast(badge & ~(ipc.notify_badge_bit | ipc.notify_exit_bit)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (badge & ipc.notify_message_bit != 0) renderCursor();
|
if (badge & ipc.notify_message_bit != 0) renderCursor();
|
||||||
if (badge & ipc.notify_timer_bit != 0) frameTick();
|
if (badge & ipc.notify_timer_bit != 0) frameTick();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,8 @@ pub fn build(b: *std.Build) void {
|
||||||
.name = "fat",
|
.name = "fat",
|
||||||
.root_source_file = b.path("fat.zig"),
|
.root_source_file = b.path("fat.zig"),
|
||||||
.imports = &.{
|
.imports = &.{
|
||||||
"block", "file-system", "ipc", "logging", "memory", "process", "service", "time",
|
"block", "envelope", "file-system", "ipc", "logging", "memory", "process",
|
||||||
"vfs-protocol",
|
"service", "time", "vfs-protocol",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
b.installArtifact(exe);
|
b.installArtifact(exe);
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,17 @@ const memory = @import("memory");
|
||||||
const logging = @import("logging");
|
const logging = @import("logging");
|
||||||
const engine = @import("engine.zig");
|
const engine = @import("engine.zig");
|
||||||
const on_disk = @import("on-disk.zig");
|
const on_disk = @import("on-disk.zig");
|
||||||
|
const envelope = @import("envelope");
|
||||||
const vfs_protocol = @import("vfs-protocol");
|
const vfs_protocol = @import("vfs-protocol");
|
||||||
|
|
||||||
|
/// The generated vfs dispatch, bound to this server. There is one FAT volume per
|
||||||
|
/// process, so the handler context is empty and the state stays where it was: in
|
||||||
|
/// this file's globals.
|
||||||
|
const Serve = vfs_protocol.Protocol.Provider(void);
|
||||||
|
|
||||||
|
const Invocation = envelope.Invocation;
|
||||||
|
const Answer = envelope.Answer;
|
||||||
|
|
||||||
const mount_point = "/volumes/usb";
|
const mount_point = "/volumes/usb";
|
||||||
|
|
||||||
// The engine's BlockDevice, backed by the `.block` driver plus a DMA bounce
|
// The engine's BlockDevice, backed by the `.block` driver plus a DMA bounce
|
||||||
|
|
@ -57,8 +66,9 @@ var ipc_block: IpcBlock = undefined;
|
||||||
var device_dirty: bool = false;
|
var device_dirty: bool = false;
|
||||||
var filesystem: engine.FileSystem = undefined;
|
var filesystem: engine.FileSystem = undefined;
|
||||||
|
|
||||||
// Open handles the VFS holds against this backend: each maps a node id to a
|
// Open handles clients hold against this backend: each maps a node id to a
|
||||||
// resolved engine node.
|
// resolved engine node, and to the client that opened it. `owner` is the
|
||||||
|
// kernel-stamped badge of the opening task — the only source identity there is.
|
||||||
const OpenNode = struct { used: bool = false, node: engine.Node = undefined, owner: u32 = 0 };
|
const OpenNode = struct { used: bool = false, node: engine.Node = undefined, owner: u32 = 0 };
|
||||||
var open_nodes = [_]OpenNode{.{}} ** 32;
|
var open_nodes = [_]OpenNode{.{}} ** 32;
|
||||||
|
|
||||||
|
|
@ -69,22 +79,32 @@ fn allocOpen() ?usize {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn openAt(id: u64) ?*OpenNode {
|
/// The open node `id` names **for `owner`** — null unless the id is in range, in
|
||||||
|
/// use, and this client's own. Node ids are small integers drawn from a table of
|
||||||
|
/// thirty-two, so they are trivially guessable; before this check every client
|
||||||
|
/// honoured every other client's ids, which is the hole
|
||||||
|
/// docs/os-development/protocol-namespace.md names ("handles must be scoped per
|
||||||
|
/// client — validated against the badge"). Nothing else about them changed: they
|
||||||
|
/// are still per-session, still swept when their owner dies.
|
||||||
|
///
|
||||||
|
/// The owner is a *task*, not a process, because the badge is: a threaded client
|
||||||
|
/// reads and writes a node from the thread that opened it, exactly as the exit
|
||||||
|
/// sweep already released a worker thread's handles when that thread died.
|
||||||
|
fn openFor(id: u64, owner: u32) ?*OpenNode {
|
||||||
if (id >= open_nodes.len) return null;
|
if (id >= open_nodes.len) return null;
|
||||||
const o = &open_nodes[@intCast(id)];
|
const o = &open_nodes[@intCast(id)];
|
||||||
return if (o.used) o else null;
|
if (!o.used or o.owner != owner) return null;
|
||||||
|
return o;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn writeReply(out: []u8, reply: vfs_protocol.Reply, payload: []const u8) usize {
|
/// What a handler returns when the thing asked for is not there — a bad node id,
|
||||||
@memcpy(out[0..vfs_protocol.reply_size], std.mem.asBytes(&reply));
|
/// a node that is someone else's, a path that does not resolve, a mutation the
|
||||||
const n = @min(payload.len, out.len - vfs_protocol.reply_size);
|
/// volume refused. One errno for all of them, because a filesystem's failures are
|
||||||
@memcpy(out[vfs_protocol.reply_size..][0..n], payload[0..n]);
|
/// all "no such thing" as far as the file API can act on them — and because
|
||||||
return vfs_protocol.reply_size + n;
|
/// *someone else's* must be indistinguishable from *nobody's*, or the refusal
|
||||||
}
|
/// would itself tell a prober which ids are live (the same discipline the
|
||||||
|
/// protocol namespace's refused open follows).
|
||||||
fn fail(out: []u8) usize {
|
const refused: isize = -envelope.ENOENT;
|
||||||
return writeReply(out, .{ .status = -1 }, &.{});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// How often to look for a block device while none is mounted. Storage arriving
|
/// How often to look for a block device while none is mounted. Storage arriving
|
||||||
/// is EVENT-shaped (the usb chain registering, possibly after a driver restart),
|
/// is EVENT-shaped (the usb chain registering, possibly after a driver restart),
|
||||||
|
|
@ -204,24 +224,133 @@ fn splitParent(path: []const u8) ParentLeaf {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handleOpen(out: []u8, path: []const u8, flags: u32, sender: u32) usize {
|
fn onOpen(_: void, invocation: Invocation(vfs_protocol.Open), answer: Answer(vfs_protocol.Opened)) isize {
|
||||||
|
const path = invocation.tail;
|
||||||
|
const flags = invocation.request.flags;
|
||||||
var node = filesystem.resolve(path);
|
var node = filesystem.resolve(path);
|
||||||
if (node == null and flags & vfs_protocol.create != 0) {
|
if (node == null and flags & vfs_protocol.create != 0) {
|
||||||
const split = splitParent(path);
|
const split = splitParent(path);
|
||||||
const parent = filesystem.resolve(split.parent) orelse return fail(out);
|
const parent = filesystem.resolve(split.parent) orelse return refused;
|
||||||
node = filesystem.createFile(parent, split.leaf);
|
node = filesystem.createFile(parent, split.leaf);
|
||||||
}
|
}
|
||||||
var resolved = node orelse return fail(out);
|
var resolved = node orelse return refused;
|
||||||
// O_TRUNC: replace an existing file's contents rather than overwriting in place
|
// O_TRUNC: replace an existing file's contents rather than overwriting in place
|
||||||
// (frees the old chain, so a shorter rewrite leaves no stale tail).
|
// (frees the old chain, so a shorter rewrite leaves no stale tail).
|
||||||
if (flags & vfs_protocol.truncate != 0 and !resolved.is_directory) {
|
if (flags & vfs_protocol.truncate != 0 and !resolved.is_directory) {
|
||||||
filesystem.truncate(&resolved);
|
filesystem.truncate(&resolved);
|
||||||
}
|
}
|
||||||
const index = allocOpen() orelse return fail(out);
|
const index = allocOpen() orelse return refused;
|
||||||
open_nodes[index] = .{ .used = true, .node = resolved, .owner = sender };
|
open_nodes[index] = .{ .used = true, .node = resolved, .owner = invocation.sender };
|
||||||
return writeReply(out, .{ .status = 0, .node = index }, &.{});
|
answer.set(.{ .node = index });
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn onRead(_: void, invocation: Invocation(vfs_protocol.Read), answer: Answer(void)) isize {
|
||||||
|
const o = openFor(invocation.target, invocation.sender) orelse return refused;
|
||||||
|
const into = answer.tail();
|
||||||
|
const want = @min(@as(usize, invocation.request.len), into.len);
|
||||||
|
return @intCast(filesystem.readFile(o.node, @intCast(invocation.request.offset), into[0..want]));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn onWrite(_: void, invocation: Invocation(vfs_protocol.Write), answer: Answer(vfs_protocol.Written)) isize {
|
||||||
|
const o = openFor(invocation.target, invocation.sender) orelse return refused;
|
||||||
|
const data = invocation.tail[0..@min(invocation.tail.len, invocation.request.len)];
|
||||||
|
const n = filesystem.writeFile(&o.node, @intCast(invocation.request.offset), data);
|
||||||
|
answer.set(.{ .count = @intCast(n) });
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn onStatus(_: void, invocation: Invocation(void), answer: Answer(vfs_protocol.FileStatus)) isize {
|
||||||
|
const o = openFor(invocation.target, invocation.sender) orelse return refused;
|
||||||
|
const kind: vfs_protocol.NodeKind = if (o.node.is_directory) .directory else .regular;
|
||||||
|
answer.set(.{ .size = o.node.size, .kind = @intFromEnum(kind), .mtime = o.node.mtime });
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One entry per call. End of directory — a node that is not a directory, or a
|
||||||
|
/// cursor past the last child — is an entry with no name, which is how the
|
||||||
|
/// protocol spells it now that the reply's length always counts the fixed part.
|
||||||
|
fn onReaddir(_: void, invocation: Invocation(vfs_protocol.Readdir), answer: Answer(vfs_protocol.DirectoryEntry)) isize {
|
||||||
|
const o = openFor(invocation.target, invocation.sender) orelse return refused;
|
||||||
|
if (!o.node.is_directory) {
|
||||||
|
answer.set(.{});
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const listing = filesystem.listEntry(o.node, @intCast(invocation.request.cursor)) orelse {
|
||||||
|
answer.set(.{});
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
const kind: vfs_protocol.NodeKind = if (listing.is_directory) .directory else .regular;
|
||||||
|
const into = answer.tail();
|
||||||
|
const name_len = @min(listing.name_len, into.len);
|
||||||
|
@memcpy(into[0..name_len], listing.name_buffer[0..name_len]);
|
||||||
|
answer.set(.{ .kind = @intFromEnum(kind), .name_len = @intCast(name_len), .size = listing.size });
|
||||||
|
return @intCast(name_len);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Closing is an operation on a node like any other, so it is scoped like any
|
||||||
|
/// other: a client may release its own handles and nobody else's. An id that is
|
||||||
|
/// not the caller's — free, out of range, or another client's — is refused
|
||||||
|
/// identically, so a close cannot be used to ask which ids are live either.
|
||||||
|
fn onClose(_: void, invocation: Invocation(void), _: Answer(void)) isize {
|
||||||
|
const o = openFor(invocation.target, invocation.sender) orelse return refused;
|
||||||
|
o.used = false;
|
||||||
|
// Durable-on-close: if any block reached the device since the last flush,
|
||||||
|
// commit its cache to stable media now (best-effort). This is what makes
|
||||||
|
// init's shutdown log flush survive a real power-off, and is the right
|
||||||
|
// default for removable media the user may unplug.
|
||||||
|
if (device_dirty) {
|
||||||
|
_ = ipc_block.device.flush();
|
||||||
|
device_dirty = false;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn onMakeDirectory(_: void, invocation: Invocation(void), _: Answer(void)) isize {
|
||||||
|
const path = invocation.tail;
|
||||||
|
if (filesystem.resolve(path) != null) return refused; // already exists — no duplicate entries
|
||||||
|
const split = splitParent(path);
|
||||||
|
const parent = filesystem.resolve(split.parent) orelse return refused;
|
||||||
|
if (filesystem.createDirectory(parent, split.leaf) == null) return refused;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn onUnlink(_: void, invocation: Invocation(void), _: Answer(void)) isize {
|
||||||
|
const split = splitParent(invocation.tail);
|
||||||
|
const parent = filesystem.resolve(split.parent) orelse return refused;
|
||||||
|
if (!filesystem.removeFile(parent, split.leaf)) return refused;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn onRename(_: void, invocation: Invocation(void), _: Answer(void)) isize {
|
||||||
|
const both = invocation.tail;
|
||||||
|
const separator = std.mem.indexOfScalar(u8, both, 0) orelse return refused;
|
||||||
|
const old_split = splitParent(both[0..separator]);
|
||||||
|
const new_split = splitParent(both[separator + 1 ..]);
|
||||||
|
// Same-directory rename only.
|
||||||
|
if (!std.mem.eql(u8, old_split.parent, new_split.parent)) return refused;
|
||||||
|
const parent = filesystem.resolve(old_split.parent) orelse return refused;
|
||||||
|
if (!filesystem.rename(parent, old_split.leaf, new_split.leaf)) return refused;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The verbs this backend implements. The three it leaves out — `mount`,
|
||||||
|
/// `unmount`, `bind` — answer `-ENOSYS` from the generated dispatch, which is
|
||||||
|
/// exactly right: path routing is the kernel's now, and only init implements
|
||||||
|
/// `bind` (docs/os-development/protocol-namespace.md). `describe` is the
|
||||||
|
/// envelope's own.
|
||||||
|
const handlers = Serve.Handlers{
|
||||||
|
.open = onOpen,
|
||||||
|
.close = onClose,
|
||||||
|
.read = onRead,
|
||||||
|
.write = onWrite,
|
||||||
|
.status = onStatus,
|
||||||
|
.readdir = onReaddir,
|
||||||
|
.mkdir = onMakeDirectory,
|
||||||
|
.unlink = onUnlink,
|
||||||
|
.rename = onRename,
|
||||||
|
};
|
||||||
|
|
||||||
/// The vfs protocol has no operation that takes a capability, so `arrived` is
|
/// The vfs protocol has no operation that takes a capability, so `arrived` is
|
||||||
/// never claimed here — which, under the harness's ownership rule, means the
|
/// never claimed here — which, under the harness's ownership rule, means the
|
||||||
/// loop closes whatever a caller attached. That is the point of the rule: this
|
/// loop closes whatever a caller attached. That is the point of the rule: this
|
||||||
|
|
@ -230,92 +359,16 @@ fn handleOpen(out: []u8, path: []const u8, flags: u32, sender: u32) usize {
|
||||||
/// thirty-two until it could accept no capability at all.
|
/// thirty-two until it could accept no capability at all.
|
||||||
fn onMessage(message: []const u8, out: []u8, sender: u32, arrived: *ipc.Arrival) usize {
|
fn onMessage(message: []const u8, out: []u8, sender: u32, arrived: *ipc.Arrival) usize {
|
||||||
_ = arrived;
|
_ = arrived;
|
||||||
if (!mounted) return fail(out); // storage not up (yet): fail politely, clients retry
|
// Storage not up (yet): fail politely, whatever was asked — clients retry.
|
||||||
if (message.len < vfs_protocol.request_size) return fail(out);
|
if (!mounted) {
|
||||||
const request = std.mem.bytesToValue(vfs_protocol.Request, message[0..vfs_protocol.request_size]);
|
const status = envelope.Status{ .status = refused, .len = 0 };
|
||||||
const payload = message[vfs_protocol.request_size..];
|
@memcpy(out[0..envelope.prefix_size], std.mem.asBytes(&status));
|
||||||
|
return envelope.prefix_size;
|
||||||
|
}
|
||||||
// Stamp create/write with the current wall-clock time (mtime). Cheap, and it
|
// Stamp create/write with the current wall-clock time (mtime). Cheap, and it
|
||||||
// keeps the engine pure (it takes the time as data, not a syscall).
|
// keeps the engine pure (it takes the time as data, not a syscall).
|
||||||
filesystem.current_time_epoch = time.wallClock();
|
filesystem.current_time_epoch = time.wallClock();
|
||||||
|
return Serve.dispatch({}, handlers, message, sender, null, out);
|
||||||
switch (request.operation) {
|
|
||||||
.open => return handleOpen(out, payload[0..@min(payload.len, request.len)], request.flags, sender),
|
|
||||||
.read => {
|
|
||||||
const o = openAt(request.node) orelse return fail(out);
|
|
||||||
var buffer: [vfs_protocol.maximum_payload]u8 = undefined;
|
|
||||||
const want = @min(@as(usize, request.len), buffer.len);
|
|
||||||
const n = filesystem.readFile(o.node, @intCast(request.offset), buffer[0..want]);
|
|
||||||
return writeReply(out, .{ .status = 0, .len = @intCast(n) }, buffer[0..n]);
|
|
||||||
},
|
|
||||||
.write => {
|
|
||||||
const o = openAt(request.node) orelse return fail(out);
|
|
||||||
const data = payload[0..@min(payload.len, request.len)];
|
|
||||||
const n = filesystem.writeFile(&o.node, @intCast(request.offset), data);
|
|
||||||
return writeReply(out, .{ .status = 0, .len = @intCast(n) }, &.{});
|
|
||||||
},
|
|
||||||
.status => {
|
|
||||||
const o = openAt(request.node) orelse return fail(out);
|
|
||||||
const kind: vfs_protocol.NodeKind = if (o.node.is_directory) .directory else .regular;
|
|
||||||
const status = vfs_protocol.FileStatus{ .size = o.node.size, .kind = @intFromEnum(kind), .mtime = o.node.mtime };
|
|
||||||
return writeReply(out, .{ .status = 0, .len = @sizeOf(vfs_protocol.FileStatus) }, std.mem.asBytes(&status));
|
|
||||||
},
|
|
||||||
.readdir => {
|
|
||||||
const o = openAt(request.node) orelse return fail(out);
|
|
||||||
if (!o.node.is_directory) return writeReply(out, .{ .status = 0, .len = 0 }, &.{});
|
|
||||||
const listing = filesystem.listEntry(o.node, @intCast(request.offset)) orelse return writeReply(out, .{ .status = 0, .len = 0 }, &.{});
|
|
||||||
const kind: vfs_protocol.NodeKind = if (listing.is_directory) .directory else .regular;
|
|
||||||
const header = vfs_protocol.DirectoryEntry{ .kind = @intFromEnum(kind), .name_len = @intCast(listing.name_len), .size = listing.size };
|
|
||||||
var buffer: [vfs_protocol.maximum_payload]u8 = undefined;
|
|
||||||
@memcpy(buffer[0..vfs_protocol.directory_entry_size], std.mem.asBytes(&header));
|
|
||||||
const nlen = @min(listing.name_len, buffer.len - vfs_protocol.directory_entry_size);
|
|
||||||
@memcpy(buffer[vfs_protocol.directory_entry_size..][0..nlen], listing.name_buffer[0..nlen]);
|
|
||||||
const total = vfs_protocol.directory_entry_size + nlen;
|
|
||||||
return writeReply(out, .{ .status = 0, .len = @intCast(total) }, buffer[0..total]);
|
|
||||||
},
|
|
||||||
.close => {
|
|
||||||
if (openAt(request.node)) |o| o.used = false;
|
|
||||||
// Durable-on-close: if any block reached the device since the last
|
|
||||||
// flush, commit its cache to stable media now (best-effort). This is
|
|
||||||
// what makes init's shutdown log flush survive a real power-off, and is
|
|
||||||
// the right default for removable media the user may unplug.
|
|
||||||
if (device_dirty) {
|
|
||||||
_ = ipc_block.device.flush();
|
|
||||||
device_dirty = false;
|
|
||||||
}
|
|
||||||
return writeReply(out, .{ .status = 0 }, &.{});
|
|
||||||
},
|
|
||||||
.mkdir => {
|
|
||||||
const path = payload[0..@min(payload.len, request.len)];
|
|
||||||
if (filesystem.resolve(path) != null) return fail(out); // already exists — no duplicate entries
|
|
||||||
const split = splitParent(path);
|
|
||||||
const parent = filesystem.resolve(split.parent) orelse return fail(out);
|
|
||||||
if (filesystem.createDirectory(parent, split.leaf) == null) return fail(out);
|
|
||||||
return writeReply(out, .{ .status = 0 }, &.{});
|
|
||||||
},
|
|
||||||
.unlink => {
|
|
||||||
const split = splitParent(payload[0..@min(payload.len, request.len)]);
|
|
||||||
const parent = filesystem.resolve(split.parent) orelse return fail(out);
|
|
||||||
if (!filesystem.removeFile(parent, split.leaf)) return fail(out);
|
|
||||||
return writeReply(out, .{ .status = 0 }, &.{});
|
|
||||||
},
|
|
||||||
.rename => {
|
|
||||||
const both = payload[0..@min(payload.len, request.len)];
|
|
||||||
const sep = std.mem.indexOfScalar(u8, both, 0) orelse return fail(out);
|
|
||||||
const old_split = splitParent(both[0..sep]);
|
|
||||||
const new_split = splitParent(both[sep + 1 ..]);
|
|
||||||
// Same-directory rename only.
|
|
||||||
if (!std.mem.eql(u8, old_split.parent, new_split.parent)) return fail(out);
|
|
||||||
const parent = filesystem.resolve(old_split.parent) orelse return fail(out);
|
|
||||||
if (!filesystem.rename(parent, old_split.leaf, new_split.leaf)) return fail(out);
|
|
||||||
return writeReply(out, .{ .status = 0 }, &.{});
|
|
||||||
},
|
|
||||||
// A backend is never itself a mount target.
|
|
||||||
// Router verbs, and the registry's claim verb: a file backend answers
|
|
||||||
// none of them (docs/os-development/protocol-namespace.md — only init
|
|
||||||
// implements `bind`).
|
|
||||||
.mount, .unmount, .bind => return fail(out),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn main() void {
|
pub fn main() void {
|
||||||
|
|
|
||||||
|
|
@ -548,34 +548,43 @@ var heartbeat_running = false;
|
||||||
/// `pending_capability`. `arrived` is the capability the *request* carried, owned
|
/// `pending_capability`. `arrived` is the capability the *request* carried, owned
|
||||||
/// by the turn — nothing here has to close it, only `bind` has to claim it.
|
/// by the turn — nothing here has to close it, only `bind` has to claim it.
|
||||||
fn serveRegistry(request_bytes: []const u8, reply: []u8, sender: u32, arrived: *Arrival) usize {
|
fn serveRegistry(request_bytes: []const u8, reply: []u8, sender: u32, arrived: *Arrival) usize {
|
||||||
if (request_bytes.len < vfs_protocol.request_size)
|
if (request_bytes.len < envelope.prefix_size)
|
||||||
return answer(reply, -envelope.EPROTO, 0, 0);
|
return answer(reply, -envelope.EPROTO, 0);
|
||||||
// The header is read field by field rather than reinterpreted whole: the
|
// The header is read field by field rather than reinterpreted whole, and the
|
||||||
// operation is an enum on the wire and the bytes come from anyone at all, so
|
// verb is compared as a number rather than decoded into the generated
|
||||||
// a value outside it must be a refusal, never a decoded enum.
|
// `Operation`: the bytes come from anyone at all, so a value outside the enum
|
||||||
|
// must be a refusal, never an `@enumFromInt`. This is deliberately NOT
|
||||||
|
// `Protocol.Provider.dispatch` for the same reason — PID 1 reads a stranger's
|
||||||
|
// packet, and it reads it by hand.
|
||||||
const operation = std.mem.readInt(u32, request_bytes[0..4], .little);
|
const operation = std.mem.readInt(u32, request_bytes[0..4], .little);
|
||||||
const cursor = std.mem.readInt(u64, request_bytes[16..24], .little);
|
const body = request_bytes[envelope.prefix_size..];
|
||||||
const declared = std.mem.readInt(u32, request_bytes[24..28], .little);
|
|
||||||
const payload_len = @min(@as(usize, declared), request_bytes.len - vfs_protocol.request_size);
|
|
||||||
const payload = request_bytes[vfs_protocol.request_size..][0..payload_len];
|
|
||||||
|
|
||||||
if (operation == @intFromEnum(vfs_protocol.Operation.bind))
|
if (operation == @intFromEnum(vfs_protocol.Operation.bind))
|
||||||
return answer(reply, onBind(sender, payload, arrived), 0, 0);
|
return answer(reply, onBind(sender, body, arrived), 0);
|
||||||
// Only `bind` claims a capability; one attached to anything else is closed by
|
// Only `bind` claims a capability; one attached to anything else is closed by
|
||||||
// the turn's `defer` in the loop, along with the ones sent to a request that
|
// the turn's `defer` in the loop, along with the ones sent to a request that
|
||||||
// was too short to name a verb at all.
|
// was too short to name a verb at all.
|
||||||
if (operation == @intFromEnum(vfs_protocol.Operation.open)) return onOpen(reply, sender, payload);
|
if (operation == @intFromEnum(vfs_protocol.Operation.open)) {
|
||||||
if (operation == @intFromEnum(vfs_protocol.Operation.readdir)) return onReaddir(reply, cursor);
|
// `open`'s fixed part is the flags word, which means nothing to a
|
||||||
|
// namespace; the name follows it as the packet's tail.
|
||||||
|
if (body.len < @sizeOf(vfs_protocol.Open)) return answer(reply, -envelope.EPROTO, 0);
|
||||||
|
return onOpen(reply, sender, body[@sizeOf(vfs_protocol.Open)..]);
|
||||||
|
}
|
||||||
|
if (operation == @intFromEnum(vfs_protocol.Operation.readdir)) {
|
||||||
|
if (body.len < @sizeOf(vfs_protocol.Readdir)) return answer(reply, -envelope.EPROTO, 0);
|
||||||
|
return onReaddir(reply, std.mem.readInt(u64, body[0..8], .little));
|
||||||
|
}
|
||||||
// Everything else a filesystem answers is meaningless here: `/protocol` holds
|
// Everything else a filesystem answers is meaningless here: `/protocol` holds
|
||||||
// contracts, not bytes.
|
// contracts, not bytes.
|
||||||
return answer(reply, -envelope.ENOSYS, 0, 0);
|
return answer(reply, -envelope.ENOSYS, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Lay down a vfs reply header (and say how many payload bytes follow it).
|
/// Lay down the envelope's reply prefix (and say how many payload bytes the
|
||||||
fn answer(reply: []u8, status: i32, node: u64, payload_len: usize) usize {
|
/// caller has already written after it).
|
||||||
const header = vfs_protocol.Reply{ .status = status, .node = node, .len = @intCast(payload_len) };
|
fn answer(reply: []u8, status: i32, payload_len: usize) usize {
|
||||||
@memcpy(reply[0..vfs_protocol.reply_size], std.mem.asBytes(&header));
|
const header = envelope.Status{ .status = status, .len = @intCast(payload_len) };
|
||||||
return vfs_protocol.reply_size + payload_len;
|
@memcpy(reply[0..envelope.prefix_size], std.mem.asBytes(&header));
|
||||||
|
return envelope.prefix_size + payload_len;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `bind(name, capability = the provider's endpoint)`. The capability is the
|
/// `bind(name, capability = the provider's endpoint)`. The capability is the
|
||||||
|
|
@ -667,15 +676,19 @@ fn onBind(sender: u32, raw_name: []const u8, arrived: *Arrival) i32 {
|
||||||
/// other, a line in a world-readable log ring, or a serial write costing
|
/// other, a line in a world-readable log ring, or a serial write costing
|
||||||
/// milliseconds.)
|
/// milliseconds.)
|
||||||
fn onOpen(reply: []u8, sender: u32, raw_name: []const u8) usize {
|
fn onOpen(reply: []u8, sender: u32, raw_name: []const u8) usize {
|
||||||
const name = contractName(raw_name) orelse return answer(reply, -envelope.ENOENT, 0, 0);
|
const name = contractName(raw_name) orelse return answer(reply, -envelope.ENOENT, 0);
|
||||||
refreshProcessTable();
|
refreshProcessTable();
|
||||||
const identity = identify(sender);
|
const identity = identify(sender);
|
||||||
const permitted = if (identity) |who| mayOpen(who, name) else false;
|
const permitted = if (identity) |who| mayOpen(who, name) else false;
|
||||||
const binding = findBinding(name);
|
const binding = findBinding(name);
|
||||||
if (!permitted) return answer(reply, -envelope.ENOENT, 0, 0);
|
if (!permitted) return answer(reply, -envelope.ENOENT, 0);
|
||||||
const found = binding orelse return answer(reply, -envelope.ENOENT, 0, 0);
|
const found = binding orelse return answer(reply, -envelope.ENOENT, 0);
|
||||||
pending_capability = found.endpoint;
|
pending_capability = found.endpoint;
|
||||||
return answer(reply, 0, 0, 0);
|
// A contract node has no node id — the capability is the whole answer — but
|
||||||
|
// the protocol says an `open` reply carries one, so it carries a zero.
|
||||||
|
const opened = vfs_protocol.Opened{ .node = 0 };
|
||||||
|
@memcpy(reply[envelope.prefix_size..][0..@sizeOf(vfs_protocol.Opened)], std.mem.asBytes(&opened));
|
||||||
|
return answer(reply, 0, @sizeOf(vfs_protocol.Opened));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `readdir(cursor)` — the namespace, browsable. One entry per turn, as the vfs
|
/// `readdir(cursor)` — the namespace, browsable. One entry per turn, as the vfs
|
||||||
|
|
@ -690,18 +703,25 @@ fn onReaddir(reply: []u8, cursor: u64) usize {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const name = binding.nameSlice();
|
const name = binding.nameSlice();
|
||||||
const entry = vfs_protocol.DirectoryEntry{
|
return writeEntry(reply, .{
|
||||||
.kind = @intFromEnum(vfs_protocol.NodeKind.protocol),
|
.kind = @intFromEnum(vfs_protocol.NodeKind.protocol),
|
||||||
.name_len = @intCast(name.len),
|
.name_len = @intCast(name.len),
|
||||||
.size = binding.task,
|
.size = binding.task,
|
||||||
};
|
}, name);
|
||||||
const total = vfs_protocol.directory_entry_size + name.len;
|
|
||||||
if (vfs_protocol.reply_size + total > reply.len) return answer(reply, -envelope.EPROTO, 0, 0);
|
|
||||||
@memcpy(reply[vfs_protocol.reply_size..][0..vfs_protocol.directory_entry_size], std.mem.asBytes(&entry));
|
|
||||||
@memcpy(reply[vfs_protocol.reply_size + vfs_protocol.directory_entry_size ..][0..name.len], name);
|
|
||||||
return answer(reply, 0, 0, total);
|
|
||||||
}
|
}
|
||||||
return answer(reply, 0, 0, 0); // end of directory
|
// End of directory, which the envelope spells as an entry with no name: the
|
||||||
|
// reply's own length cannot say it any more, because the fixed reply part
|
||||||
|
// always travels.
|
||||||
|
return writeEntry(reply, .{}, &.{});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One `readdir` reply: the entry, then its name inline.
|
||||||
|
fn writeEntry(reply: []u8, entry: vfs_protocol.DirectoryEntry, name: []const u8) usize {
|
||||||
|
const total = vfs_protocol.directory_entry_size + name.len;
|
||||||
|
if (envelope.prefix_size + total > reply.len) return answer(reply, -envelope.EPROTO, 0);
|
||||||
|
@memcpy(reply[envelope.prefix_size..][0..vfs_protocol.directory_entry_size], std.mem.asBytes(&entry));
|
||||||
|
@memcpy(reply[envelope.prefix_size + vfs_protocol.directory_entry_size ..][0..name.len], name);
|
||||||
|
return answer(reply, 0, total);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn main(startup: process.Init) void {
|
pub fn main(startup: process.Init) void {
|
||||||
|
|
@ -882,9 +902,12 @@ fn onPowerEvent(sender: u32, payload: []const u8) void {
|
||||||
std.log.info("ignored a power event from pid {d}: /protocol/power is pid {d}", .{ sender, authorized });
|
std.log.info("ignored a power event from pid {d}: /protocol/power is pid {d}", .{ sender, authorized });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (payload.len < 2) return;
|
// Read as an envelope packet, never by byte offset: the kind IS the packet's
|
||||||
if (payload[0] != @intFromEnum(power_protocol.Operation.event)) return;
|
// verb, so a power event is decoded exactly the way every other event in the
|
||||||
if (payload[1] == @intFromEnum(power_protocol.Event.power_button)) shutDown();
|
// system is. A packet whose operation is not one of this protocol's events —
|
||||||
|
// anything else that lands in this mailbox — decodes to null and is dropped.
|
||||||
|
const kind = power_protocol.Protocol.eventOf(payload) orelse return;
|
||||||
|
if (kind == .power_button) shutDown();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A supervised boot service died. Find which one and restart it — unless it exited
|
/// A supervised boot service died. Find which one and restart it — unless it exited
|
||||||
|
|
@ -935,9 +958,11 @@ fn restartChild(id: u32) void {
|
||||||
/// init calls owes the same discipline.
|
/// init calls owes the same discipline.
|
||||||
fn subscribePower() void {
|
fn subscribePower() void {
|
||||||
const handle = power_endpoint orelse return;
|
const handle = power_endpoint orelse return;
|
||||||
const request = power_protocol.Subscribe{};
|
// The reserved `subscribe` verb: nothing but the header, with our own
|
||||||
|
// endpoint riding as the call's capability.
|
||||||
|
const header = envelope.Header{ .operation = envelope.operation_subscribe };
|
||||||
var reply: [power_protocol.message_maximum]u8 = undefined;
|
var reply: [power_protocol.message_maximum]u8 = undefined;
|
||||||
_ = ipc.callCap(handle, std.mem.asBytes(&request), &reply, supervision_endpoint) catch {};
|
_ = ipc.callCap(handle, std.mem.asBytes(&header), &reply, supervision_endpoint) catch {};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The stop sequence: persist the log while storage is still up, then terminate
|
/// The stop sequence: persist the log while storage is still up, then terminate
|
||||||
|
|
@ -956,9 +981,11 @@ fn shutDown() void {
|
||||||
if (child_ids[i] != 0) process.stop(child_ids[i], 2000, supervision_endpoint);
|
if (child_ids[i] != 0) process.stop(child_ids[i], 2000, supervision_endpoint);
|
||||||
}
|
}
|
||||||
if (power_endpoint) |h| {
|
if (power_endpoint) |h| {
|
||||||
const request = power_protocol.Shutdown{};
|
var packet: [power_protocol.message_maximum]u8 = undefined;
|
||||||
var reply: [power_protocol.message_maximum]u8 = undefined;
|
if (power_protocol.Protocol.encodeRequest(.shutdown, 0, {}, &.{}, &packet)) |framed| {
|
||||||
_ = ipc.call(h, std.mem.asBytes(&request), &reply) catch {};
|
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.
|
// If S5 did not take, init has nothing left to do but idle.
|
||||||
while (true) time.sleepMillis(1000);
|
while (true) time.sleepMillis(1000);
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ pub fn build(b: *std.Build) void {
|
||||||
const exe = build_support.userBinary(b, .{
|
const exe = build_support.userBinary(b, .{
|
||||||
.name = "input",
|
.name = "input",
|
||||||
.root_source_file = b.path("input.zig"),
|
.root_source_file = b.path("input.zig"),
|
||||||
.imports = &.{ "channel", "input-client", "input-protocol", "ipc", "logging", "process", "service" },
|
.imports = &.{ "envelope", "input-protocol", "ipc", "logging", "service" },
|
||||||
});
|
});
|
||||||
b.installArtifact(exe);
|
b.installArtifact(exe);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,146 +17,70 @@
|
||||||
//!
|
//!
|
||||||
//! A subscriber registers by handing the service its own endpoint as a capability (M13
|
//! A subscriber registers by handing the service its own endpoint as a capability (M13
|
||||||
//! capability passing — this service is its first real consumer). The service keeps that
|
//! capability passing — this service is its first real consumer). The service keeps that
|
||||||
//! handle and `ipc.send`s each event to it.
|
//! handle and `ipc.send`s each event to it. That is the envelope's reserved `subscribe`
|
||||||
|
//! verb, which this protocol adopts rather than defining its own.
|
||||||
|
//!
|
||||||
|
//! P4a moved this service onto the shared harness (library/kernel/service.zig) — it was the
|
||||||
|
//! last hand-rolled receive loop in the tree, and the one service that answered neither the
|
||||||
|
//! universal ping nor a `terminate` signal. P4c finished the job: the subscriber table, the
|
||||||
|
//! fan-out, and the dead-subscriber sweep are the harness's now
|
||||||
|
//! (`service.Subscribers`), so what is left here is what is actually about input — which
|
||||||
|
//! device class an event belongs to, and which classes a subscriber asked for. The sweep
|
||||||
|
//! that replaced the old prune is the one idiom the system uses everywhere: published
|
||||||
|
//! process-exit notifications, not a poll of the process list on every subscribe.
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
const channel = @import("channel");
|
|
||||||
const ipc = @import("ipc");
|
const ipc = @import("ipc");
|
||||||
const process = @import("process");
|
|
||||||
const service = @import("service");
|
const service = @import("service");
|
||||||
const input = @import("input-client");
|
|
||||||
const logging = @import("logging");
|
const logging = @import("logging");
|
||||||
const input_protocol = @import("input-protocol");
|
const input_protocol = @import("input-protocol");
|
||||||
|
const envelope = @import("envelope");
|
||||||
|
|
||||||
/// One registered subscriber: the endpoint we push events to (a capability it handed us at
|
/// The subscriber side of the input contract: the table, the reserved `subscribe`
|
||||||
/// subscribe time) and the task id that owns it (the subscribe call's badge), so a slot
|
/// verb, the exit sweep, and the fan-out. One fan-out point per process, so the
|
||||||
/// left behind by a subscriber that exited can be reclaimed.
|
/// handler context is empty.
|
||||||
const Subscriber = struct {
|
const Subscriptions = service.Subscribers(input_protocol.Protocol, void);
|
||||||
used: bool = false,
|
|
||||||
endpoint: ipc.Handle = 0,
|
|
||||||
task_id: u32 = 0,
|
|
||||||
/// Which device classes this subscriber wants (an OR of input_protocol.device_*). An event
|
|
||||||
/// is delivered only if its device's bit is set here.
|
|
||||||
device_mask: u32 = 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
var subscribers = [_]Subscriber{.{}} ** 8;
|
const Invocation = envelope.Invocation;
|
||||||
|
const Answer = envelope.Answer;
|
||||||
|
|
||||||
/// Drop any subscriber whose owning process is no longer alive, so its slot (and the
|
/// Push `event` to every subscriber whose interest mask includes its device class. The
|
||||||
/// endpoint reference it holds) can be reused. Cheap and only run on subscribe — the async
|
/// class is the packet's operation, so this picks *which event* to publish and the harness
|
||||||
/// `send` to a dead subscriber's orphaned endpoint is harmless (it just fills a queue no
|
/// frames it once for the whole fan-out — the mapping from device to class is the only part
|
||||||
/// one drains), so this is housekeeping, not correctness.
|
/// of a broadcast that is this service's own.
|
||||||
fn pruneDeadSubscribers() void {
|
|
||||||
var table: [32]process.ProcessDescriptor = undefined;
|
|
||||||
const total = process.processes(&table);
|
|
||||||
const count = @min(total, table.len);
|
|
||||||
for (&subscribers) |*sub| {
|
|
||||||
if (!sub.used) continue;
|
|
||||||
var alive = false;
|
|
||||||
for (table[0..count]) |descriptor| {
|
|
||||||
if (descriptor.id == sub.task_id) {
|
|
||||||
alive = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// The slot owns the endpoint capability it was handed, so reclaiming the
|
|
||||||
// slot closes it — otherwise a process that subscribes and dies costs a
|
|
||||||
// handle-table slot that never comes back.
|
|
||||||
if (!alive) {
|
|
||||||
_ = ipc.close(sub.endpoint);
|
|
||||||
sub.* = .{};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Register `endpoint` (owned by task `task_id`) to receive the device classes in
|
|
||||||
/// `device_mask`. Returns false if the subscriber table is full.
|
|
||||||
fn addSubscriber(endpoint: ipc.Handle, task_id: u32, device_mask: u32) bool {
|
|
||||||
for (&subscribers) |*sub| {
|
|
||||||
if (!sub.used) {
|
|
||||||
sub.* = .{ .used = true, .endpoint = endpoint, .task_id = task_id, .device_mask = device_mask };
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Push `event` to every subscriber whose interest mask includes its device class.
|
|
||||||
/// `ipc.send` never blocks, so a slow or dead subscriber cannot stall delivery to others.
|
|
||||||
fn broadcast(event: input_protocol.InputEvent) void {
|
fn broadcast(event: input_protocol.InputEvent) void {
|
||||||
const bytes = std.mem.asBytes(&event);
|
const class = input_protocol.deviceBit(event.device);
|
||||||
const bit = input_protocol.deviceBit(event.device);
|
switch (input_protocol.eventOfDevice(event.device) orelse return) { // no class wants it
|
||||||
for (&subscribers) |*sub| {
|
.keyboard => Subscriptions.publishClass(.keyboard, 0, event.asKeyboard() orelse return, class),
|
||||||
if (sub.used and sub.device_mask & bit != 0) _ = ipc.send(sub.endpoint, bytes);
|
.mouse => Subscriptions.publishClass(.mouse, 0, event.asMouse() orelse return, class),
|
||||||
|
.joystick => Subscriptions.publishClass(.joystick, 0, event.asJoystick() orelse return, class),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handle one request. `got` carries the sender badge (a task id); `arrived` carries the
|
fn onPublish(_: void, invocation: Invocation(input_protocol.InputEvent), _: Answer(void)) isize {
|
||||||
/// capability the request came with, under the same ownership rule the service harness
|
broadcast(invocation.request);
|
||||||
/// states (`ipc.Arrival`): **it belongs to the turn, and only a handler that means to keep
|
return 0;
|
||||||
/// it says `take`.** Everything else here — a short message, a `publish`, a subscribe that
|
}
|
||||||
/// finds the table full — simply returns, and the loop closes what arrived. Writes a
|
|
||||||
/// `Reply` into `out` and returns its length.
|
|
||||||
fn handle(message: []const u8, got: ipc.Received, out: []u8, arrived: *ipc.Arrival) usize {
|
|
||||||
const reply = struct {
|
|
||||||
fn write(buffer: []u8, status: i32) usize {
|
|
||||||
const header = input_protocol.Reply{ .status = status };
|
|
||||||
@memcpy(buffer[0..input_protocol.reply_size], std.mem.asBytes(&header));
|
|
||||||
return input_protocol.reply_size;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (message.len < input_protocol.request_size) return reply.write(out, -1);
|
/// `subscribe` and `unsubscribe` are absent on purpose: the harness answers both.
|
||||||
const request = std.mem.bytesToValue(input_protocol.Request, message[0..input_protocol.request_size]);
|
const handlers = Subscriptions.Handlers{ .publish = onPublish };
|
||||||
|
|
||||||
switch (@as(input_protocol.Operation, @enumFromInt(request.operation))) {
|
fn onMessage(message: []const u8, out: []u8, sender: u32, arrived: *ipc.Arrival) usize {
|
||||||
.subscribe => {
|
return Subscriptions.dispatch({}, handlers, message, sender, arrived, out);
|
||||||
const endpoint = arrived.peek() orelse return reply.write(out, -1); // no endpoint passed
|
}
|
||||||
// A zero mask means "everything" (a subscriber that named no class still wants input).
|
|
||||||
const mask = if (request.device_mask == 0) input_protocol.device_all else request.device_mask;
|
fn initialise(_: ipc.Handle) bool {
|
||||||
pruneDeadSubscribers();
|
// The harness has already bound `/protocol/input` by the time this runs, so
|
||||||
if (!addSubscriber(endpoint, @intCast(got.badge), mask)) return reply.write(out, -1); // table full
|
// "ready" still means what it always meant: the name is claimed and the loop
|
||||||
_ = arrived.take(); // claimed: the subscriber table holds it until that task dies
|
// is about to serve it.
|
||||||
return reply.write(out, 0);
|
_ = logging.write("/system/services/input: ready\n");
|
||||||
},
|
return true;
|
||||||
.publish => {
|
|
||||||
broadcast(request.event);
|
|
||||||
return reply.write(out, 0);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn main() void {
|
pub fn main() void {
|
||||||
const endpoint = ipc.createIpcEndpoint() orelse {
|
service.run(input_protocol.message_maximum, .{
|
||||||
_ = logging.write("/system/services/input: no endpoint\n");
|
.service = "input",
|
||||||
return;
|
.init = initialise,
|
||||||
};
|
.on_message = onMessage,
|
||||||
if (!channel.bindPatiently("input", endpoint)) {
|
.subscribers = Subscriptions.hooks,
|
||||||
_ = logging.write("/system/services/input: could not bind /protocol/input\n");
|
});
|
||||||
return;
|
|
||||||
}
|
|
||||||
_ = logging.write("/system/services/input: ready\n");
|
|
||||||
|
|
||||||
var reply_buffer: [input_protocol.reply_size]u8 = undefined;
|
|
||||||
var reply_len: usize = 0;
|
|
||||||
var receive: [input_protocol.request_size]u8 = undefined;
|
|
||||||
while (true) {
|
|
||||||
const got = ipc.replyWait(endpoint, reply_buffer[0..reply_len], &receive, null);
|
|
||||||
// Whatever capability came with this turn is the turn's, and the turn closes it
|
|
||||||
// unless `handle` claims it (`ipc.Arrival`). The kernel installs a sent capability
|
|
||||||
// whatever the message's length or kind, so this covers the notification
|
|
||||||
// `continue` and every refusal inside `handle` — otherwise about thirty-two
|
|
||||||
// capability-carrying calls, which need no authorization at all, exhaust this
|
|
||||||
// service's handle table and no further subscribe can ever land.
|
|
||||||
var arrived: ipc.Arrival = .{ .handle = got.cap };
|
|
||||||
defer arrived.release();
|
|
||||||
|
|
||||||
// Only synchronous client requests (subscribe/publish) arrive here; nothing sends
|
|
||||||
// this service asynchronous messages, so a notification wake would be spurious.
|
|
||||||
if (got.isNotification()) {
|
|
||||||
reply_len = 0;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
reply_len = handle(receive[0..got.len], got, &reply_buffer, &arrived);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -658,6 +658,18 @@ CASES = [
|
||||||
"timeout": 150,
|
"timeout": 150,
|
||||||
"expect": r"fat-test: rename ok",
|
"expect": r"fat-test: rename ok",
|
||||||
"fail": r"fat-test: mutations FAILED|DANOS-TEST-RESULT: FAIL"},
|
"fail": r"fat-test: mutations FAILED|DANOS-TEST-RESULT: FAIL"},
|
||||||
|
# P4c: badge-scoped per-client ids. Two processes of one fixture, on the same
|
||||||
|
# boot: the owner holds a FAT node id and a compositor layer id, the intruder
|
||||||
|
# names both and must be refused — while its own node and layer, and the
|
||||||
|
# owner's after the attempt, keep working. Every refusal is paired with a
|
||||||
|
# control, so a provider that refused everything (or honoured everything)
|
||||||
|
# fails this case rather than passing it. Reuses the fat-mount build.
|
||||||
|
{"name": "badge-scope",
|
||||||
|
"build_case": "fat-mount",
|
||||||
|
"smp": 4,
|
||||||
|
"timeout": 150,
|
||||||
|
"expect": r"(?s)(?=.*badge-scope-test: ok)(?=.*badge-scope-test: owner intact)",
|
||||||
|
"fail": r"badge-scope-test: FAILED|DANOS-TEST-RESULT: FAIL"},
|
||||||
# Phase 2d: filesystem timestamps — a freshly-created file's mtime is a real
|
# Phase 2d: filesystem timestamps — a freshly-created file's mtime is a real
|
||||||
# current wall-clock time (stamped from the RTC), read back through stat.
|
# current wall-clock time (stamped from the RTC), read back through stat.
|
||||||
{"name": "fat-mtime",
|
{"name": "fat-mtime",
|
||||||
|
|
@ -889,6 +901,29 @@ CASES = [
|
||||||
r"(?=.*protocol-denied: ok)"
|
r"(?=.*protocol-denied: ok)"
|
||||||
r"(?=.*DANOS-TEST-RESULT: PASS)",
|
r"(?=.*DANOS-TEST-RESULT: PASS)",
|
||||||
"fail": r"DANOS-TEST-RESULT: FAIL|protocol-denied: FAIL"},
|
"fail": r"DANOS-TEST-RESULT: FAIL|protocol-denied: FAIL"},
|
||||||
|
# The reserved verbs, asked of live providers (P4a). Every protocol built on
|
||||||
|
# envelope.Define answers `describe` out of its specification and `-ENOSYS`
|
||||||
|
# for a verb it does not define, without its provider implementing either —
|
||||||
|
# so a fixture that walks /protocol's own listing and asks both of whatever
|
||||||
|
# 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. 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)"
|
||||||
|
r"(?=.*-> -ENOSYS)"
|
||||||
|
r"(?=.*protocol-conformance: 2 provider\(s\) answered the reserved verbs identically)"
|
||||||
|
r"(?=.*protocol-conformance: ok)"
|
||||||
|
r"(?=.*DANOS-TEST-RESULT: PASS)",
|
||||||
|
"fail": r"DANOS-TEST-RESULT: FAIL|protocol-conformance: FAIL"},
|
||||||
# Device manager: a ring-3 service enumerates /system/devices, matches the PCI host
|
# Device manager: a ring-3 service enumerates /system/devices, matches the PCI host
|
||||||
# bridge to pci-bus, and spawns it — end-to-end proof of discover -> match -> spawn
|
# bridge to pci-bus, and spawns it — end-to-end proof of discover -> match -> spawn
|
||||||
# -> driver-up (the spawned pci-bus logs "<N> functions found").
|
# -> driver-up (the spawned pci-bus logs "<N> functions found").
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,216 @@
|
||||||
|
//! test/system/services/badge-scope-test — the guessable-id probe. Two providers
|
||||||
|
//! hand small integers to a client and then take them back from anyone who says
|
||||||
|
//! the number: the FAT server's node ids (a table of thirty-two) and the
|
||||||
|
//! compositor's layer ids (a table of sixteen). P4c scoped both to the badge that
|
||||||
|
//! opened them (docs/os-development/protocol-namespace.md — *handles must be
|
||||||
|
//! scoped per client, validated against the badge*), and this fixture is the
|
||||||
|
//! proof.
|
||||||
|
//!
|
||||||
|
//! It runs as two processes of the same binary, because the hole is a
|
||||||
|
//! *cross-client* one and a single process cannot demonstrate it:
|
||||||
|
//!
|
||||||
|
//! - the **owner** (no arguments — the scenario spawns this one) opens a file
|
||||||
|
//! on the volume and creates a layer, keeps both, and spawns
|
||||||
|
//! - the **intruder** (the two ids as argv), which opens a file and a layer of
|
||||||
|
//! its own and then names the owner's.
|
||||||
|
//!
|
||||||
|
//! Every refusal is paired with the same operation on the intruder's own id, so
|
||||||
|
//! the case cannot pass against a provider that refuses everything; and the owner
|
||||||
|
//! re-uses both ids after the intruder has finished — including after its attempt
|
||||||
|
//! to *close* the file node — so the case cannot pass against a provider that
|
||||||
|
//! honoured the intruder and merely reported failure. The pass marker is
|
||||||
|
//! `badge-scope-test: ok` from the intruder plus `badge-scope-test: owner intact`
|
||||||
|
//! from the owner.
|
||||||
|
//!
|
||||||
|
//! What it cannot check: which id the intruder was refused *for*. A refusal is
|
||||||
|
//! deliberately identical to "no such id" — that is the discipline being tested —
|
||||||
|
//! so the fixture proves the boundary by construction (an id it was told about by
|
||||||
|
//! its parent, which is holding it) rather than by reading anything back.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const display = @import("display-client");
|
||||||
|
const fs = @import("file-system");
|
||||||
|
const ipc = @import("ipc");
|
||||||
|
const logging = @import("logging");
|
||||||
|
const process = @import("process");
|
||||||
|
const time = @import("time");
|
||||||
|
|
||||||
|
/// A scratch file on the volume, so the node the intruder tries to write through
|
||||||
|
/// is one nothing else reads. (A foreign write that *succeeded* would prove the
|
||||||
|
/// bug — it must not also damage the boot volume proving it.)
|
||||||
|
const held_path = "/volumes/usb/BADGE.TXT";
|
||||||
|
const held_contents = "held";
|
||||||
|
|
||||||
|
fn line(comptime format: []const u8, arguments: anytype) void {
|
||||||
|
var buffer: [192]u8 = undefined;
|
||||||
|
_ = logging.write(std.fmt.bufPrint(&buffer, format, arguments) catch return);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The fat server mounts /volumes/usb only after the whole USB storage chain is
|
||||||
|
/// up, and both instances race it.
|
||||||
|
fn waitForVolume() bool {
|
||||||
|
var tries: u32 = 0;
|
||||||
|
while (tries < 1400) : (tries += 1) {
|
||||||
|
if (fs.openDirectory("/volumes/usb")) |opened| {
|
||||||
|
var directory = opened;
|
||||||
|
directory.close();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
time.sleepMillis(50);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn main(init: process.Init) void {
|
||||||
|
if (init.arguments.get(1)) |node_text| {
|
||||||
|
const layer_text = init.arguments.get(2) orelse {
|
||||||
|
_ = logging.write("badge-scope-test: FAILED (intruder without a layer id)\n");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
const node = std.fmt.parseInt(u64, node_text, 10) catch return;
|
||||||
|
const layer = std.fmt.parseInt(u32, layer_text, 10) catch return;
|
||||||
|
intrude(node, layer);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
own();
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- the owner ---------------------------------------------------------------
|
||||||
|
|
||||||
|
fn own() void {
|
||||||
|
if (!waitForVolume()) {
|
||||||
|
_ = logging.write("badge-scope-test: FAILED (/volumes/usb never became available)\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var held = fs.open(held_path, .{ .create = true, .truncate = true }) orelse {
|
||||||
|
_ = logging.write("badge-scope-test: FAILED (owner could not create its file)\n");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if ((held.writeAll(held_contents) orelse 0) != held_contents.len) {
|
||||||
|
_ = logging.write("badge-scope-test: FAILED (owner could not write its file)\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const layer = display.createLayer(240, 40, 32, 32, 3) orelse {
|
||||||
|
_ = logging.write("badge-scope-test: FAILED (owner could not create a layer)\n");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
_ = layer.fill(0, 0, 32, 32, display.color(0x20, 0x80, 0xC0));
|
||||||
|
line("badge-scope-test: owner holds node {d} and layer {d}\n", .{ held.node, layer.id });
|
||||||
|
|
||||||
|
// The intruder is told both ids outright: guessing them is not what is being
|
||||||
|
// tested (they are small integers — a prober would simply enumerate), and a
|
||||||
|
// fixture that had to search would be asserting on a race instead of on the
|
||||||
|
// rule.
|
||||||
|
var node_text: [24]u8 = undefined;
|
||||||
|
var layer_text: [12]u8 = undefined;
|
||||||
|
const arguments = [_][]const u8{
|
||||||
|
std.fmt.bufPrint(&node_text, "{d}", .{held.node}) catch return,
|
||||||
|
std.fmt.bufPrint(&layer_text, "{d}", .{layer.id}) catch return,
|
||||||
|
};
|
||||||
|
const endpoint = ipc.createIpcEndpoint() orelse {
|
||||||
|
_ = logging.write("badge-scope-test: FAILED (owner has no exit endpoint)\n");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
const intruder = process.spawnSupervised("badge-scope-test", &arguments, endpoint) orelse {
|
||||||
|
_ = logging.write("badge-scope-test: FAILED (could not spawn the intruder)\n");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
var receive: [8]u8 = undefined;
|
||||||
|
while (true) {
|
||||||
|
const got = ipc.replyWait(endpoint, &.{}, &receive, null);
|
||||||
|
if (got.isChildExit() and got.childProcessId() == intruder) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Both ids must still be the owner's, and still work — the half of the proof
|
||||||
|
// the intruder cannot give, because a provider that had honoured its close or
|
||||||
|
// its destroy would have answered it exactly as one that refused.
|
||||||
|
held.seekTo(0);
|
||||||
|
var buffer: [16]u8 = undefined;
|
||||||
|
const read = held.read(&buffer) orelse 0;
|
||||||
|
const node_intact = read == held_contents.len and std.mem.eql(u8, buffer[0..read], held_contents);
|
||||||
|
const layer_intact = layer.fill(0, 0, 32, 32, display.color(0x20, 0xC0, 0x80));
|
||||||
|
if (node_intact and layer_intact) {
|
||||||
|
_ = logging.write("badge-scope-test: owner intact\n");
|
||||||
|
} else {
|
||||||
|
line("badge-scope-test: FAILED (owner lost its ids: node={} layer={})\n", .{ node_intact, layer_intact });
|
||||||
|
}
|
||||||
|
_ = layer.destroy();
|
||||||
|
held.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- the intruder -------------------------------------------------------------
|
||||||
|
|
||||||
|
fn intrude(foreign_node: u64, foreign_layer: u32) void {
|
||||||
|
if (!waitForVolume()) {
|
||||||
|
_ = logging.write("badge-scope-test: FAILED (/volumes/usb never became available)\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const node_verdict = probeNode(foreign_node);
|
||||||
|
const layer_verdict = probeLayer(foreign_layer);
|
||||||
|
if (node_verdict and layer_verdict) {
|
||||||
|
_ = logging.write("badge-scope-test: ok\n");
|
||||||
|
} else {
|
||||||
|
_ = logging.write("badge-scope-test: FAILED\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The FAT half: the intruder's own node works, the owner's does not.
|
||||||
|
fn probeNode(foreign: u64) bool {
|
||||||
|
var mine = fs.open(held_path, .{}) orelse {
|
||||||
|
_ = logging.write("badge-scope-test: FAILED (intruder could not open its own file)\n");
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
defer mine.close();
|
||||||
|
if (mine.backend == null or mine.node == foreign) {
|
||||||
|
line("badge-scope-test: FAILED (intruder's node {d} is not distinct from {d})\n", .{ mine.node, foreign });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var buffer: [16]u8 = undefined;
|
||||||
|
const own_read = (mine.read(&buffer) orelse 0) == held_contents.len;
|
||||||
|
|
||||||
|
// The same backend channel, the same verbs, one different integer. Every one
|
||||||
|
// of these worked before the owner check went in.
|
||||||
|
var forged = fs.File{ .node = foreign, .backend = mine.backend };
|
||||||
|
const read_refused = forged.read(&buffer) == null;
|
||||||
|
const status_refused = forged.attributes() == null;
|
||||||
|
const write_refused = forged.write("intruded") == null;
|
||||||
|
forged.close(); // must not release the owner's node — the owner proves that after we exit
|
||||||
|
|
||||||
|
mine.seekTo(0);
|
||||||
|
const own_read_again = (mine.read(&buffer) orelse 0) == held_contents.len;
|
||||||
|
|
||||||
|
const ok = own_read and own_read_again and read_refused and status_refused and write_refused;
|
||||||
|
line("badge-scope-test: node own={} read_refused={} status_refused={} write_refused={} own_again={}\n", .{
|
||||||
|
own_read, read_refused, status_refused, write_refused, own_read_again,
|
||||||
|
});
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The display half: the intruder's own layer obeys it, the owner's ignores it.
|
||||||
|
fn probeLayer(foreign: u32) bool {
|
||||||
|
const mine = display.createLayer(200, 40, 32, 32, 4) orelse {
|
||||||
|
_ = logging.write("badge-scope-test: FAILED (intruder could not create a layer)\n");
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
if (mine.id == foreign) {
|
||||||
|
line("badge-scope-test: FAILED (intruder's layer {d} is not distinct)\n", .{mine.id});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const own_fill = mine.fill(0, 0, 32, 32, display.color(0xC0, 0x40, 0x40));
|
||||||
|
const own_configure = mine.configure(200, 80, 4, true);
|
||||||
|
|
||||||
|
const forged = display.Layer{ .id = foreign };
|
||||||
|
const fill_refused = !forged.fill(0, 0, 32, 32, display.color(0xFF, 0, 0));
|
||||||
|
const configure_refused = !forged.configure(0, 0, 9, false);
|
||||||
|
const destroy_refused = !forged.destroy();
|
||||||
|
|
||||||
|
const own_still = mine.fill(0, 0, 32, 32, display.color(0x40, 0xC0, 0x40));
|
||||||
|
_ = mine.destroy();
|
||||||
|
|
||||||
|
const ok = own_fill and own_configure and own_still and fill_refused and configure_refused and destroy_refused;
|
||||||
|
line("badge-scope-test: layer own={} fill_refused={} configure_refused={} destroy_refused={} own_again={}\n", .{
|
||||||
|
own_fill, fill_refused, configure_refused, destroy_refused, own_still,
|
||||||
|
});
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
//! The badge-scope-test fixture as a binary package (docs/build-packages-plan.md):
|
||||||
|
//! this file names the binary and EXACTLY the modules its source imports —
|
||||||
|
//! build-support resolves each name from the domains this zon declares.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const build_support = @import("build-support");
|
||||||
|
|
||||||
|
pub fn build(b: *std.Build) void {
|
||||||
|
const exe = build_support.userBinary(b, .{
|
||||||
|
.name = "badge-scope-test",
|
||||||
|
.root_source_file = b.path("badge-scope-test.zig"),
|
||||||
|
.imports = &.{ "display-client", "file-system", "ipc", "logging", "process", "time" },
|
||||||
|
});
|
||||||
|
b.installArtifact(exe);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
.{
|
||||||
|
.name = .badge_scope_test,
|
||||||
|
.version = "0.0.0",
|
||||||
|
.fingerprint = 0x92ab15512c4e5b49, // Changing this has security and trust implications.
|
||||||
|
.minimum_zig_version = "0.16.0",
|
||||||
|
.dependencies = .{
|
||||||
|
// build-support supplies the shared recipe; kernel is implicit in
|
||||||
|
// every binary (the root shim + link script live there). The rest
|
||||||
|
// are exactly the homes of this binary's declared imports.
|
||||||
|
.@"build-support" = .{ .path = "../../../../build-support" },
|
||||||
|
.kernel = .{ .path = "../../../../library/kernel" },
|
||||||
|
// client: the compositor half of the probe talks to /protocol/display
|
||||||
|
// through the same client library every other display client uses.
|
||||||
|
.client = .{ .path = "../../../../library/client" },
|
||||||
|
},
|
||||||
|
.paths = .{""},
|
||||||
|
}
|
||||||
|
|
@ -34,9 +34,17 @@ pub fn main(init: process.Init) void {
|
||||||
if (manager == null) time.sleepMillis(20);
|
if (manager == null) time.sleepMillis(20);
|
||||||
}
|
}
|
||||||
const h = manager orelse return;
|
const h = manager orelse return;
|
||||||
const hello = device_manager_protocol.Hello{ .role = @intFromEnum(device_manager_protocol.Role.device), .device_id = assigned };
|
// The assigned device is the packet's target, the manager's object addressing.
|
||||||
|
var packet: [device_manager_protocol.message_maximum]u8 = undefined;
|
||||||
|
const framed = device_manager_protocol.Protocol.encodeRequest(
|
||||||
|
.hello,
|
||||||
|
assigned,
|
||||||
|
.{ .role = @intFromEnum(device_manager_protocol.Role.device) },
|
||||||
|
&.{},
|
||||||
|
&packet,
|
||||||
|
) orelse return;
|
||||||
var reply: [device_manager_protocol.message_maximum]u8 = undefined;
|
var reply: [device_manager_protocol.message_maximum]u8 = undefined;
|
||||||
_ = ipc.call(h, std.mem.asBytes(&hello), &reply) catch return;
|
_ = ipc.call(h, framed, &reply) catch return;
|
||||||
|
|
||||||
_ = logging.write("crash-test: faulting now\n");
|
_ = logging.write("crash-test: faulting now\n");
|
||||||
const poison: *volatile u32 = @ptrFromInt(0xdead0000);
|
const poison: *volatile u32 = @ptrFromInt(0xdead0000);
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ pub fn build(b: *std.Build) void {
|
||||||
const exe = build_support.userBinary(b, .{
|
const exe = build_support.userBinary(b, .{
|
||||||
.name = "device-list",
|
.name = "device-list",
|
||||||
.root_source_file = b.path("device-list.zig"),
|
.root_source_file = b.path("device-list.zig"),
|
||||||
.imports = &.{ "channel", "device-manager-protocol", "ipc", "logging", "time" },
|
.imports = &.{ "channel", "device-manager-protocol", "envelope", "ipc", "logging", "time" },
|
||||||
});
|
});
|
||||||
b.installArtifact(exe);
|
b.installArtifact(exe);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const channel = @import("channel");
|
const channel = @import("channel");
|
||||||
|
const envelope = @import("envelope");
|
||||||
const ipc = @import("ipc");
|
const ipc = @import("ipc");
|
||||||
const time = @import("time");
|
const time = @import("time");
|
||||||
const logging = @import("logging");
|
const logging = @import("logging");
|
||||||
|
|
@ -29,36 +30,41 @@ pub fn main() void {
|
||||||
};
|
};
|
||||||
|
|
||||||
// The snapshot — polled briefly, because at boot the bus drivers may still
|
// The snapshot — polled briefly, because at boot the bus drivers may still
|
||||||
// be scanning: an empty first answer usually just means "too early".
|
// be scanning: an empty first answer usually just means "too early". This is
|
||||||
|
// the envelope's reserved `enumerate` verb, so the request is nothing but a
|
||||||
|
// header and the answer is one `ChildEntry` per record in the reply's tail —
|
||||||
|
// how many arrived is the reply's own length, which is why no count header
|
||||||
|
// says it a second time.
|
||||||
|
const Entry = device_manager_protocol.ChildEntry;
|
||||||
|
const enumerate = envelope.Header{ .operation = envelope.operation_enumerate };
|
||||||
var reply: [device_manager_protocol.message_maximum]u8 = undefined;
|
var reply: [device_manager_protocol.message_maximum]u8 = undefined;
|
||||||
var count: u32 = 0;
|
var count: usize = 0;
|
||||||
var length: usize = 0;
|
|
||||||
tries = 0;
|
tries = 0;
|
||||||
while (tries < 20) : (tries += 1) {
|
while (tries < 20) : (tries += 1) {
|
||||||
const request = device_manager_protocol.Enumerate{};
|
const length = ipc.call(h, std.mem.asBytes(&enumerate), &reply) catch 0;
|
||||||
length = ipc.call(h, std.mem.asBytes(&request), &reply) catch 0;
|
if (envelope.statusOf(reply[0..length])) |status| {
|
||||||
if (length >= @sizeOf(device_manager_protocol.EnumerateReply)) {
|
if (status.status == 0) {
|
||||||
count = std.mem.bytesToValue(device_manager_protocol.EnumerateReply, reply[0..@sizeOf(device_manager_protocol.EnumerateReply)]).count;
|
const carried = @min(@as(usize, status.len), length - envelope.prefix_size);
|
||||||
if (count != 0) break;
|
count = carried / @sizeOf(Entry);
|
||||||
|
if (count != 0) break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
time.sleepMillis(100);
|
time.sleepMillis(100);
|
||||||
}
|
}
|
||||||
writeLine("device-list: {d} devices\n", .{count});
|
writeLine("device-list: {d} devices\n", .{count});
|
||||||
var offset: usize = @sizeOf(device_manager_protocol.EnumerateReply);
|
for (0..count) |index| {
|
||||||
var index: u32 = 0;
|
const entry = std.mem.bytesToValue(Entry, reply[envelope.prefix_size + index * @sizeOf(Entry) ..][0..@sizeOf(Entry)]);
|
||||||
while (index < count and offset + @sizeOf(device_manager_protocol.ChildEntry) <= length) : (index += 1) {
|
|
||||||
const entry = std.mem.bytesToValue(device_manager_protocol.ChildEntry, reply[offset..][0..@sizeOf(device_manager_protocol.ChildEntry)]);
|
|
||||||
writeLine("device-list: device {d} port {d} identity {d}\n", .{ entry.parent, entry.bus_address, entry.identity });
|
writeLine("device-list: device {d} port {d} identity {d}\n", .{ entry.parent, entry.bus_address, entry.identity });
|
||||||
offset += @sizeOf(device_manager_protocol.ChildEntry);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The subscription: our endpoint rides as the call's capability; events
|
// The subscription — the reserved `subscribe` verb: our endpoint rides as
|
||||||
// arrive as buffered messages carrying the same structs the bus sends.
|
// the call's capability, and events arrive as buffered packets carrying the
|
||||||
|
// same structs the bus drivers send, under the events' own numbering.
|
||||||
const endpoint = ipc.createIpcEndpoint() orelse {
|
const endpoint = ipc.createIpcEndpoint() orelse {
|
||||||
_ = logging.write("device-list: no endpoint\n");
|
_ = logging.write("device-list: no endpoint\n");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
const subscribe = device_manager_protocol.Subscribe{};
|
const subscribe = envelope.Header{ .operation = envelope.operation_subscribe };
|
||||||
_ = ipc.callCap(h, std.mem.asBytes(&subscribe), &reply, endpoint) catch {
|
_ = ipc.callCap(h, std.mem.asBytes(&subscribe), &reply, endpoint) catch {
|
||||||
_ = logging.write("device-list: subscribe failed\n");
|
_ = logging.write("device-list: subscribe failed\n");
|
||||||
return;
|
return;
|
||||||
|
|
@ -68,19 +74,18 @@ pub fn main() void {
|
||||||
var receive: [device_manager_protocol.message_maximum]u8 = undefined;
|
var receive: [device_manager_protocol.message_maximum]u8 = undefined;
|
||||||
while (true) {
|
while (true) {
|
||||||
const got = ipc.replyWait(endpoint, &.{}, &receive, null);
|
const got = ipc.replyWait(endpoint, &.{}, &receive, null);
|
||||||
if (!got.isMessage() or got.len < 1) continue;
|
if (!got.isMessage()) continue;
|
||||||
switch (receive[0]) {
|
const packet = receive[0..got.len];
|
||||||
@intFromEnum(device_manager_protocol.Operation.child_added) => {
|
const event = device_manager_protocol.Protocol.eventOf(packet) orelse continue;
|
||||||
if (got.len < device_manager_protocol.child_added_size) continue;
|
switch (event) {
|
||||||
const event = std.mem.bytesToValue(device_manager_protocol.ChildAdded, receive[0..device_manager_protocol.child_added_size]);
|
.child_added => {
|
||||||
writeLine("device-list: added (device {d} port {d})\n", .{ event.parent, event.bus_address });
|
const added = device_manager_protocol.Protocol.decodeEvent(.child_added, packet) orelse continue;
|
||||||
|
writeLine("device-list: added (device {d} port {d})\n", .{ added.parent, added.bus_address });
|
||||||
},
|
},
|
||||||
@intFromEnum(device_manager_protocol.Operation.child_removed) => {
|
.child_removed => {
|
||||||
if (got.len < device_manager_protocol.child_removed_size) continue;
|
const removed = device_manager_protocol.Protocol.decodeEvent(.child_removed, packet) orelse continue;
|
||||||
const event = std.mem.bytesToValue(device_manager_protocol.ChildRemoved, receive[0..device_manager_protocol.child_removed_size]);
|
writeLine("device-list: removed (device {d} port {d})\n", .{ removed.parent, removed.bus_address });
|
||||||
writeLine("device-list: removed (device {d} port {d})\n", .{ event.parent, event.bus_address });
|
|
||||||
},
|
},
|
||||||
else => {},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
//! The protocol-conformance-test fixture as a binary package (docs/build-packages-plan.md):
|
||||||
|
//! this file names the binary and EXACTLY the modules its source imports —
|
||||||
|
//! build-support resolves each name from the domains this zon declares.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const build_support = @import("build-support");
|
||||||
|
|
||||||
|
pub fn build(b: *std.Build) void {
|
||||||
|
const exe = build_support.userBinary(b, .{
|
||||||
|
.name = "protocol-conformance-test",
|
||||||
|
.root_source_file = b.path("protocol-conformance-test.zig"),
|
||||||
|
.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",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
b.installArtifact(exe);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
.{
|
||||||
|
.name = .protocol_conformance_test,
|
||||||
|
.version = "0.0.0",
|
||||||
|
.fingerprint = 0xea942427118ba350, // Changing this has security and trust implications.
|
||||||
|
.minimum_zig_version = "0.16.0",
|
||||||
|
.dependencies = .{
|
||||||
|
// build-support supplies the shared recipe; kernel is implicit in
|
||||||
|
// every binary (the root shim + link script live there). The rest
|
||||||
|
// are exactly the homes of this binary's declared imports.
|
||||||
|
.@"build-support" = .{ .path = "../../../../build-support" },
|
||||||
|
.kernel = .{ .path = "../../../../library/kernel" },
|
||||||
|
// envelope: the reserved verbs and the errno a refused one answers;
|
||||||
|
// vfs-protocol: the registry's own readdir, which is where the set
|
||||||
|
// under test comes from; the four -protocol modules: the name, the
|
||||||
|
// version and the verb count each contract's `describe` must report,
|
||||||
|
// read off the contract itself rather than copied beside it.
|
||||||
|
.protocol = .{ .path = "../../../../library/protocol" },
|
||||||
|
},
|
||||||
|
.paths = .{""},
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,348 @@
|
||||||
|
//! protocol-conformance-test — P4a's evidence that `envelope.Define` gives every
|
||||||
|
//! provider the reserved verbs, uniformly and without the provider writing a line
|
||||||
|
//! for them (docs/security-track-plan.md P4a;
|
||||||
|
//! docs/os-development/protocol-namespace.md). One binary, one role, driven by
|
||||||
|
//! the `protocol-conformance` kernel case:
|
||||||
|
//!
|
||||||
|
//! - `protocol-conformance-test run` — for each contract it can reach:
|
||||||
|
//! 1. `describe` — the reserved verb 0 — is answered, and the answer names
|
||||||
|
//! *that* protocol: the name it was opened under, the version its module
|
||||||
|
//! declares, and the number of verbs its module declares. No provider in
|
||||||
|
//! the system implements `describe`; the generated dispatch answers it out
|
||||||
|
//! of the specification, which is exactly the claim being checked;
|
||||||
|
//! 2. a verb number no protocol in the system defines answers `-ENOSYS`, and
|
||||||
|
//! carries no capability. That is the other half of the same generated
|
||||||
|
//! dispatch: a provider does not have to reject strangers, it gets the
|
||||||
|
//! rejection for free and every provider gives the same one;
|
||||||
|
//! 3. `describe` again, after the refusal — a refused verb is an *answer*,
|
||||||
|
//! not a wedged service, so the channel is still good afterwards.
|
||||||
|
//!
|
||||||
|
//! **The set it checks is read, never hardcoded.** The fixture asks `/protocol`
|
||||||
|
//! for its own listing (`readdir`, which the namespace publishes on purpose) and
|
||||||
|
//! walks what it finds, so the case cannot drift from what this boot actually
|
||||||
|
//! bound. What it opens is bounded by P3: the manifest names this binary against
|
||||||
|
//! 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.** 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);
|
||||||
|
//! - `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.
|
||||||
|
//!
|
||||||
|
//! 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.
|
||||||
|
//!
|
||||||
|
//! 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
|
||||||
|
//! stranger's packet by hand, `system/services/init/init.zig`), so `describe` is
|
||||||
|
//! not asked of it and nothing here claims it.
|
||||||
|
//!
|
||||||
|
//! Prints `protocol-conformance: ok` on success, or a `protocol-conformance:
|
||||||
|
//! FAIL` line naming the step. Spawned bare (the initial-ramdisk sweep starts
|
||||||
|
//! every bundled binary), it exits silently so it cannot derange other tests.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const channel = @import("channel");
|
||||||
|
const envelope = @import("envelope");
|
||||||
|
const file_system = @import("file-system");
|
||||||
|
const ipc = @import("ipc");
|
||||||
|
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 -----------------------------------
|
||||||
|
|
||||||
|
/// One contract this fixture knows how to check, and what the answer must say.
|
||||||
|
/// Every field is read off the protocol module itself, so the expectation is the
|
||||||
|
/// contract's own definition rather than a number copied beside it — a version
|
||||||
|
/// bump or a new verb updates this table by recompiling.
|
||||||
|
const Contract = struct {
|
||||||
|
name: []const u8,
|
||||||
|
version: u32,
|
||||||
|
/// How many verbs the module declares — `describe` reports it, so it is
|
||||||
|
/// checked. The reserved verbs are not counted: they are the envelope's.
|
||||||
|
operations: u32,
|
||||||
|
/// Whether this scenario boots a provider for it. A required contract that
|
||||||
|
/// is missing, unreachable or non-conforming fails the case; the rest are
|
||||||
|
/// checked when some other scenario happens to bind them.
|
||||||
|
required: bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn contractOf(comptime Protocol: type, required: bool) Contract {
|
||||||
|
return .{
|
||||||
|
.name = Protocol.protocol_name,
|
||||||
|
.version = Protocol.version,
|
||||||
|
.operations = @typeInfo(Protocol.Operation).@"enum".fields.len,
|
||||||
|
.required = required,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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
|
||||||
|
/// above the reserved range, so it is unambiguously a protocol verb, and far
|
||||||
|
/// above any protocol's verb count, so the generated dispatch has nothing to
|
||||||
|
/// match it against. The answer must be `-ENOSYS` at every provider.
|
||||||
|
const stranger_operation: u32 = envelope.first_protocol_operation + 4096;
|
||||||
|
|
||||||
|
fn fail(step: []const u8) noreturn {
|
||||||
|
_ = logging.write("protocol-conformance: FAIL ");
|
||||||
|
_ = logging.write(step);
|
||||||
|
_ = logging.write("\n");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn report(comptime format: []const u8, arguments: anytype) void {
|
||||||
|
var line: [192]u8 = undefined;
|
||||||
|
_ = logging.write(std.fmt.bufPrint(&line, format, arguments) catch return);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- reading the namespace ---------------------------------------------------
|
||||||
|
|
||||||
|
/// The cadence every client in the tree spends finding a service.
|
||||||
|
const resolve_attempts: u32 = 200;
|
||||||
|
const resolve_retry_ms: u64 = 20;
|
||||||
|
|
||||||
|
/// The registry's endpoint, obtained the way every process obtains it: resolve
|
||||||
|
/// `/protocol`. The handle is the kernel's, shared with every other user of the
|
||||||
|
/// mount, so it is never ours to close. Patient, because the harness starts the
|
||||||
|
/// registrar and this fixture together and a first resolve can land before init
|
||||||
|
/// has mounted `/protocol` at all.
|
||||||
|
fn registryEndpoint() ?ipc.Handle {
|
||||||
|
var attempt: u32 = 0;
|
||||||
|
while (attempt < resolve_attempts) : (attempt += 1) {
|
||||||
|
var relative: [channel.path_maximum]u8 = undefined;
|
||||||
|
if (file_system.fsResolve(channel.root, 0, &relative)) |route| switch (route) {
|
||||||
|
.kernel => return null, // a kernel route means something other than the registry owns the name
|
||||||
|
.backend => |backend| return backend.handle,
|
||||||
|
};
|
||||||
|
time.sleepMillis(resolve_retry_ms);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One `readdir(cursor)` at the registry, into `into`. Null at end of directory
|
||||||
|
/// or on any failure — the caller is walking a listing, and both mean "stop".
|
||||||
|
///
|
||||||
|
/// The listing is what makes this test un-driftable: `/protocol` publishes what
|
||||||
|
/// is bound (protocol-namespace.md — the tree stays diagnosable), so the set
|
||||||
|
/// under test is the set this boot actually produced.
|
||||||
|
fn entryAt(registry: ipc.Handle, cursor: u64, into: []u8) ?[]u8 {
|
||||||
|
var packet: [vfs_protocol.message_maximum]u8 = undefined;
|
||||||
|
const framed = vfs_protocol.Protocol.encodeRequest(.readdir, 0, .{ .cursor = cursor }, &.{}, &packet) orelse return null;
|
||||||
|
|
||||||
|
var reply: [vfs_protocol.message_maximum]u8 = undefined;
|
||||||
|
const got = ipc.callCap(registry, framed, &reply, null) catch return null;
|
||||||
|
// A readdir owes no capability; one that arrived anyway is a handle slot.
|
||||||
|
if (got.cap) |handle| _ = ipc.close(handle);
|
||||||
|
|
||||||
|
const answer = reply[0..got.len];
|
||||||
|
const status = envelope.statusOf(answer) orelse return null;
|
||||||
|
if (status.status != 0) return null;
|
||||||
|
const entry = vfs_protocol.Protocol.decodeReply(.readdir, answer) orelse return null;
|
||||||
|
if (entry.name_len == 0) return null; // end of directory
|
||||||
|
const text = vfs_protocol.Protocol.replyTail(.readdir, answer);
|
||||||
|
const length = @min(@as(usize, entry.name_len), @min(text.len, into.len));
|
||||||
|
@memcpy(into[0..length], text[0..length]);
|
||||||
|
return into[0..length];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The listing, taken once so every later question is asked of one observation
|
||||||
|
/// rather than of a namespace that may have moved underneath it.
|
||||||
|
const maximum_listed: usize = 32;
|
||||||
|
var listed_names: [maximum_listed][channel.name_maximum]u8 = undefined;
|
||||||
|
var listed_lengths: [maximum_listed]usize = undefined;
|
||||||
|
var listed_count: usize = 0;
|
||||||
|
|
||||||
|
fn listedName(index: usize) []const u8 {
|
||||||
|
return listed_names[index][0..listed_lengths[index]];
|
||||||
|
}
|
||||||
|
|
||||||
|
fn takeListing(registry: ipc.Handle) void {
|
||||||
|
listed_count = 0;
|
||||||
|
var cursor: u64 = 0;
|
||||||
|
while (cursor < maximum_listed) : (cursor += 1) {
|
||||||
|
const name = entryAt(registry, cursor, &listed_names[listed_count]) orelse return;
|
||||||
|
listed_lengths[listed_count] = name.len;
|
||||||
|
listed_count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether `/protocol` currently lists `name`.
|
||||||
|
fn listed(registry: ipc.Handle, name: []const u8) bool {
|
||||||
|
var cursor: u64 = 0;
|
||||||
|
while (cursor < maximum_listed) : (cursor += 1) {
|
||||||
|
var scratch: [channel.name_maximum]u8 = undefined;
|
||||||
|
const entry = entryAt(registry, cursor, &scratch) orelse return false;
|
||||||
|
if (std.mem.eql(u8, entry, name)) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wait until `/protocol` lists `name` — the providers this case needs come up
|
||||||
|
/// alongside the fixture, and racing them would make the listing a boot race
|
||||||
|
/// rather than an observation.
|
||||||
|
fn awaitListed(registry: ipc.Handle, name: []const u8) void {
|
||||||
|
var attempts: u32 = 0;
|
||||||
|
while (attempts < 400) : (attempts += 1) {
|
||||||
|
if (listed(registry, name)) return;
|
||||||
|
time.sleepMillis(20);
|
||||||
|
}
|
||||||
|
report("protocol-conformance: FAIL /protocol never listed {s}\n", .{name});
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- the assertions ----------------------------------------------------------
|
||||||
|
|
||||||
|
/// The three checks, against one open channel. Every failure is fatal: the point
|
||||||
|
/// of the case is that these hold at *every* provider, so one that does not is
|
||||||
|
/// not a degraded result, it is the regression.
|
||||||
|
fn conform(link: channel.Channel, contract: Contract) void {
|
||||||
|
var buffer: [envelope.packet_maximum]u8 = undefined;
|
||||||
|
|
||||||
|
// 1. The reserved verb no provider implements. `describe` is answered from
|
||||||
|
// the specification by the generated dispatch, so what comes back is the
|
||||||
|
// contract's own identity — checked field by field against the module
|
||||||
|
// this fixture compiled against.
|
||||||
|
const described = link.describe(&buffer) orelse fail("describe was not answered");
|
||||||
|
if (!std.mem.eql(u8, described.name, contract.name)) fail("describe named a different protocol");
|
||||||
|
if (described.description.version != contract.version) fail("describe answered the wrong version");
|
||||||
|
if (described.description.operation_count != contract.operations) fail("describe counted the wrong number of verbs");
|
||||||
|
|
||||||
|
// 2. A number no protocol wears. Nothing in the provider looks at it; the
|
||||||
|
// dispatch table finds no handler and refuses, identically everywhere.
|
||||||
|
var into: [envelope.packet_maximum]u8 = undefined;
|
||||||
|
const answered = link.call(.{ .operation = stranger_operation }, &.{}, &into) orelse
|
||||||
|
fail("a stranger verb was not answered at all");
|
||||||
|
if (answered.status.status != -envelope.ENOSYS) fail("a stranger verb did not answer -ENOSYS");
|
||||||
|
if (answered.status.len != 0) fail("a refused verb promised a payload");
|
||||||
|
if (answered.capability) |handle| {
|
||||||
|
_ = ipc.close(handle);
|
||||||
|
fail("a refused verb handed back a capability");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. A refusal is an answer, not a wedge — so the same channel still works.
|
||||||
|
const again = link.describe(&buffer) orelse fail("the provider stopped answering after a refused verb");
|
||||||
|
if (!std.mem.eql(u8, again.name, contract.name)) fail("describe changed its answer after a refused verb");
|
||||||
|
|
||||||
|
report("protocol-conformance: {s} v{d} describes itself ({d} verbs), verb {d} -> -ENOSYS\n", .{
|
||||||
|
contract.name,
|
||||||
|
contract.version,
|
||||||
|
contract.operations,
|
||||||
|
stranger_operation,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn contractIndex(name: []const u8) ?usize {
|
||||||
|
for (contracts, 0..) |contract, index| {
|
||||||
|
if (std.mem.eql(u8, contract.name, name)) return index;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run() void {
|
||||||
|
const registry = registryEndpoint() orelse fail("resolve /protocol");
|
||||||
|
|
||||||
|
// Every contract this scenario is supposed to be able to check must be bound
|
||||||
|
// before the listing is taken, or the case would assert nothing on a slow
|
||||||
|
// boot instead of failing on a broken one.
|
||||||
|
for (contracts) |contract| {
|
||||||
|
if (contract.required) awaitListed(registry, contract.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
takeListing(registry);
|
||||||
|
if (listed_count == 0) fail("/protocol listed nothing at all");
|
||||||
|
report("protocol-conformance: /protocol lists {d} contract(s)\n", .{listed_count});
|
||||||
|
|
||||||
|
var checked = [_]bool{false} ** contracts.len;
|
||||||
|
for (0..listed_count) |index| {
|
||||||
|
const name = listedName(index);
|
||||||
|
const found = contractIndex(name) orelse {
|
||||||
|
// Not a lie of omission: named on serial, with the reason.
|
||||||
|
report("protocol-conformance: {s} skipped — not built on envelope.Define yet\n", .{name});
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
const contract = contracts[found];
|
||||||
|
const link = channel.Channel.connect(name) orelse {
|
||||||
|
// P3 is in force: an ungranted name is absent for this binary, and
|
||||||
|
// that is a manifest fact, not a failure — unless the scenario is
|
||||||
|
// supposed to have granted it.
|
||||||
|
if (contract.required) fail("a contract this fixture is granted would not open");
|
||||||
|
report("protocol-conformance: {s} skipped — not granted to this fixture\n", .{name});
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
conform(link, contract);
|
||||||
|
link.close();
|
||||||
|
checked[found] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The vacuity guard, and the honest tail: a required contract that went
|
||||||
|
// unchecked fails the case, and every other one this fixture knows how to
|
||||||
|
// check but found no provider for is named, so the coverage is legible on
|
||||||
|
// serial rather than inferred from what is absent.
|
||||||
|
var count: usize = 0;
|
||||||
|
for (contracts, 0..) |contract, index| {
|
||||||
|
if (checked[index]) {
|
||||||
|
count += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (contract.required) fail("a contract this scenario boots was never conformance-checked");
|
||||||
|
report("protocol-conformance: {s} not bound in this scenario — no provider to ask\n", .{contract.name});
|
||||||
|
}
|
||||||
|
report("protocol-conformance: {d} provider(s) answered the reserved verbs identically\n", .{count});
|
||||||
|
_ = logging.write("protocol-conformance: ok\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn main(startup: process.Init) void {
|
||||||
|
const role = startup.arguments.get(1) orelse return; // bare (ramdisk sweep): stay silent
|
||||||
|
if (std.mem.eql(u8, role, "run")) run();
|
||||||
|
}
|
||||||
|
|
@ -95,9 +95,13 @@ const Answer = struct {
|
||||||
/// Whether a capability rode the reply. The one field that actually matters
|
/// Whether a capability rode the reply. The one field that actually matters
|
||||||
/// to a client: the capability IS the channel.
|
/// to a client: the capability IS the channel.
|
||||||
capability: bool = false,
|
capability: bool = false,
|
||||||
/// The reply header, decoded — compared field by field as well as byte for
|
/// The reply's envelope `Status`, decoded — compared field by field as well
|
||||||
/// byte, so a failure says *which* field diverged.
|
/// as byte for byte, so a failure says *which* field diverged.
|
||||||
reply: vfs_protocol.Reply = .{ .status = 0, .node = 0, .len = 0 },
|
status: envelope.Status = .{ .status = 0, .len = 0 },
|
||||||
|
/// The node id the reply carried, or null when it carried no reply body at
|
||||||
|
/// all. A refusal has none; the open of a contract carries a zero, because
|
||||||
|
/// the capability is the whole answer.
|
||||||
|
node: ?u64 = null,
|
||||||
|
|
||||||
fn bytes(self: *const Answer) []const u8 {
|
fn bytes(self: *const Answer) []const u8 {
|
||||||
return self.packet[0..self.length];
|
return self.packet[0..self.length];
|
||||||
|
|
@ -129,33 +133,30 @@ fn registryEndpoint() ?ipc.Handle {
|
||||||
const resolve_attempts: u32 = 200;
|
const resolve_attempts: u32 = 200;
|
||||||
const resolve_retry_ms: u64 = 20;
|
const resolve_retry_ms: u64 = 20;
|
||||||
|
|
||||||
/// One vfs-protocol request at the registry: the fixed header, then the contract
|
/// One vfs-protocol request at the registry: the folded header, the verb's own
|
||||||
/// name inline. Names go bare (`input`, not `/input`) — the registrar normalises
|
/// fixed part, then the contract name as the packet's tail. Names go bare
|
||||||
/// both, and bare is what `bind` sends.
|
/// (`input`, not `/input`) — the registrar normalises both, and bare is what
|
||||||
fn transact(registry: ipc.Handle, operation: vfs_protocol.Operation, name: []const u8, cursor: u64) ?Answer {
|
/// `bind` sends.
|
||||||
var request: [vfs_protocol.message_maximum]u8 = undefined;
|
fn transact(
|
||||||
if (vfs_protocol.request_size + name.len > request.len) return null;
|
registry: ipc.Handle,
|
||||||
const header = vfs_protocol.Request{
|
comptime operation: vfs_protocol.Operation,
|
||||||
.operation = operation,
|
request: vfs_protocol.Protocol.RequestOf(operation),
|
||||||
.node = 0,
|
name: []const u8,
|
||||||
.offset = cursor,
|
) ?Answer {
|
||||||
.len = @intCast(name.len),
|
var packet: [vfs_protocol.message_maximum]u8 = undefined;
|
||||||
.flags = 0,
|
const framed = vfs_protocol.Protocol.encodeRequest(operation, 0, request, name, &packet) orelse return null;
|
||||||
};
|
|
||||||
@memcpy(request[0..vfs_protocol.request_size], std.mem.asBytes(&header));
|
|
||||||
@memcpy(request[vfs_protocol.request_size..][0..name.len], name);
|
|
||||||
|
|
||||||
var answer: Answer = .{};
|
var answer: Answer = .{};
|
||||||
const got = ipc.callCap(
|
const got = ipc.callCap(registry, framed, &answer.packet, null) catch return null;
|
||||||
registry,
|
|
||||||
request[0 .. vfs_protocol.request_size + name.len],
|
|
||||||
&answer.packet,
|
|
||||||
null,
|
|
||||||
) catch return null;
|
|
||||||
if (got.len < vfs_protocol.reply_size) return null;
|
|
||||||
answer.length = got.len;
|
answer.length = got.len;
|
||||||
answer.capability = got.cap != null;
|
answer.capability = got.cap != null;
|
||||||
answer.reply = std.mem.bytesToValue(vfs_protocol.Reply, answer.packet[0..vfs_protocol.reply_size]);
|
answer.status = envelope.statusOf(answer.bytes()) orelse return null;
|
||||||
|
answer.node = if (operation == .open) blk: {
|
||||||
|
const opened = vfs_protocol.Protocol.decodeReply(.open, answer.bytes()) orelse break :blk null;
|
||||||
|
// A short reply decodes as garbage rather than absence, so the promised
|
||||||
|
// length is what says whether a body is there at all.
|
||||||
|
break :blk if (answer.status.len < @sizeOf(vfs_protocol.Opened)) null else opened.node;
|
||||||
|
} else null;
|
||||||
// A capability we did not ask to keep is a handle slot spent; the assertions
|
// A capability we did not ask to keep is a handle slot spent; the assertions
|
||||||
// below only care that one arrived.
|
// below only care that one arrived.
|
||||||
if (got.cap) |handle| _ = ipc.close(handle);
|
if (got.cap) |handle| _ = ipc.close(handle);
|
||||||
|
|
@ -165,7 +166,7 @@ fn transact(registry: ipc.Handle, operation: vfs_protocol.Operation, name: []con
|
||||||
/// `open(name)`, kept whole. Null only if the registry could not be reached at
|
/// `open(name)`, kept whole. Null only if the registry could not be reached at
|
||||||
/// all — a registrar that answered has decided, and its decision is the subject.
|
/// all — a registrar that answered has decided, and its decision is the subject.
|
||||||
fn openContract(registry: ipc.Handle, name: []const u8) Answer {
|
fn openContract(registry: ipc.Handle, name: []const u8) Answer {
|
||||||
return transact(registry, .open, name, 0) orelse fail("the registry stopped answering");
|
return transact(registry, .open, .{ .flags = 0 }, name) orelse fail("the registry stopped answering");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether `/protocol` currently lists `name`. The namespace is browsable on
|
/// Whether `/protocol` currently lists `name`. The namespace is browsable on
|
||||||
|
|
@ -177,12 +178,11 @@ fn openContract(registry: ipc.Handle, name: []const u8) Answer {
|
||||||
fn listed(registry: ipc.Handle, name: []const u8) bool {
|
fn listed(registry: ipc.Handle, name: []const u8) bool {
|
||||||
var cursor: u64 = 0;
|
var cursor: u64 = 0;
|
||||||
while (cursor < 64) : (cursor += 1) {
|
while (cursor < 64) : (cursor += 1) {
|
||||||
const answer = transact(registry, .readdir, "", cursor) orelse return false;
|
const answer = transact(registry, .readdir, .{ .cursor = cursor }, "") orelse return false;
|
||||||
if (answer.reply.status != 0 or answer.reply.len == 0) return false; // end of directory
|
if (answer.status.status != 0) return false;
|
||||||
const payload = answer.packet[vfs_protocol.reply_size..answer.length];
|
const entry = vfs_protocol.Protocol.decodeReply(.readdir, answer.bytes()) orelse return false;
|
||||||
if (payload.len < vfs_protocol.directory_entry_size) return false;
|
if (entry.name_len == 0) return false; // end of directory
|
||||||
const entry = std.mem.bytesToValue(vfs_protocol.DirectoryEntry, payload[0..vfs_protocol.directory_entry_size]);
|
const text = vfs_protocol.Protocol.replyTail(.readdir, answer.bytes());
|
||||||
const text = payload[vfs_protocol.directory_entry_size..];
|
|
||||||
const length = @min(@as(usize, entry.name_len), text.len);
|
const length = @min(@as(usize, entry.name_len), text.len);
|
||||||
if (std.mem.eql(u8, text[0..length], name)) return true;
|
if (std.mem.eql(u8, text[0..length], name)) return true;
|
||||||
}
|
}
|
||||||
|
|
@ -207,14 +207,24 @@ fn awaitListed(registry: ipc.Handle, name: []const u8) void {
|
||||||
/// Every caller-visible field of two answers, compared. `step` names the pair so
|
/// Every caller-visible field of two answers, compared. `step` names the pair so
|
||||||
/// a failure says which comparison broke and in which field.
|
/// a failure says which comparison broke and in which field.
|
||||||
fn expectIdentical(step: []const u8, refused: Answer, absent: Answer) void {
|
fn expectIdentical(step: []const u8, refused: Answer, absent: Answer) void {
|
||||||
if (refused.reply.status != absent.reply.status) fail(step); // the errno
|
if (refused.status.status != absent.status.status) fail(step); // the errno
|
||||||
if (refused.reply.node != absent.reply.node) fail(step); // the node id an open would return
|
if (!nodesMatch(refused.node, absent.node)) fail(step); // the node id an open would return
|
||||||
if (refused.reply.len != absent.reply.len) fail(step); // payload bytes promised
|
if (refused.status.len != absent.status.len) fail(step); // payload bytes promised
|
||||||
if (refused.length != absent.length) fail(step); // reply packet length
|
if (refused.length != absent.length) fail(step); // reply packet length
|
||||||
if (refused.capability != absent.capability) fail(step); // the channel itself
|
if (refused.capability != absent.capability) fail(step); // the channel itself
|
||||||
if (!std.mem.eql(u8, refused.bytes(), absent.bytes())) fail(step); // and every byte of it
|
if (!std.mem.eql(u8, refused.bytes(), absent.bytes())) fail(step); // and every byte of it
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Two node ids agree when both are absent or both are the same value. A refusal
|
||||||
|
/// carries none at all now — the envelope sends a bare `Status` — so "no node"
|
||||||
|
/// is itself one of the observations that has to match.
|
||||||
|
fn nodesMatch(one: ?u64, other: ?u64) bool {
|
||||||
|
if (one) |a| {
|
||||||
|
return if (other) |b| a == b else false;
|
||||||
|
}
|
||||||
|
return other == null;
|
||||||
|
}
|
||||||
|
|
||||||
fn run() void {
|
fn run() void {
|
||||||
const registry = registryEndpoint() orelse fail("resolve /protocol");
|
const registry = registryEndpoint() orelse fail("resolve /protocol");
|
||||||
|
|
||||||
|
|
@ -232,14 +242,14 @@ fn run() void {
|
||||||
// 1. The control. A granted, bound contract opens: success, and the
|
// 1. The control. A granted, bound contract opens: success, and the
|
||||||
// capability that IS the channel.
|
// capability that IS the channel.
|
||||||
const allowed = openContract(registry, granted_contract);
|
const allowed = openContract(registry, granted_contract);
|
||||||
if (allowed.reply.status != 0) fail("a granted open was refused");
|
if (allowed.status.status != 0) fail("a granted open was refused");
|
||||||
if (!allowed.capability) fail("a granted open carried no channel");
|
if (!allowed.capability) fail("a granted open carried no channel");
|
||||||
_ = logging.write("protocol-denied: granted open succeeded\n");
|
_ = logging.write("protocol-denied: granted open succeeded\n");
|
||||||
|
|
||||||
// 2. The refusal. `input` is bound — the listing above proved it — and no
|
// 2. The refusal. `input` is bound — the listing above proved it — and no
|
||||||
// manifest row names this binary against it.
|
// manifest row names this binary against it.
|
||||||
const refused = openContract(registry, forbidden_contract);
|
const refused = openContract(registry, forbidden_contract);
|
||||||
if (refused.reply.status != -envelope.ENOENT) fail("an ungranted open did not answer -ENOENT");
|
if (refused.status.status != -envelope.ENOENT) fail("an ungranted open did not answer -ENOENT");
|
||||||
if (refused.capability) fail("an ungranted open carried a channel");
|
if (refused.capability) fail("an ungranted open carried a channel");
|
||||||
_ = logging.write("protocol-denied: ungranted open refused as absent\n");
|
_ = logging.write("protocol-denied: ungranted open refused as absent\n");
|
||||||
|
|
||||||
|
|
@ -259,7 +269,7 @@ fn run() void {
|
||||||
// what it should, so what steps 2-4 saw was policy and not a registry
|
// what it should, so what steps 2-4 saw was policy and not a registry
|
||||||
// that had wedged.
|
// that had wedged.
|
||||||
const again = openContract(registry, granted_contract);
|
const again = openContract(registry, granted_contract);
|
||||||
if (again.reply.status != 0 or !again.capability) fail("the granted contract stopped opening");
|
if (again.status.status != 0 or !again.capability) fail("the granted contract stopped opening");
|
||||||
_ = logging.write("protocol-denied: ok\n");
|
_ = logging.write("protocol-denied: ok\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -288,8 +288,10 @@ fn run() void {
|
||||||
// — the strongest one available, because a regression does not fail this
|
// — the strongest one available, because a regression does not fail this
|
||||||
// line, it takes the entire boot down with it.
|
// line, it takes the entire boot down with it.
|
||||||
const registry = registryEndpoint() orelse fail("resolve /protocol");
|
const registry = registryEndpoint() orelse fail("resolve /protocol");
|
||||||
const forged = power_protocol.EventMessage{ .event = @intFromEnum(power_protocol.Event.power_button) };
|
var forged: [envelope.post_maximum]u8 = undefined;
|
||||||
if (!ipc.send(registry, std.mem.asBytes(&forged))) fail("post a forged power event");
|
const packet = power_protocol.Protocol.encodeEvent(.power_button, 0, .{}, &forged) orelse
|
||||||
|
fail("frame a forged power event");
|
||||||
|
if (!ipc.send(registry, packet)) fail("post a forged power event");
|
||||||
const still_serving = verdictWithin(forbidden, spare) orelse
|
const still_serving = verdictWithin(forbidden, spare) orelse
|
||||||
fail("the registrar went silent after a forged power event — it acted on it");
|
fail("the registrar went silent after a forged power event — it acted on it");
|
||||||
if (still_serving != -envelope.EPERM) fail("the registry misanswered after a forged power event");
|
if (still_serving != -envelope.EPERM) fail("the registry misanswered after a forged power event");
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue