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:
Daniel Samson
2026-07-11 15:03:24 +01:00
parent 2a583d55a8
commit 65244e3103
19 changed files with 765 additions and 5 deletions
+115
View File
@@ -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.