summaryrefslogtreecommitdiff
path: root/src/detached/server.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-08-27 16:42:15 -0300
committerGabriel Schneider <[email protected]>2026-08-27 22:07:32 -0300
commit147ebd4a36ec7199074ba05bcfb79d4a656c0b74 (patch)
tree400441fc7b103152cd741aec7ee42b82b943aed8 /src/detached/server.zig
parentdef843b2f59b867ee9b1d501f559f59fb335d4cc (diff)
downloadpardes-147ebd4a36ec7199074ba05bcfb79d4a656c0b74.tar.gz
pardes-147ebd4a36ec7199074ba05bcfb79d4a656c0b74.zip
9p: the client half, and a board that serves its own tree over the UART
Step 5 of the 9P chain (docs/9p.typ 12.5, docs/registry.typ 9P-22, 9P-11, BOARD-1). THE CLIENT. `Client` in src/9p.zig is the mirror of `Server` and the same shape: sans-io, no allocator, no threads, no descriptor, caller-owned buffers, and it builds freestanding. 152 bytes of struct against the server's 9,488, because a client owns neither a fid table nor a park table -- the far end does. The API is submit / push+output+wrote / take. Completion is a PULL: a callback would fire inside push, inside the transport's read, inside the host's poll dispatch, which is exactly where fs9_service says filesystem work must not happen. `take()` returns the next completed operation or null, which is `Server.next()`'s loop-until-null contract read from the other side. Tags are a fixed 16-entry table indexed BY the tag, so an out-of-order reply -- which 9P allows and both reference clients rely on -- costs one bounds check. The reply's TYPE is checked against the request's op, because a tag is only as good as the table behind it. A `Done` borrows the input buffer and is valid until the next call; `take()` releases the previous frame on entry, so the rule is mechanical rather than remembered, and read data and error strings are zero-copy. And one real caller, so this is not a library with no user: the `9p` word takes a dial and a path, walks another instance's tree, and opens the bytes in a pane like any other `Look`. THE BOARD. A SECOND image, not a second role: the console runtime keeps UART0 bidirectionally and is behaviourally untouched. On the new one the UART carries 9P AND NOTHING ELSE -- no ANSI, no vaxis, no allocator, no heap module. The loop is uart.read -> push / retry+next -> handle -> reply / output -> writeSome -> wrote. `writeSome` is new and additive: `write`'s bounded spin DROPS bytes on a stalled transmitter, which on a protocol stream truncates a reply mid-message and desynchronises for good, where a short count cannot. BOARD-1's one divider write raises the line to 921600. 88,000 B text, 49,424 B bss, an 88,080-byte image -- 5.7% of the 1,536,000 B partition, against the console image's 809,536 B. THE COMPTIME BRIDGE, which is the part worth reading. `board9p.caps` is the ONLY place the GPIO tree is described; node ids, parents, names, permissions, handlers, buffer size and the per-pin directories are all derived from it, and `fan.dirs` makes `gpio/<n>/value` one table entry serving eleven pins. Modes are derived from which handlers a file has rather than declared. A second capability is a table entry, not new tree code. JP1 became a real table in the new leaf `src/board_pins.zig`, with the ASCII drawing RENDERED from it at comptime and the pin list COLLECTED from it -- the 9P image links no core and so cannot import board_memory.zig, and copying the table was not acceptable. A golden test pins the drawing byte for byte, the console's own shape test still passes, and the identical bytes are present in all three artifacts. PROVED. Two daemons: B read A's `/1/body` through the `9p` word into a pane, byte-identical to plan9port's `9p read` of the same path. Both board images build. No hardware was attached, so nothing about the board is claimed beyond what builds and what the host tests cover. zig build unit-test 585/585. fs-bench unchanged and still zero allocations on every read row. --- REVIEW FIXES FOLDED IN. Steps 3, 4 and 5 were verified on the happy path and then adversarially reviewed by three agents; eight defects, six fixed here, five of them reproduced with measurements before and after. Full writeup in docs/registry.typ `9P-27`. In brief: * a remote crash of the WHOLE daemon: one `size[4]` of zero plus one byte hit `unreachable` in `fs9_service.fill`. Also 99.7% of a core when the stuck buffer made `room == 0` return without reading. Now `srv.dead` is a hangup, checked before the room guard. * the editor froze 177 s on a dial: `connect(2)` ran on a still-BLOCKING socket before the deadline existed, and a full accept backlog waits forever. Now non-blocking with the wait spent against the budget. After: 2.03 s. * a 64 KiB pty read is exactly `queue_cap` and wiped every unread byte AND dropped itself. `notePtyOutput` splits at half the cap. Deterministic. * four silent sockets denied `--fs9` forever; connections now expire on the same five-second rule the frontend transport already had. * EMFILE spun a core; the listener pauses and leaves the poll set, as the frontend listener does. * `max_fids = 32` made `find` over `9pfuse` fail with 57 consecutive `Rerror`s -- refuting this step's own acceptance clause. 256 for a host, `board_fids` 32 for the microcontroller. Found clean and worth recording: `sig` reaches the foreground process group; the two-namespace pty lookup is right over both transports; `PaneFile`'s u4 wall is guarded; reader counts release on every abrupt-death path; `fs_origin` routing and the reply arithmetic hold under probing.
Diffstat (limited to 'src/detached/server.zig')
-rw-r--r--src/detached/server.zig17
1 files changed, 16 insertions, 1 deletions
diff --git a/src/detached/server.zig b/src/detached/server.zig
index 2d7a5238..7d286177 100644
--- a/src/detached/server.zig
+++ b/src/detached/server.zig
@@ -1056,6 +1056,9 @@ pub const Session = struct {
// `Twrite` to `ctl` can `push_spawn`, and `dispatch` is mid-iteration
// over a descriptor snapshot when it runs.
if (s.ninep) |l| {
+ // Before the drain, so a slot held by silence is taken back on the
+ // same frame it expires rather than one drain later.
+ l.expire();
var pending = false;
for (0..fs9_service.max_conns) |i| {
const t = l.transport(@intCast(i)) orelse continue;
@@ -1431,7 +1434,11 @@ pub const Session = struct {
// a pty's — asking for it unconditionally makes every idle socket a
// ready descriptor and turns the poll into a spin.
if (s.ninep) |l| {
- if (l.fd >= 0) {
+ // `accepting`, not `fd >= 0`: a listener paused after an EMFILE
+ // must leave the set, or the backlog it could not drain reports
+ // ready on every poll and spins the core. `nextDue` carries the
+ // moment it comes back.
+ if (l.accepting()) {
fds[n] = .{ .fd = l.fd, .events = poll_in, .revents = 0 };
src[n] = .ninep_listener;
n += 1;
@@ -1600,6 +1607,14 @@ pub const Session = struct {
}
if (s.listener >= 0 and s.accept_paused_ms > now)
due = if (due) |d| @min(d, s.accept_paused_ms) else s.accept_paused_ms;
+ // ...and the 9P listener's own greet deadline, for the reason its
+ // `expire` gives: four slots is a cheaper denial than thirty-two.
+ if (s.ninep) |l| {
+ if (l.nextDue()) |ms| {
+ const at9 = now + ms;
+ due = if (due) |d| @min(d, at9) else at9;
+ }
+ }
const at = due orelse return null;
return @intCast(@max(0, @min(at - now, std.math.maxInt(c_int))));
}