diff options
| author | Gabriel Schneider <[email protected]> | 2026-08-27 15:32:02 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-08-27 16:42:08 -0300 |
| commit | def843b2f59b867ee9b1d501f559f59fb335d4cc (patch) | |
| tree | d7c1650c045653ebc93a77d7a90985e5e31725c2 /src/main.zig | |
| parent | f5927a033f0c83753b5cc004e514568eec8c24f8 (diff) | |
| download | pardes-def843b2f59b867ee9b1d501f559f59fb335d4cc.tar.gz pardes-def843b2f59b867ee9b1d501f559f59fb335d4cc.zip | |
9p: serve the acme tree over 9P2000 on a unix socket, beside the mount
Step 4 of the 9P chain (docs/9p.typ 12.4, docs/registry.typ 9P-15/16/17/4/5).
src/9p.zig is a base 9P2000 codec and a SANS-IO server: it never touches a
descriptor, takes no allocator, starts no thread, and builds for
wasm32-freestanding and riscv32-freestanding. That is what lets the same code
serve a unix socket here and a UART on the board later.
Server(comptime fs: type) duck-typed on fs.Req/fs.Reply/fs.Reply.Attr, so it
never imports acmefs and acmefs never learns 9P
init{ in, out, root } the caller owns the buffers; msize is derived
retry/next/reply the three fs_service.Transport ops, by name
push/output/wrote/hangup bytes in, bytes out, partial writes supported
next() is a PUMP, not one-message-one-request: a 3-element Twalk is three
lookups, Topen|OTRUNC is a setattr then an open, Tversion is none at all.
Decisions that were open and are now taken, each recorded in the file:
* qid.version is ALWAYS 0, which makes Linux set P9L_DIRECT and skip its
cache -- the 9P equivalent of the FOPEN_DIRECT_IO fuse.zig relies on.
* Every Rread is clamped to the client's count. An over-long one is a hard
-EIO in Linux, not a truncation.
* Rerror carries Linux's exact strerror text (registry 9P-4 option A), so a
mount recovers the errno instead of ESERVERFAULT. Asserted as literals,
because a typo there is 'Unknown error 526' on every mount.
* `.` and `..` are resolved BY THE SERVER. Under FUSE the kernel does it
and acmefs says so; 9P has no kernel, and forwarding `..` as a lookup
would break every client that normalises a path.
* Topen checks the perm bits itself. Under FUSE the kernel enforced them;
over 9P nobody is above the server, and `errors` would have been readable.
* Tcreate and Tremove are Rerror: `new/` creates a pane on WALK, so the
capability exists and is not spelled Tcreate.
THE INTEGRATION BUG, which was not in the protocol: the daemon's push_fs_reply
sent every reply to the FUSE mount, whose park table has no 9P tag, so it
dropped it -- Tversion worked (no core involved) and Tattach hung forever. That
is exactly the 'no routing origin for the 9P descriptor' cell in the layering
table of docs/9p.typ. Session.fs_origin now carries the transport that asked.
Proved with plan9port against a live daemon serving BOTH transports at once:
9p ls / and /1, read index/ctl/tag, write /1/body, stat, a walk through
/1/../index, pane creation through `new/body`, and the two refusals arriving as
strings -- 'permission denied' and 'No such file or directory' -- confirmed on
the raw wire as Rerror text rather than numbers. A write over 9P reads back
through FUSE and a write through FUSE reads back over 9P.
msize 8192, 34,072 bytes per connection (Server 9,488 + in 8,192 + out 16,384,
out being two msize so that every reply is infallible), four connections.
zig build unit-test: 468 tests before, 503 after.
Diffstat (limited to 'src/main.zig')
| -rw-r--r-- | src/main.zig | 33 |
1 files changed, 33 insertions, 0 deletions
diff --git a/src/main.zig b/src/main.zig index 1104aeb8..66d25816 100644 --- a/src/main.zig +++ b/src/main.zig @@ -85,6 +85,15 @@ const help_text = \\ PARDES_FS and PARDES_PANE into every pane shell \\ --fs=<dir> ...at <dir> instead. Must be absolute; pardes \\ unmounts it on exit but leaves the directory + \\ --fs9 serve that same tree over 9P2000 on a unix socket + \\ at $XDG_RUNTIME_DIR/pardes-9p-<name>.sock, where + \\ <name> is the session name or this pid. Dial it + \\ with `9p -a <socket>` or mount with `9pfuse`. + \\ Independent of --fs: either, both or neither + \\ --fs9=<name> ...named <name> rather than derived. One path + \\ component: no '/' and nothing empty. Served by + \\ --detach sessions today; the tty and GUI shells + \\ still take --fs only \\ --detach run this session with NO terminal of its own, \\ serving frontends over a unix socket beside the \\ nested-instance one. The core, the panes and the @@ -166,6 +175,8 @@ fn nativeMain(init: std.process.Init) !void { if (!std.mem.eql(u8, a, "--nested") and !std.mem.eql(u8, a, "--fs") and !std.mem.startsWith(u8, a, "--fs=") and + !std.mem.eql(u8, a, "--fs9") and + !std.mem.startsWith(u8, a, "--fs9=") and !std.mem.eql(u8, a, "--detach") and !std.mem.startsWith(u8, a, "--detach=") and !std.mem.eql(u8, a, "--attach") and @@ -211,6 +222,14 @@ fn nativeMain(init: std.process.Init) !void { // mount at ./README and open no file — the flag would silently eat // the FILE argument. One spelling, and it carries its own value. opts.fs = a["--fs=".len..]; + } else if (std.mem.eql(u8, a, "--fs9")) { + opts.fs9 = ""; + } else if (std.mem.startsWith(u8, a, "--fs9=")) { + // One spelling that carries its own value, for the reason `--fs` + // gives directly above: the flag is useful bare, so a two-word + // form would make `pardes --fs9 README` a socket called README + // that opens no file. + opts.fs9 = a["--fs9=".len..]; } else if (std.mem.eql(u8, a, "--nested")) { opts.nested = true; } else if (std.mem.eql(u8, a, "--detach")) { @@ -321,6 +340,12 @@ fn nativeMain(init: std.process.Init) !void { // that most wants a control filesystem, because it is the one whose panes // outlive every terminal that could otherwise have scripted them. if (opts.fs != null and attach != null) return error.BadArgs; + // ...and `--fs9` for exactly that reason and no other: it is the same tree + // over a different transport, and an `--attach` has no core to serve it + // from either. Checked separately rather than folded into the line above + // so that neither flag's refusal is a side effect of the other's — they + // are independent everywhere else. + if (opts.fs9 != null and attach != null) return error.BadArgs; // `--detach` replaces the frontend rather than choosing among them: the // core runs here, with no terminal, and the frontends are elsewhere on a // socket (src/detached/). It is checked before `platform` because it is not @@ -408,6 +433,14 @@ test { _ = @import("detached/wire.zig"); _ = @import("detached/server.zig"); _ = @import("detached/client.zig"); + // The 9P listener, and it needs its name here for the reason the + // panel_compositor line below states rather than the fuse.zig one above: + // detached/server.zig imports it, but naming a file does not make Zig + // analyse the tests of what IT imports — measured, by three tests that + // compiled and never ran. A standalone b.addTest is not an option either, + // because this file reaches pardes.zig (`Server(acmefs)`); src/9p.zig, the + // half that does NOT, has one in build.zig. + _ = @import("fs9_service.zig"); if (comptime pardes.platform == .tty) { _ = @import("tty/tty.zig"); // tty.zig calls the compositor only from its runtime loop, so merely |
