danos/system/services/input/input.zig

87 lines
4.6 KiB
Zig

//! system/services/input — the user-space input service. Shipped in the initial_ramdisk,
//! spawned as a ring-3 process, and bound at `/protocol/input`. It
//! is the fan-out point between **sources** (keyboard, mouse, and joystick/gamepad drivers)
//! and **subscribers** (any program that wants input): a source `publish`es an
//! `InputEvent`, and the service pushes it to every subscriber whose interest mask includes
//! that event's device class (keyboard / mouse / joystick).
//!
//! The delivery discipline is the whole design (see docs/input.md). The kernel's IPC is a
//! synchronous rendezvous: a server holds one pending reply, so it cannot park N
//! subscribers blocked in a "wait for next event" call. Broadcasting therefore has to be
//! *push* — the service delivering to subscribers. But a synchronous push (`ipc_call`)
//! would let one dead or wedged subscriber hang the whole broadcast, since the kernel
//! never wakes a sender parked on a dead peer's endpoint. So delivery uses the
//! asynchronous `ipc.send`: it posts the event to each subscriber's endpoint queue and
//! returns at once, and can never block on a subscriber. That primitive exists for exactly
//! this ([ipc.md](../../../docs/ipc.md), "asynchronous / buffered send").
//!
//! 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. 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 ipc = @import("ipc");
const service = @import("service");
const logging = @import("logging");
const input_protocol = @import("input-protocol");
const envelope = @import("envelope");
/// 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;
/// 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.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),
}
}
fn onPublish(_: void, invocation: Invocation(input_protocol.InputEvent), _: Answer(void)) isize {
broadcast(invocation.request);
return 0;
}
/// `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 {
return Subscriptions.dispatch({}, handlers, message, sender, arrived, out);
}
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 {
service.run(input_protocol.message_maximum, .{
.service = "input",
.init = initialise,
.on_message = onMessage,
.subscribers = Subscriptions.hooks,
});
}