summaryrefslogtreecommitdiff
path: root/src/ninep
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-10-01 04:00:32 -0300
committerGabriel Schneider <[email protected]>2026-10-01 04:00:32 -0300
commit8dc84e6f9f1c3802f1d28de4d49907381cdf224b (patch)
tree3ed7727a61de9ae4c473413d81de3ac95844af78 /src/ninep
parenta4e17d350a7afdeee4916584c44c4770243c84e4 (diff)
downloadpardes-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/ninep')
-rw-r--r--src/ninep/ctl.zig53
-rw-r--r--src/ninep/tree.zig26
2 files changed, 56 insertions, 23 deletions
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);