diff options
| author | Gabriel Schneider <[email protected]> | 2026-08-27 15:32:02 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-08-27 16:15:35 -0300 |
| commit | f5927a033f0c83753b5cc004e514568eec8c24f8 (patch) | |
| tree | e31d0f29aac1d4266c42dac99fcdd0013bfbe64d /src/fuse.zig | |
| parent | 5f4719da21f06b58694d52d354f5fda431ff8543 (diff) | |
| download | pardes-f5927a033f0c83753b5cc004e514568eec8c24f8.tar.gz pardes-f5927a033f0c83753b5cc004e514568eec8c24f8.zip | |
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.
Diffstat (limited to 'src/fuse.zig')
| -rw-r--r-- | src/fuse.zig | 7 |
1 files changed, 5 insertions, 2 deletions
diff --git a/src/fuse.zig b/src/fuse.zig index 3a903e8b..3bd263bd 100644 --- a/src/fuse.zig +++ b/src/fuse.zig @@ -59,8 +59,11 @@ const libc = std.c; const linux = std.os.linux; const acmefs = @import("acmefs.zig"); /// Only for `Transport`, the three-function shape this mount presents to the -/// host loop. No cycle: `fs_service` names no type from here any more, which -/// is the point of the seam. +/// host loop. This IS a cycle — `fs_service` imports this file back for +/// `Fs.mount`, `sweepStale` and `exportPaneEnv`, and still names `*Fs` in three +/// of its own signatures — and Zig accepts it because imports are analysed +/// lazily. What the seam removed is `drain`'s dependency on the concrete type, +/// not the file's dependency on this one. Do not read it as more than that. const fs_service = @import("fs_service.zig"); /// Everything below the mount is Linux kernel ABI. Off Linux the module still |
