library: five protocols speak the envelope
The folded header stops being a rule in a document and becomes the layout on the wire. Verbs number from sixteen, leaving describe, enumerate, subscribe and unsubscribe reserved and answered the same way by every provider — none of them writes a line to do it. What each protocol used to carry in a field of its own now travels in the header: a vfs node and a display layer are the packet's target, and a reply opens with a status the envelope stamps rather than one each protocol spelled for itself. Display gains the most. One forty-byte request had served eleven verbs, so attach_scanout smuggled stride through x, refresh through y and format through colour, and every coordinate crossed as a bitcast. Per-operation structs end all three: the fields have their own names and their own signs, and the tile payload grows to 224 bytes because the prefix shrank. Scanout loses a message maximum of 64 it had no business declaring — it answers calls, and the floor for a call is 256 — and virtio-gpu stops hard-coding that number at its harness. Two changes are semantic rather than notational. A directory now ends at an entry with no name, because the fixed part of a reply always travels and a zero-length reply no longer exists to mean anything. And input joins the service harness, the last loop in the tree that answered no ping and heard no terminate; its subscriber table, its pruning and its fan-out are the same code, and a shutdown now asks it to stop instead of killing it. A new conformance case reads the registry's own listing and asks every protocol it finds for its name, its version and its verb count, then offers a verb nobody defines and requires -ENOSYS — the envelope's promise, checked against providers rather than against itself. What it cannot reach in that boot it names on the serial line instead of passing quietly. Suite 110/110.
This commit is contained in:
@@ -9,7 +9,7 @@ pub fn build(b: *std.Build) void {
|
||||
const exe = build_support.userBinary(b, .{
|
||||
.name = "input",
|
||||
.root_source_file = b.path("input.zig"),
|
||||
.imports = &.{ "channel", "input-client", "input-protocol", "ipc", "logging", "process", "service" },
|
||||
.imports = &.{ "envelope", "input-protocol", "ipc", "logging", "process", "service" },
|
||||
});
|
||||
b.installArtifact(exe);
|
||||
}
|
||||
|
||||
@@ -17,17 +17,29 @@
|
||||
//!
|
||||
//! 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
|
||||
//! 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 — so a shutdown had to kill it. The subscriber
|
||||
//! table, the fan-out, and the prune-on-subscribe below are unchanged; lifting *those* into
|
||||
//! the harness is a later milestone, and doing it here would have hidden this one.
|
||||
|
||||
const std = @import("std");
|
||||
const channel = @import("channel");
|
||||
const envelope = @import("envelope");
|
||||
const ipc = @import("ipc");
|
||||
const process = @import("process");
|
||||
const service = @import("service");
|
||||
const input = @import("input-client");
|
||||
const logging = @import("logging");
|
||||
const input_protocol = @import("input-protocol");
|
||||
|
||||
/// The generated input dispatch. One fan-out point per process, so the handler
|
||||
/// context is empty and the subscriber table stays in this file's globals.
|
||||
const Serve = input_protocol.Protocol.Provider(void);
|
||||
|
||||
const Invocation = envelope.Invocation;
|
||||
const Answer = envelope.Answer;
|
||||
|
||||
/// One registered subscriber: the endpoint we push events to (a capability it handed us at
|
||||
/// subscribe time) and the task id that owns it (the subscribe call's badge), so a slot
|
||||
/// left behind by a subscriber that exited can be reclaimed.
|
||||
@@ -83,80 +95,72 @@ fn addSubscriber(endpoint: ipc.Handle, task_id: u32, device_mask: u32) bool {
|
||||
|
||||
/// 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.
|
||||
///
|
||||
/// The class is the packet's operation, so the fan-out picks the event by device and the
|
||||
/// packet is framed once, outside the loop — every subscriber of a class gets identical
|
||||
/// bytes, which is what "one fan-out point per event domain" means on the wire.
|
||||
fn broadcast(event: input_protocol.InputEvent) void {
|
||||
const bytes = std.mem.asBytes(&event);
|
||||
const class = input_protocol.eventOfDevice(event.device) orelse return; // no class wants it
|
||||
var packet: [envelope.post_maximum]u8 = undefined;
|
||||
const framed = switch (class) {
|
||||
.keyboard => input_protocol.Protocol.encodeEvent(.keyboard, 0, event.asKeyboard() orelse return, &packet),
|
||||
.mouse => input_protocol.Protocol.encodeEvent(.mouse, 0, event.asMouse() orelse return, &packet),
|
||||
.joystick => input_protocol.Protocol.encodeEvent(.joystick, 0, event.asJoystick() orelse return, &packet),
|
||||
} orelse return;
|
||||
|
||||
const bit = input_protocol.deviceBit(event.device);
|
||||
for (&subscribers) |*sub| {
|
||||
if (sub.used and sub.device_mask & bit != 0) _ = ipc.send(sub.endpoint, bytes);
|
||||
if (sub.used and sub.device_mask & bit != 0) _ = ipc.send(sub.endpoint, framed);
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle one request. `got` carries the sender badge (a task id); `arrived` carries the
|
||||
/// capability the request came with, under the same ownership rule the service harness
|
||||
/// states (`ipc.Arrival`): **it belongs to the turn, and only a handler that means to keep
|
||||
/// 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;
|
||||
}
|
||||
};
|
||||
/// Set by `onSubscribe` when the subscriber table has taken ownership of the capability the
|
||||
/// call carried, and read by `onMessage`, 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 capability_claimed = false;
|
||||
|
||||
if (message.len < input_protocol.request_size) return reply.write(out, -1);
|
||||
const request = std.mem.bytesToValue(input_protocol.Request, message[0..input_protocol.request_size]);
|
||||
/// The reserved `subscribe` verb: register the caller's endpoint (the call's capability) for
|
||||
/// the classes in the packet's tail. Refusals simply return, and the turn closes what arrived
|
||||
/// — the ownership rule the harness states (`ipc.Arrival`), unchanged by the move onto it.
|
||||
fn onSubscribe(_: void, invocation: Invocation(void), _: Answer(void)) isize {
|
||||
const endpoint = invocation.capability orelse return -envelope.EPROTO; // no endpoint passed
|
||||
// A zero mask means "everything" (a subscriber that named no class still wants input).
|
||||
const requested = input_protocol.decodeSubscribe(invocation.tail).device_mask;
|
||||
const mask = if (requested == 0) input_protocol.device_all else requested;
|
||||
pruneDeadSubscribers();
|
||||
if (!addSubscriber(endpoint, invocation.sender, mask)) return -envelope.ENOSPC; // table full
|
||||
capability_claimed = true; // the subscriber table holds it until that task dies
|
||||
return 0;
|
||||
}
|
||||
|
||||
switch (@as(input_protocol.Operation, @enumFromInt(request.operation))) {
|
||||
.subscribe => {
|
||||
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;
|
||||
pruneDeadSubscribers();
|
||||
if (!addSubscriber(endpoint, @intCast(got.badge), mask)) return reply.write(out, -1); // table full
|
||||
_ = arrived.take(); // claimed: the subscriber table holds it until that task dies
|
||||
return reply.write(out, 0);
|
||||
},
|
||||
.publish => {
|
||||
broadcast(request.event);
|
||||
return reply.write(out, 0);
|
||||
},
|
||||
}
|
||||
fn onPublish(_: void, invocation: Invocation(input_protocol.InputEvent), _: Answer(void)) isize {
|
||||
broadcast(invocation.request);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const handlers = Serve.Handlers{ .publish = onPublish, .subscribe = onSubscribe };
|
||||
|
||||
fn onMessage(message: []const u8, out: []u8, sender: u32, arrived: *ipc.Arrival) usize {
|
||||
capability_claimed = false;
|
||||
const written = Serve.dispatch({}, handlers, message, sender, arrived.peek(), out);
|
||||
if (capability_claimed) _ = arrived.take();
|
||||
return written;
|
||||
}
|
||||
|
||||
fn initialise(_: ipc.Handle) bool {
|
||||
// The harness has already bound `/protocol/input` by the time this runs, so
|
||||
// "ready" still means what it always meant: the name is claimed and the loop
|
||||
// is about to serve it.
|
||||
_ = logging.write("/system/services/input: ready\n");
|
||||
return true;
|
||||
}
|
||||
|
||||
pub fn main() void {
|
||||
const endpoint = ipc.createIpcEndpoint() orelse {
|
||||
_ = logging.write("/system/services/input: no endpoint\n");
|
||||
return;
|
||||
};
|
||||
if (!channel.bindPatiently("input", endpoint)) {
|
||||
_ = 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);
|
||||
}
|
||||
service.run(input_protocol.message_maximum, .{
|
||||
.service = "input",
|
||||
.init = initialise,
|
||||
.on_message = onMessage,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user