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:
@@ -52,6 +52,7 @@ pub const SystemCall = enum(u64) {
|
||||
clock = 23, // clock() -> nanoseconds since boot: a monotonic time source (for timeouts/delays)
|
||||
process_enumerate = 24, // process_enumerate(buffer, maximum) -> total: snapshot the task table
|
||||
process_kill = 25, // process_kill(id) -> 0/-errno: end a process this process spawned
|
||||
ipc_send = 26, // ipc_send(handle, message_ptr, message_len) -> 0/-errno: post a payload to an endpoint's async queue without blocking
|
||||
_,
|
||||
};
|
||||
|
||||
@@ -83,6 +84,15 @@ pub const notify_badge_bit: u64 = 1 << 63;
|
||||
/// notifications, which never set this bit). The microkernel's SIGCHLD.
|
||||
pub const notify_exit_bit: u64 = 1 << 62;
|
||||
|
||||
/// Set (alongside `notify_badge_bit`) in the badge of a **buffered message** — a payload
|
||||
/// posted to an endpoint's async queue by `ipc_send`, delivered through `ipc_reply_wait`
|
||||
/// like a notification (no reply owed) but carrying bytes in the receive buffer, not just
|
||||
/// a badge. This is what distinguishes a payload-bearing async message from a bare IRQ /
|
||||
/// child-exit notification (which sets neither this nor `notify_exit_bit`). The low bits
|
||||
/// carry the sender's task id. The async counterpart of the synchronous `ipc_call`, for
|
||||
/// broadcasts where a rendezvous is the wrong shape (the input service is the first user).
|
||||
pub const notify_message_bit: u64 = 1 << 61;
|
||||
|
||||
/// Capacity of `ProcessDescriptor.name` — matches the longest name `system_spawn`
|
||||
/// accepts, so a process's recorded name (its argv[0]) is never truncated.
|
||||
pub const maximum_process_name = 64;
|
||||
@@ -113,6 +123,7 @@ pub const ProcessDescriptor = extern struct {
|
||||
/// during bring-up. The VFS server registers under `vfs`; clients look it up.
|
||||
pub const ServiceId = enum(u32) {
|
||||
vfs = 1,
|
||||
input = 2,
|
||||
_,
|
||||
};
|
||||
|
||||
|
||||
@@ -36,8 +36,21 @@ pub fn main() void {
|
||||
// served through the bus and does not claim the controller itself.
|
||||
_ = runtime.system.write("system/drivers/ps2-bus/keyboard: served by ps2-bus (controller owned by bus)\n");
|
||||
|
||||
// Broadcast keyboard events through the input service so programs can listen for them
|
||||
// (docs/input.md). Until the bus reads real IRQ1 scancodes and hands them here (a
|
||||
// follow-up), we publish the same synthetic stand-in stream the demo source uses — the
|
||||
// fan-out path is real, only the source of the bytes is placeholder.
|
||||
var source = runtime.input.connectSource() orelse {
|
||||
_ = runtime.system.write("system/drivers/ps2-bus/keyboard: input service unavailable\n");
|
||||
return;
|
||||
};
|
||||
_ = runtime.system.write("system/drivers/ps2-bus/keyboard: ok\n");
|
||||
while (true) runtime.system.sleep(1000);
|
||||
|
||||
var step: usize = 0;
|
||||
while (true) : (step +%= 1) {
|
||||
_ = source.publish(runtime.input.syntheticEvent(step));
|
||||
runtime.system.sleep(200);
|
||||
}
|
||||
}
|
||||
|
||||
pub const panic = runtime.panic;
|
||||
|
||||
@@ -57,6 +57,29 @@ pub const EPERM: i64 = 9; // not permitted (process_kill by anyone but the super
|
||||
/// shared kernel↔user ABI (system/abi.zig), because ring 3 has to test the same bit.
|
||||
pub const notify_badge_bit: u64 = abi.notify_badge_bit;
|
||||
|
||||
/// Set (with `notify_badge_bit`) when a `replyWait` wake carries a buffered payload
|
||||
/// posted by `send` (`ipc_send`), rather than a bare IRQ/exit notification. Shared with
|
||||
/// ring 3 through the ABI so the receiver can tell "a message arrived" from "the hardware
|
||||
/// spoke".
|
||||
pub const notify_message_bit: u64 = abi.notify_message_bit;
|
||||
|
||||
/// Largest payload a single `send` (`ipc_send`) may post. Kept small — the payload rides
|
||||
/// inline in every `Endpoint`, and the async path is for events (a `KeyEvent` is 16
|
||||
/// bytes), not bulk transfer, which is what `call` and future shared pages are for.
|
||||
pub const POST_MAXIMUM: usize = 64;
|
||||
|
||||
/// Depth of an endpoint's async payload ring. Absorbs a burst while a receiver is briefly
|
||||
/// busy; a full ring drops the *oldest* message (see `send`).
|
||||
const post_capacity: usize = 16;
|
||||
|
||||
/// One buffered message: a length-prefixed payload plus the sender's task id (delivered
|
||||
/// in the low bits of the receiver's badge).
|
||||
const PostSlot = struct {
|
||||
length: u16 = 0,
|
||||
sender_id: u64 = 0,
|
||||
bytes: [POST_MAXIMUM]u8 = undefined,
|
||||
};
|
||||
|
||||
/// End of the user (low) canonical half — user buffers must lie below it.
|
||||
const user_half_end: u64 = 0x0000_8000_0000_0000;
|
||||
|
||||
@@ -74,6 +97,12 @@ pub const Endpoint = struct {
|
||||
notify_buffer: [8]u64 = undefined,
|
||||
notify_head: u8 = 0,
|
||||
notify_tail: u8 = 0,
|
||||
// Pending buffered messages (payloads posted by `send`), a small FIFO ring. Unlike
|
||||
// notifications — which are a level and coalesce — these are discrete messages, so a
|
||||
// full ring drops the oldest rather than merging.
|
||||
post_buffer: [post_capacity]PostSlot = undefined,
|
||||
post_head: u16 = 0,
|
||||
post_tail: u16 = 0,
|
||||
};
|
||||
|
||||
pub fn createIpcEndpoint() ?*Endpoint {
|
||||
@@ -265,12 +294,23 @@ pub fn replyWait(endpoint: *Endpoint, reply_ptr: u64, reply_len: u64, receive_pt
|
||||
scheduler.readyLocked(client); // its `call` now returns
|
||||
}
|
||||
|
||||
// (2) Receive the next request (or notification), blocking until one is ready.
|
||||
// (2) Receive the next request (or notification / buffered message), blocking until
|
||||
// one is ready. Bare notifications (IRQ/exit) come first — they're latency-sensitive
|
||||
// and carry no payload — then buffered messages, then synchronous client requests.
|
||||
while (true) {
|
||||
if (popNotify(endpoint)) |badge| {
|
||||
out_badge.* = badge | notify_badge_bit;
|
||||
return 0; // notification: no payload, no reply owed, no cap
|
||||
}
|
||||
if (popPost(endpoint)) |slot| {
|
||||
const n = @min(@as(usize, slot.length), receive_cap);
|
||||
// Copy from the kernel-resident ring slot (source aspace 0) into the receiver.
|
||||
if (!copyAcross(0, @intFromPtr(&slot.bytes), me.aspace, receive_ptr, n)) {
|
||||
continue; // bad receive buffer: drop this message, keep serving
|
||||
}
|
||||
out_badge.* = slot.sender_id | notify_badge_bit | notify_message_bit;
|
||||
return @intCast(n); // async message: payload delivered, no reply owed, no cap
|
||||
}
|
||||
if (dequeueSender(endpoint)) |caller| {
|
||||
const n = @min(caller.ipc_send_len, receive_cap);
|
||||
if (!copyAcross(caller.aspace, caller.ipc_send_ptr, me.aspace, receive_ptr, n)) {
|
||||
@@ -306,6 +346,44 @@ fn popNotify(endpoint: *Endpoint) ?u64 {
|
||||
return badge;
|
||||
}
|
||||
|
||||
/// Take the oldest buffered message from the post ring, or null if empty. Returns a
|
||||
/// pointer into the endpoint's own storage — valid until the next `send`/`popPost` under
|
||||
/// the same lock region, which is all the copy-out in `replyWait` needs.
|
||||
fn popPost(endpoint: *Endpoint) ?*const PostSlot {
|
||||
if (endpoint.post_head == endpoint.post_tail) return null;
|
||||
const slot = &endpoint.post_buffer[endpoint.post_head % post_capacity];
|
||||
endpoint.post_head +%= 1;
|
||||
return slot;
|
||||
}
|
||||
|
||||
/// Client-free side of async IPC (`ipc_send`): copy `[source_va, len)` from address space
|
||||
/// `source_as` into `endpoint`'s post ring and wake a waiting receiver — **without
|
||||
/// blocking the sender** and with no reply owed. `sender_id` rides along, delivered in the
|
||||
/// low bits of the receiver's badge. Returns 0, or a negative errno (`-E2BIG` if the
|
||||
/// payload exceeds `POST_MAXIMUM`, `-EFAULT` if the source buffer is unmapped / out of the
|
||||
/// user half). A full ring drops the *oldest* message (advancing `post_head`), because a
|
||||
/// buffered message is discrete, not a level: keeping the newest keeps input responsive.
|
||||
/// Precondition: the big kernel lock is held.
|
||||
pub fn sendLocked(endpoint: *Endpoint, source_as: u64, source_va: u64, len: u64, sender_id: u64) i64 {
|
||||
if (len > POST_MAXIMUM) return -E2BIG;
|
||||
// Drop the oldest if the ring is full, so this newest message always lands.
|
||||
if (endpoint.post_tail -% endpoint.post_head >= post_capacity) endpoint.post_head +%= 1;
|
||||
const slot = &endpoint.post_buffer[endpoint.post_tail % post_capacity];
|
||||
if (!copyFromUser(source_as, source_va, slot.bytes[0..@intCast(len)])) return -EFAULT;
|
||||
slot.length = @intCast(len);
|
||||
slot.sender_id = sender_id;
|
||||
endpoint.post_tail +%= 1;
|
||||
scheduler.wakeLocked(&endpoint.receive_wait_queue);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// `sendLocked` wrapped in its own critical section, for the `ipc_send` syscall path.
|
||||
pub fn send(endpoint: *Endpoint, source_as: u64, source_va: u64, len: u64, sender_id: u64) i64 {
|
||||
const flags = sync.enter();
|
||||
defer sync.leave(flags);
|
||||
return sendLocked(endpoint, source_as, source_va, len, sender_id);
|
||||
}
|
||||
|
||||
/// Post an asynchronous notification carrying `badge` to `endpoint` and wake a waiting
|
||||
/// receiver. Precondition: the big kernel lock is held.
|
||||
///
|
||||
|
||||
@@ -183,6 +183,7 @@ fn system_call(state: *architecture.CpuState) void {
|
||||
.ipc_lookup => systemIpcLookup(state),
|
||||
.ipc_call => systemIpcCall(state),
|
||||
.ipc_reply_wait => systemIpcReplyWait(state),
|
||||
.ipc_send => systemIpcSend(state),
|
||||
.device_enumerate => systemDeviceEnumerate(state),
|
||||
.device_claim => systemDeviceClaim(state),
|
||||
.mmio_map => systemMmioMap(state),
|
||||
@@ -263,6 +264,18 @@ fn systemIpcReplyWait(state: *architecture.CpuState) void {
|
||||
architecture.setSystemCallResult3(state, received_cap);
|
||||
}
|
||||
|
||||
/// ipc_send(handle, message_ptr, message_len) -> 0/-errno: post a payload to an
|
||||
/// endpoint's async queue and wake a receiver, without blocking the caller. The async
|
||||
/// counterpart of ipc_call — for broadcasts (the input service) where a rendezvous would
|
||||
/// let one dead subscriber hang the sender. Delivered through ipc_reply_wait as a
|
||||
/// buffered message (badge carries notify_message_bit and the caller's task id).
|
||||
fn systemIpcSend(state: *architecture.CpuState) void {
|
||||
const me = scheduler.current();
|
||||
const endpoint = ipc.resolveHandle(me, architecture.systemCallArg(state, 0)) orelse return failErr(state, ipc.EBADF);
|
||||
const r = ipc.send(endpoint, me.aspace, architecture.systemCallArg(state, 1), architecture.systemCallArg(state, 2), me.id);
|
||||
architecture.setSystemCallResult(state, @bitCast(r));
|
||||
}
|
||||
|
||||
/// device_enumerate(buffer, maximum) -> total: snapshot the device table into the caller's
|
||||
/// buffer (up to `maximum` entries), returning the total device count.
|
||||
fn systemDeviceEnumerate(state: *architecture.CpuState) void {
|
||||
|
||||
@@ -136,6 +136,8 @@ pub fn run(case: []const u8, boot_information: *const BootInformation) void {
|
||||
initialRamdiskTest(boot_information);
|
||||
} else if (eql(case, "vfs")) {
|
||||
vfsTest(boot_information);
|
||||
} else if (eql(case, "input")) {
|
||||
inputTest(boot_information);
|
||||
} else if (eql(case, "hpet")) {
|
||||
hpetTest(boot_information);
|
||||
} else if (eql(case, "iopass")) {
|
||||
@@ -1585,6 +1587,50 @@ fn vfsTest(boot_information: *const BootInformation) void {
|
||||
result();
|
||||
}
|
||||
|
||||
/// The full input path: spawn the input service, a synthetic keyboard source, and a
|
||||
/// subscriber from the initial_ramdisk. The source publishes key events; the service
|
||||
/// broadcasts them (with the asynchronous ipc_send); the subscriber receives them and —
|
||||
/// only once it has — heartbeats "input-test: ok". Seeing that marker proves an event
|
||||
/// travelled source -> service -> subscriber over IPC, exercising the async buffered-send
|
||||
/// primitive and capability-passing subscription. The source and service stay silent
|
||||
/// after startup so the subscriber's line is the one left in the shared evidence buffer.
|
||||
fn inputTest(boot_information: *const BootInformation) void {
|
||||
log("DANOS-TEST-BEGIN: input\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.write_count = 0;
|
||||
process.write_from_user = false;
|
||||
_ = spawnNamed(rd, "input"); // the fan-out service
|
||||
_ = spawnNamed(rd, "input-source"); // a synthetic keyboard publishing events
|
||||
_ = spawnNamed(rd, "input-test"); // the subscriber whose "ok" line is the marker
|
||||
|
||||
// Wait for the subscriber's success heartbeat (it beats once per received event).
|
||||
const prefix = "input-test: ok";
|
||||
scheduler.setPriority(1);
|
||||
const deadline = architecture.millis() + 12000;
|
||||
while (architecture.millis() < deadline) {
|
||||
if (process.write_len >= prefix.len and eql(process.write_buffer[0..prefix.len], prefix) and process.write_count >= 2) break;
|
||||
scheduler.yield();
|
||||
}
|
||||
scheduler.setPriority(4);
|
||||
|
||||
const ok = process.write_len >= prefix.len and eql(process.write_buffer[0..prefix.len], prefix);
|
||||
check("a subscriber received a broadcast key event over IPC (source -> service -> subscriber)", ok);
|
||||
check("events kept flowing (service + async send stay up)", process.write_count >= 2);
|
||||
check("client syscalls came from user mode (CPL 3)", process.write_from_user);
|
||||
result();
|
||||
}
|
||||
|
||||
/// Process arguments, end to end: spawn args-echo bare (its argv[0] is the
|
||||
/// initial-ramdisk name). Instance 1 sees argc == 1 and respawns itself through
|
||||
/// `system_spawn` with the extra arguments "alpha beta-42" — the syscall argument
|
||||
|
||||
@@ -17,7 +17,7 @@ const runtime = @import("runtime");
|
||||
/// microkernel keeps such choices in user space, not the kernel. Drivers are absent
|
||||
/// on purpose: the device manager owns those. (A future init reads this from a
|
||||
/// manifest under /system/services instead of a hardcoded list.)
|
||||
const boot_services = [_][]const u8{ "vfs", "device-manager" };
|
||||
const boot_services = [_][]const u8{ "vfs", "input", "device-manager" };
|
||||
|
||||
pub fn main() void {
|
||||
// Prove the heap end to end: allocate through the runtime allocator (which
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
//! system/services/input-source — a hardware-free synthetic keyboard source, used to
|
||||
//! exercise the input service end to end without a real PS/2 controller (the `input` test
|
||||
//! case, and any bring-up where there is no keyboard). It stands in for a driver: it
|
||||
//! connects to the input service and `publish`es a rolling stream of key events, which the
|
||||
//! service broadcasts to every subscriber.
|
||||
//!
|
||||
//! It stays silent after startup (no per-event logging) so it can share the boot serial
|
||||
//! transcript with a subscriber whose output is the test's success marker. The real
|
||||
//! keyboard driver publishes the same synthetic stream today; swapping in decoded
|
||||
//! scancodes is a follow-up (see docs/input.md).
|
||||
|
||||
const runtime = @import("runtime");
|
||||
const input = runtime.input;
|
||||
const system = runtime.system;
|
||||
|
||||
pub fn main() void {
|
||||
var source = input.connectSource() orelse {
|
||||
_ = system.write("input-source: input service unavailable\n");
|
||||
return;
|
||||
};
|
||||
_ = system.write("input-source: publishing synthetic key events\n");
|
||||
|
||||
var step: usize = 0;
|
||||
while (true) : (step +%= 1) {
|
||||
_ = source.publish(input.syntheticEvent(step));
|
||||
system.sleep(200);
|
||||
}
|
||||
}
|
||||
|
||||
pub const panic = runtime.panic;
|
||||
comptime {
|
||||
_ = &runtime.start._start;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
//! system/services/input-test — the input service's client and test oracle, the input
|
||||
//! counterpart of vfs-test. It `subscribe`s to the input service, then loops receiving the
|
||||
//! events a source broadcasts. Once it has received at least one event it heartbeats
|
||||
//! `"input-test: ok"` (repeatedly), which the in-kernel `input` test case watches for on
|
||||
//! the serial log: seeing it proves an event travelled source -> service -> subscriber
|
||||
//! over IPC and arrived intact.
|
||||
|
||||
const std = @import("std");
|
||||
const runtime = @import("runtime");
|
||||
const input = runtime.input;
|
||||
const system = runtime.system;
|
||||
|
||||
fn writeLine(comptime fmt: []const u8, arguments: anytype) void {
|
||||
var line: [128]u8 = undefined;
|
||||
_ = system.write(std.fmt.bufPrint(&line, fmt, arguments) catch return);
|
||||
}
|
||||
|
||||
pub fn main() void {
|
||||
var listener = input.subscribe() orelse {
|
||||
_ = system.write("input-test: could not subscribe\n");
|
||||
return;
|
||||
};
|
||||
_ = system.write("input-test: subscribed\n");
|
||||
|
||||
var received: usize = 0;
|
||||
while (true) {
|
||||
const event = listener.next() orelse continue;
|
||||
received += 1;
|
||||
// Report the round trip. The kernel test matches the "input-test: ok" prefix and
|
||||
// requires it to recur, so the source staying up keeps this beating.
|
||||
const kind: input.EventKind = @enumFromInt(event.kind);
|
||||
writeLine("input-test: ok received {d} last kind={s} code={d} char={d}\n", .{ received, @tagName(kind), event.keycode, event.character });
|
||||
}
|
||||
}
|
||||
|
||||
pub const panic = runtime.panic;
|
||||
comptime {
|
||||
_ = &runtime.start._start;
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
//! system/services/input — the user-space input service. Shipped in the initial_ramdisk,
|
||||
//! spawned as a ring-3 process, and published under the well-known `input` service id. It
|
||||
//! is the fan-out point between **sources** (keyboard drivers) and **subscribers** (any
|
||||
//! program that wants keyboard events): a source `publish`es a `KeyEvent`, and the service
|
||||
//! pushes it to every subscriber.
|
||||
//!
|
||||
//! 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.
|
||||
|
||||
const std = @import("std");
|
||||
const runtime = @import("runtime");
|
||||
const protocol = runtime.input_protocol;
|
||||
const ipc = runtime.ipc;
|
||||
const system = runtime.system;
|
||||
|
||||
/// 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,
|
||||
};
|
||||
|
||||
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]system.ProcessDescriptor = undefined;
|
||||
const total = system.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;
|
||||
}
|
||||
}
|
||||
if (!alive) sub.* = .{};
|
||||
}
|
||||
}
|
||||
|
||||
/// Register `endpoint` (owned by task `task_id`) to receive events. Returns false if the
|
||||
/// subscriber table is full.
|
||||
fn addSubscriber(endpoint: ipc.Handle, task_id: u32) bool {
|
||||
for (&subscribers) |*sub| {
|
||||
if (!sub.used) {
|
||||
sub.* = .{ .used = true, .endpoint = endpoint, .task_id = task_id };
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Push `event` to every registered subscriber. `ipc.send` never blocks, so a slow or
|
||||
/// dead subscriber cannot stall delivery to the others.
|
||||
fn broadcast(event: protocol.KeyEvent) void {
|
||||
const bytes = std.mem.asBytes(&event);
|
||||
for (&subscribers) |*sub| {
|
||||
if (sub.used) _ = ipc.send(sub.endpoint, bytes);
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle one request. `got` carries the sender badge (a task id) and, for subscribe, the
|
||||
/// subscriber's endpoint capability in `got.cap`. Writes a `Reply` into `out` and returns
|
||||
/// its length.
|
||||
fn handle(message: []const u8, got: ipc.Received, out: []u8) usize {
|
||||
const reply = struct {
|
||||
fn write(buffer: []u8, status: i32) usize {
|
||||
const header = protocol.Reply{ .status = status };
|
||||
@memcpy(buffer[0..protocol.reply_size], std.mem.asBytes(&header));
|
||||
return protocol.reply_size;
|
||||
}
|
||||
};
|
||||
|
||||
if (message.len < protocol.request_size) return reply.write(out, -1);
|
||||
const request = std.mem.bytesToValue(protocol.Request, message[0..protocol.request_size]);
|
||||
|
||||
switch (@as(protocol.Operation, @enumFromInt(request.operation))) {
|
||||
.subscribe => {
|
||||
const endpoint = got.cap orelse return reply.write(out, -1); // no endpoint passed
|
||||
pruneDeadSubscribers();
|
||||
if (!addSubscriber(endpoint, @intCast(got.badge))) return reply.write(out, -1); // table full
|
||||
return reply.write(out, 0);
|
||||
},
|
||||
.publish => {
|
||||
broadcast(request.event);
|
||||
return reply.write(out, 0);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn main() void {
|
||||
const endpoint = ipc.createIpcEndpoint() orelse {
|
||||
_ = system.write("input: no endpoint\n");
|
||||
return;
|
||||
};
|
||||
if (!ipc.register(.input, endpoint)) {
|
||||
_ = system.write("input: register failed\n");
|
||||
return;
|
||||
}
|
||||
_ = system.write("input: ready\n");
|
||||
|
||||
var reply_buffer: [protocol.reply_size]u8 = undefined;
|
||||
var reply_len: usize = 0;
|
||||
var receive: [protocol.request_size]u8 = undefined;
|
||||
while (true) {
|
||||
const got = ipc.replyWait(endpoint, reply_buffer[0..reply_len], &receive, null);
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
pub const panic = runtime.panic;
|
||||
comptime {
|
||||
_ = &runtime.start._start;
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
//! The input wire protocol — the message format spoken between the user-space input
|
||||
//! service ([input.zig](input.zig)) and the two kinds of process that reach it: a
|
||||
//! **source** (a keyboard driver) that `publish`es events, and a **subscriber** (any
|
||||
//! program) that `subscribe`s and is then pushed each event.
|
||||
//!
|
||||
//! Two message shapes ride over one endpoint, tagged by `Operation`, exactly like the
|
||||
//! [VFS protocol](../vfs/protocol.zig):
|
||||
//!
|
||||
//! - **subscribe / publish**: a synchronous `ipc_call` carrying a `Request`. `subscribe`
|
||||
//! hands the service the subscriber's own endpoint as a capability (`send_cap`);
|
||||
//! `publish` carries a `KeyEvent`. The reply is a `Reply`.
|
||||
//! - **delivery**: the service pushes each `KeyEvent` to every subscriber with the
|
||||
//! asynchronous `ipc_send` — no reply owed, and a dead subscriber can never stall the
|
||||
//! broadcast (the reason the async primitive exists). The wire form is a bare
|
||||
//! `KeyEvent`, received in the subscriber's buffer with `Received.isMessage()` set.
|
||||
//!
|
||||
//! This is a danos-native contract; shared by the input service, the `runtime.input`
|
||||
//! client helpers, and every source/subscriber. Everything fits one IPC message.
|
||||
|
||||
/// What happened to a key. `key_down`/`key_up` are the physical make/break; `key_press`
|
||||
/// is the higher-level "a character was produced" event a source emits alongside a
|
||||
/// `key_down` for keys that map to a character (carrying it in `KeyEvent.character`).
|
||||
pub const EventKind = enum(u32) {
|
||||
key_down = 0, // a key was pressed (make)
|
||||
key_up = 1, // a key was released (break)
|
||||
key_press = 2, // a character-producing press; `character` is the Unicode scalar
|
||||
};
|
||||
|
||||
/// One keyboard event, as broadcast to subscribers. Fixed layout (`extern`) because it
|
||||
/// crosses the IPC boundary by memory copy. A hardware-independent `keycode` names the
|
||||
/// physical key; `character` is the Unicode scalar for `key_press` (else 0); `modifiers`
|
||||
/// is a bitmask of the shift/ctrl/alt state (`modifier_*`), 0 until a source tracks it.
|
||||
pub const KeyEvent = extern struct {
|
||||
kind: u32, // an EventKind
|
||||
keycode: u32, // a Keycode — the physical key, layout-independent
|
||||
character: u32, // Unicode scalar for key_press, else 0
|
||||
modifiers: u32, // OR of modifier_* bits
|
||||
};
|
||||
|
||||
/// Modifier bits for `KeyEvent.modifiers`.
|
||||
pub const modifier_shift: u32 = 1 << 0;
|
||||
pub const modifier_control: u32 = 1 << 1;
|
||||
pub const modifier_alt: u32 = 1 << 2;
|
||||
|
||||
/// A minimal danos-native keycode namespace — enough for the synthetic source and to
|
||||
/// show the shape. A real set (USB HID usage-style) fills in with the scancode decoder.
|
||||
pub const Keycode = enum(u32) {
|
||||
unknown = 0,
|
||||
a = 4, // deliberately USB-HID-usage-aligned so a real decoder can extend this
|
||||
b = 5,
|
||||
c = 6,
|
||||
d = 7,
|
||||
e = 8,
|
||||
enter = 40,
|
||||
_,
|
||||
};
|
||||
|
||||
/// Which side of a request this is.
|
||||
pub const Operation = enum(u32) {
|
||||
subscribe = 0, // register the caller's endpoint (passed as send_cap) to receive events
|
||||
publish = 1, // a source submits `event` to broadcast to every subscriber
|
||||
};
|
||||
|
||||
/// Request header. For `subscribe`, `event` is ignored and the caller's receive endpoint
|
||||
/// travels as the call's capability. For `publish`, `event` is the event to broadcast.
|
||||
pub const Request = extern struct {
|
||||
operation: u32, // an Operation
|
||||
_padding: u32 = 0,
|
||||
event: KeyEvent,
|
||||
};
|
||||
|
||||
/// 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(KeyEvent);
|
||||
|
||||
comptime {
|
||||
// The delivery path posts a bare KeyEvent through ipc_send, so it must fit an
|
||||
// endpoint's async payload slot (abi has no dependency the other way, so the bound
|
||||
// lives here where the wire form is defined: POST_MAXIMUM is 64).
|
||||
if (event_size > 64) @compileError("KeyEvent must fit the ipc_send payload (POST_MAXIMUM)");
|
||||
}
|
||||
Reference in New Issue
Block a user