From f5927a033f0c83753b5cc004e514568eec8c24f8 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Thu, 27 Aug 2026 15:32:02 -0300 Subject: detached: drop /dev/fuse from the poll set once the connection dies Review fixes to steps 1 and 2, found by an adversarial pass over the committed chain. One is a real bug and the rest are comments that were false. THE BUG. `waitInput` put the FUSE descriptor in the poll set whenever the mount existed, and the `.fuse` arm ignored every revent. Linux's `fuse_dev_poll` answers EPOLLERR once the connection is gone, and POSIX reports POLLERR whatever the events mask asked for -- so an external `fusermount3 -u`, a sysfs abort, or systemd taking /run/user/$UID away at final logout (exactly when a detached session is supposed to keep running) made poll(2) return instantly, forever. The daemon then spun the whole pump at 100% of a core for the rest of its life, and re-offered every parked slot to the core at that rate. Measured on the unfixed commit: 0 CPU ticks over 10 s idle, then 1000 ticks over the next 10 s after unmounting its own mount point. Measured after the fix: 0 ticks over 8 s in the same scenario, process alive and in state S. fuse.zig's own poll thread has carried the equivalent guard all along, which is why the desktop shells never showed this and the daemon did. THE COMMENTS, each checkable and each wrong: * `Source.fuse` said the drain is not done in `dispatch` to avoid re-entering the core. Both `pull_wait_input` and `push_poll_frame` are called from inside `pump`, so either re-enters. The real reason is that `dispatch` is mid-iteration over a SNAPSHOT of the descriptors, and one `push_spawn` replaces a pane's master under it. * `fuse.zig`'s new import said "no cycle". There is a cycle: fs_service imports fuse.zig back and still names `*Fs` in three signatures. * `Transport`'s rationale said tty.zig and gui.zig store one in a struct field. Neither does; all four call sites build it inline. * `deinit` justified its ordering against "as long as the harvest takes". `harvest` is waitpid(WNOHANG) and blocks for nothing. The real reason to go first is that aborting the connection wakes a parked reader while its own shell is still alive to run its exit path. * `harvest` claimed pane shells are the only children this process forks. Since step 1 it also forks `fusermount3`, three times over -- reaped by its own spawner, so the conclusion holds and the premise did not. * docs/detached.md, docs/lsp.md and docs/design.typ still said the daemon implements sixteen of twenty-one methods and mounts no /dev/fuse. --- docs/lsp.md | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) (limited to 'docs/lsp.md') diff --git a/docs/lsp.md b/docs/lsp.md index 1adb397b..b35c8eeb 100644 --- a/docs/lsp.md +++ b/docs/lsp.md @@ -39,8 +39,8 @@ shell and the ESP32-P4 object: there `lsp.supports` is empty, which makes response; a host that links a backend would add both. **In a DETACHED session every query answers EMPTY.** -`src/detached/server.zig`'s vtable implements sixteen of `host.zig`'s -twenty-one methods, and `pull_lsp` is one of the five it leaves null — a +`src/detached/server.zig`'s vtable implements seventeen of `host.zig`'s +twenty-one methods, and `pull_lsp` is one of the four it leaves null — a worker pool is precisely what its deliberately single-threaded loop does not have. A null method is NOT automatically a dropped effect: `perform` decides that per arm, and the `.lsp` arm's answer is to synthesise one on the spot — @@ -69,16 +69,14 @@ has moved by the time the prong runs, the guard fails, and the Tab really is eaten. The local shells have the same race over a wider window, so this is a property of the late-indent repair rather than of detaching. -None of the detached core's five null methods silently drops a reachable +None of the detached core's four null methods silently drops a reachable effect. `pull_lsp` and `pull_pipe` have the fallbacks above; `pull_gpio_toggle` is `orelse return Error.NoPads` (`board_memory.zig`), which lands on the message row; `push_post_present` is a `pump` hook fired after presenting, and there is -nothing to notify in a process with no screen; and `push_fs_reply` is the one -`perform` arm with no fallback at all, but it is only ever emitted in answer to -an `Event.fs_req`, which is not on the wire (`wire.zig`: it has no `ClientTag`, -because neither half of that pair may cross an attachment) and which the -daemon raises none of, mounting no `/dev/fuse` by its own vtable comment. +nothing to notify in a process with no screen. `push_fs_reply` was listed here +too until the daemon began mounting its own `/dev/fuse`; it implements that one +now, and `--fs` works in a detached session. Four rules make it safe: -- cgit v1.3