diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-30 11:14:09 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:17 -0300 |
| commit | c820afd8601ef14fda651c63f542476e12adcf11 (patch) | |
| tree | 181aca8abdffa387f1eff3a9bd8f6acf109a5deb /src/ninep | |
| parent | f18b1adc04dad68e115bb70c42e26472733daf1d (diff) | |
| download | pardes-c820afd8601ef14fda651c63f542476e12adcf11.tar.gz pardes-c820afd8601ef14fda651c63f542476e12adcf11.zip | |
Nothing about a pane is logged before its new, and the boot terminal's new carries the session directory, not / and a rename
A pane named in the update that made it (Config's init file, a look
renamed on open) logged `rename N <name>` before `new N`, so a follower met
a serial it had never seen. The boot shell, started with no directory, was
announced as `new 1 /` and renamed once its shell spoke. Now noteLog drops
the rename of a pane not yet announced, since its new carries the name, and
announces a pane first for any other record about it. A terminal whose shell
has not said where it is is named by the session's directory, where it was
started, and the shell saying so is no rename.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src/ninep')
| -rw-r--r-- | src/ninep/events.zig | 38 | ||||
| -rw-r--r-- | src/ninep/pane.zig | 28 | ||||
| -rw-r--r-- | src/ninep/tree.zig | 2 |
3 files changed, 51 insertions, 17 deletions
diff --git a/src/ninep/events.zig b/src/ninep/events.zig index 906e7a9a..c6505f52 100644 --- a/src/ninep/events.zig +++ b/src/ninep/events.zig @@ -153,10 +153,17 @@ fn bootFirst(p: *Pardes) void { /// Records `<kind> <serial> <name>`. pub fn noteLog(p: *Pardes, kind: LogKind, pane: *Pane) void { + // Nothing about a pane comes before its `new`: one not announced yet + // is announced first, and a rename of it is no record, as its `new` + // carries the name it has by then. + if (pane.fs.unannounced and kind != .new) { + if (kind == .rename) return; + announce(p); + } var buf: [4 * 4096 + 64]u8 = undefined; var name_buf: [4 * 4096]u8 = undefined; // As /index shows it: a newline in the name is `\n`. - const name = shown(pane_files.nameOf(pane), &name_buf); + const name = shown(pane_files.nameOf(p, pane), &name_buf); pushLog(p, std.fmt.bufPrint(&buf, "{s} {d} {s}\n", .{ @tagName(kind), pane.serial, name }) catch return); } @@ -1011,7 +1018,7 @@ test "the log records whether or not anyone reads, and an open that follows wait // Nobody holds /log open, and the pane is recorded anyway. const early = try th.newPane(p); - const early_line = try std.fmt.bufPrint(&expected, "new {d} {s}\n", .{ early, pane_files.nameOf(p.panes[p.paneBySerial(early).?].?) }); + const early_line = try std.fmt.bufPrint(&expected, "new {d} {s}\n", .{ early, pane_files.nameOf(p, p.panes[p.paneBySerial(early).?].?) }); const early_owned = try gpa.dupe(u8, early_line); defer gpa.free(early_owned); @@ -1048,7 +1055,7 @@ test "the log records whether or not anyone reads, and an open that follows wait const serial = try th.newPane(p); const id = p.paneBySerial(serial).?; try testing.expectEqualStrings( - try std.fmt.bufPrint(&expected, "new {d} {s}\n", .{ serial, pane_files.nameOf(p.panes[id].?) }), + try std.fmt.bufPrint(&expected, "new {d} {s}\n", .{ serial, pane_files.nameOf(p, p.panes[id].?) }), rdf.next(p, log, fh, frozen).bytes, ); try testing.expectEqual(Status.ok, wr(p, Node.of(serial, .name), "/tmp/logged.txt\n").reply.status); @@ -1190,6 +1197,31 @@ test "a Restore gives columns fresh serials, past every serial the old session h try testing.expect(pardes.layout.columnSerial(restored, 0) != first); } +test "a pane named before it is announced has its new under that name, and no rename before it" { + const p = try withFile(testing.allocator, "one\n"); + defer p.deinit(); + const pane = p.panes[0].?; + // As a pane Config makes and names in one update. + pane.fs.unannounced = true; + try pane_files.nameBuffer(p, 0, "/tmp/named-first.txt", false); + announce(p); + const log = try freezeLog(p); + defer p.gpa.free(log.bytes); + var want: [64]u8 = undefined; + try testing.expect(std.mem.endsWith(u8, log.bytes, try std.fmt.bufPrint(&want, "new {d} /tmp/named-first.txt\n", .{pane.serial}))); + try testing.expect(std.mem.indexOf(u8, log.bytes, try std.fmt.bufPrint(&want, "rename {d} ", .{pane.serial})) == null); + // A shell given no directory is named by the session's from the first, + // and saying it is there is no rename. + const free = p.freeSlot().?; + const shell = try p.newShell(free, ""); + try testing.expectEqualStrings(p.sessionDir(), pane_files.nameOf(p, shell)); + announce(p); + p.setCwd(free, p.sessionDir()); + const after = try freezeLog(p); + defer p.gpa.free(after.bytes); + try testing.expect(std.mem.indexOf(u8, after.bytes, try std.fmt.bufPrint(&want, "rename {d} ", .{shell.serial})) == null); +} + test "opens of the log share the open records, and a closed one frees its record" { const gpa = testing.allocator; const p = try withFile(gpa, "x\n"); diff --git a/src/ninep/pane.zig b/src/ninep/pane.zig index eb317655..30530c3b 100644 --- a/src/ninep/pane.zig +++ b/src/ninep/pane.zig @@ -134,12 +134,14 @@ pub fn dirtyOf(pane: *const Pane) bool { } /// The pane's name: its file, its document, or a terminal's directory. -pub fn nameOf(pane: *const Pane) []const u8 { +pub fn nameOf(p: *Pardes, pane: *const Pane) []const u8 { if (pane.file) |*f| return f.path; if (pane.pdfPath()) |path| return path; if (pane.image) |state| return state.path; const cwd = pane.cwdSlice(); - return if (cwd.len > 0) cwd else "/"; + // A shell given no directory was started in the session's, and is + // named by it until it says where it is (a `new` then, not a rename). + return if (cwd.len > 0) cwd else p.sessionDir(); } pub fn kindOf(pane: *const Pane) []const u8 { @@ -333,7 +335,7 @@ pub fn fileSize(p: *Pardes, id: usize, f: PaneFile) u64 { .body => if (pane.file == null and pane.isTerminal()) panes.terminal.screenTextLen(pane) else if (pdfText(p, pane)) |t| t.len else bodyOf(pane).len, .data, .xdata => bodyOf(pane).len, .tag => tagOf(p, pane).len, - .name => nameOf(pane).len + 1, + .name => nameOf(p, pane).len + 1, .ctl => ctl: { var buf: [ctl.pane_text_cap]u8 = undefined; break :ctl ctl.paneText(p, pane, &buf).len; @@ -380,7 +382,7 @@ pub fn indexLen(p: *Pardes) u64 { // unreachable: a u32 serial is at most 10 digits n += (std.fmt.bufPrint(&digits, "{d}", .{serial}) catch unreachable).len; var name_buf: [4 * 4096]u8 = undefined; - n += 1 + kindOf(pane).len + 3 + events.shown(nameOf(pane), &name_buf).len + 1; + n += 1 + kindOf(pane).len + 3 + events.shown(nameOf(p, pane), &name_buf).len + 1; // unreachable: a column index is under 16 digits n += 1 + (std.fmt.bufPrint(&digits, "{d}", .{columnOf(p, p.paneBySerial(serial).?)}) catch unreachable).len; } @@ -394,7 +396,7 @@ pub fn read(p: *Pardes, req: Req, id: usize, pane: *Pane, file: PaneFile) Reply return switch (file) { .name => name: { const out = p.fs.stage(p.gpa); - out.appendSlice(p.gpa, nameOf(pane)) catch {}; + out.appendSlice(p.gpa, nameOf(p, pane)) catch {}; out.append(p.gpa, '\n') catch {}; break :name tree.stagedReply(p, req); }, @@ -1016,7 +1018,7 @@ test "index lists serial kind dirty flag and name and stat reports its length" { try testing.expectEqual(@as(u64, both.bytes.len), indexLen(p)); var expected: [4200]u8 = undefined; const made = p.panes[p.paneBySerial(serial).?].?; - try testing.expect(std.mem.endsWith(u8, both.bytes, try std.fmt.bufPrint(&expected, "\n{d} text 0 {s} {d}\n", .{ serial, nameOf(made), columnOf(p, p.paneBySerial(serial).?) }))); + try testing.expect(std.mem.endsWith(u8, both.bytes, try std.fmt.bufPrint(&expected, "\n{d} text 0 {s} {d}\n", .{ serial, nameOf(p, made), columnOf(p, p.paneBySerial(serial).?) }))); } test "a terminal is listed as term with its directory as name" { @@ -1353,13 +1355,13 @@ test "a write of two lines to name is refused EINVAL, on a held open or not" { const h = call(p, .{ .tag = 1, .op = .open, .node = node, .omode = 1 }).reply.handle; try testing.expectEqual(E.INVAL, call(p, .{ .tag = 2, .op = .write, .node = node, .handle = h, .data = "/tmp/a\n/tmp/b\n" }).errno()); _ = call(p, .{ .tag = 3, .op = .release, .node = node, .handle = h, .opened = true }); - try testing.expectEqualStrings("/test.txt", nameOf(p.panes[p.paneBySerial(serial).?].?)); + try testing.expectEqualStrings("/test.txt", nameOf(p, p.panes[p.paneBySerial(serial).?].?)); // Line by line, as bash writes it: the second line is refused. const h2 = call(p, .{ .tag = 1, .op = .open, .node = node, .omode = 1 }).reply.handle; try testing.expectEqual(Status.ok, call(p, .{ .tag = 2, .op = .write, .node = node, .handle = h2, .data = "/tmp/pardes-a\n" }).reply.status); try testing.expectEqual(E.INVAL, call(p, .{ .tag = 2, .op = .write, .node = node, .handle = h2, .data = "/tmp/pardes-b\n" }).errno()); _ = call(p, .{ .tag = 3, .op = .release, .node = node, .handle = h2, .opened = true }); - try testing.expectEqualStrings("/tmp/pardes-a", nameOf(p.panes[p.paneBySerial(serial).?].?)); + try testing.expectEqualStrings("/tmp/pardes-a", nameOf(p, p.panes[p.paneBySerial(serial).?].?)); } test "a name cut across writes is one name, applied once at its newline or its close" { @@ -1373,16 +1375,16 @@ test "a name cut across writes is one name, applied once at its newline or its c p.fs.write_room = "/tmp/pardes-na".len; _ = call(p, .{ .tag = 2, .op = .write, .node = node, .handle = h, .data = "/tmp/pardes-na" }); p.fs.write_room = 0; - try testing.expectEqualStrings("/test.txt", nameOf(p.panes[p.paneBySerial(serial).?].?)); + try testing.expectEqualStrings("/test.txt", nameOf(p, p.panes[p.paneBySerial(serial).?].?)); _ = call(p, .{ .tag = 2, .op = .write, .node = node, .handle = h, .data = "me.txt\n" }); - try testing.expectEqualStrings("/tmp/pardes-name.txt", nameOf(p.panes[p.paneBySerial(serial).?].?)); + try testing.expectEqualStrings("/tmp/pardes-name.txt", nameOf(p, p.panes[p.paneBySerial(serial).?].?)); _ = call(p, .{ .tag = 3, .op = .release, .node = node, .handle = h, .opened = true }); // No newline, the whole write: applied at once, on its own open. const h3 = call(p, .{ .tag = 1, .op = .open, .node = node, .omode = 1 }).reply.handle; _ = call(p, .{ .tag = 2, .op = .write, .node = node, .handle = h3, .data = "/tmp/pardes-closed.txt" }); - try testing.expectEqualStrings("/tmp/pardes-closed.txt", nameOf(p.panes[p.paneBySerial(serial).?].?)); + try testing.expectEqualStrings("/tmp/pardes-closed.txt", nameOf(p, p.panes[p.paneBySerial(serial).?].?)); _ = call(p, .{ .tag = 3, .op = .release, .node = node, .handle = h3, .opened = true }); - try testing.expectEqualStrings("/tmp/pardes-closed.txt", nameOf(p.panes[p.paneBySerial(serial).?].?)); + try testing.expectEqualStrings("/tmp/pardes-closed.txt", nameOf(p, p.panes[p.paneBySerial(serial).?].?)); } test "a replace through data, truncate then write, is one undo step" { @@ -1469,7 +1471,7 @@ test "name reads the file name and writing it promotes a scratch without touchin const name = Node.of(serial, .name); const body = Node.of(serial, .body); var scratch_name: [4200]u8 = undefined; - const scratch_line = try std.fmt.bufPrint(&scratch_name, "{s}\n", .{nameOf(pane)}); + const scratch_line = try std.fmt.bufPrint(&scratch_name, "{s}\n", .{nameOf(p, pane)}); try testing.expectEqualStrings(scratch_line, rd(p, name, 0, 4096).bytes); try testing.expectEqual(@as(u64, scratch_line.len), call(p, .{ .tag = 1, .op = .getattr, .node = name }).reply.attr.size); _ = wr(p, body, "scratch "); diff --git a/src/ninep/tree.zig b/src/ninep/tree.zig index 61ac7e87..10208666 100644 --- a/src/ninep/tree.zig +++ b/src/ninep/tree.zig @@ -1053,7 +1053,7 @@ fn readFile(p: *Pardes, req: Req, target: Target) Reply { // A name as /log shows it: one line of UTF-8, a // newline in it `\n` (events.shown). var name_buf: [4 * 4096]u8 = undefined; - const name = events.shown(pane.nameOf(pn), &name_buf); + const name = events.shown(pane.nameOf(p, pn), &name_buf); out.print(p.gpa, "{d} {s} {d} {s} {d}\n", .{ serial, pane.kindOf(pn), @intFromBool(pane.dirtyOf(pn)), name, pane.columnOf(p, id) }) catch {}; } break :index stagedReply(p, req); |
