summaryrefslogtreecommitdiff
path: root/src/tty/tty.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/tty/tty.zig')
-rw-r--r--src/tty/tty.zig99
1 files changed, 97 insertions, 2 deletions
diff --git a/src/tty/tty.zig b/src/tty/tty.zig
index 404fe582..c5abf0fe 100644
--- a/src/tty/tty.zig
+++ b/src/tty/tty.zig
@@ -17,6 +17,8 @@ const file_watch = @import("../file_watch.zig");
const user_config = @import("../user_config.zig");
const selection_pipe = @import("../selection_pipe.zig");
const nested = @import("../nested.zig");
+const fuse = @import("../fuse.zig");
+const fs_service = @import("../fs_service.zig");
const panel_compositor = @import("panel_compositor.zig");
const host_api = @import("../host.zig");
@@ -61,6 +63,14 @@ pub const Command = struct {
/// a pardes launched inside this one sent us a builtin command line
/// (see lookServer); gpa-owned, like pty_read
command: []u8,
+ /// `--fs`: the /dev/fuse descriptor has requests on it. Carries
+ /// nothing and is applied as a no-op — its whole job is to end the
+ /// blocking `nextEvent`, because the drain itself lives in pollFrame
+ /// beside the file-watch reload. Same shape and same reason as
+ /// `files_changed` above, and posted from two places: the poll thread
+ /// when the kernel makes the descriptor readable, and pollFrame itself
+ /// when a batch hit its cap with requests still pending.
+ fs_ready,
} = .nop;
};
const Loop = vaxis.Loop(@TypeOf(Command.value));
@@ -521,6 +531,24 @@ pub fn run(init: std.process.Init, opts: pardes.Options) !void {
defer paste_buf.deinit();
var loop: Loop = .init(io, &tty, &vx);
+ // `--fs`: mount before the initial spawns, because those shells are the
+ // ones that need PARDES_FS in their environment, and before the first
+ // frame, because a script racing startup must find panes that are already
+ // there. Also before any thread of ours exists — the mount forks the
+ // setuid fusermount3 helper, and forking from a multithreaded process is
+ // the hazard this whole region is ordered around. Null covers both "no
+ // --fs" and "--fs but the mount failed"; the second is reported on a
+ // message row inside `start` and the session runs on regardless.
+ //
+ // The teardown answers every held request, aborts the connection,
+ // unmounts and removes `<parent>/<pid>`. The PARENT (`.../pardes`) stays,
+ // like nested.zig's socket directory: another session may be living in it,
+ // and rmdir of a shared directory is not ours to attempt.
+ var fs = fs_service.start(gpa, core);
+ // Covers the error paths only: the ordinary exit unmounts at the END OF
+ // THE LOOP instead, see there.
+ defer if (fs) |f| f.deinit();
+
var sh: Shell = .{
.io = io,
.gpa = gpa,
@@ -538,6 +566,7 @@ pub fn run(init: std.process.Init, opts: pardes.Options) !void {
// mark the file a positional path argument opened. -1 off linux:
// watchPane goes quiet and the core simply never gets a file_changed.
.inotify_fd = if (builtin.os.tag == .linux) libc.inotify_init1(linux.IN.CLOEXEC) else -1,
+ .fs = fs,
};
defer {
// reap the reader tasks (cancel interrupts a blocked read) before
@@ -604,6 +633,11 @@ pub fn run(init: std.process.Init, opts: pardes.Options) !void {
// blocking accept(2) never returns either, so an io.concurrent task would
// hang the teardown that joins it.
if (sock_fd >= 0) (try std.Thread.spawn(.{}, lookServer, .{ gpa, sock_fd, &loop })).detach();
+ // ...and the /dev/fuse poller, which is the same kind of thread again: it
+ // waits for POLLIN and posts, never touching the core or the descriptor's
+ // data. Joined by `Fs.deinit` rather than detached, because unlike accept4
+ // it CAN be woken — fuse.zig gives it a control pipe for exactly that.
+ fs_service.wake(fs, &loop, wakeFs);
// Capability handshake — SEND the probes, do not wait on them. This was
// queryTerminal(2ms), which blocks on a futex until DA1 comes back. The
// number has to beat one terminal round trip: a local terminal answers in
@@ -705,6 +739,18 @@ pub fn run(init: std.process.Init, opts: pardes.Options) !void {
}
}
}
+ // THE FILESYSTEM GOES FIRST, ahead of every deferred teardown below.
+ // `loop.stop()` joins a reader parked in `read(2)` on the tty, so it does
+ // not return until the next keystroke — and a session that has decided to
+ // exit must not spend that wait holding a mount nobody is serving. A
+ // client blocked on `<id>/event` when the last pane is deleted through
+ // `ctl` then gets ENOTCONN at once instead of hanging until somebody
+ // touches the keyboard.
+ if (fs) |f| {
+ f.deinit();
+ fs = null;
+ sh.fs = null;
+ }
}
/// Everything the terminal shell owns and the core cannot: the ptys, the
@@ -727,6 +773,10 @@ const Shell = struct {
/// it, and the markers are the only thing that says where it ends.
paste_buf: *std.Io.Writer.Allocating,
inotify_fd: c_int,
+ /// acme's control filesystem for this session, or null when `--fs` was not
+ /// given (or its mount failed). Owned by `run`, which mounts it before the
+ /// first fork and tears it down on every path out.
+ fs: ?*fuse.Fs = null,
ptys: [pardes.MAX_PANES]?Pty = @splat(null),
/// per-slot spawn generation: a reused pane id ignores the old shell's
/// late pty_eof (which would otherwise close the NEW pty on that slot)
@@ -790,6 +840,7 @@ const Shell = struct {
.push_open_link = openLink,
.pull_lsp = lsp,
.pull_pipe = pipe,
+ .push_fs_reply = fsReply,
};
// ---- input ------------------------------------------------------------
@@ -960,6 +1011,12 @@ const Shell = struct {
// `git checkout`) collapses into ONE pass below, so it cannot
// queue a reload — or an undo entry — per write.
.files_changed => s.check_files = true,
+ // A wake and nothing more. The requests behind it are drained in
+ // pollFrame, where the file-watch reload also happens: both want to
+ // run once per frame with the whole batch already in, not once per
+ // event. So this arm has nothing to do — which is the point, since
+ // its only job was ending the blocking wait above.
+ .fs_ready => {},
.lsp_done => |d| {
core.update(.{ .lsp_resp = .{ .id = d.id, .rows = d.rows } });
s.lsp_gpa.free(d.rows);
@@ -995,6 +1052,18 @@ const Shell = struct {
fn pollFrame(ctx: ?*anyopaque) void {
const s = of(ctx);
+ // acme's filesystem, answered here for the same reason the file-watch
+ // reload is (see reloadWatched): one batch per frame, not one frame per
+ // request. First in the pass, so an edit a script just made through
+ // `body` is in the surface this frame composes rather than the next.
+ if (s.fs) |f| if (fs_service.drain(f, s.core).pending) {
+ // The batch hit its cap with requests still pending, and no ack has
+ // gone to the poll thread — so nothing else will wake us. Re-arm
+ // the loop ourselves. tryPostEvent, not postEvent: this runs on the
+ // only thread that drains the queue, so blocking on a full one
+ // would deadlock, and a full queue already holds a wake.
+ _ = s.loop.tryPostEvent(.fs_ready) catch {};
+ };
// live cwd for tags/look: cheap per-pane lookup, per frame. Whether the
// pane's tty still belongs to the prompt we forked is NOT polled here —
// it is a walk through /proc and nothing draws it, so the core pulls it
@@ -1156,7 +1225,7 @@ const Shell = struct {
cwd_buf[cwd.len] = 0;
cwd_z = @ptrCast(&cwd_buf);
}
- const child = forkShell(s.core, pane, s.prompt_rcs, s.core.shellBin(), cwd_z, s.core.screen_h, s.core.screen_w);
+ const child = forkShell(s.core, pane, s.prompt_rcs, s.core.shellBin(), cwd_z, s.core.screen_h, s.core.screen_w, s.fs);
s.ptys[pane] = .{ .file = child.file, .pid = child.pid, .reader = .{ .any_future = null, .result = {} } };
// report the pane's starting directory back to the core (tags). The
// slot needs no occupancy reset: nothing is remembered, and the next
@@ -1246,6 +1315,17 @@ const Shell = struct {
s.loop.postEvent(.files_changed) catch {};
}
+ /// The core's answer to one filesystem request, handed straight back to the
+ /// transport that is holding it. `bytes` was resolved by `pardes.fsPayload`
+ /// inside `perform` and is borrowed only for this call — a body read is a
+ /// window onto the pane's live text, so there is nothing to copy and
+ /// nothing to free. `.again` needs no special case here: `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);
+ }
+
fn dumpThemes(ctx: ?*anyopaque, pane: u8) void {
const s = of(ctx);
const config_dir = s.core.opts.config_dir orelse return;
@@ -1454,12 +1534,27 @@ fn winchWatch(loop: *Loop, vx: *vaxis.Vaxis, tty: *vaxis.Tty) void {
}
}
-fn forkShell(core: *pardes.Pardes, pane: usize, prompt_rcs: *const shell_bin.PromptRcs, bin: []const u8, cwd: ?[*:0]const u8, rows: u16, cols: u16) struct { file: std.Io.File, pid: posix.pid_t } {
+/// The /dev/fuse poller's wake, and deliberately nothing else — the thread that
+/// calls this has no business in the core, so all it does is end the blocking
+/// `nextEvent`. tryPostEvent rather than postEvent for the same reason readPty's
+/// final post uses it: this can fire after the loop has already been left, and a
+/// blocking push into a full queue nobody is draining would never return.
+fn wakeFs(ctx: ?*anyopaque) void {
+ const loop: *Loop = @ptrCast(@alignCast(ctx.?));
+ _ = loop.tryPostEvent(.fs_ready) catch {};
+}
+
+fn forkShell(core: *pardes.Pardes, pane: usize, prompt_rcs: *const shell_bin.PromptRcs, bin: []const u8, cwd: ?[*:0]const u8, rows: u16, cols: u16, fs: ?*const fuse.Fs) struct { file: std.Io.File, pid: posix.pid_t } {
var master: c_int = undefined;
// resolved BEFORE the fork, into this frame, which the child inherits:
// nothing between fork and exec may allocate, and a PATH search would
var path_buf: [std.fs.max_path_bytes]u8 = undefined;
const spawn = shell_bin.resolve(bin, &path_buf, prompt_rcs);
+ // ...and so is the pane's own address on the control filesystem, for a
+ // second reason on top of that one: acme puts `winid` in the child, which
+ // is safe there only because rfork(RFENVG) has just given it a private
+ // environment group. See fs_service.exportPaneEnv.
+ fs_service.exportPaneEnv(fs, if (core.panes[pane]) |pn| pn.serial else 0);
const ws = posix.winsize{ .row = rows, .col = cols, .xpixel = 0, .ypixel = 0 };
const pid = forkpty(&master, null, null, &ws);
if (pid == 0) {