diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-29 00:50:45 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:15 -0300 |
| commit | 7aa057e29b729ad6d9ac53e3a06f99858fc449e4 (patch) | |
| tree | a0010efb342018f32f98d9e805680306feb88836 | |
| parent | e563be42a5b3e9243e2c01d34cf8e94d2047b753 (diff) | |
| download | pardes-7aa057e29b729ad6d9ac53e3a06f99858fc449e4.tar.gz pardes-7aa057e29b729ad6d9ac53e3a06f99858fc449e4.zip | |
A column has a ctl, an exec and an rmdir over 9P, and Restore maps its serial
A script could read and write a column's tag but not act on the column.
col/<serial>/ctl takes Delcol, Joincol, New and Tty, each acting on that
column as from its tag; col/<serial>/exec runs a word as a middle click in
the tag; rmdir col/<serial> closes an empty column and refuses one with
panes (ENOTEMPTY, an err record saying why). The root ctl's refusal of
Delcol and Joincol points at col/<serial>/ctl. A dump keeps each column's
serial and Restore logs restoredcol <old> <new>; the dump golden gains
them. Documented: Joincol's survivor, the column on the right, keeps its
serial and tag.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
| -rw-r--r-- | .agents/skills/pardes-9p/SKILL.md | 2 | ||||
| -rw-r--r-- | docs/fs.md | 13 | ||||
| -rw-r--r-- | src/dump.zig | 25 | ||||
| -rw-r--r-- | src/fs-help.txt | 1 | ||||
| -rw-r--r-- | src/ninep/cols.zig | 82 | ||||
| -rw-r--r-- | src/ninep/ctl.zig | 11 | ||||
| -rw-r--r-- | src/ninep/events.zig | 4 | ||||
| -rw-r--r-- | src/ninep/tree.zig | 38 | ||||
| -rw-r--r-- | src/pardes.zig | 2 | ||||
| -rw-r--r-- | test/fs.py | 2 | ||||
| -rw-r--r-- | test/snapshots/dump.golden | 2 |
11 files changed, 161 insertions, 21 deletions
diff --git a/.agents/skills/pardes-9p/SKILL.md b/.agents/skills/pardes-9p/SKILL.md index 8c383962..844e028f 100644 --- a/.agents/skills/pardes-9p/SKILL.md +++ b/.agents/skills/pardes-9p/SKILL.md @@ -84,6 +84,8 @@ $m/ctl the settings, one a line as a write takes them; write a setting or $m/commands every builtin: `Word`, `Word arg`, then `root` or `pane` (which ctl takes it) $m/layout one line per column: serial index x width current|notcurrent empty|full pane-serials...; active <serial> $m/tag the workspace tag (> replaces, >> appends, one line); $m/col/<serial>/tag a column's (serials stay, as panes' do) +$m/col/<serial>/ctl Delcol, Joincol, New, Tty on that column; col/<serial>/exec a word as a click in its tag; + rmdir col/<serial> closes an empty column $m/pane/new open it to make a pane (a scratch named <dir>/+New), read names it; it goes in the ACTIVE column (the one last typed or clicked in, or Newcol's), filling it if empty, else taking the bottom half of its last pane (ctl `Placement pardes`: the old rules); @@ -111,6 +111,9 @@ Existing Plan9port/v9fs clients need a userspace bridge for QUIC. pane/new and a look place a pane next (- when there is none) /tag the workspace tag; > replaces it, >> appends, one line /col/<n>/tag the tag of the column with serial n, the same way +/col/<n>/ctl write Delcol, Joincol, New or Tty: each acts on that column, as from its tag +/col/<n>/exec write a word: a middle click on it in that column's tag; rmdir col/<n> closes + an empty column (a column with panes is refused, ENOTEMPTY) /pane/new open it to make a pane; the read answers that pane's serial. A session holds 64 panes (16 on the board); at that, every route that would open one -- this open, look, exec, New, Tty -- fails with `no pane slots: 64 max` and an err @@ -130,7 +133,12 @@ clears it and drops the newline that ends it, as a pane tag's does. A column is named by its serial, as a pane is: it stays while the column lives, whatever opens or closes beside it, and is never reused; /layout gives each column's serial and its index left to right, and the log says -`newcol <serial>` and `delcol <serial>` as columns come and go. +`newcol <serial>` and `delcol <serial>` as columns come and go, and after a +Restore `restoredcol <old> <new>` for each column as `restored` does for +panes. Joincol folds a column into the one on its right, which keeps its own +serial and tag; the joined one's serial is gone (`delcol`). The root ctl +takes no column word: its `Delcol` is refused, pointing at +`col/<serial>/ctl`. Control messages are split by what they act on, as acme keeps window verbs on a window's ctl and webfs and upas/fs keep session settings on a root ctl. @@ -563,7 +571,8 @@ the `del` of a terminal whose shell exited by itself, `ask <serial> <what> and `save <serial> <name>`, `dump <path>` when a Dump is written and `restore <path>` in a Restore's new log after its panes' `new`s, then `restored <old> <new>` for each pane, -mapping the serial it had to the one it has now, and `msg <serial|-> <text>` +mapping the serial it had to the one it has now, and `restoredcol <old> +<new>` for each column, and `msg <serial|-> <text>` for every line the editor says, repeats included (with `verbose` on, that includes each builtin announcing itself as it runs, on purpose: the log says which ran; its serial is the pane it ran at, `-` when the keyboard was on a diff --git a/src/dump.zig b/src/dump.zig index 61aeabbf..5b0983ad 100644 --- a/src/dump.zig +++ b/src/dump.zig @@ -180,6 +180,9 @@ pub const Pane = struct { }; pub const Column = struct { + /// Its serial when dumped (0 in older dumps): a Restore logs + /// `restoredcol <old> <new>` for it. + serial: u32 = 0, tag: ?[]const u8 = null, weight: f32 = 1, panes: []const usize = &.{}, @@ -665,6 +668,7 @@ pub fn dumpState(p: *Pardes) !void { ids_len += 1; }; columns[columns_len] = .{ + .serial = layout.columnSerial(p, c), .tag = p.col_tags[c].own, .weight = @as(f32, @floatFromInt(p.col_weight[c])) / @as(f32, @floatFromInt(column_weight_unit)), .panes = column_ids[c][0..ids_len], @@ -733,14 +737,18 @@ pub fn restore(p: *Pardes, zon_bytes: []const u8, from: []const u8) !*Pardes { var opts = p.opts; opts.cols = p.screen_w; opts.rows = p.screen_h; - var old: [MAX_PANES]u32 = @splat(0); + var old: OldSerials = .{}; const replacement = try initDump(p.gpa, opts, zon_bytes, p, &old); pardes.ctlfs.events.notePath(replacement, "restore", from); - // Which serial each pane had, for a script that held the old ones. - for (replacement.panes, old) |slot, was| if (slot) |pane| if (was != 0) { + // Which serial each pane and column had, for a script that held them. + for (replacement.panes, old.panes) |slot, was| if (slot) |pane| if (was != 0) { var rec: [32]u8 = undefined; pardes.ctlfs.events.notePath(replacement, "restored", std.fmt.bufPrint(&rec, "{d} {d}", .{ was, pane.serial }) catch continue); }; + for (old.cols[0..replacement.ncol], 0..) |was, c| if (was != 0) { + var rec: [32]u8 = undefined; + pardes.ctlfs.events.notePath(replacement, "restoredcol", std.fmt.bufPrint(&rec, "{d} {d}", .{ was, layout.columnSerial(replacement, c) }) catch continue); + }; return replacement; } @@ -748,7 +756,13 @@ pub fn initFromDump(gpa: std.mem.Allocator, opts: Options, zon_bytes: []const u8 return initDump(gpa, opts, zon_bytes, null, null); } -fn initDump(gpa: std.mem.Allocator, opts: Options, zon_bytes: []const u8, previous: ?*const Pardes, old_serials: ?*[MAX_PANES]u32) !*Pardes { +/// The serials a dump's panes and columns had, read as it is restored. +const OldSerials = struct { + panes: [MAX_PANES]u32 = @splat(0), + cols: [MAX_COLS]u32 = @splat(0), +}; + +fn initDump(gpa: std.mem.Allocator, opts: Options, zon_bytes: []const u8, previous: ?*const Pardes, old_serials: ?*OldSerials) !*Pardes { const image_gpa = opts.image_allocator orelse gpa; const pdf_gpa = opts.pdf_allocator orelse gpa; const tree_sitter_gpa = opts.tree_sitter_allocator orelse gpa; @@ -799,7 +813,7 @@ fn initDump(gpa: std.mem.Allocator, opts: Options, zon_bytes: []const u8, previo } for (st.panes, 0..) |src, i| { if (i >= MAX_PANES) break; - if (old_serials) |o| o[i] = src.serial; + if (old_serials) |o| o.panes[i] = src.serial; const pane: *pardes.panes.Pane = switch (src.kind) { .terminal => terminal: { const t = src.terminal.?; @@ -869,6 +883,7 @@ fn initDump(gpa: std.mem.Allocator, opts: Options, zon_bytes: []const u8, previo p.col_serial = @splat(0); if (st.topbar_custom) |bar| p.global_tag.own = try p.gpa.dupe(u8, bar); for (st.columns[0..p.ncol], 0..) |col, c| { + if (old_serials) |o| o.cols[c] = col.serial; if (col.tag) |bar| p.col_tags[c].own = try p.gpa.dupe(u8, bar); const scaled = @as(f64, @floatCast(col.weight)) * @as(f64, @floatFromInt(column_weight_unit)); const bounded = @min(scaled, @as(f64, @floatFromInt(max_column_weight))); diff --git a/src/fs-help.txt b/src/fs-help.txt index 50e9e7fc..08a7e262 100644 --- a/src/fs-help.txt +++ b/src/fs-help.txt @@ -14,6 +14,7 @@ commands every builtin: its word, arg if it takes one, and root or pane: whic layout one line per column: serial index x width current|notcurrent empty|full pane-serials...; then active <serial> tag the workspace tag: read it, > replaces it, >> appends (one line) col/<n>/tag the tag of the column with serial n, the same way (a serial stays while the column lives) +col/<n>/ctl Delcol, Joincol, New or Tty on that column; col/<n>/exec a word as a click in its tag; rmdir col/<n> closes an empty one pane/new open it to make a pane (named +New) in the active column, filling it if empty; the read answers its serial pane/<n>/ name body tag ctl addr dot limit data xdata sel dirty mark scroll errors (write-only: appended to its directory's +Errors pane) event look exec, pty/ on terminals; rmdir closes diff --git a/src/ninep/cols.zig b/src/ninep/cols.zig index 79943b1b..b6dc7d26 100644 --- a/src/ninep/cols.zig +++ b/src/ninep/cols.zig @@ -122,6 +122,52 @@ pub fn released(p: *Pardes, serial: ?u32) void { p.fs.header_held = false; } +/// A column's ctl: the words that act on a column, as its tag's clicks do -- +/// Delcol, Joincol, New, Tty -- one a line, each run as from that tag. +pub fn writeCtl(p: *Pardes, req: Req, serial: u32) Reply { + var lines = std.mem.splitScalar(u8, req.data, '\n'); + while (lines.next()) |raw| { + const line = std.mem.trim(u8, raw, " \t\r"); + if (line.len == 0) continue; + const word = line[0 .. std.mem.indexOfAny(u8, line, " \t") orelse line.len]; + const takes = for ([_][]const u8{ "Delcol", "Joincol", "New", "Tty" }) |w| { + if (std.mem.eql(u8, w, word)) break true; + } else false; + if (!takes) return tree.failText(req.tag, E.INVAL, "a column's ctl takes Delcol, Joincol, New or Tty"); + const col = layout.columnBySerial(p, serial) orelse return Reply.fail(req.tag, E.NOENT); + p.exec_column = col; + defer p.exec_column = null; + if (pardes.ctlfs.ctl.runBuiltin(p, req, p.active, line)) |refusal| return refusal; + } + return .{ .tag = req.tag, .written = @intCast(req.data.len) }; +} + +/// A column's exec: each line run as a middle click on it in that column's +/// tag, acting on that column. +pub fn writeExec(p: *Pardes, req: Req, serial: u32) Reply { + var lines = std.mem.splitScalar(u8, req.data, '\n'); + while (lines.next()) |raw| { + const line = std.mem.trim(u8, raw, " \t\r"); + if (line.len == 0) continue; + for (line) |c| if (c < ' ' and c != '\t') return Reply.fail(req.tag, E.INVAL); + const col = layout.columnBySerial(p, serial) orelse return Reply.fail(req.tag, E.NOENT); + p.exec_column = col; + defer p.exec_column = null; + pardes.ctlfs.ctl.run(p, p.active, true, line); + } + return .{ .tag = req.tag, .written = @intCast(req.data.len) }; +} + +/// rmdir of a column closes it when it is empty; one with panes is +/// refused, saying so (close them, or Delcol on its ctl, which asks first). +pub fn remove(p: *Pardes, req: Req, serial: u32, file: tree.ColFile) Reply { + if (file != .dir) return Reply.fail(req.tag, E.PERM); + const col = layout.columnBySerial(p, serial) orelse return Reply.fail(req.tag, E.NOENT); + if (p.col_n[col] > 0) return tree.failText(req.tag, E.NOTEMPTY, "column not empty: close its panes, or write Delcol to its ctl"); + p.removeColumn(col) catch return Reply.fail(req.tag, E.IO); + return .{ .tag = req.tag }; +} + // ---- tests ---- const testing = std.testing; @@ -200,3 +246,39 @@ test "a focus write makes its pane's column the active one: layout says so and p try testing.expectEqual(@as(usize, 0), layout.findPane(p, p.paneBySerial(made).?).?.col); } } + +test "a column's ctl and exec act on it as its tag would, and rmdir closes it only empty" { + const p = try th.withFile(testing.allocator, "x\n"); + defer p.deinit(); + const first = layout.columnSerial(p, 0); + p.newColumn(p.active); + const empty = layout.columnSerial(p, 1); + // New on the empty column's ctl fills it, wherever the keyboard is. + try testing.expectEqual(tree.Status.ok, th.wr(p, Node.ofCol(empty, .ctl), "New\n").reply.status); + try testing.expectEqual(@as(usize, 1), p.col_n[1]); + try testing.expectEqual(E.INVAL, th.wr(p, Node.ofCol(empty, .ctl), "Exit\n").errno()); + // rmdir refuses a column with a pane, and says why. + const full = th.call(p, .{ .tag = 1, .op = .release, .node = Node.ofCol(empty, .dir), .remove = true }); + try testing.expectEqual(E.NOTEMPTY, full.errno()); + try testing.expect(th.logHas(p, "column not empty")); + // exec runs a word as a click in that column's tag: Delcol closes it. + try testing.expectEqual(tree.Status.ok, th.wr(p, Node.ofCol(empty, .exec), "Delcol\n").reply.status); + try testing.expectEqual(@as(?usize, null), layout.columnBySerial(p, empty)); + // An empty column goes on rmdir. + p.newColumn(p.active); + const made = layout.columnSerial(p, 1); + try testing.expectEqual(tree.Status.ok, th.call(p, .{ .tag = 2, .op = .release, .node = Node.ofCol(made, .dir), .remove = true }).reply.status); + try testing.expectEqual(@as(?usize, null), layout.columnBySerial(p, made)); + try testing.expectEqual(first, layout.columnSerial(p, 0)); + // Joincol folds a column into the one on its right, which keeps its own + // serial and tag. + p.newColumn(p.active); + const right = layout.columnSerial(p, 1); + _ = th.wr(p, Node.ofCol(right, .tag), " Mine"); + try testing.expectEqual(tree.Status.ok, th.wr(p, Node.ofCol(first, .ctl), "Joincol\n").reply.status); + try testing.expectEqual(@as(?usize, null), layout.columnBySerial(p, first)); + try testing.expectEqual(@as(?usize, 0), layout.columnBySerial(p, right)); + try testing.expect(std.mem.endsWith(u8, headerText(p, right).?, " Mine")); + // The root's ctl points a column word at the column's own. + try testing.expect(std.mem.endsWith(u8, th.wr(p, @intFromEnum(tree.TopFile.ctl), "Delcol\n").reply.ename, "write it to col/<serial>/ctl")); +} diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig index 98120555..e1dd3fc5 100644 --- a/src/ninep/ctl.zig +++ b/src/ninep/ctl.zig @@ -51,7 +51,7 @@ fn noteResult(p: *Pardes, serial: u32) void { /// Runs one look or exec from the pane `id`, then records the panes it /// created; failing that, a look records the pane it focused and an exec /// the pane it acted on (even one it closed). -fn run(p: *Pardes, id: usize, exec: bool, text: []const u8) void { +pub fn run(p: *Pardes, id: usize, exec: bool, text: []const u8) void { const context = p.panes[id].?.serial; // A pane an event reader holds hears its clicks instead of acting on // them (editors/acme/look.c:53 and exec.c:173 send them to winevent), @@ -277,7 +277,12 @@ fn checkBuiltin(p: *Pardes, req: Req, line: []const u8, scope: builtins.Scope) ? }; // Edit is the one pane word the root takes too: at the active pane. if (builtins.registry.scope(b) != scope and b != .Edit) - return refuseTo(p, req, if (scope == .pane) "not a window control message" else "not a session control message", line, if (scope == .pane) "/ctl" else "pane/<n>/ctl"); + return refuseTo(p, req, if (scope == .pane) "not a window control message" else "not a session control message", line, if (scope == .pane) + "/ctl" + else if (b == .Delcol or b == .Joincol) + "col/<serial>/ctl" // a column's own ctl, the one /layout names + else + "pane/<n>/ctl"); if (arg.len > 0 and !builtins.registry.takesArg(b)) return refuse(p, req, "wrong #args in control message", line); if (arg.len == 0 and builtins.registry.requiresArg(b)) return refuse(p, req, "wrong #args in control message", line); const setting = config.Runtime.find(word) orelse return null; @@ -308,7 +313,7 @@ fn checkBuiltin(p: *Pardes, req: Req, line: []const u8, scope: builtins.Scope) ? /// would. Answers the refusal when it would have opened a prompt for an /// argument, which nobody writing to a file is there to type, or when it /// reported an error: `Mount: AlreadyMounted "Mount peer /tmp/s"`. -fn runBuiltin(p: *Pardes, req: Req, id: usize, line: []const u8) ?Reply { +pub fn runBuiltin(p: *Pardes, req: Req, id: usize, line: []const u8) ?Reply { p.fs.no_prompt = true; p.fs.refused = false; p.fs.failure_len = 0; diff --git a/src/ninep/events.zig b/src/ninep/events.zig index e918f09a..ccc61352 100644 --- a/src/ninep/events.zig +++ b/src/ninep/events.zig @@ -944,7 +944,9 @@ test "a Dump written and a Restore made are in the log, with their files" { const log = try freezeLog(restored); defer restored.gpa.free(log.bytes); var want: [64]u8 = undefined; - try testing.expect(std.mem.endsWith(u8, log.bytes, try std.fmt.bufPrint(&want, "restore /tmp/pardes.dump.zon\nrestored {d} {d}\n", .{ p.panes[0].?.serial, restored.panes[0].?.serial }))); + try testing.expect(std.mem.indexOf(u8, log.bytes, try std.fmt.bufPrint(&want, "restore /tmp/pardes.dump.zon\nrestored {d} {d}\n", .{ p.panes[0].?.serial, restored.panes[0].?.serial })) != null); + // ...and each column's: restoredcol <old> <new>. + try testing.expect(std.mem.endsWith(u8, log.bytes, try std.fmt.bufPrint(&want, "restoredcol {d} {d}\n", .{ pardes.layout.columnSerial(p, 0), pardes.layout.columnSerial(restored, 0) }))); } test "opens of the log share the open records, and a closed one frees its record" { diff --git a/src/ninep/tree.zig b/src/ninep/tree.zig index c4ade1f7..baf8c772 100644 --- a/src/ninep/tree.zig +++ b/src/ninep/tree.zig @@ -272,7 +272,16 @@ pub const PaneFile = enum(u5) { /// /col/<serial> and its tag: a column is named by its serial, which stays /// while it lives as a pane's does; this bit, past a pane serial's 32, keeps /// the two apart. -pub const ColFile = enum(u5) { dir = 0, tag = 1 }; +pub const ColFile = enum(u5) { + dir = 0, + tag = 1, + ctl = 2, + exec = 3, + + pub fn fileName(f: ColFile) []const u8 { + return @tagName(f); + } +}; const col_bit: u59 = 1 << 58; pub const Node = packed struct(u64) { @@ -454,10 +463,14 @@ fn attrOf(p: *Pardes, target: Target) ?Reply.Attr { .col => |c| { if (layout.columnBySerial(p, c.serial) == null) return null; return .{ - .name = if (c.file == .dir) (std.fmt.bufPrint(&p.fs.node_name, "{d}", .{c.serial}) catch unreachable) else "tag", + .name = if (c.file == .dir) (std.fmt.bufPrint(&p.fs.node_name, "{d}", .{c.serial}) catch unreachable) else c.file.fileName(), .node = Node.ofCol(c.serial, c.file), .dir = c.file == .dir, - .mode = if (c.file == .dir) 0o755 else 0o666, + .mode = switch (c.file) { + .dir => 0o755, + .ctl, .exec => 0o222, + .tag => 0o666, + }, .size = if (c.file == .tag) cols.headerText(p, c.serial).?.len + 1 else 0, .mtime = p.fs.started, .append = c.file == .tag, @@ -562,8 +575,9 @@ fn lookup(p: *Pardes, req: Req, target: Target) Reply { }, .col => |c| col: { if (c.file != .dir) return Reply.fail(req.tag, E.NOTDIR); - if (!std.mem.eql(u8, name, "tag")) return Reply.fail(req.tag, E.NOENT); - break :col Node.ofCol(c.serial, .tag); + const f = std.meta.stringToEnum(ColFile, name) orelse return Reply.fail(req.tag, E.NOENT); + if (f == .dir) return Reply.fail(req.tag, E.NOENT); + break :col Node.ofCol(c.serial, f); }, .pane => |t| pane: { _ = p.paneBySerial(t.serial) orelse return Reply.fail(req.tag, E.NOENT); @@ -638,7 +652,9 @@ fn readdir(p: *Pardes, req: Req, target: Target) Reply { .col => |c| { if (layout.columnBySerial(p, c.serial) == null) return Reply.fail(req.tag, E.NOENT); if (c.file != .dir) return Reply.fail(req.tag, E.NOTDIR); - if (skip > 0) skip -= 1 else stageDirent(out, p.gpa, Node.ofCol(c.serial, .tag), false, "tag"); + inline for (.{ ColFile.tag, ColFile.ctl, ColFile.exec }) |f| { + if (skip > 0) skip -= 1 else stageDirent(out, p.gpa, Node.ofCol(c.serial, f), false, f.fileName()); + } }, .pane => |t| { const id = p.paneBySerial(t.serial) orelse return Reply.fail(req.tag, E.NOENT); @@ -729,7 +745,8 @@ fn remove(p: *Pardes, req: Req) Reply { const target = Node.target(req.node) orelse return Reply.fail(req.tag, E.NOENT); const t = switch (target) { .pane => |t| t, - .top, .col => return Reply.fail(req.tag, E.PERM), + .top => return Reply.fail(req.tag, E.PERM), + .col => |c| return cols.remove(p, req, c.serial, c.file), }; if (t.file != .dir) return Reply.fail(req.tag, E.PERM); const id = p.paneBySerial(t.serial) orelse return Reply.fail(req.tag, E.NOENT); @@ -902,7 +919,12 @@ fn write(p: *Pardes, req: Req, target: Target) Reply { if (t.file.inPty() and !pn.isTerminal()) return Reply.fail(req.tag, E.NOENT); return pane.write(p, req, id, pn, t.file); }, - .col => |c| return if (c.file == .tag) cols.write(p, req, c.serial) else Reply.fail(req.tag, E.PERM), + .col => |c| return switch (c.file) { + .tag => cols.write(p, req, c.serial), + .ctl => cols.writeCtl(p, req, c.serial), + .exec => cols.writeExec(p, req, c.serial), + .dir => Reply.fail(req.tag, E.PERM), + }, } } diff --git a/src/pardes.zig b/src/pardes.zig index a6a088de..8b3687a0 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -5309,7 +5309,7 @@ pub const Pardes = struct { p.raw_hover_intent = false; look.cancelLookHover(p); const reply = ctlfs.handle(p, req); - if (reply.status == .err and (req.op == .write or req.op == .setattr or (req.op == .open and reply.errno == ctlfs.E.NFILE))) ctlfs.events.noteError(p, req, reply); + if (reply.status == .err and (req.op == .write or req.op == .setattr or (req.op == .open and reply.errno == ctlfs.E.NFILE) or (req.op == .release and req.remove))) ctlfs.events.noteError(p, req, reply); // The request was a whole step of its own, so it settles the way a // step does: the cursor and scroll reconciled, the scripted panes // told, and the panes it made announced to /log now rather than at @@ -728,7 +728,7 @@ def test(binary, quic=False): log = restored.read('/log') assert b'\nrestore ' + str(saved).encode() + b'\n' in log, log # then which serial each pane had: restored <old> <new> - assert log.endswith(b'\n'.join(b'restored %d %d' % (old, new) for old, new in zip(sorted(old_ids), sorted(int(r.split()[0]) for r in restored.read('/index').splitlines()))) + b'\n'), log + assert (b'\n'.join(b'restored %d %d' % (old, new) for old, new in zip(sorted(old_ids), sorted(int(r.split()[0]) for r in restored.read('/index').splitlines()))) + b'\nrestoredcol ') in log, log restored_ids = [int(row.split()[0]) for row in restored.read('/index').splitlines()] assert len(restored_ids) == 2 and old_ids.isdisjoint(restored_ids), restored_ids file_id, control_id = restored_ids diff --git a/test/snapshots/dump.golden b/test/snapshots/dump.golden index 277d3d34..9220c450 100644 --- a/test/snapshots/dump.golden +++ b/test/snapshots/dump.golden @@ -15,10 +15,12 @@ }, .tree_context_tag_style = true, .columns = .{ .{ + .serial = 1, .tag = null, .weight = 1, .panes = .{ 0, 1 }, }, .{ + .serial = 2, .tag = null, .weight = 1, .panes = .{2}, |
