diff options
| -rw-r--r-- | .agents/skills/pardes-9p/SKILL.md | 3 | ||||
| -rw-r--r-- | docs/fs.md | 5 | ||||
| -rw-r--r-- | src/ninep/events.zig | 22 |
3 files changed, 27 insertions, 3 deletions
diff --git a/.agents/skills/pardes-9p/SKILL.md b/.agents/skills/pardes-9p/SKILL.md index e363b7cb..f0ae51e4 100644 --- a/.agents/skills/pardes-9p/SKILL.md +++ b/.agents/skills/pardes-9p/SKILL.md @@ -65,7 +65,8 @@ $m/pane/<n>/pty/run write one line, read `exit N` + its output, or `busy` / `e $m/log recent events, then EOF: new|del|rename|save <serial> <name>, msg <serial|-> <text>, run <serial> <line>, exit <serial> <N|?>, send <from> <to> <repl-id>, dump|restore <path>, err <serial|-> <file>: <why> - (exec 3<>$m/log; echo follow >&3; cat <&3 waits for new ones; tail -f does not; + (exec 3<>$m/log; echo follow >&3; cat <&3 replays what is there, then waits for new + ones; `echo follow new` waits for new ones only; tail -f does not wait; while read -r line <&3; do ...; done loses nothing within a session: a Restore hangs every connection up, so dial again, and restart a 9ns mount) $m/screen the rendered screen as JSON, frozen per open @@ -721,7 +721,10 @@ text, a cluster to a cell, is apart from this.) A click in a tag gives offsets into the whole tag as `tag` reads it, the path first.) An open freezes the ring's text the way `/screen` freezes a frame: reads walk it and end. Writing `follow` to that same open makes reads past it wait for the -next record, one per read; a follower the ring outran reads `lost N` first. +next record, one per read, after first reading all that the open froze (the +ring's whole history, up to 64 KiB); `follow new` skips that and waits for +what comes after, as `tail -n0 -f` does. A follower the ring outran reads +`lost N` first. Closing the open is the only way back, as with rio's `consctl`. A follower misses nothing within a session only: a Restore hangs its connection up (and a 9ns mount with it, which must be restarted), so it dials again and diff --git a/src/ninep/events.zig b/src/ninep/events.zig index a3f8c278..1067b60e 100644 --- a/src/ninep/events.zig +++ b/src/ninep/events.zig @@ -371,7 +371,12 @@ pub fn readLog(p: *Pardes, req: Req) Reply { /// records instead of ending; closing it is the way back. pub fn writeLog(p: *Pardes, req: Req) Reply { const slot = &(tree.openOf(p, req) orelse return Reply.fail(req.tag, E.INVAL)).what.log; - if (!std.mem.eql(u8, std.mem.trim(u8, req.data, " \t\r\n"), "follow")) return Reply.fail(req.tag, E.INVAL); + const word = std.mem.trim(u8, req.data, " \t\r\n"); + // `follow` reads what the open froze, then waits; `follow new` skips + // what was there and waits for what comes after, as tail -n0 -f does. + if (std.mem.eql(u8, word, "follow new")) { + slot.pos = slot.bytes.len; + } else if (!std.mem.eql(u8, word, "follow")) return Reply.fail(req.tag, E.INVAL); slot.follow = true; return .{ .tag = req.tag, .written = @intCast(req.data.len) }; } @@ -1110,3 +1115,18 @@ test "a click in the body reports the offsets of the word it takes, so the recor _ = call(p, .{ .tag = 3, .op = .release, .node = event, .handle = h }); } } + +test "follow new skips what the log held and waits for what comes after" { + const p = try withFile(testing.allocator, "x\n"); + defer p.deinit(); + p.update(.tick); // the pane's `new` is logged first + while (p.nextEffect()) |_| {} + p.setMessage(0, "before the open"); + const log = @intFromEnum(tree.TopFile.log); + const h = call(p, .{ .tag = 1, .op = .open, .node = log }).reply.handle; + try testing.expectEqual(Status.ok, call(p, .{ .tag = 2, .op = .write, .node = log, .handle = h, .data = "follow new\n" }).reply.status); + try testing.expectEqual(Status.again, call(p, .{ .tag = 3, .op = .read, .node = log, .handle = h, .size = 4096 }).reply.status); + p.setMessage(0, "after it"); + try testing.expect(std.mem.endsWith(u8, call(p, .{ .tag = 4, .op = .read, .node = log, .handle = h, .size = 4096 }).bytes, " after it\n")); + _ = call(p, .{ .tag = 5, .op = .release, .node = log, .handle = h }); +} |
