From d572edefe32284db343259b0b7e12c9470366b28 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Mon, 28 Sep 2026 18:23:34 -0300 Subject: A followed log, event or pty/data answers a read inside its last answer from it again, so bash's read loses nothing bash's read on a seekable fd takes a chunk, keeps one line and lseeks back to just past it; a stream had already moved on, so the rest of the chunk was lost and a while-read loop over the log dropped records. Each stream open keeps its last answer and the offset it was read at, and a read strictly inside it is answered from it; a read at the same offset, as a client that ignores offsets makes, still gets the next record. Co-Authored-By: Claude Opus 5.5 --- src/ninep/tree.zig | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) (limited to 'src/ninep/tree.zig') diff --git a/src/ninep/tree.zig b/src/ninep/tree.zig index f118e4b4..b3f3dde6 100644 --- a/src/ninep/tree.zig +++ b/src/ninep/tree.zig @@ -130,6 +130,13 @@ pub const Open = struct { /// place of the older. The listener fills it in and answers it /// (src/9p_io.zig, `answerHeld`). held: ?Held = null, + /// The last answer a stream open (a followed log, event, pty/data) gave, + /// and the offset it was read at. A read at an offset inside it is + /// answered from it again: bash's `read` takes a chunk, keeps a line and + /// lseeks back to just past it, and a stream has already moved on. + replay: ?Replay = null, + + pub const Replay = struct { off: u64, bytes: []u8 }; /// `asker` is the connection the read came on, which only the listener /// knows. @@ -145,6 +152,7 @@ pub const Open = struct { }, .event, .pty_data, .ctl => {}, } + if (o.replay) |r| gpa.free(r.bytes); o.* = .{}; } }; @@ -692,6 +700,32 @@ fn setattr(p: *Pardes, req: Req, target: Target) Reply { } fn read(p: *Pardes, req: Req, target: Target) Reply { + const stream = if (openOf(p, req)) |o| switch (o.what) { + .log => o, + .event => |reader| if (reader) o else null, + .pty_data => o, + else => null, + } else null; + const s = stream orelse return readFile(p, req, target); + // Strictly inside: a client that reads every record at one offset (the + // offset is ignored on a stream, as in Plan 9) always gets new ones. + if (s.replay) |r| if (req.off > r.off and req.off < r.off + r.bytes.len) { + const from = r.bytes[@intCast(req.off - r.off)..]; + const n = @min(from.len, req.size); + p.fs.stage(p.gpa).appendSlice(p.gpa, from[0..n]) catch return Reply.fail(req.tag, E.NOMEM); + return .{ .tag = req.tag, .payload = .{ .staged = @intCast(n) } }; + }; + const reply = readFile(p, req, target); + if (reply.status != .ok) return reply; + const bytes = p.fsPayload(reply); + if (bytes.len == 0) return reply; + const kept = p.gpa.dupe(u8, bytes) catch return reply; + if (s.replay) |r| p.gpa.free(r.bytes); + s.replay = .{ .off = req.off, .bytes = kept }; + return reply; +} + +fn readFile(p: *Pardes, req: Req, target: Target) Reply { switch (target) { .top => |f| return switch (f) { .README => help: { -- cgit v1.3