diff options
| author | Gabriel Schneider <[email protected]> | 2026-10-01 04:00:32 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 04:00:32 -0300 |
| commit | 8dc84e6f9f1c3802f1d28de4d49907381cdf224b (patch) | |
| tree | 3ed7727a61de9ae4c473413d81de3ac95844af78 /src | |
| parent | a4e17d350a7afdeee4916584c44c4770243c84e4 (diff) | |
| download | pardes-8dc84e6f9f1c3802f1d28de4d49907381cdf224b.tar.gz pardes-8dc84e6f9f1c3802f1d28de4d49907381cdf224b.zip | |
pardes - asks /pager for its +Pager and reads the serial on the same open: another client's exec meanwhile can never take the text into its pane
pardes - wrote `pager <dir>` to /ctl, then read /exec on a fresh open,
which answers the session's last answer: a client that exec'd in
between had its pane overwritten by the paged text. /pager takes the
directory and answers the +Pager's serial on the open that wrote it,
as /pane/new answers its own open; the ctl verb is gone. Client.ask
writes and reads on one open. The README's look recipe reads its
answer on its own open too.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src')
| -rw-r--r-- | src/9p_io.zig | 26 | ||||
| -rw-r--r-- | src/fs-help.txt | 3 | ||||
| -rw-r--r-- | src/main.zig | 9 | ||||
| -rw-r--r-- | src/ninep/ctl.zig | 53 | ||||
| -rw-r--r-- | src/ninep/tree.zig | 26 |
5 files changed, 89 insertions, 28 deletions
diff --git a/src/9p_io.zig b/src/9p_io.zig index b11a4997..9fc19df9 100644 --- a/src/9p_io.zig +++ b/src/9p_io.zig @@ -2319,6 +2319,32 @@ pub const Client = struct { gpa.destroy(s); } + /// Writes `request` to `path` and reads the answer on the same open, + /// as /pane/new answers its own open: a session's last answer, from + /// another client, can never be what comes back. `exec 3<>file`. + pub fn ask(gpa: std.mem.Allocator, dial: []const u8, path: []const u8, request: []const u8) ![]u8 { + if (comptime !supported) return error.Unsupported; + var names: [max_depth][]const u8 = undefined; + const n = try elements(path, &names); + var sock_buf: [sun_path_len]u8 = undefined; + const sock = try resolve(&sock_buf, dial); + var remote: RemoteError = .{}; + errdefer said = remote; + const s = try startSession(gpa, sock, path); + defer endSession(gpa, s); + const fid = (try walkTo(s, names[0..n], &remote)).fid; + _ = try s.ask(.{ .open = .{ .fid = fid, .mode = ninep.ordwr } }, &remote); + _ = try s.ask(.{ .write = .{ .fid = fid, .offset = 0, .data = request } }, &remote); + var out: std.ArrayList(u8) = .empty; + errdefer out.deinit(gpa); + while (true) { + const data = (try s.ask(.{ .read = .{ .fid = fid, .offset = out.items.len, .count = s.cl.maxRead() } }, &remote)).read; + if (data.len == 0) return out.toOwnedSlice(gpa); + if (out.items.len + data.len > 4096) return Error.FileTooLarge; + try out.appendSlice(gpa, data); + } + } + /// Opens `path` on one connection and writes `first` to it (/log's /// `follow new`), all with the usual deadline; then, once `ready(ctx)` /// has said it is not done already, reads it with no deadline, a read at diff --git a/src/fs-help.txt b/src/fs-help.txt index 83aa536f..aea3af5b 100644 --- a/src/fs-help.txt +++ b/src/fs-help.txt @@ -5,6 +5,7 @@ index a line per pane: serial kind(text|term|pdf|image) dirty name column; status pid, version and pane count look write a line: a right click on it at the active pane; read: the serials it touched (this open's) exec write a line: a middle click, a builtin or a shell line; read the same (every exec file) +pager write a directory: its one +Pager, made or emptied; read the same open: its serial (pardes -) log events: new del rename save newcol delcol run exit send ask answer changed dump restore restored restoredcol msg err screen the rendered screen as JSON, frozen from open to close listeners the session's dial addresses @@ -22,7 +23,7 @@ os/, src/ the host filesystem; the editor's own sources $m below: $NINE_MOUNT/pardes/<pid|NAME> under 9ns --mntgen; $NINE_MOUNT under 9ns --unix; $PARDES_MOUNT in a Tty9p shell. cat $m/index which panes exist - echo /etc/hosts:3 > $m/look; cat $m/look open a file, see the pane it went to + exec 3<>$m/look; echo /etc/hosts:3 >&3; cat <&3 open a file, see the pane it went to (a fresh open reads the last anyone's) n=$(cat $m/pane/new) make a pane (each open makes one), take its serial printf 'text\n' >> $m/pane/$n/body append to it (> replaces it all) echo 'Msg hello' > $m/exec show text in the editor diff --git a/src/main.zig b/src/main.zig index 1075b596..6c46b22c 100644 --- a/src/main.zig +++ b/src/main.zig @@ -101,7 +101,7 @@ const help_text = ; /// `pardes -` inside a session: `text` into the directory's one +Pager -/// (root ctl `pager <dir>`, refilled when it is there), written into its +/// (/pager, refilled when it is there), written into its /// body through one open, as one undo step, and left clean. A text /// the session would not take is never lost: it goes to stderr, with why, /// and the exit is 1. @@ -125,9 +125,10 @@ fn pageInto(io: std.Io, gpa: std.mem.Allocator, dial: []const u8, text: []const if (c < ' ' or c == 0x7f) break false; } else true; var line_buf: [4200]u8 = undefined; - const ctl_line = std.fmt.bufPrint(&line_buf, "pager {s}\n", .{if (plain) cwd else ""}) catch "pager\n"; - ninep_io.Client.write(gpa, dial, "/ctl", ctl_line) catch |err| Fail.with(io, text, "no +Pager pane", err); - const answer = ninep_io.Client.read(gpa, dial, "/exec", "/exec") catch |err| Fail.with(io, text, "no +Pager pane", err); + const dir_line = std.fmt.bufPrint(&line_buf, "{s}\n", .{if (plain) cwd else ""}) catch "\n"; + // Its serial read back on the open that asked: another client's exec + // meanwhile can never send this text into that client's pane. + const answer = ninep_io.Client.ask(gpa, dial, "/pager", dir_line) catch |err| Fail.with(io, text, "no +Pager pane", err); const serial = std.fmt.parseInt(u32, std.mem.trim(u8, answer, " \n"), 10) catch |err| Fail.with(io, text, "no +Pager pane", err); // The whole text through one open of the body (truncated once, then // written at rising offsets): one undo step, and every byte of it. diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig index 42b88b91..bcf9ffd6 100644 --- a/src/ninep/ctl.zig +++ b/src/ninep/ctl.zig @@ -640,6 +640,23 @@ pub fn readRoot(p: *Pardes, req: Req) Reply { return tree.stagedReply(p, req); } +/// /pager, `pardes -`'s: the directory written (empty, or not absolute: the +/// session's) gets its one +Pager, made or emptied; the serial is what a +/// read of this same open answers, never another client's. +pub fn writePager(p: *Pardes, req: Req) Reply { + var it = Messages.init(p, req.data); + const line = it.next() orelse ""; + if (it.next() != null) return tree.failText(req.tag, E.INVAL, "pager: one directory a write"); + p.fs.results_len = 0; + const id = pardes.panes.Output.openPager(p, p.active, std.mem.trim(u8, line, " \t")) catch |err| + return tree.failText(req.tag, if (err == error.NoPaneRoom or err == error.NoPaneSlots) E.NOSPC else E.IO, switch (err) { + error.NoPaneRoom, error.NoPaneSlots => "pager: no space for a +Pager pane", + else => "pager: could not make a +Pager pane", + }); + noteResult(p, p.panes[id].?.serial); + return .{ .tag = req.tag, .written = @intCast(req.data.len) }; +} + /// One session builtin a line, at whichever pane has the keyboard as each /// runs, as the root's look and exec click there; all are checked first. pub fn writeRoot(p: *Pardes, req: Req) Reply { @@ -648,18 +665,6 @@ pub fn writeRoot(p: *Pardes, req: Req) Reply { var it = Messages.init(p, req.data); while (it.next()) |line| { if (line.len == 0 or exec_line.isComment(line)) continue; - // `pardes -`'s: the directory's +Pager made or emptied, its - // serial what /exec reads back. - if (std.mem.startsWith(u8, line, "pager ") or std.mem.eql(u8, line, "pager")) { - if (!apply) continue; - const id = pardes.panes.Output.openPager(p, p.active, std.mem.trim(u8, line["pager".len..], " \t")) catch |err| - return tree.failText(req.tag, if (err == error.NoPaneRoom or err == error.NoPaneSlots) E.NOSPC else E.IO, switch (err) { - error.NoPaneRoom, error.NoPaneSlots => "pager: no space for a +Pager pane", - else => "pager: could not make a +Pager pane", - }); - noteResult(p, p.panes[id].?.serial); - continue; - } if (std.mem.startsWith(u8, line, "size ") or std.mem.eql(u8, line, "size")) { // The offending word is the value: `size 5 5` quotes "5 5". const value = std.mem.trim(u8, line["size".len..], " \t"); @@ -3999,3 +4004,27 @@ test "a refused Delcol changes nothing: no pane, no focus" { try testing.expectEqual(active_column, p.active_column); try testing.expect(th.logHas(p, "unsaved ")); } + +test "pager answers its +Pager on the open that asked, whatever another client exec'd meanwhile" { + const p = try withFile(testing.allocator, "x\n"); + defer p.deinit(); + const pager = @intFromEnum(tree.TopFile.pager); + const exec = @intFromEnum(tree.TopFile.exec); + const asked = call(p, .{ .tag = 1, .op = .open, .node = pager, .omode = 2 }).reply.handle; + try testing.expectEqual(Status.ok, call(p, .{ .tag = 2, .op = .write, .node = pager, .handle = asked, .data = "/tmp/pager-race\n" }).reply.status); + const paged = for (p.panes) |slot| { + const pn = slot orelse continue; + if (pn.file) |f| if (std.mem.eql(u8, f.path, "/tmp/pager-race/+Pager")) break pn.serial; + } else return error.NoPager; + // Another client execs between the request and its read: a pane of its own. + const racer = call(p, .{ .tag = 3, .op = .open, .node = exec, .omode = 2 }).reply.handle; + try testing.expectEqual(Status.ok, call(p, .{ .tag = 4, .op = .write, .node = exec, .handle = racer, .data = "New\n" }).reply.status); + var line: [16]u8 = undefined; + const want = try std.fmt.bufPrint(&line, "{d}\n", .{paged}); + // A fresh open reads the session's last answer, the racer's pane: what + // `pardes -` once read, and wrote its text into. + try testing.expect(!std.mem.eql(u8, want, rd(p, exec, 0, 64).bytes)); + try testing.expectEqualStrings(want, call(p, .{ .tag = 5, .op = .read, .node = pager, .handle = asked, .size = 64 }).bytes); + // One directory a write. + try testing.expectEqual(E.INVAL, wr(p, pager, "/a\n/b\n").errno()); +} diff --git a/src/ninep/tree.zig b/src/ninep/tree.zig index cc8f938a..9c3e1e9c 100644 --- a/src/ninep/tree.zig +++ b/src/ninep/tree.zig @@ -233,6 +233,9 @@ pub const TopFile = enum(u5) { /// The files opened lately, closed ones too, a line each: /// `open|closed <path>`, most recent first. recent, + /// `pardes -`'s: a directory written, its +Pager made or emptied; a + /// read of the same open answers its serial. + pager, pub fn fileName(f: TopFile) []const u8 { return if (f == .root) "/" else @tagName(f); @@ -241,7 +244,7 @@ pub const TopFile = enum(u5) { pub fn mode(f: TopFile) u16 { return switch (f) { .root, .pane, .col => 0o755, - .look, .exec, .log, .focus, .ctl, .tag, .tagexec => 0o666, + .look, .exec, .log, .focus, .ctl, .tag, .tagexec, .pager => 0o666, .README, .index, .status, .screen, .listeners, .new, .commands, .layout, .recent => 0o444, }; } @@ -645,7 +648,7 @@ fn topSize(p: *Pardes, f: TopFile) u64 { .tag => cols.headerText(p, null).?.len, .README => fs.help.len, .status => ctl.statusLen(p), - .look, .exec, .tagexec => ctl.resultsLen(p), + .look, .exec, .tagexec, .pager => ctl.resultsLen(p), // A stream: a length would read as its end to a kernel that caches // one (a 9ns mount's splice), and a follower's reads go past it. .log => 0, @@ -749,7 +752,7 @@ fn readdir(p: *Pardes, req: Req, target: Target) Reply { .root => { // The guide, then what describes the session, then the // files that act, then the directories. - inline for (.{ TopFile.README, TopFile.index, TopFile.status, TopFile.focus, TopFile.ctl, TopFile.commands, TopFile.recent, TopFile.look, TopFile.exec, TopFile.log, TopFile.screen, TopFile.listeners, TopFile.layout, TopFile.tag, TopFile.tagexec, TopFile.pane, TopFile.col }) |t| { + inline for (.{ TopFile.README, TopFile.index, TopFile.status, TopFile.focus, TopFile.ctl, TopFile.commands, TopFile.recent, TopFile.look, TopFile.exec, TopFile.pager, TopFile.log, TopFile.screen, TopFile.listeners, TopFile.layout, TopFile.tag, TopFile.tagexec, TopFile.pane, TopFile.col }) |t| { if (skip > 0) skip -= 1 else stageDirent(out, p.gpa, @intFromEnum(t), t.dir(), t.fileName()); } if (skip > 0) skip -= 1 else stageDirent(out, p.gpa, fs.os_root, true, "os"); @@ -1066,7 +1069,7 @@ fn readFile(p: *Pardes, req: Req, target: Target) Reply { break :index stagedReply(p, req); }, .status => ctl.readStatus(p, req), - .look, .exec, .tagexec => ctl.readResults(p, req), + .look, .exec, .tagexec, .pager => ctl.readResults(p, req), .log => events.readLog(p, req), .screen => screen.readSnapshot(p, req, null), .listeners => screen.readListeners(p, req), @@ -1125,7 +1128,7 @@ fn readFile(p: *Pardes, req: Req, target: Target) Reply { /// The files whose writes are lines of commands, run as each line is whole. fn linesFile(target: Target) bool { return switch (target) { - .top => |f| f == .look or f == .exec or f == .ctl or f == .tagexec, + .top => |f| f == .look or f == .exec or f == .ctl or f == .tagexec or f == .pager, .col => |c| c.file == .ctl or c.file == .exec, // `name` too: one name, however its writes cut it, applied once // its newline comes, or with its write when that is whole. @@ -1136,7 +1139,7 @@ fn linesFile(target: Target) bool { /// The ones of those a read answers with the panes a write touched. fn resultsFile(target: Target) bool { return switch (target) { - .top => |f| f == .look or f == .exec or f == .tagexec, + .top => |f| f == .look or f == .exec or f == .tagexec or f == .pager, .col => |c| c.file == .exec, .pane => |t| t.file == .look or t.file == .exec or t.file == .tagexec, }; @@ -1225,6 +1228,7 @@ fn writeNow(p: *Pardes, req: Req, target: Target) Reply { .log => events.writeLog(p, req), .focus => ctl.writeFocus(p, req), .ctl => ctl.writeRoot(p, req), + .pager => ctl.writePager(p, req), .tag => cols.write(p, req, null), .tagexec => cols.writeExec(p, req, null), else => Reply.fail(req.tag, E.PERM), @@ -1420,13 +1424,13 @@ test "readdir lists the root and a pane directory without creating anything" { @memcpy(top_bytes[0..top_reply.bytes.len], top_reply.bytes); const top = th.dirents(top_bytes[0..top_reply.bytes.len], &buf); try testing.expect(top.len >= 10); - for ([_][]const u8{ "README", "index", "status", "focus", "ctl", "commands", "recent", "look", "exec", "log", "screen", "listeners", "layout", "tag", "tagexec", "pane", "col", "os" }, 0..) |name, i| { + for ([_][]const u8{ "README", "index", "status", "focus", "ctl", "commands", "recent", "look", "exec", "pager", "log", "screen", "listeners", "layout", "tag", "tagexec", "pane", "col", "os" }, 0..) |name, i| { try testing.expectEqualStrings(name, top[i].name); - try testing.expectEqual(i >= 15, top[i].dir); + try testing.expectEqual(i >= 16, top[i].dir); } - try testing.expectEqual(@intFromEnum(TopFile.pane), top[15].node); - try testing.expectEqual(@intFromEnum(TopFile.col), top[16].node); - try testing.expectEqual(fs.os_root, top[17].node); + try testing.expectEqual(@intFromEnum(TopFile.pane), top[16].node); + try testing.expectEqual(@intFromEnum(TopFile.col), top[17].node); + try testing.expectEqual(fs.os_root, top[18].node); var idbuf: [16]u8 = undefined; try testing.expect(th.nameAt(top, try std.fmt.bufPrint(&idbuf, "{d}", .{serial})) == null); try testing.expect(th.nameAt(top, "self") == null); |
