diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-29 12:54:24 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:16 -0300 |
| commit | 7ec97214a7a6ee905167b7f94a800a8c1d70b100 (patch) | |
| tree | e765f9343ad1b6f7b87ec6aa33b98e0ea4b83f94 /src/ninep/tree.zig | |
| parent | d622163fa4c7ecb5f9ab47afb995199edcfb3d7f (diff) | |
| download | pardes-7ec97214a7a6ee905167b7f94a800a8c1d70b100.tar.gz pardes-7ec97214a7a6ee905167b7f94a800a8c1d70b100.zip | |
A listing freezes at its open, so a read in several chunks never splices two moments
/index and the other listings were rendered afresh for each read, so a client reading one in chunks while panes changed got rows from two moments spliced together (the fs-test flake). /index, /layout, /recent, /commands, /status, /listeners and a ctl opened only to read now freeze at the open, as /screen does.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src/ninep/tree.zig')
| -rw-r--r-- | src/ninep/tree.zig | 54 |
1 files changed, 52 insertions, 2 deletions
diff --git a/src/ninep/tree.zig b/src/ninep/tree.zig index 66f73e9c..60778b9c 100644 --- a/src/ninep/tree.zig +++ b/src/ninep/tree.zig @@ -134,6 +134,10 @@ pub const Open = struct { /// An open writing a file pane's body, data or xdata: its writes are /// one undo step (pane.zig `joined_by`), another open's its own. writes, + /// A listing (`/index`, `/layout`, `/recent`, `/commands`, `/status`, + /// `/listeners`, a ctl opened to read) frozen at the open, as + /// `/screen` is: reads in several chunks never splice two moments. + frozen: []const u8, } = .{ .snapshot = null }, /// What a write to a file of command lines left unfinished: a last line /// with no newline yet, or an Edit block still open. A write through a @@ -183,6 +187,7 @@ pub const Open = struct { gpa.free(run.output); gpa.free(run.line); }, + .frozen => |bytes| gpa.free(bytes), .event, .pty_data, .ctl, .lines, .writes => {}, } o.pending.deinit(gpa); @@ -744,11 +749,14 @@ fn open(p: *Pardes, req: Req, target: Target) Reply { }, .screen => .{ .snapshot = null }, .log => .{ .log = .{} }, + .index, .layout, .recent, .commands, .status, .listeners => .{ .frozen = &.{} }, + // The root's ctl opened only to read: its settings, frozen. + .ctl => if (linesOpen(target, req)) .lines else .{ .frozen = &.{} }, else => if (linesOpen(target, req)) .lines else return .{ .tag = req.tag }, }, .col => |c| if (layout.columnBySerial(p, c.serial) == null) return Reply.fail(req.tag, E.NOENT) - else if (linesOpen(target, req)) .lines else return .{ .tag = req.tag }, + else if (linesOpen(target, req)) .lines else if (c.file == .ctl) .{ .frozen = &.{} } else return .{ .tag = req.tag }, .pane => |t| what: { const pn = p.panes[p.paneBySerial(t.serial) orelse return Reply.fail(req.tag, E.NOENT)].?; if (t.file.inPty() and !pn.isTerminal()) return Reply.fail(req.tag, E.NOENT); @@ -791,6 +799,13 @@ fn open(p: *Pardes, req: Req, target: Target) Reply { o.* = .{}; return Reply.fail(req.tag, E.NOMEM); }, + .frozen => |*bytes| { + const whole = readFile(p, .{ .tag = req.tag, .op = .read, .node = req.node, .size = 1 << 22 }, target); + bytes.* = if (whole.status == .ok) p.gpa.dupe(u8, p.fsPayload(whole)) catch { + o.* = .{}; + return Reply.fail(req.tag, E.NOMEM); + } else &.{}; + }, .event => |reader| { const pn = pane_of.?; pn.fs.readers +|= 1; @@ -877,7 +892,7 @@ fn releaseHandle(p: *Pardes, req: Req) void { .ctl => if (pn.fs.lock == req.handle) { pn.fs.lock = null; }, - .snapshot, .log, .lines, .writes => {}, + .snapshot, .log, .lines, .writes, .frozen => {}, }; o.deinit(p.gpa); } @@ -897,6 +912,14 @@ fn setattr(p: *Pardes, req: Req, target: Target) Reply { } fn read(p: *Pardes, req: Req, target: Target) Reply { + // A listing reads what it was at the open. + if (openOf(p, req)) |o| if (o.what == .frozen) { + const bytes = o.what.frozen; + const off = @min(req.off, bytes.len); + const n = @min(bytes.len - off, req.size); + p.fs.stage(p.gpa).appendSlice(p.gpa, bytes[off..][0..n]) catch return Reply.fail(req.tag, E.NOMEM); + return .{ .tag = req.tag, .payload = .{ .staged = @intCast(n) } }; + }; const stream = if (openOf(p, req)) |o| switch (o.what) { .log => o, .event => |reader| if (reader) o else null, @@ -1113,6 +1136,33 @@ const look_up = th.look_up; const withFile = th.withFile; const serialOf = th.serialOf; +test "a listing read in chunks is what it was at the open, however the panes change between reads" { + const p = try th.withFile(testing.allocator, "x\n"); + defer p.deinit(); + for ([_]TopFile{ .index, .layout, .recent, .ctl }) |file| { + const node = @intFromEnum(file); + const whole = try testing.allocator.dupe(u8, th.rd(p, node, 0, 1 << 16).bytes); + defer testing.allocator.free(whole); + const h = th.call(p, .{ .tag = 1, .op = .open, .node = node }).reply.handle; + try testing.expect(h != 0); + var got: std.ArrayList(u8) = .empty; + defer got.deinit(testing.allocator); + try got.appendSlice(testing.allocator, th.call(p, .{ .tag = 2, .op = .read, .node = node, .handle = h, .size = 7 }).bytes); + // Something changes between the chunks. + const made = try th.newPane(p); + _ = th.wr(p, Node.of(made, .name), "/tmp/pardes-frozen-listing.txt\n"); + _ = th.wr(p, @intFromEnum(TopFile.ctl), "Verbose off\n"); + while (true) { + const more = th.call(p, .{ .tag = 2, .op = .read, .node = node, .handle = h, .off = got.items.len, .size = 7 }).bytes; + if (more.len == 0) break; + try got.appendSlice(testing.allocator, more); + } + _ = th.call(p, .{ .tag = 3, .op = .release, .node = node, .handle = h, .opened = true }); + try testing.expectEqualStrings(whole, got.items); + _ = th.wr(p, @intFromEnum(TopFile.ctl), "Verbose on\n"); + } +} + test "filesystem inspection preserves pending and displayed Look hover" { const delay = config.look_preview_delay_frames orelse return; const gpa = testing.allocator; |
