diff options
| author | Gabriel Schneider <[email protected]> | 2026-08-25 02:07:23 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-08-25 09:42:07 -0300 |
| commit | 6f48508aa08396bcf9dd4da2cab1d221bcc53f78 (patch) | |
| tree | 83daec3db5ac27ea3172651df2b3e9cb62eddb4a /src/gui | |
| parent | 28c70cabb6ceb7e5fecfd74f6984f5a995269f01 (diff) | |
| download | pardes-6f48508aa08396bcf9dd4da2cab1d221bcc53f78.tar.gz pardes-6f48508aa08396bcf9dd4da2cab1d221bcc53f78.zip | |
acmefs: pardes --fs serves acme's control filesystem over raw Linux FUSE
Diffstat (limited to 'src/gui')
| -rw-r--r-- | src/gui/gui.zig | 96 |
1 files changed, 93 insertions, 3 deletions
diff --git a/src/gui/gui.zig b/src/gui/gui.zig index e080dd9a..092ac2e4 100644 --- a/src/gui/gui.zig +++ b/src/gui/gui.zig @@ -26,6 +26,8 @@ const selection_pipe = @import("../selection_pipe.zig"); const shell_bin = @import("../shell_bin.zig"); const nested = @import("../nested.zig"); +const fuse = @import("../fuse.zig"); +const fs_service = @import("../fs_service.zig"); pub const c = @cImport({ @cDefine("SDL_DISABLE_OLD_NAMES", "1"); @@ -851,6 +853,12 @@ const Msg = union(enum) { /// a pardes launched inside this one sent us a builtin command line (see /// lookThread); gpa-owned, like `output` bytes command: []u8, + /// `--fs`: the /dev/fuse descriptor has requests on it. Carries nothing — + /// the drain lives in pollFrame, and this only ends a blocking + /// SDL_WaitEventTimeout. Posted by the poll thread and, when a batch hits + /// its cap, by pollFrame itself. Lossy under backpressure on purpose: a + /// full queue already holds something that will wake the loop. + fs_ready, fn deinit(m: Msg, gpa: std.mem.Allocator, lsp_allocator: std.mem.Allocator) void { switch (m) { @@ -861,7 +869,7 @@ const Msg = union(enum) { response.deinit(gpa); }, .command => |line| gpa.free(line), - .eof, .files_changed => {}, + .eof, .files_changed, .fs_ready => {}, } } }; @@ -1903,6 +1911,18 @@ fn runNative(init: std.process.Init, opts_in: pardes.Options) !void { const sock_fd: c_int = if (opts.nested) -1 else nested.listen(); defer nested.unlisten(sock_fd); + // `--fs`: mounted before the initial spawns (they are the shells that need + // PARDES_FS) and before any thread of ours exists (the mount forks the + // setuid fusermount3 helper). 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 without a filesystem. Teardown answers everything + // held, aborts the connection, unmounts and removes `<parent>/<pid>`; the + // parent stays, like nested.zig's socket directory. + 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 shell: Shell = .{ .core = core, .gui = &g, @@ -1916,6 +1936,7 @@ fn runNative(init: std.process.Init, opts_in: pardes.Options) !void { .pipe_tasks = &pipe_tasks, .inotify_fd = inotify_fd, .watches = &watches, + .fs = fs, .test_mode = test_mode, }; const host = shell.host(); @@ -1935,6 +1956,10 @@ fn runNative(init: std.process.Init, opts_in: pardes.Options) !void { // ...and the nested-instance listener, detached like every other blocking // worker here if (sock_fd >= 0) if (std.Thread.spawn(.{}, lookThread, .{ gpa, sock_fd, &queue })) |th| th.detach() else |_| {}; + // ...and the /dev/fuse poller, which is the same kind of thread again — + // except joined by `Fs.deinit` rather than detached, because fuse.zig gives + // it a control pipe that CAN wake it out of poll(). + fs_service.wake(fs, &queue, wakeFs); _ = c.SDL_StartTextInput(window); @@ -1987,6 +2012,16 @@ fn runNative(init: std.process.Init, opts_in: pardes.Options) !void { syncTaglineFont(&g, core); } } + + // THE FILESYSTEM GOES FIRST, ahead of every deferred teardown below: a + // session that has decided to exit must not spend its teardown holding a + // mount nobody is serving, so a client blocked on `<id>/event` when the + // last pane is deleted through `ctl` gets ENOTCONN at once. + if (fs) |f| { + f.deinit(); + fs = null; + shell.fs = null; + } } /// PARDES_TEST_GRID=1: headless. No SDL at all — stdin escape sequences in, @@ -2059,6 +2094,11 @@ fn runGrid(init: std.process.Init, opts_in: pardes.Options) !void { // scripted input event, and a reload that arrives on its own clock would // put a frame in the stream nothing asked for. -1 makes watchPane a no-op. var watches: file_watch.Table = @splat(null); + // The filesystem IS served here, unlike the file watcher above: `--fs=<dir>` + // names a predictable mount point precisely so a snapshot can drive this + // mode through it. No poll thread though — see gridPollFrame. + const fs = fs_service.start(gpa, core); + defer if (fs) |f| f.deinit(); var shell: Shell = .{ .core = core, .io = io, @@ -2071,6 +2111,7 @@ fn runGrid(init: std.process.Init, opts_in: pardes.Options) !void { .pipe_tasks = &pipe_tasks, .inotify_fd = -1, .watches = &watches, + .fs = fs, }; const host = shell.host(); // The core owns the loop here too, but not the scripted stdin: EOF ends @@ -2997,6 +3038,10 @@ const Shell = struct { gui: ?*Gui = null, io: std.Io, gpa: std.mem.Allocator, + /// acme's control filesystem for this session, or null when `--fs` was not + /// given (or its mount failed, or this is the headless grid harness, which + /// serves nothing). Owned by `run`. + fs: ?*fuse.Fs = null, lsp_allocator: std.mem.Allocator, prompt_rcs: *const shell_bin.PromptRcs, ptys: *[pardes.MAX_PANES]?Pty, @@ -3048,6 +3093,7 @@ const Shell = struct { .push_open_link = openLink, .pull_lsp = lsp, .pull_pipe = pipe, + .push_fs_reply = fsReply, }; /// The headless grid harness. It reads its scripted stdin itself, because @@ -3110,12 +3156,34 @@ const Shell = struct { // `git checkout`) collapses into ONE pass below, so it cannot queue // a reload — or an undo entry — per write. .files_changed => check_files = true, + // A wake and nothing more; the requests behind it are drained in + // pollFrame, which is where a whole batch can be answered against + // one render instead of one render per request. + .fs_ready => {}, }; if (check_files and file_watch.reloadChanged(s.core, s.io, s.gpa, s.watches)) s.queue.push(.files_changed); } }; +/// 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 megabyte body read copies +/// nothing. `.again` needs no 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 = shellOf(ctx); + if (s.fs) |f| f.reply(reply, bytes); +} + +/// The /dev/fuse poller's wake. `Queue.push` is the thread-safe door and +/// already raises the SDL user event that ends a blocking WaitEventTimeout, so +/// this is the whole callback — the same shape as watchThread's. +fn wakeFs(ctx: ?*anyopaque) void { + const q: *Queue = @ptrCast(@alignCast(ctx.?)); + q.push(.fs_ready); +} + fn shellOf(ctx: ?*anyopaque) *Shell { return @ptrCast(@alignCast(ctx.?)); } @@ -3159,6 +3227,14 @@ fn waitInput(ctx: ?*anyopaque, timeout_ms: u32) void { fn pollFrame(ctx: ?*anyopaque) void { const s = shellOf(ctx); const core = s.core; + // acme's filesystem: one batch per frame, answered before anything else in + // the pass, so an edit a script just made through `body` is in the surface + // this frame composes. Ahead of the `s.gui orelse return` below because it + // has nothing to do with pixels. Hitting the cap means no ack reached the + // poll thread, so nothing else will wake us — re-arm the loop ourselves; + // `Queue.push` is lossy for this variant, which is correct, because a queue + // too full to take a wake is already holding one. + if (s.fs) |f| if (fs_service.drain(f, core).pending) s.queue.push(.fs_ready); const g = s.gui orelse return; // TaglineSize is pure renderer state: update the smaller face and its // visual band immediately, without changing the body metrics or grid. @@ -3257,6 +3333,16 @@ fn postPresent(ctx: ?*anyopaque) void { /// animation step, and the text of the grid itself. fn gridPollFrame(ctx: ?*anyopaque) void { const s = shellOf(ctx); + // The filesystem, drained on the pass rather than woken by a thread: this + // mode's contract is one frame per scripted event, and a poller posting on + // its own clock would put frames in the stream nothing asked for. fuse.zig + // supports exactly this — skip `wakeThread` and drain from the frame poll — + // and here it is not a degradation but the point. A request that changed + // something IS an event, so the pass renders: that is what lets a snapshot + // `wait` for text a script wrote through the mount. + if (s.fs) |f| { + if (fs_service.drain(f, s.core).count != 0) s.saw_event = true; + } pollCwds(s.core, s.ptys); // The harness polls stdin at the same 16 ms cadence as native SDL, so a // pass IS a frame interval and the tick is due here rather than behind a @@ -3307,7 +3393,7 @@ fn spawnPane(ctx: ?*anyopaque, pane: u8, cwd: []const u8) void { cwd_buf[cwd.len] = 0; cwd_z = @ptrCast(&cwd_buf); } - const pt = forkShell(s.core, pane, s.prompt_rcs, s.core.shellBin(), cwd_z, s.core.screen_h, s.core.screen_w); + const pt = 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] = pt; // report the pane's starting directory back to the core (tags); the slot // needs no occupancy reset, nothing about it is remembered @@ -3448,12 +3534,16 @@ fn pipe(ctx: ?*anyopaque, id: u32) void { if (s.threads_ok) spawnPipe(s.core, s.io, s.gpa, s.queue, s.pipe_tasks, id); } -fn forkShell(core: *pardes.Pardes, pane: usize, prompt_rcs: *const shell_bin.PromptRcs, bin: []const u8, cwd: ?[*:0]const u8, rows: u16, cols: u16) Pty { +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) Pty { 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: 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) { |
