diff options
| -rw-r--r-- | src/9p_io.zig | 5 | ||||
| -rw-r--r-- | src/fs.zig | 7 | ||||
| -rw-r--r-- | src/ninep/pane.zig | 1 | ||||
| -rw-r--r-- | src/ninep/tree.zig | 76 | ||||
| -rw-r--r-- | src/pardes.zig | 2 | ||||
| -rw-r--r-- | test/selfmount.py | 14 |
6 files changed, 92 insertions, 13 deletions
diff --git a/src/9p_io.zig b/src/9p_io.zig index 57df0c5f..cb345b00 100644 --- a/src/9p_io.zig +++ b/src/9p_io.zig @@ -266,7 +266,7 @@ pub const Listener = struct { const restores = pardes.turn.restores; // Asked first: a release that runs a held line has none after. const changes = pardes.ctlfs.changesPane(core, req); - const fills = pardes.ctlfs.holdsPage(core, req); + const fills = pardes.ctlfs.handsOff(core, req); const reply = core.serveFs(req); if (req.op == .release and quiet) l.collectOs(); // A read with nothing yet stays parked in the engine, and the core @@ -301,7 +301,8 @@ pub const Listener = struct { // whole after the wait. l.kick(); // A read that put a /pager open's text in (ctl.fillPager), or a - // close that handed it to the editor's step, waits on nothing the + // close that handed its text or last line to the editor's step + // (ctl.fillClosedPagers, tree.runClosedLines), waits on nothing the // editor performs: its answer goes now, while the turn still keeps // a read's payload whole. Waiting would hang a close through a // mount: 9ns answers nothing else on it while a clunk is out, and @@ -1636,6 +1636,11 @@ pub const Namespace = struct { /// a close through a mount may not wait for the editor to be quiet, /// as 9ns answers nothing else on that mount while a clunk is out. pages: std.ArrayList(struct { serial: u32, bytes: std.ArrayList(u8) }) = .empty, + /// The last line each closed open wrote with no newline after it, with + /// the node it was written to, for the editor's step to run + /// (tree.runClosedLines): a close is answered at once, for the same + /// reason as `pages`. + closed_lines: std.ArrayList(struct { node: u64, bytes: std.ArrayList(u8) }) = .empty, batch: struct { serial: u32 = 0, node: u64 = 0, @@ -1685,6 +1690,8 @@ pub const Namespace = struct { for (&st.opens) |*o| o.deinit(gpa); for (st.pages.items) |*page| page.bytes.deinit(gpa); st.pages.deinit(gpa); + for (st.closed_lines.items) |*line| line.bytes.deinit(gpa); + st.closed_lines.deinit(gpa); st.log.deinit(gpa); st.out.deinit(gpa); } 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")); diff --git a/src/pardes.zig b/src/pardes.zig index 5d9f8606..612a09aa 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -5756,6 +5756,8 @@ pub const Pardes = struct { ctlfs.pane.flushPausedBatch(p); // ...and what a /pager open was written before its close. ctlfs.ctl.fillClosedPagers(p); + // ...and the last line of an open closed with no newline after it. + ctlfs.runClosedLines(p); while (p.nextQueued()) |ev| p.update(ev); while (p.nextEffect()) |e| p.perform(e); turn.settle(); diff --git a/test/selfmount.py b/test/selfmount.py index 57dfffa8..8d78487d 100644 --- a/test/selfmount.py +++ b/test/selfmount.py @@ -196,6 +196,18 @@ def run(binary): while (said := client.read(f'/pane/{paged[0]}/body')) != b'red\n' and time.monotonic() < deadline: time.sleep(.05) check(said == b'red\n', f'its body is the plain text ({said!r})') + # `>`'s truncate of exec changes nothing and waits on nothing, + # and a last line with no newline runs once its open closes, at + # the editor's next step, the close answered at once. Either + # waiting on the editor hung: 9ns answers nothing else on the + # mount while a setattr or a clunk is out, and the step may be + # out reading a pane's file through it (the mounted index above). + got = run(f"for i in $(seq 40); do printf 'Msg held-%d' $i > {mount}/exec || exit 9; done\n".encode()) + deadline = time.monotonic() + 5 + while 'held-40\n' not in (log := client.read('/log').decode()) and time.monotonic() < deadline: + time.sleep(.05) + check(got == '0' and all(f' held-{i}\n' in log for i in range(1, 41)), + f'40 closes holding a last line each through the mount, all answered, every line run ({got!r})') # Forty event reads held through the mount, a follower each, and # the mount still answers the rest: 9ns keeps workers past what # the editor holds (128), so `cat layout` is not queued behind @@ -225,8 +237,6 @@ def run(binary): f'a bash read loop on event reads a whole record ({got_line!r})') # Past the session's 64 open records (write opens of exec take # them too) an open is refused EMFILE, "Too many open files". - # (>> here: a truncating open of exec through the mount can - # hang, reported, not yet understood.) many = Path(f'/tmp/{name}.many') ran = run((f"timeout 10 bash -c 'for i in $(seq 80); do exec {{fd}}>>{mount}/exec || exit 9; done' 2> {many}; " f"st=$?; echo rc=$st >> {many}; exit $st\n").encode()) |
