library: the harness keeps the subscribers, and an id belongs to whoever opened it
Three services had each written the same thing and got it three different ways: input polled the process list to notice a dead subscriber, and only when someone else subscribed; the power service never noticed at all; the device manager noticed drivers but not subscribers. The harness owns the table now, driven by the events a protocol declares — it registers on the reserved verb, frames each event once, posts to everyone interested without waiting on any of them, and reclaims a slot when the kernel says its owner died. Interest masks moved to the envelope, so a subscriber that wants only mice asks the same way everywhere. Two consequences the plan had not foreseen. The device manager now hears a supervised child's death twice, once as its supervisor and once as a subscriber, so restart backoff counted every crash twice and gave up after half as many; it retires the id before counting. And the kernel's published exit table had eight slots for what is now six subscriptions in a plain boot, so it holds sixteen. The other half is a hole the design named early and left standing: a backend handed out a small integer and then honoured it from anyone. A process that guessed a file's node id read another client's file; a display layer had no owner at all, so any client could reconfigure or destroy any layer; a USB device token was never checked against the client that opened it. Each is now bound to the task that opened it, and a wrong owner gets exactly what an unknown id gets — the refusal must not become the oracle the identical answers elsewhere were designed to remove. Closing a file changed with it: it used to succeed unconditionally, which would have told a caller which ids existed. Suite 111/111, with a new case in which one process holds a file and a layer, hands both ids to a second process, and finds them untouched after that process has tried everything with them.
This commit is contained in:
+25
-105
@@ -20,133 +20,52 @@
|
||||
//! 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
|
||||
//! 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.
|
||||
//! 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 envelope = @import("envelope");
|
||||
const ipc = @import("ipc");
|
||||
const process = @import("process");
|
||||
const service = @import("service");
|
||||
const logging = @import("logging");
|
||||
const input_protocol = @import("input-protocol");
|
||||
const envelope = @import("envelope");
|
||||
|
||||
/// 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);
|
||||
/// The subscriber side of the input contract: the table, the reserved `subscribe`
|
||||
/// verb, the exit sweep, and the fan-out. One fan-out point per process, so the
|
||||
/// handler context is empty.
|
||||
const Subscriptions = service.Subscribers(input_protocol.Protocol, 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.
|
||||
const Subscriber = struct {
|
||||
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;
|
||||
|
||||
/// Drop any subscriber whose owning process is no longer alive, so its slot (and the
|
||||
/// endpoint reference it holds) can be reused. Cheap and only run on subscribe — the async
|
||||
/// `send` to a dead subscriber's orphaned endpoint is harmless (it just fills a queue no
|
||||
/// one drains), so this is housekeeping, not correctness.
|
||||
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.
|
||||
///
|
||||
/// 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.
|
||||
/// Push `event` to every subscriber whose interest mask includes its device class. The
|
||||
/// class is the packet's operation, so this picks *which event* to publish and the harness
|
||||
/// frames it once for the whole fan-out — the mapping from device to class is the only part
|
||||
/// of a broadcast that is this service's own.
|
||||
fn broadcast(event: input_protocol.InputEvent) void {
|
||||
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, framed);
|
||||
const class = input_protocol.deviceBit(event.device);
|
||||
switch (input_protocol.eventOfDevice(event.device) orelse return) { // no class wants it
|
||||
.keyboard => Subscriptions.publishClass(.keyboard, 0, event.asKeyboard() orelse return, class),
|
||||
.mouse => Subscriptions.publishClass(.mouse, 0, event.asMouse() orelse return, class),
|
||||
.joystick => Subscriptions.publishClass(.joystick, 0, event.asJoystick() orelse return, class),
|
||||
}
|
||||
}
|
||||
|
||||
/// 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;
|
||||
|
||||
/// 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;
|
||||
}
|
||||
|
||||
fn onPublish(_: void, invocation: Invocation(input_protocol.InputEvent), _: Answer(void)) isize {
|
||||
broadcast(invocation.request);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const handlers = Serve.Handlers{ .publish = onPublish, .subscribe = onSubscribe };
|
||||
/// `subscribe` and `unsubscribe` are absent on purpose: the harness answers both.
|
||||
const handlers = Subscriptions.Handlers{ .publish = onPublish };
|
||||
|
||||
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;
|
||||
return Subscriptions.dispatch({}, handlers, message, sender, arrived, out);
|
||||
}
|
||||
|
||||
fn initialise(_: ipc.Handle) bool {
|
||||
@@ -162,5 +81,6 @@ pub fn main() void {
|
||||
.service = "input",
|
||||
.init = initialise,
|
||||
.on_message = onMessage,
|
||||
.subscribers = Subscriptions.hooks,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user