From 31cb659ded4cf50af5903fc107f8c868ee3c7311 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Tue, 22 Sep 2026 11:15:46 -0300 Subject: Answer 9P on the connection's task, so a session can open its own tree The editor's loop was the only thing that could answer a 9P request, which made the editor's own syscalls through a mount of its own tree -- a Look at /mnt/9p/pardes//anything under a `9ns --mntgen` view, a Save into it -- requests only the blocked loop could serve. The name-based refusal that followed (ownMountSuffix) and the in-process routing of a mount of oneself (Client.sameSession) were patches over that, and both are gone, with the mailbox that shipped every request to the editor's thread. One rule replaces them, `pardes.turn`: the core is single-threaded, the editor's thread has the turn by default and gives it up in two kinds of gap -- while it waits for input and while a step of it is out in a host syscall -- and a cloud9 connection task takes it in those gaps to answer. `out` counts the steps that are out, from any thread: while one is, the core reads consistently but that step still holds pointers into it, so a request that would change a pane (a write, a truncation, an rmdir) is parked in the engine and retried when the turn is next given up with nothing out, and the editor's own wake waits for the count to reach zero. It is never a write of its own that a step waits on out there -- writes come from a shell performing a save between steps -- so a parked request is never the syscall's own, and making a pane or rendering a screen need not park: every yield sits before its step's mutation, so the layout and the surface are whole under it. A changing request that queued effects is answered once the editor has performed them (`echo Save > exec` returns with the file written, as acme's `put` does), and it settles the way a step does, because without that a /log reader waited for the user's next keystroke. Every host syscall on a user path has to give the turn up, not fs.zig's alone: the first end-to-end run hung in `inotify_add_watch` performing the new pane's watch effect. PDFs and images are read whole at open, so no draw goes out into the host. The core's allocator takes its fixed buffer through the lock-free interface, since a connection task allocates while the editor's thread is out in a syscall that allocates too. A Restore puts the replacement in first and releases every task waiting on the old core. cloud9 (pinned at eb1a104) parks an open, a truncating wstat, a clunk and a remove on `again`, not only reads and writes, and answers a parked job whose fid was clunked without asking the backend. Verified: test/selfmount.py runs the editor under `9ns --mntgen` and Looks at, reads and Saves its own tree through the mount; a unit test pins that a change parks while the editor is out mid-step and lands when it rests, while a read is answered in the window. 9P over the Unix socket against a tty session, same machine, Debug builds: a read of /index 278us -> 61us, a truncating body write 1184us -> 609us, exec Save 718us -> 583us; the gesture benchmark is unchanged (geometric mean 0.997 over 53 cells). Also from the reviews: a notice chip over an image or PDF pane was painted out by the picture drawn after the cells, so pictures give up the rows; in the GUI a tree-sitter context band painted over the chip, so body layers are emitted first; a message is one row of printable text, its 256-byte cut never leaves half a glyph, and one wider than its pane keeps its tail (the file name, the reason) rather than its head. Co-Authored-By: Claude Fable 5.1 --- docs/cloud9.md | 13 ++++++++----- docs/fs.md | 27 ++++++++++++++++++--------- docs/v9fs.md | 6 +++--- 3 files changed, 29 insertions(+), 17 deletions(-) (limited to 'docs') diff --git a/docs/cloud9.md b/docs/cloud9.md index d030db1d..6eb76238 100644 --- a/docs/cloud9.md +++ b/docs/cloud9.md @@ -16,11 +16,14 @@ Mounting, Unix namespace discovery and permissions, the editor event loop, connection limits, and exported tree policy remain here. The Unix and TCP listeners run on cloud9's `serve.Runner` (`std.Io`: an accept task per listener, a reader and a writer task per connection, four slots); its handler -queues every backend request for the editor's thread, which answers them all -in `Listener.tick` after each editor update, retries parked reads there, and -has the runner ship the replies. `src/9p_quic.zig` selects the existing -`pardes-9p` ALPN for cloud9's optional OpenSSL transport; QUIC still runs on -the poll loop in `src/9p_io.zig`, since cloud9's QUIC adapter is +answers every backend request on the connection's task, taking the editor's +turn with the core (`pardes.turn`) while the editor waits for input or is out +in a syscall. A request that would change a pane while the editor is mid-step +is parked with `Status.again` -- the engine parks reads, writes, opens, +truncations, clunks and removes -- and retried by `Runner.wakeAll` when the +editor next rests. `src/9p_quic.zig` selects the existing `pardes-9p` ALPN for +cloud9's optional OpenSSL transport; QUIC still runs on the poll loop in +`src/9p_io.zig` on the editor's thread, since cloud9's QUIC adapter is nonblocking-descriptor based rather than `std.Io` based. The standalone protocol/GPIO tests import the same module. The separate `05-zig-p4` build also supplies cloud9 for the GPIO firmware entry. diff --git a/docs/fs.md b/docs/fs.md index 6d2891e0..335ee4e6 100644 --- a/docs/fs.md +++ b/docs/fs.md @@ -106,15 +106,24 @@ at the cost of acme's one-step `new/body`. Nothing in the tree is created by list, stat, walk or read; only that one open. Every other name in `/pane` is a serial. -A session cannot open its own tree through a mount. Looking at -`/mnt/9p/pardes//anything` from inside that very editor used to hang it -outright: the realpath, the stat and the read all leave through the mount and -come back as 9P requests only that editor's loop can answer, while the loop is -blocked making them, and the filesystem then stops answering anybody. Such a -path is now refused by name before any syscall runs, because the syscall is the -thing that never returns. Address your own tree as `/n/self/...`, which the -editor serves from memory without leaving the process. Another session's mount -is perfectly fine to open. +A session can open its own tree through a mount: a Look at +`/mnt/9p/pardes//pane/2/body` from inside that very editor opens it, and +a Save of that pane writes back through the mount into pane 2. Requests on +the Unix and TCP listeners are answered on the 9P connection's own task, not +by the editor's loop, so the realpath, the stat and the read the editor makes +out through the mount come back while it waits for them. The one rule is +whose turn it is with the core (`pardes.turn`): the editor has it, and gives +it up while it waits for input and while a step of it is out in a syscall. A +step of a connection task's own -- a Look written to `look` -- goes out the +same way, and the editor waits for it to return before it takes a step of +its own. While any step is out, a request that would change a pane (a write, +a truncation, an rmdir) parks in the engine until none is; everything else, +opening `pane/new` and `screen` included, is answered at once, which is why a +Look at any path in the tree comes back. A write into the tree from the +editor itself only ever happens between steps (a Save), so nothing it waits +on out there is a request that has to park. QUIC is still served on the +editor's loop, so through QUIC the old hang remains. `/n/self/...` names the +same tree without leaving the process. `/look` and `/exec` are the editor's two clicks, one per line of a write: diff --git a/docs/v9fs.md b/docs/v9fs.md index cfbbd35b..a105fe06 100644 --- a/docs/v9fs.md +++ b/docs/v9fs.md @@ -72,9 +72,9 @@ remain available for subsequent explicit authentication. The launcher waits for forwards termination signals, and removes its empty temporary directory on exit. Namespace destruction releases the mount when its last process exits. -The core stays outside the mount namespace because its event loop serves 9P. -A blocking filesystem operation through its own mount could wait for a request -that the blocked event loop must service. Even pathname resolution may do this. +The core stays outside the mount namespace, which is the helper's private +one: a path under `$PARDES_MOUNT` names nothing in the editor's own namespace, +so a Look on one from that shell opens nothing. Each `Tty9p` currently creates its own mount and consumes a server connection; the session has four application connection slots across all transports. This -- cgit v1.3