summaryrefslogtreecommitdiff
path: root/src/detached
diff options
context:
space:
mode:
Diffstat (limited to 'src/detached')
-rw-r--r--src/detached/server.zig130
1 files changed, 108 insertions, 22 deletions
diff --git a/src/detached/server.zig b/src/detached/server.zig
index 93c964b4..4437cc22 100644
--- a/src/detached/server.zig
+++ b/src/detached/server.zig
@@ -161,6 +161,20 @@ const host_io = @import("../host_io.zig");
/// there, `waitInput`'s poll set here.
const file_watch = @import("../file_watch.zig");
+/// acme's control filesystem, which a detached session had no way to serve
+/// until now: `push_fs_reply` was the one machine-local effect this host left
+/// null, so a script could drive a tty or an SDL session and not a daemon —
+/// the configuration whose whole promise is outliving the terminal.
+///
+/// It costs less here than it does in the desktop shells. They need a thread
+/// blocked on `poll()` to notice a request and wake their loop
+/// (`fs_service.wake`); this process already owns a `poll(2)` over everything
+/// else it waits on, so `/dev/fuse` is one more descriptor in that set and
+/// there is no thread at all. `Source.fuse` is the arm; `pollFrame` is the
+/// drain, at the same point in the frame that tty.zig drains.
+const fuse = @import("../fuse.zig");
+const fs_service = @import("../fs_service.zig");
+
/// Host-lifetime storage for the OSC 133 rc files a forked shell sources, held
/// by `Session` because a Session is exactly one host's lifetime.
const shell_bin = @import("../shell_bin.zig");
@@ -236,9 +250,9 @@ const read_chunk = 16 * 1024;
const pty_chunk = 64 * 1024;
/// Descriptors in the ONE poll this process runs: the listener, every frontend,
-/// every pane's pty master, and the inotify descriptor behind every watch. 50
-/// on a full house, and one syscall covers all of them.
-const poll_slots = 1 + max_clients + pardes.MAX_PANES + 1;
+/// every pane's pty master, the inotify descriptor behind every watch, and
+/// `/dev/fuse`. 51 on a full house, and one syscall covers all of them.
+const poll_slots = 1 + max_clients + pardes.MAX_PANES + 2;
/// How many times `reloadWatched` will honour `file_watch.reloadChanged`'s
/// request for another pass within one round. See `reloadWatched`.
@@ -380,6 +394,12 @@ const Source = union(enum) {
client: u8,
pty: u8,
inotify,
+ /// The acme filesystem's descriptor. Its arm does nothing: being in the set
+ /// is the whole point, because a readable `/dev/fuse` must end the sleep so
+ /// that `pollFrame` — which runs after `pull_wait_input` returns — reaches
+ /// the drain. Answering it here instead would re-enter the core from inside
+ /// its own `pump`.
+ fuse,
};
pub const Session = struct {
@@ -424,6 +444,14 @@ pub const Session = struct {
/// The one inotify descriptor behind every watch this session holds, and the
/// last member of the poll set. Opened lazily — see `inotify`.
inotify_fd: c_int = -1,
+ /// acme's control filesystem, or null when `--fs` was not asked for or the
+ /// mount failed. Owned here rather than by `run` so that `deinit` unmounts
+ /// on every path out, including the error ones.
+ fs: ?*fuse.Fs = null,
+ /// The last drain stopped at `max_batch` with requests still in the kernel.
+ /// Same role as `check_files`: nothing else will wake us, because no
+ /// acknowledgement has gone back, so the next round must not sleep.
+ fs_pending: bool = false,
/// Which directory mark belongs to which pane, and the generation the core
/// has already accepted from each. file_watch.zig owns the shape and the
/// transaction; this host owns only the descriptor and the wake.
@@ -456,6 +484,15 @@ pub const Session = struct {
// ---- lifetime ---------------------------------------------------------
pub fn deinit(s: *Session) void {
+ // First, and before the pane shells: a script blocked on `event` is
+ // holding a kernel request, and `Fs.deinit` answers everything still
+ // parked and aborts the connection before unmounting. Leaving it until
+ // after the harvest would leave that reader in uninterruptible sleep
+ // for as long as the harvest takes.
+ if (s.fs) |f| {
+ f.deinit();
+ s.fs = null;
+ }
// Tell everyone the session is over before the socket disappears, so a
// frontend exits on a `quit` rather than on a read error whose meaning
// it has to guess. Best effort by construction: these descriptors are
@@ -553,21 +590,26 @@ pub const Session = struct {
return @ptrCast(@alignCast(ctx.?));
}
- /// Sixteen methods, and NOT the fullest host in the tree — that claim stood
- /// here, was believed, and was copied into docs/detached.md before an audit
- /// counted the others. The tty and SDL shells fill NINETEEN each (everything
- /// but `pull_gpio_toggle` and `push_detach`) and macOS fourteen, so this host
- /// is the only one that implements `push_detach` and otherwise the least
- /// complete of the three desktop hosts. What is true is narrower and is the
- /// point anyway: it performs every MACHINE-LOCAL effect there is, and the
- /// five of host.zig's twenty-one it leaves null are null because there is
- /// nothing here for them to do. Three of those five are real losses a person
- /// can notice — no `pull_lsp` and no `pull_pipe`, because both want the
- /// worker pool this deliberately single-threaded loop does not have, and no
- /// `push_fs_reply`, because this process mounted no /dev/fuse. The other two
- /// are not losses at all: `push_post_present` marks the moment a frame
- /// reached a screen and this process has no screen, and `pull_gpio_toggle`
- /// wants pads.
+ /// Seventeen methods, and NOT the fullest host in the tree — that claim
+ /// stood here, was believed, and was copied into docs/detached.md before an
+ /// audit counted the others. The tty and SDL shells fill NINETEEN each
+ /// (everything but `pull_gpio_toggle` and `push_detach`) and macOS fourteen,
+ /// so this host is the only one that implements `push_detach` and otherwise
+ /// the least complete of the three desktop hosts. What is true is narrower
+ /// and is the point anyway: it performs every MACHINE-LOCAL effect there is,
+ /// and the four of host.zig's twenty-one it leaves null are null because
+ /// there is nothing here for them to do. Two of those four are real losses a
+ /// person can notice — no `pull_lsp` and no `pull_pipe`, because both want
+ /// the worker pool this deliberately single-threaded loop does not have. The
+ /// other two are not losses at all: `push_post_present` marks the moment a
+ /// frame reached a screen and this process has no screen, and
+ /// `pull_gpio_toggle` wants pads.
+ ///
+ /// `push_fs_reply` was the third real loss until this commit. It was null
+ /// because the daemon mounted no /dev/fuse, and the consequence was that a
+ /// detached session — the configuration whose whole promise is outliving the
+ /// terminal — was the one configuration no script could drive. It mounts one
+ /// now; see `fs` and `pollFrame`.
///
/// `push_detach` is the one entry here that is not an effect. See `detach`.
const vtable: host_api.Host.VTable = .{
@@ -587,6 +629,7 @@ pub const Session = struct {
.pull_read_clipboard = readClipboard,
.push_open_link = openLink,
.push_detach = detach,
+ .push_fs_reply = fsReply,
};
// ---- routing ----------------------------------------------------------
@@ -647,7 +690,7 @@ pub const Session = struct {
// still sized like the pane the core reflowed.
s.core.screen_h,
s.core.screen_w,
- null, // no `--fs` mount in a daemon: nothing here answers /dev/fuse
+ s.fs,
);
// A `forkpty` that failed left `master` holding a number this process
// does not own. The shells get away with not checking because they hand
@@ -880,6 +923,16 @@ pub const Session = struct {
if (s.origins()) |c| s.send(c, .detach);
}
+ /// The core's answer to one filesystem request, handed straight back to the
+ /// transport holding it. `bytes` was resolved by `pardes.fsPayload` inside
+ /// `perform` and is borrowed only for this call, so a body read is a window
+ /// onto the pane's live text and copies nothing. `.again` needs no case:
+ /// `Fs.reply` reads the status and re-parks the request itself.
+ fn fsReply(ctx: ?*anyopaque, reply: *const pardes.acmefs.Reply, bytes: []const u8) void {
+ const s = of(ctx);
+ if (s.fs) |f| f.reply(reply, bytes);
+ }
+
// ---- pane shells ------------------------------------------------------
/// Each pane's live cwd, for the tags. One readlink of /proc per pane that
@@ -894,6 +947,12 @@ pub const Session = struct {
/// watching. The pids are here now, so it does.
fn pollFrame(ctx: ?*anyopaque) void {
const s = of(ctx);
+ // acme's filesystem first in the pass, for the reason tty.zig gives at
+ // its own call site: an edit a script just made through `body` belongs
+ // in the surface this frame composes, not the next one. The flag is
+ // read by `waitInput`, which must not sleep while the kernel still has
+ // requests we have not acknowledged.
+ if (s.fs) |f| s.fs_pending = fs_service.drain(f, s.core).pending;
for (&s.ptys, 0..) |*pt, pane| {
if (pt.fd < 0) continue;
var lbuf: [1024]u8 = undefined;
@@ -1228,15 +1287,27 @@ pub const Session = struct {
src[n] = .inotify;
n += 1;
}
+ // ...and acme's filesystem, when there is one. Its arm in `dispatch`
+ // does nothing: this descriptor is here to END THE SLEEP, so that the
+ // `pollFrame` after `pull_wait_input` returns reaches the drain. The
+ // desktop shells buy the same wake with a thread; one poll slot is
+ // cheaper and cannot race the loop.
+ if (s.fs) |f| if (f.fd >= 0) {
+ fds[n] = .{ .fd = f.fd, .events = poll_in, .revents = 0 };
+ src[n] = .fuse;
+ n += 1;
+ };
// A session with no listener, no clients, no shells and no watches has
// no event source at all. Returning immediately would spin the outer
// `while (!core.quit)` at full speed, so sleep the interval the core
// offered and, when it offered none, a frame's worth.
//
// Nothing is owed on this path. `check_files` is only ever set by a
- // watch, and a watch means the inotify descriptor is in the set; and
+ // watch, and a watch means the inotify descriptor is in the set;
// `reconcile` posts a resize only when a client is ATTACHED, which means
- // its socket is in the set — so `n == 0` implies `!regridded` too.
+ // its socket is in the set; and `fs_pending` is only ever set by a drain,
+ // which runs only when `fs` is live, which puts `/dev/fuse` in the set.
+ // So `n == 0` implies `!regridded` and `!fs_pending` too.
if (n == 0) return nap(if (timeout_ms == 0) 16 else timeout_ms);
// Zero is the core's word for "sleep until something happens" (see
// pardes.zig `pump`: it passes a frame interval only while an animation
@@ -1254,7 +1325,7 @@ pub const Session = struct {
// during `perform`, outside this function) and a regrid this round has
// already performed. Both are consumed before this function returns, so
// the round must not sleep before reaching them.
- if (s.check_files or regridded) timeout = 0;
+ if (s.check_files or regridded or s.fs_pending) timeout = 0;
const ready = libc.poll(&fds, @intCast(n), timeout);
// A timeout is an ordinary frame boundary and EINTR is a signal we do not
// handle here. Neither skips anything below any more: what used to be an
@@ -1329,6 +1400,9 @@ pub const Session = struct {
}
},
.inotify => if (pfd.revents & poll_in != 0) s.drainInotify(),
+ // Nothing. See `Source.fuse`: the wake IS the work, and the drain
+ // belongs to `pollFrame`, where re-entering the core is legal.
+ .fuse => {},
};
}
@@ -1761,6 +1835,18 @@ pub fn run(init: std.process.Init, opts: pardes.Options, name: []const u8) !void
return error.NoSocket;
}
+ // Before the host is installed and before the startup drain, so a script
+ // that races the daemon's launch finds a tree whose panes already exist.
+ // Null on every failure — no fuse3, no `user_allow_other`, a kernel without
+ // FUSE — and a failure must cost the operator their scripting, never their
+ // session. `fs_service.start` has already said so on pane 0's message row.
+ //
+ // NO `fs_service.wake`. That call exists to start a thread that blocks on
+ // `poll()` and pokes a loop the thread does not otherwise share; this
+ // process polls `/dev/fuse` itself, in the same syscall as everything else.
+ // See `Source.fuse`.
+ session.fs = fs_service.start(gpa, core);
+
const h = session.host();
core.host = h;
while (core.nextEffect()) |effect| core.perform(effect);