Add input module: broadcast keyboard events over IPC
Programs can now subscribe to keyboard events (key_down/key_up/key_press) and drivers can broadcast them, through a new user-space input service. The delivery model is forced by danos IPC: a synchronous rendezvous holds one pending reply, so a server cannot park N subscribers blocked in a "wait for next event" call — delivery must be push. But a synchronous push has no timeout and the kernel never wakes a sender parked on a dead peer's endpoint, so one dying subscriber would hang all input. So this lands the roadmap's planned asynchronous buffered send and builds the service on it: - ipc_send (syscall 26): non-blocking post to an endpoint's bounded payload ring, delivered through reply_wait as a buffered message (notify_message_bit). A full ring drops the oldest. It can never hang on a dead/slow peer. - input-protocol + runtime.input helpers (subscribe/next, connectSource/ publish) — the first real consumer of M13 capability passing: a subscriber hands the service its own endpoint as a capability. - input service (fan-out via ipc_send, dead-subscriber pruning), a synthetic input-source, and input-test; the ps2-bus keyboard driver publishes to it. Real IRQ1 scancode decoding (which must live in the bus, the PNP0303 owner) is a documented follow-up; the source is synthetic for now. - build/init wiring, an `input` QEMU case, and docs/input.md. Full QEMU suite 48/48, including the new input case and every IPC/endpoint regression (ipc, ipc-call, ipc-cap, vfs, hpet, bus, irqfree).
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
//! User-space input helpers: the client and publisher sides of the input service, so a
|
||||
//! program listening for keyboard events — or a driver broadcasting them — doesn't
|
||||
//! hand-roll the IPC. Layered over `ipc` (endpoints, capability passing, `send`) and the
|
||||
//! shared `input-protocol` wire format, the same way `device.zig` layers over the raw
|
||||
//! `device_*` calls. See system/services/input/input.zig.
|
||||
//!
|
||||
//! A **subscriber** does:
|
||||
//! var listener = input.subscribe() orelse return;
|
||||
//! while (true) { const event = listener.next() orelse continue; ... }
|
||||
//!
|
||||
//! A **source** (keyboard driver) does:
|
||||
//! var source = input.connectSource() orelse return;
|
||||
//! _ = source.publish(.{ .kind = ..., .keycode = ..., ... });
|
||||
|
||||
const std = @import("std");
|
||||
const abi = @import("abi");
|
||||
const ipc = @import("ipc.zig");
|
||||
const system = @import("system.zig");
|
||||
const protocol = @import("input-protocol");
|
||||
|
||||
pub const KeyEvent = protocol.KeyEvent;
|
||||
pub const EventKind = protocol.EventKind;
|
||||
pub const Keycode = protocol.Keycode;
|
||||
|
||||
/// Look up the input service, retrying while it is still coming up. Both a subscriber and
|
||||
/// a source race the service's registration at boot, so both wait for it here rather than
|
||||
/// failing. Returns the service endpoint handle, or null if it never appears.
|
||||
fn lookupService() ?ipc.Handle {
|
||||
var attempts: usize = 0;
|
||||
while (attempts < 100) : (attempts += 1) {
|
||||
if (ipc.lookup(.input)) |handle| return handle;
|
||||
system.sleep(50);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// A subscription to the input service: our own endpoint, which the service pushes events
|
||||
/// to. Keep it and call `next` in a loop.
|
||||
pub const Subscriber = struct {
|
||||
/// 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).
|
||||
endpoint: ipc.Handle,
|
||||
receive: [protocol.event_size]u8 = undefined,
|
||||
|
||||
/// 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
|
||||
/// empty reply this issues is a harmless no-op (a pure subscriber holds no client).
|
||||
/// Returns null for any non-event wake-up (there should be none), so callers can loop.
|
||||
pub fn next(self: *Subscriber) ?KeyEvent {
|
||||
const got = ipc.replyWait(self.endpoint, &.{}, &self.receive, null);
|
||||
if (!got.isMessage() or got.len < protocol.event_size) return null;
|
||||
return std.mem.bytesToValue(KeyEvent, self.receive[0..protocol.event_size]);
|
||||
}
|
||||
};
|
||||
|
||||
/// Subscribe to keyboard events: create an endpoint for the service to push to, and hand
|
||||
/// it over as a capability. Returns a `Subscriber` to loop `next` on, or null on failure
|
||||
/// (the service never came up, out of handles, or the subscribe call failed).
|
||||
pub fn subscribe() ?Subscriber {
|
||||
const service = lookupService() orelse return null;
|
||||
const endpoint = ipc.createIpcEndpoint() orelse return null;
|
||||
|
||||
var request = protocol.Request{ .operation = @intFromEnum(protocol.Operation.subscribe), .event = undefined };
|
||||
var reply: [protocol.reply_size]u8 = undefined;
|
||||
const result = ipc.callCap(service, std.mem.asBytes(&request), &reply, endpoint) catch return null;
|
||||
if (result.len < protocol.reply_size) return null;
|
||||
const header = std.mem.bytesToValue(protocol.Reply, reply[0..protocol.reply_size]);
|
||||
if (header.status != 0) return null;
|
||||
return .{ .endpoint = endpoint };
|
||||
}
|
||||
|
||||
/// A connection to the input service for a source (a keyboard driver) that publishes
|
||||
/// events. Cheap to hold; `publish` is a short synchronous call the service answers at
|
||||
/// once (its own fan-out to subscribers is asynchronous, so publishing never blocks on a
|
||||
/// slow subscriber).
|
||||
pub const Publisher = struct {
|
||||
service: ipc.Handle,
|
||||
|
||||
/// Broadcast one event to every subscriber. Returns false if the call to the service
|
||||
/// failed (e.g. the service is gone).
|
||||
pub fn publish(self: Publisher, event: KeyEvent) bool {
|
||||
var request = protocol.Request{ .operation = @intFromEnum(protocol.Operation.publish), .event = event };
|
||||
var reply: [protocol.reply_size]u8 = undefined;
|
||||
const len = ipc.call(self.service, std.mem.asBytes(&request), &reply) catch return false;
|
||||
if (len < protocol.reply_size) return false;
|
||||
return std.mem.bytesToValue(protocol.Reply, reply[0..protocol.reply_size]).status == 0;
|
||||
}
|
||||
};
|
||||
|
||||
/// Connect to the input service as an event source, waiting for it to come up. Returns a
|
||||
/// `Publisher`, or null if the service never registered.
|
||||
pub fn connectSource() ?Publisher {
|
||||
return .{ .service = lookupService() orelse return null };
|
||||
}
|
||||
|
||||
/// A tiny synthetic key-event generator, shared by the demo source and the keyboard
|
||||
/// driver's placeholder stream while real scancode decoding is still a follow-up. `step`
|
||||
/// is a monotonically increasing tick; the result rolls through the keys A..E, emitting
|
||||
/// for each one a `key_down`, then a `key_press` carrying the character, then a `key_up`.
|
||||
/// This is deliberately not wire protocol — it is scaffolding, so it lives with the
|
||||
/// helpers, not in `input-protocol`.
|
||||
pub fn syntheticEvent(step: usize) KeyEvent {
|
||||
const Key = struct { code: Keycode, character: u32 };
|
||||
const keys = [_]Key{
|
||||
.{ .code = .a, .character = 'A' },
|
||||
.{ .code = .b, .character = 'B' },
|
||||
.{ .code = .c, .character = 'C' },
|
||||
.{ .code = .d, .character = 'D' },
|
||||
.{ .code = .e, .character = 'E' },
|
||||
};
|
||||
const key = keys[(step / 3) % keys.len];
|
||||
return switch (step % 3) {
|
||||
0 => .{ .kind = @intFromEnum(EventKind.key_down), .keycode = @intFromEnum(key.code), .character = 0, .modifiers = 0 },
|
||||
1 => .{ .kind = @intFromEnum(EventKind.key_press), .keycode = @intFromEnum(key.code), .character = key.character, .modifiers = 0 },
|
||||
else => .{ .kind = @intFromEnum(EventKind.key_up), .keycode = @intFromEnum(key.code), .character = 0, .modifiers = 0 },
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user