claude/input-module-keyboard-events-379361 #6
20
build.zig
20
build.zig
|
|
@ -178,6 +178,13 @@ pub fn build(b: *std.Build) void {
|
|||
.root_source_file = b.path("system/services/vfs/protocol.zig"),
|
||||
});
|
||||
|
||||
// The input wire protocol: the input service's public interface, exposed as its own
|
||||
// module the same way vfs-protocol is. Shared by the input service, the runtime's
|
||||
// `input` helper (subscribe/publish), and every source and subscriber.
|
||||
const input_protocol_module = b.addModule("input-protocol", .{
|
||||
.root_source_file = b.path("system/services/input/protocol.zig"),
|
||||
});
|
||||
|
||||
// The danos-native user-space runtime: system_call wrappers, the C-convention
|
||||
// heap, IPC helpers, the process start shim, device access. This is the stable
|
||||
// application ABI; POSIX compatibility is a separate library on top (see below).
|
||||
|
|
@ -193,6 +200,7 @@ pub fn build(b: *std.Build) void {
|
|||
.{ .name = "abi", .module = abi_module },
|
||||
.{ .name = "device-abi", .module = device_abi_module },
|
||||
.{ .name = "vfs-protocol", .module = vfs_protocol_module },
|
||||
.{ .name = "input-protocol", .module = input_protocol_module },
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -302,6 +310,11 @@ pub fn build(b: *std.Build) void {
|
|||
const ps2_keyboard_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, "ps2-keyboard", "system/drivers/ps2-bus/keyboard.zig");
|
||||
const ps2_mouse_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, "ps2-mouse", "system/drivers/ps2-bus/mouse.zig");
|
||||
const device_manager_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, "device-manager", "system/services/device-manager/device-manager.zig");
|
||||
// The input service and its exercisers: the fan-out server, a hardware-free synthetic
|
||||
// source, and a subscriber that doubles as the `input` test's oracle. See docs/input.md.
|
||||
const input_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, "input", "system/services/input/input.zig");
|
||||
const input_source_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, "input-source", "system/services/input-source/input-source.zig");
|
||||
const input_test_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, "input-test", "system/services/input-test/input-test.zig");
|
||||
const args_echo_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, "args-echo", "system/services/args-echo/args-echo.zig");
|
||||
const process_test_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, "process-test", "system/services/process-test/process-test.zig");
|
||||
|
||||
|
|
@ -327,6 +340,12 @@ pub fn build(b: *std.Build) void {
|
|||
mk_run.addFileArg(ps2_mouse_exe.getEmittedBin());
|
||||
mk_run.addArg("device-manager");
|
||||
mk_run.addFileArg(device_manager_exe.getEmittedBin());
|
||||
mk_run.addArg("input");
|
||||
mk_run.addFileArg(input_exe.getEmittedBin());
|
||||
mk_run.addArg("input-source");
|
||||
mk_run.addFileArg(input_source_exe.getEmittedBin());
|
||||
mk_run.addArg("input-test");
|
||||
mk_run.addFileArg(input_test_exe.getEmittedBin());
|
||||
mk_run.addArg("args-echo");
|
||||
mk_run.addFileArg(args_echo_exe.getEmittedBin());
|
||||
mk_run.addArg("process-test");
|
||||
|
|
@ -337,6 +356,7 @@ pub fn build(b: *std.Build) void {
|
|||
for ([_]struct { *std.Build.Step.Compile, []const u8 }{
|
||||
.{ vfs_exe, "system/services" },
|
||||
.{ device_manager_exe, "system/services" },
|
||||
.{ input_exe, "system/services" },
|
||||
.{ hpet_exe, "system/drivers" },
|
||||
.{ bus_exe, "system/drivers" },
|
||||
.{ ps2_bus_exe, "system/drivers" },
|
||||
|
|
|
|||
|
|
@ -52,7 +52,10 @@ rather than restate it. Roughly in the order things happen at runtime:
|
|||
microkernel's `ps`/`kill`/SIGCHLD: enumerate as a table snapshot, the
|
||||
supervision link as the kill authority, and child-exit notifications over the
|
||||
same endpoints IRQs arrive on.
|
||||
16. **[halting.md](halting.md) — halting.** Why a kernel can't just "exit", and
|
||||
16. **[input.md](input.md) — the input module.** Broadcasting keyboard events: why a
|
||||
synchronous rendezvous can't fan out to many listeners, the asynchronous `ipc_send`
|
||||
primitive built to fix it, and the subscribe/publish service layered on top.
|
||||
17. **[halting.md](halting.md) — halting.** Why a kernel can't just "exit", and
|
||||
how `while (true) hlt` parks the CPU safely once there's nothing left to do.
|
||||
|
||||
Start with the north star:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,115 @@
|
|||
# The input module: broadcasting keyboard events
|
||||
|
||||
A keyboard driver has one keystroke and *many* programs that might want it — a shell, a
|
||||
window server, a logger. None of them owns the hardware, and the driver should not know
|
||||
who is listening. So between the driver and the listeners sits the **input service**
|
||||
(`system/services/input/`): drivers **publish** events to it, programs **subscribe**, and
|
||||
it fans each event out to every subscriber. It is an ordinary ring-3 process reached over
|
||||
IPC, like the [VFS server](../system/services/vfs/vfs.zig) — no kernel knows what a key is.
|
||||
|
||||
Three event kinds cross the wire ([protocol.zig](../system/services/input/protocol.zig)):
|
||||
`key_down` and `key_up` are the physical make/break; `key_press` is the higher-level
|
||||
"a character was produced", carrying the Unicode scalar. A `KeyEvent` also has a
|
||||
layout-independent `keycode` and a `modifiers` bitmask.
|
||||
|
||||
## Why this needed a new kernel primitive
|
||||
|
||||
The interesting part is delivery, and it runs straight into the shape of danos IPC.
|
||||
[ipc.md](ipc.md) describes a **synchronous rendezvous**: a server holds exactly one
|
||||
pending reply (`Task.ipc_client`) and *must* answer it on its next `replyWait`. Two
|
||||
consequences decide the whole design:
|
||||
|
||||
1. **You cannot block N subscribers waiting for "the next event".** A server can hold only
|
||||
one caller at a time, so the natural "subscriber calls `next_event()` and blocks" API
|
||||
is impossible for more than one subscriber. Delivery therefore has to be **push** — the
|
||||
service reaching out to subscribers — not pull.
|
||||
|
||||
2. **A synchronous push can hang the whole service.** If the service delivered with
|
||||
`ipc_call`, it would block until each subscriber replied. `ipc_call` has no timeout, and
|
||||
the kernel does **not** wake a caller parked on a *dead* peer's endpoint (it only fails a
|
||||
peer that was mid-reply — see [process.zig](../system/kernel/process.zig)
|
||||
`releaseTaskResourcesLocked`). One subscriber that exits mid-delivery would wedge input
|
||||
for everyone. That is the opposite of the resilience the microkernel is for.
|
||||
|
||||
The fix is the asynchronous send that [ipc.md](ipc.md) had already earmarked as future
|
||||
work ("asynchronous / buffered send … for notifications between servers"):
|
||||
|
||||
```
|
||||
ipc_send(handle, message_ptr, message_len) -> 0 / -errno
|
||||
```
|
||||
|
||||
`ipc_send` copies a small payload into the endpoint's **bounded queue** and wakes a
|
||||
receiver, then returns immediately — it never blocks and so can never hang on a dead or
|
||||
slow subscriber. The receiver picks it up through the same `replyWait` it already runs:
|
||||
the wake arrives as a **buffered message** — `notify_badge_bit | notify_message_bit` set in
|
||||
the badge (distinguishing it from a bare IRQ/child-exit notification), the sender's task id
|
||||
in the low bits, and the payload in the receive buffer, with no reply owed. The queue holds
|
||||
16 messages per endpoint; a full queue **drops the oldest**, because a buffered message is
|
||||
discrete data, not a coalescing "level" like an interrupt. See
|
||||
[ipc-synchronous.zig](../system/kernel/ipc-synchronous.zig) (`sendLocked`, `popPost`, and
|
||||
the `replyWait` receive loop).
|
||||
|
||||
This is the async counterpart of `ipc_call`, and the input service is its first consumer.
|
||||
|
||||
## How the pieces fit
|
||||
|
||||
```
|
||||
keyboard driver / input-source input service subscriber(s)
|
||||
-------------------------------- ------------- -------------
|
||||
connectSource(); loop: replyWait: subscribe():
|
||||
publish(event) ── ipc_call ──▶ publish → broadcast: createIpcEndpoint()
|
||||
for each sub: callCap(subscribe,
|
||||
ipc_send(sub_ep) ──────────▶ send_cap = ep)
|
||||
reply ok loop: next()
|
||||
subscribe → store sub_ep cap └─ replyWait(ep)
|
||||
(from the call's capability) → KeyEvent
|
||||
```
|
||||
|
||||
- A **subscriber** calls `input.subscribe()`
|
||||
([library/runtime/input.zig](../library/runtime/input.zig)): it creates its own endpoint
|
||||
and hands it to the service as a **capability** (M13 capability passing — the input
|
||||
service is that feature's first real user). Then it loops on `Subscriber.next()`, which
|
||||
is a `replyWait` on that endpoint returning each pushed `KeyEvent`.
|
||||
- A **source** (a keyboard driver) calls `input.connectSource()` and
|
||||
`Publisher.publish(event)`. Publishing is a short synchronous `ipc_call` the service
|
||||
answers at once; the service's own fan-out is asynchronous, so publishing never blocks on
|
||||
a slow subscriber.
|
||||
- The **service** ([input.zig](../system/services/input/input.zig)) keeps a small
|
||||
subscriber table (endpoint handle + owning task id). On `publish` it `ipc_send`s the event
|
||||
to every subscriber. On `subscribe` it stores the passed capability and, as housekeeping,
|
||||
prunes any slot whose owning process has exited (checked against `process_enumerate`) —
|
||||
not for correctness (an async send to an orphaned endpoint is harmless) but to reclaim
|
||||
the slot.
|
||||
|
||||
Publisher and subscriber must be **separate processes**: a single thread that both
|
||||
published and serviced its own subscription would deadlock (its `publish` call blocks until
|
||||
the service delivers to its endpoint, which only the same thread could receive).
|
||||
|
||||
## Status and follow-ups
|
||||
|
||||
- **Synthetic source, for now.** The `ps2-bus` driver owns PNP0303, which carries *both*
|
||||
the 0x60/0x64 ports and IRQ1, so reading real scancodes has to live in the bus, not in
|
||||
[keyboard.zig](../system/drivers/ps2-bus/keyboard.zig). Until that lands, the keyboard
|
||||
driver (and the hardware-free `input-source` used by the test) publish a synthetic rolling
|
||||
`A..E` stream via `input.syntheticEvent`. The fan-out path is real; only the bytes are
|
||||
placeholder. **Follow-up:** the bus binds IRQ1, reads port 0x60, and `ps2-library`
|
||||
translates scan-set-1 → keycodes; the keyboard driver publishes decoded events.
|
||||
- **Drop-oldest under overflow** is a defined loss; the 16-slot ring absorbs normal bursts.
|
||||
Real backpressure/flow-control is future work.
|
||||
- **`publish` is unauthenticated** — any process may publish, consistent with the current
|
||||
bring-up trust model (see [driver-model.md](driver-model.md)). A source capability is
|
||||
future work.
|
||||
|
||||
## Verifying it
|
||||
|
||||
The `input` case (`python3 test/qemu_test.py input`, in
|
||||
[tests.zig](../system/kernel/tests.zig) `inputTest`) boots the real kernel and spawns the
|
||||
service, the synthetic source, and a subscriber. It passes only when the subscriber
|
||||
heartbeats `input-test: ok` — proof that an event travelled source → service → subscriber
|
||||
over IPC, exercising both `ipc_send` and capability-passing subscription.
|
||||
|
||||
## See also
|
||||
|
||||
- [ipc.md](ipc.md) — the synchronous rendezvous and the notification path `ipc_send` extends.
|
||||
- [syscall.md](syscall.md) — the system-call surface, including `ipc_send`.
|
||||
- [driver-model.md](driver-model.md) — class drivers, capability passing (M13), the trust model.
|
||||
|
|
@ -95,6 +95,11 @@ This is what makes a user-space driver possible at all, and it's the subject of
|
|||
every capability is either well-known (the registry) or inherited — there's no way
|
||||
to delegate one.
|
||||
- **Asynchronous / buffered send** for the cases where a rendezvous is the wrong
|
||||
shape (logging, notifications between servers).
|
||||
shape (logging, notifications between servers). *Landed as `ipc_send`* — a
|
||||
non-blocking post to an endpoint's bounded payload queue, delivered through
|
||||
`reply_wait` as a buffered message (badge bit `notify_message_bit`). Built for, and
|
||||
first used by, the [input service](input.md)'s keyboard-event broadcast, where a
|
||||
synchronous push would let one dead subscriber hang the fan-out. A full queue drops
|
||||
the oldest (discrete messages, not a coalescing level like the notification ring).
|
||||
- **A bounded reply.** `MSG_MAX` is 256 bytes and the copy runs under the big kernel
|
||||
lock; a bulk transfer wants shared pages, not a copy.
|
||||
|
|
|
|||
|
|
@ -47,6 +47,8 @@ Everything else---including`read()`,`write()`,`malloc()`, and`fork()`---will run
|
|||
- **What it does:**Used strictly by your background user-space servers (like your disk driver or filesystem). It sends a reply to the last client that called it, and immediately puts the server to sleep until the next request arrives.[[1](https://news.ycombinator.com/item?id=33078441)]
|
||||
3. **`Yield()`/`Thread_Ctrl()`**
|
||||
- **What it does:**Allows a thread to voluntarily give up its CPU time slice, or allows a root task to spawn/kill threads.
|
||||
4. **`ipc_send(endpoint, message_buffer)`(Asynchronous Send)**
|
||||
- **What it does:**Posts a small payload to an endpoint's bounded queue and returns *without* blocking — no rendezvous, no reply. The receiver picks it up through the same `IPC_ReplyWait`, as a buffered message. It is the async counterpart of `IPC_Call`, for one-to-many broadcasts where a synchronous rendezvous would let one dead or slow receiver hang the sender. The [input service](input.md) — keyboard-event fan-out — is its first user. A full queue drops the oldest message (a buffered message is discrete data, unlike a coalescing interrupt notification).
|
||||
|
||||
* * * * *
|
||||
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
};
|
||||
}
|
||||
|
|
@ -79,6 +79,16 @@ pub fn call(h: Handle, message: []const u8, reply: []u8) CallError!usize {
|
|||
return (try callCap(h, message, reply, null)).len;
|
||||
}
|
||||
|
||||
/// Post `message` to endpoint `h`'s asynchronous queue and return immediately — no
|
||||
/// rendezvous, no reply, no blocking. The receiver picks it up through `replyWait` as a
|
||||
/// buffered message (`Received.isMessage`). Unlike `call`, this **cannot hang on a dead
|
||||
/// or slow peer**, which is why a broadcaster (the input service) delivers events this
|
||||
/// way. The payload must fit an endpoint slot (64 bytes); a full queue drops the oldest
|
||||
/// message. Returns false on failure (bad handle, oversized payload, bad buffer).
|
||||
pub fn send(h: Handle, message: []const u8) bool {
|
||||
return !failed(sc.systemCall3(.ipc_send, h, @intFromPtr(message.ptr), message.len));
|
||||
}
|
||||
|
||||
/// Set in `Received.badge` when what arrived is an asynchronous notification — a
|
||||
/// bound device interrupt — rather than a client's message. The low bits carry the
|
||||
/// GSI. See `isNotification`.
|
||||
|
|
@ -89,6 +99,12 @@ pub const notify_badge_bit: u64 = abi.notify_badge_bit;
|
|||
/// rather than a device interrupt. The low bits carry the child's process id.
|
||||
pub const notify_exit_bit: u64 = abi.notify_exit_bit;
|
||||
|
||||
/// Set alongside `notify_badge_bit` when the wake-up is a **buffered message** — a payload
|
||||
/// posted with `send` (`ipc_send`) — rather than a bare device interrupt or child-exit
|
||||
/// notice. The payload is in the `replyWait` receive buffer (`Received.len` bytes); the
|
||||
/// low bits of the badge carry the sender's task id. See `Received.isMessage`.
|
||||
pub const notify_message_bit: u64 = abi.notify_message_bit;
|
||||
|
||||
/// The result of a `replyWait`: the request length, the sender's badge (a task id, or
|
||||
/// an IRQ notification if the high bit is set), and any capability the request carried.
|
||||
pub const Received = struct {
|
||||
|
|
@ -109,6 +125,19 @@ pub const Received = struct {
|
|||
return self.isNotification() and self.badge & notify_exit_bit != 0;
|
||||
}
|
||||
|
||||
/// True if this wake-up is a **buffered message** posted with `send` (`ipc_send`):
|
||||
/// there is a payload in the receive buffer (`self.len` bytes) and no reply is owed.
|
||||
/// The subscriber side of a broadcast branches on this.
|
||||
pub fn isMessage(self: Received) bool {
|
||||
return self.isNotification() and self.badge & notify_message_bit != 0;
|
||||
}
|
||||
|
||||
/// The task id of whoever posted a buffered message, meaningful only when
|
||||
/// `isMessage`. (The badge's low bits, with the three high marker bits masked off.)
|
||||
pub fn senderTaskId(self: Received) u32 {
|
||||
return @intCast(self.badge & ~(notify_badge_bit | notify_exit_bit | notify_message_bit));
|
||||
}
|
||||
|
||||
/// The interrupt source (a GSI), meaningful only when `isNotification` and
|
||||
/// not `isChildExit`.
|
||||
pub fn source(self: Received) u64 {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,11 @@ pub const ipc = @import("ipc.zig");
|
|||
pub const start = @import("start.zig");
|
||||
/// The VFS wire protocol (shared with the VFS server).
|
||||
pub const vfs_protocol = @import("vfs-protocol");
|
||||
/// Keyboard-event listening (subscribe/next) and broadcasting (publish), over the input
|
||||
/// service. See library/runtime/input.zig and system/services/input/.
|
||||
pub const input = @import("input.zig");
|
||||
/// The input wire protocol (shared with the input service and its clients).
|
||||
pub const input_protocol = @import("input-protocol");
|
||||
/// POSIX-style file API: open/read/write/lseek/stat/close.
|
||||
/// C stdio: fopen/fread/fwrite/fseek/ftell/fclose over unistd.
|
||||
/// Device access for drivers: enumerate/claim/mmioMap.
|
||||
|
|
|
|||
|
|
@ -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)");
|
||||
}
|
||||
|
|
@ -250,6 +250,12 @@ CASES = [
|
|||
{"name": "vfs",
|
||||
"expect": r"DANOS-TEST-RESULT: PASS",
|
||||
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
||||
# The input service: a synthetic keyboard source publishes events, the service
|
||||
# broadcasts them over the async ipc_send primitive, and a subscriber (which joined by
|
||||
# passing its endpoint as a capability) receives them — source -> service -> subscriber.
|
||||
{"name": "input",
|
||||
"expect": r"DANOS-TEST-RESULT: PASS",
|
||||
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
||||
# IO passthrough + IRQ-as-IPC: a user-space HPET driver maps device MMIO into
|
||||
# its own address space, binds the device's interrupt to an IPC endpoint, and
|
||||
# is woken by the hardware five times while blocked (never polling).
|
||||
|
|
|
|||
Loading…
Reference in New Issue