summaryrefslogtreecommitdiff
path: root/src/ninep
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-10-01 10:45:57 -0300
committerGabriel Schneider <[email protected]>2026-10-01 11:09:59 -0300
commitc5e5db988e59689b44feb42c59090c07687bcd42 (patch)
treecfbe54c55de94676c160d75600393e11d8523bf7 /src/ninep
parent25844b54fd08dd9cadff2275f2161ee5dd316395 (diff)
downloadpardes-c5e5db988e59689b44feb42c59090c07687bcd42.tar.gz
pardes-c5e5db988e59689b44feb42c59090c07687bcd42.zip
`> exec` through a mount no longer hangs the editor, and a close holding a last line is answered at once: the line runs at the editor's next step
A shell's truncating open of exec (or look, ctl, log, pager) sends a setattr that changes nothing; it waited for the editor to be quiet, and through a 9ns mount that quiet could never come: 9ns answers nothing else on the mount while the setattr is out, and the editor's step may be out reading a pane's file through it. This was the "truncating open of exec hangs, only in selfmount" mystery. Only a truncate of a pane's file or a tag waits now. A close holding a last line with no newline hands it to the editor's step (tree.runClosedLines, beside fillClosedPagers), as a /pager close does, and is answered at once; a refusal is the log's err. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src/ninep')
-rw-r--r--src/ninep/pane.zig1
-rw-r--r--src/ninep/tree.zig76
2 files changed, 68 insertions, 9 deletions
diff --git a/src/ninep/pane.zig b/src/ninep/pane.zig
index e2ec31d1..c496571f 100644
--- a/src/ninep/pane.zig
+++ b/src/ninep/pane.zig
@@ -1571,6 +1571,7 @@ test "a name cut across writes is one name, applied once at its newline or its c
_ = call(p, .{ .tag = 2, .op = .write, .node = node, .handle = h3, .data = "/tmp/pardes-closed.txt" });
try testing.expectEqualStrings("/tmp/pardes-name.txt", nameOf(p, p.panes[p.paneBySerial(serial).?].?));
_ = call(p, .{ .tag = 3, .op = .release, .node = node, .handle = h3, .opened = true });
+ tree.runClosedLines(p);
try testing.expectEqualStrings("/tmp/pardes-closed.txt", nameOf(p, p.panes[p.paneBySerial(serial).?].?));
}
diff --git a/src/ninep/tree.zig b/src/ninep/tree.zig
index 5499a337..b0ffdd68 100644
--- a/src/ninep/tree.zig
+++ b/src/ninep/tree.zig
@@ -87,18 +87,34 @@ pub const features: cloud9.fs.Features = .{ .remove = true, .create = true };
/// so what parks here is never the syscall's own request.
pub fn needsQuiet(p: *Pardes, req: Req) bool {
return switch (req.op) {
- .write, .setattr => true,
- .release => req.remove or holdsLines(p, req),
+ .write => true,
+ .setattr => truncates(req),
+ .release => req.remove,
.read => holdsPage(p, req),
else => false,
};
}
+/// A setattr that changes something: a truncate of a pane's file or a
+/// tag. `> exec` (a shell's truncating open) changes nothing, so it waits
+/// on nothing: through a mount it hung, as 9ns answers nothing else on the
+/// mount while it is out, and the editor's step may be out reading a pane's
+/// file through that mount, the quiet it waited for never coming.
+fn truncates(req: Req) bool {
+ if (!req.truncate) return false;
+ return switch (Node.target(req.node) orelse return false) {
+ .pane => true,
+ .col => |c| c.file == .tag,
+ .top => |f| f == .tag,
+ };
+}
+
/// `needsQuiet`, plus the open that makes a pane: what costs a frame and a
/// wake of the editor.
pub fn changesPane(p: *Pardes, req: Req) bool {
return switch (req.op) {
- .write, .setattr => true,
+ .write => true,
+ .setattr => truncates(req),
.open => req.node == @intFromEnum(TopFile.new),
.release => req.remove or holdsLines(p, req) or holdsPage(p, req),
.read => holdsPage(p, req),
@@ -973,13 +989,11 @@ fn releaseHandle(p: *Pardes, req: Req) void {
p.fs.pages.append(p.gpa, .{ .serial = o.what.pager, .bytes = o.pending }) catch o.pending.deinit(p.gpa);
o.pending = .empty;
}
- // The last line an open wrote with no newline after it runs now.
+ // The last line an open wrote with no newline after it runs at the
+ // editor's next step (runClosedLines), as a /pager open's text does.
if (o.pending.items.len > 0) {
- const rest = p.scratch.allocator().dupe(u8, o.pending.items) catch "";
- o.pending.clearRetainingCapacity();
- const w: Req = .{ .tag = req.tag, .op = .write, .node = req.node, .handle = req.handle, .data = rest };
- const reply = writeNow(p, w, t);
- if (reply.status == .err) events.noteError(p, w, reply);
+ p.fs.closed_lines.append(p.gpa, .{ .node = req.node, .bytes = o.pending }) catch o.pending.deinit(p.gpa);
+ o.pending = .empty;
}
const of: ?*Pane = if (t == .pane) (if (p.paneBySerial(t.pane.serial)) |id| p.panes[id] else null) else null;
if (of) |pn| switch (o.what) {
@@ -1169,6 +1183,25 @@ fn linesOpen(target: Target, req: Req) bool {
return req.omode & 3 != 0 or resultsFile(target);
}
+/// The editor's step: the last lines of closed opens run, each as a write
+/// of its own; a refusal is the log's err, as no open is left to answer.
+pub fn runClosedLines(p: *Pardes) void {
+ for (p.fs.closed_lines.items) |*line| {
+ defer line.bytes.deinit(p.gpa);
+ const t = Node.target(line.node) orelse continue;
+ const w: Req = .{ .tag = 0, .op = .write, .node = line.node, .data = line.bytes.items };
+ const reply = writeNow(p, w, t);
+ if (reply.status == .err) events.noteError(p, w, reply);
+ }
+ p.fs.closed_lines.clearRetainingCapacity();
+}
+
+/// A close that hands its open's held text, a last line or a /pager's
+/// page, to the editor's step: answered at once, never waiting on it.
+pub fn handsOff(p: *Pardes, req: Req) bool {
+ return (req.op == .release and holdsLines(p, req)) or holdsPage(p, req);
+}
+
/// A release that will run a line its open held back.
fn holdsLines(p: *Pardes, req: Req) bool {
const o = openOf(p, req) orelse return false;
@@ -1806,6 +1839,24 @@ test "at the pane cap, pane/new, look and New each say so, and look reads back e
try testing.expect(std.mem.indexOf(u8, made.reply.ename, said) != null);
}
+test "nothing a mount's close or a no-op truncate sends waits on the editor: a held line runs at its next step" {
+ const p = try withFile(testing.allocator, "x\n");
+ defer p.deinit();
+ const exec = @intFromEnum(TopFile.exec);
+ // `> exec` truncates nothing; `> body` does.
+ try testing.expect(!needsQuiet(p, .{ .tag = 1, .op = .setattr, .node = exec, .truncate = true }));
+ try testing.expect(!changesPane(p, .{ .tag = 1, .op = .setattr, .node = exec, .truncate = true }));
+ try testing.expect(needsQuiet(p, .{ .tag = 1, .op = .setattr, .node = Node.of(serialOf(p), .body), .truncate = true }));
+ const h = call(p, .{ .tag = 2, .op = .open, .node = exec, .omode = 1 }).reply.handle;
+ _ = call(p, .{ .tag = 3, .op = .write, .node = exec, .handle = h, .data = "Msg at-step" });
+ const close: Req = .{ .tag = 4, .op = .release, .node = exec, .handle = h, .opened = true };
+ try testing.expect(!needsQuiet(p, close) and handsOff(p, close));
+ _ = call(p, close);
+ try testing.expect(!th.logHas(p, " at-step\n"));
+ runClosedLines(p);
+ try testing.expect(th.logHas(p, " at-step\n"));
+}
+
test "a command line cut across writes runs once whole, and the last runs at release; an Edit block waits for its end" {
const p = try th.withFile(testing.allocator, "x\n");
defer p.deinit();
@@ -1821,6 +1872,7 @@ test "a command line cut across writes runs once whole, and the last runs at rel
try testing.expect(th.logHas(p, " hello\n") and th.logHas(p, " world\n"));
try testing.expect(!th.logHas(p, " he\n") and !th.logHas(p, " tail\n"));
_ = call(p, .{ .tag = 3, .op = .release, .node = exec, .handle = h, .opened = true });
+ runClosedLines(p);
try testing.expect(th.logHas(p, " tail\n"));
// An Edit block's text lines come in later writes on the same open.
const serial = serialOf(p);
@@ -1829,6 +1881,7 @@ test "a command line cut across writes runs once whole, and the last runs at rel
for ([_][]const u8{ "Edit ,a\n", "one\ntw", "o\n", ".\n" }) |piece|
try testing.expectEqual(Status.ok, call(p, .{ .tag = 5, .op = .write, .node = ctl_node, .handle = c, .data = piece }).reply.status);
_ = call(p, .{ .tag = 6, .op = .release, .node = ctl_node, .handle = c, .opened = true });
+ runClosedLines(p);
try testing.expectEqualStrings("x\none\ntwo\n", p.panes[0].?.file.?.content);
}
@@ -1878,6 +1931,7 @@ test "a write with no newline waits for its newline or the close, whatever its s
try testing.expectEqual(Status.ok, call(p, .{ .tag = 6, .op = .write, .node = ctl_node, .handle = c, .data = "bogus" }).reply.status);
try testing.expect(!th.logHas(p, "bogus"));
_ = call(p, .{ .tag = 7, .op = .release, .node = ctl_node, .handle = c, .opened = true });
+ runClosedLines(p);
try testing.expect(th.logHas(p, "unknown control message \"bogus\""));
// A name: a NUL fails its write, not the close after it.
const name = Node.of(serial, .name);
@@ -1886,6 +1940,7 @@ test "a write with no newline waits for its newline or the close, whatever its s
try testing.expectEqual(Status.err, bad.reply.status);
try testing.expect(std.mem.indexOf(u8, bad.reply.ename, "a NUL") != null);
try testing.expectEqual(Status.ok, call(p, .{ .tag = 12, .op = .release, .node = name, .handle = n, .opened = true }).reply.status);
+ runClosedLines(p);
}
test "look and exec read back what their own open's write touched; a fresh open reads the session's last" {
@@ -1973,6 +2028,7 @@ test "an Edit block still open when its open closes fails there with an err and
const c = call(p, .{ .tag = 1, .op = .open, .node = ctl_node, .omode = 1 }).reply.handle;
_ = call(p, .{ .tag = 2, .op = .write, .node = ctl_node, .handle = c, .data = "Edit ,x/b/{\ni/[/\n" });
_ = call(p, .{ .tag = 3, .op = .release, .node = ctl_node, .handle = c, .opened = true });
+ runClosedLines(p);
try testing.expect(th.logHas(p, "unmatched"));
try testing.expectEqualStrings("abc\n", p.panes[0].?.file.?.content);
}
@@ -1984,6 +2040,7 @@ test "an a, c or i text still open when its open closes fails there too and chan
const c = call(p, .{ .tag = 1, .op = .open, .node = ctl_node, .omode = 1 }).reply.handle;
_ = call(p, .{ .tag = 2, .op = .write, .node = ctl_node, .handle = c, .data = "Edit $a\nhalf of a block\n" });
_ = call(p, .{ .tag = 3, .op = .release, .node = ctl_node, .handle = c, .opened = true });
+ runClosedLines(p);
try testing.expect(th.logHas(p, "not ended by a . line"));
try testing.expectEqualStrings("abc\n", p.panes[0].?.file.?.content);
}
@@ -1995,6 +2052,7 @@ test "an a, c or i text cut short through exec is an err in the log, not a msg"
const c = call(p, .{ .tag = 1, .op = .open, .node = exec, .omode = 1 }).reply.handle;
_ = call(p, .{ .tag = 2, .op = .write, .node = exec, .handle = c, .data = "Edit $a\nhalf\n" });
_ = call(p, .{ .tag = 3, .op = .release, .node = exec, .handle = c, .opened = true });
+ runClosedLines(p);
var want: [64]u8 = undefined;
try testing.expect(th.logHas(p, try std.fmt.bufPrint(&want, "err {d} exec: Edit: a, c or i text not ended", .{serialOf(p)})));
try testing.expect(!th.logHas(p, "msg 1 Edit: a, c or i"));