From f4412d6dfdb1e2bd8549a7edba785ee242fd38f2 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Sun, 27 Sep 2026 17:52:31 -0300 Subject: Wake reads waiting on /log, event and pty/data; one reader per consuming file; pty/ctl reads back A read parked on a queue was retried only when some unrelated write had to wait, so a follower of /log (and a reader of event or pty/data) slept until then. Pushing a record now marks the turn parked, and giving the turn up wakes them (measured: stuck past 3 s before, 0 s after). event and pty/data consume what they read, so a second open for reading is refused with rio's "file in use" (EBUSY through 9ns); writers still get in, and pty/data queues output only for an actual reader. pty/ctl reads back "winsize C R", in the words it takes. /log fixes from review: a record longer than a read comes in pieces (a shell read loop failed on long lines), every repeated message is logged, a record bigger than the ring is cut to fit instead of emptying it, the ring is reserved at boot so recording never allocates, and panes present at boot are recorded first. Co-Authored-By: Claude Opus 5.5 --- src/ninep/pty.zig | 33 +++++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) (limited to 'src/ninep/pty.zig') diff --git a/src/ninep/pty.zig b/src/ninep/pty.zig index c657d27d..b34bd59d 100644 --- a/src/ninep/pty.zig +++ b/src/ninep/pty.zig @@ -76,6 +76,18 @@ fn verb(p: *Pardes, id: usize, line: []const u8, apply: bool) bool { return true; } +/// ctl reads back its state in the words it takes, as a Plan 9 ctl does. +pub fn readCtl(p: *Pardes, req: Req, pane: *Pane) Reply { + p.fs.stage(p.gpa).print(p.gpa, "winsize {d} {d}\n", .{ pane.cols, pane.rows }) catch + return Reply.fail(req.tag, E.NOMEM); + return tree.stagedReply(p, req); +} + +pub fn ctlLen(pane: *const Pane) u64 { + var buf: [32]u8 = undefined; + return (std.fmt.bufPrint(&buf, "winsize {d} {d}\n", .{ pane.cols, pane.rows }) catch unreachable).len; +} + /// Three right-aligned fields: cols, rows and whether the host holds the tty. pub const status_len: u64 = 36; @@ -183,7 +195,7 @@ test "a terminal pane's pty/ holds exactly ctl, status and data" { const ctl = look_up(p, Node.of(serial, .pty), "ctl"); try testing.expectEqual(Node.of(serial, .pty_ctl), ctl.reply.attr.node); - try testing.expectEqual(@as(u16, 0o222), ctl.reply.attr.mode); + try testing.expectEqual(@as(u16, 0o666), ctl.reply.attr.mode); try testing.expectEqual(@as(u16, 0o444), look_up(p, Node.of(serial, .pty), "status").reply.attr.mode); try testing.expectEqual(E.NOENT, look_up(p, Node.of(serial, .pty), "body").errno()); try testing.expectEqual(E.NOENT, look_up(p, Node.of(serial, .pty), "pty").errno()); @@ -191,7 +203,7 @@ test "a terminal pane's pty/ holds exactly ctl, status and data" { try testing.expectEqual(E.NOTDIR, look_up(p, Node.of(serial, .pty_ctl), "x").errno()); try testing.expectEqual(E.NOTDIR, rdir(p, Node.of(serial, .pty_ctl), 0).errno()); try testing.expectEqual(E.PERM, rd(p, Node.of(serial, .pty), 0, 16).errno()); - try testing.expectEqual(E.PERM, rd(p, Node.of(serial, .pty_ctl), 0, 16).errno()); + try testing.expect(std.mem.startsWith(u8, rd(p, Node.of(serial, .pty_ctl), 0, 64).bytes, "winsize ")); try testing.expectEqual(E.PERM, wr(p, Node.of(serial, .pty_status), "x").errno()); } @@ -204,6 +216,8 @@ test "every pty/ctl verb, and every refusal" { const pane = p.panes[0].?; const cols = pane.cols; const rows = pane.rows; + var said: [32]u8 = undefined; + try testing.expectEqualStrings(try std.fmt.bufPrint(&said, "winsize {d} {d}\n", .{ cols, rows }), rd(p, ctl, 0, 64).bytes); const ws = wr(p, ctl, "winsize 132 44\n"); try testing.expectEqual(@as(u32, "winsize 132 44\n".len), ws.reply.written); try testing.expectEqual(@as(u16, 132), ws.winsize.?.cols); @@ -331,7 +345,12 @@ test "pty/data writes at the shell and reads the raw stream" { try testing.expectEqual(@as(usize, 0), p.fs.panes[0].pty_out.buf.items.len); try testing.expectEqual(Status.again, rd(p, data, 0, 64).reply.status); - _ = call(p, .{ .tag = 5, .op = .open, .node = data }); + // An open that only writes is not a reader: nothing queues for it. + const writer = call(p, .{ .tag = 4, .op = .open, .node = data, .omode = 1 }); + try testing.expectEqual(@as(u16, 0), p.fs.panes[0].pty_readers); + _ = call(p, .{ .tag = 4, .op = .release, .node = data, .handle = writer.reply.handle }); + + const reader = call(p, .{ .tag = 5, .op = .open, .node = data }); try testing.expectEqual(@as(u16, 1), p.fs.panes[0].pty_readers); try testing.expectEqual(@as(u16, 0), p.fs.listeners); try testing.expect(!p.fs.scripted(0)); @@ -351,14 +370,16 @@ test "pty/data writes at the shell and reads the raw stream" { p.update(.{ .output = .{ .pane = 0, .bytes = "orphan" } }); while (p.nextEffect()) |_| {} - _ = call(p, .{ .tag = 6, .op = .release, .node = data }); + _ = call(p, .{ .tag = 6, .op = .release, .node = data, .handle = reader.reply.handle }); try testing.expectEqual(@as(u16, 0), p.fs.panes[0].pty_readers); try testing.expectEqual(@as(usize, 0), p.fs.panes[0].pty_out.buf.capacity); try testing.expectEqual(Status.again, rd(p, data, 0, 64).reply.status); + // A second reader would take half the stream from the first: refused. _ = call(p, .{ .tag = 7, .op = .open, .node = data }); - _ = call(p, .{ .tag = 8, .op = .open, .node = data }); - _ = call(p, .{ .tag = 9, .op = .release, .node = data }); + const second = call(p, .{ .tag = 8, .op = .open, .node = data, .omode = 2 }); + try testing.expectEqual(Status.err, second.reply.status); + try testing.expectEqualStrings(tree.e_in_use, second.reply.ename); try testing.expectEqual(@as(u16, 1), p.fs.panes[0].pty_readers); p.update(.{ .output = .{ .pane = 0, .bytes = "still" } }); while (p.nextEffect()) |_| {} -- cgit v1.3