From 7dc3891ed574d6e96548f592b09b0f8abc87ae2a Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Sun, 27 Sep 2026 20:20:27 -0300 Subject: Keep what each open holds in one table of open records Snapshots (and /log's cursor), runs, and the reader_handle constants for event and pty/data each reused the open handle and each validated handle and node on its own. Now p.fs.opens is one table of 64 records, each the node it was opened on and a tagged union of what it holds, like lib9p's per-fid aux and acme's Fid (editors/acme/dat.h:373-385): one lookup (openOf), one release, ENFILE when full. A held read lives in its open's record, so it goes with the release. Opens that hold nothing answer handle 0 and take no record. Co-Authored-By: Claude Opus 5.5 --- src/ninep/tree.zig | 231 ++++++++++++++++++++++++++++++++++------------------- 1 file changed, 147 insertions(+), 84 deletions(-) (limited to 'src/ninep/tree.zig') diff --git a/src/ninep/tree.zig b/src/ninep/tree.zig index f08d20c8..bc32ad33 100644 --- a/src/ninep/tree.zig +++ b/src/ninep/tree.zig @@ -65,11 +65,6 @@ pub const e_bad_event = "bad event syntax"; /// (windows/rio/xfid.c:25), which 9ns turns into EBUSY. pub const e_in_use = "file in use"; -/// Handles that mark an open as the one reading `event` or `pty/data`, so the -/// release knows to give the file up; other opens get `open_handle`. -const open_handle: u32 = 1; -const reader_handle: u32 = 2; - fn reads(omode: u8) bool { return omode & 3 != 1; // OWRITE is the one access mode that never reads } @@ -107,30 +102,59 @@ pub fn changesPane(req: Req) bool { pub const out_reserve = 4 * 1024; -/// A read that found nothing yet (`.again`), kept to be answered when what it -/// waits on has something, the way factotum keeps its log's waiting reads -/// and answers them on append (security/auth/factotum/log.c:4-52) and acme -/// an event read (editors/acme/xfid.c:994). `asker` is the connection it -/// came on, which only the listener knows (src/9p_io.zig, `answerHeld`). -pub const Held = struct { asker: *anyopaque, req: Req }; - -/// Keeps a read that answered `.again`. An open waits with one read at a -/// time, as acme's window keeps one `eventx`: a newer read on the same open -/// takes the place of the older. -pub fn hold(p: *Pardes, req: Req, asker: *anyopaque) void { - var free: ?*?Held = null; - for (&p.fs.held) |*slot| { - const held = slot.* orelse { - if (free == null) free = slot; - continue; - }; - if (held.req.node == req.node and held.req.handle == req.handle) { - slot.* = .{ .asker = asker, .req = req }; - return; +// ---- opens ---- + +/// What one open holds from its open to its release, the way lib9p hangs a +/// server's own state off each fid (`Fid.aux`) and acme keeps it on its Fid +/// (editors/acme/dat.h:373-385). An open that holds something answers its +/// record's index plus one as the handle; one that holds nothing answers 0. +pub const Open = struct { + /// The node it was opened on, which every use checks; 0 is a free record. + node: u64 = 0, + what: union(enum) { + /// /screen's frame from the open, a terminal body's history from its + /// first read. + snapshot: ?[]const u8, + log: events.LogOpen, + run: pty.Run, + /// An open of a pane's event, holding the pane scripted; `true` for + /// the one that reads it. + event: bool, + /// The one open reading a pane's pty/data. + pty_data, + } = .{ .snapshot = null }, + /// A read that found nothing yet (`.again`), kept to be answered when + /// this open's file has something, the way factotum keeps its log's + /// waiting reads and answers them on append (security/auth/factotum/ + /// log.c:4-52) and acme an event read (editors/acme/xfid.c:994). One at a + /// time, as acme's window keeps one `eventx`: a newer read takes the + /// place of the older. The listener fills it in and answers it + /// (src/9p_io.zig, `answerHeld`). + held: ?Held = null, + + /// `asker` is the connection the read came on, which only the listener + /// knows. + pub const Held = struct { asker: *anyopaque, req: Req }; + + pub fn deinit(o: *Open, gpa: std.mem.Allocator) void { + switch (o.what) { + .snapshot => |bytes| if (bytes) |b| gpa.free(b), + .log => |log| gpa.free(log.bytes), + .run => |run| gpa.free(run.output), + .event, .pty_data => {}, } + o.* = .{}; } - // One slot per open that can wait, so a free one is always there. - if (free) |slot| slot.* = .{ .asker = asker, .req = req }; +}; + +/// Records for 64 opens at once; one more that would hold something fails. +pub const open_slots = 64; + +/// The record a request's handle names, when it was opened on that node. +pub fn openOf(p: *Pardes, req: Req) ?*Open { + if (req.handle == 0 or req.handle > p.fs.opens.len) return null; + const o = &p.fs.opens[req.handle - 1]; + return if (o.node == req.node) o else null; } // ---- nodes ---- @@ -533,41 +557,57 @@ fn makePane(p: *Pardes) ?u32 { } fn open(p: *Pardes, req: Req, target: Target) Reply { - switch (target) { + var pane_of: ?*Pane = null; + const what: @FieldType(Open, "what") = switch (target) { .top => |f| switch (f) { .new => return .{ .tag = req.tag, .handle = makePane(p) orelse return Reply.fail(req.tag, E.NFILE) }, - .screen => return screen.openSnapshot(p, req, true), - .log => return events.openLog(p, req), - else => {}, + .screen => .{ .snapshot = null }, + .log => .{ .log = .{} }, + else => return .{ .tag = req.tag }, }, - .pane => |t| { - const id = p.paneBySerial(t.serial) orelse return Reply.fail(req.tag, E.NOENT); - const pn = p.panes[id].?; - const pf = &pn.fs; + .pane => |t| what: { + const pn = p.panes[p.paneBySerial(t.serial) orelse return Reply.fail(req.tag, E.NOENT)].?; if (t.file.inPty() and !pn.isTerminal()) return Reply.fail(req.tag, E.NOENT); - switch (t.file) { - .body => if (pn.isTerminal()) return screen.openSnapshot(p, req, false), + pane_of = pn; + const reader = reads(req.omode); + break :what switch (t.file) { + .body => if (pn.isTerminal()) .{ .snapshot = null } else return .{ .tag = req.tag }, // Reads of these consume, so two readers would each see half // the stream; the second is refused rather than robbed. - .event => { - const reader = reads(req.omode); - if (reader and pf.event_reader) return failText(req.tag, E.BUSY, e_in_use); - pf.readers +|= 1; - p.fs.listeners +|= 1; - pf.event_reader = pf.event_reader or reader; - if (reader) return .{ .tag = req.tag, .handle = reader_handle }; - }, - .pty_run => return pty.openRun(p, req, t.serial), - .pty_data => if (reads(req.omode)) { - if (pf.pty_readers > 0) return failText(req.tag, E.BUSY, e_in_use); - pf.pty_readers = 1; - return .{ .tag = req.tag, .handle = reader_handle }; - }, - else => {}, - } + .event => if (reader and pn.fs.event_reader) return failText(req.tag, E.BUSY, e_in_use) else .{ .event = reader }, + .pty_data => if (!reader) return .{ .tag = req.tag } else if (pn.fs.pty_reader) return failText(req.tag, E.BUSY, e_in_use) else .pty_data, + .pty_run => .{ .run = .{} }, + else => return .{ .tag = req.tag }, + }; + }, + }; + const i = for (p.fs.opens, 0..) |o, i| { + if (o.node == 0) break i; + } else return Reply.fail(req.tag, E.NFILE); + const o = &p.fs.opens[i]; + o.* = .{ .node = req.node, .what = what }; + switch (o.what) { + // A screen freezes at open; a terminal body at its first read. + .snapshot => |*bytes| if (pane_of == null) { + bytes.* = screen.render(p) catch { + o.* = .{}; + return Reply.fail(req.tag, E.NOMEM); + }; }, + .log => |*log| log.* = events.freezeLog(p) catch { + o.* = .{}; + return Reply.fail(req.tag, E.NOMEM); + }, + .event => |reader| { + const pn = pane_of.?; + pn.fs.readers +|= 1; + p.fs.listeners +|= 1; + pn.fs.event_reader = pn.fs.event_reader or reader; + }, + .pty_data => pane_of.?.fs.pty_reader = true, + .run => {}, } - return .{ .tag = req.tag, .handle = open_handle }; + return .{ .tag = req.tag, .handle = @intCast(i + 1) }; } /// Tremove closes a pane. Nothing else in the tree can be removed, and this @@ -585,41 +625,36 @@ fn remove(p: *Pardes, req: Req) Reply { } fn release(p: *Pardes, req: Req) Reply { - // A read held on this open goes with it: a hangup pays its releases - // before the connection's slot is reused, so none outlives its asker. - for (&p.fs.held) |*slot| if (slot.*) |held| { - if (held.req.node == req.node and held.req.handle == req.handle) slot.* = null; - }; // The handle's bookkeeping runs whether or not the removal is allowed. - const done = releaseHandle(p, req); - return if (req.remove) remove(p, req) else done; + releaseHandle(p, req); + return if (req.remove) remove(p, req) else .{ .tag = req.tag }; } -fn releaseHandle(p: *Pardes, req: Req) Reply { - if (screen.releaseSnapshot(p, req)) return .{ .tag = req.tag }; - const target = Node.target(req.node) orelse return .{ .tag = req.tag }; - switch (target) { - .top => {}, - .pane => |t| { - // A run's open outlives its pane: it still holds the answer. - if (t.file == .pty_run) return pty.releaseRun(p, req); - if (t.file != .event and t.file != .pty_data) return .{ .tag = req.tag }; - const id = p.paneBySerial(t.serial) orelse return .{ .tag = req.tag }; - const pf = &p.panes[id].?.fs; - if (t.file == .pty_data) { - if (req.handle != reader_handle or pf.pty_readers == 0) return .{ .tag = req.tag }; - pf.pty_readers -= 1; - if (pf.pty_readers == 0) pf.pty_out.clearAndFree(p.gpa); - return .{ .tag = req.tag }; - } - if (pf.readers == 0) return .{ .tag = req.tag }; - if (req.handle == reader_handle) pf.event_reader = false; - pf.readers -= 1; +/// Gives up what the open held, a read held on it included: a hangup pays +/// its releases before the connection's slot is reused, so none outlives +/// the connection it came on. +fn releaseHandle(p: *Pardes, req: Req) void { + const o = openOf(p, req) orelse return; + const t = Node.target(req.node).?; + const of: ?*Pane = if (t == .pane) (if (p.paneBySerial(t.pane.serial)) |id| p.panes[id] else null) else null; + if (of) |pn| switch (o.what) { + .event => |reader| { + if (reader) pn.fs.event_reader = false; + pn.fs.readers -|= 1; p.fs.listeners -|= 1; - if (pf.readers == 0) pf.tag_snap.clearAndFree(p.gpa); + if (pn.fs.readers == 0) pn.fs.tag_snap.clearAndFree(p.gpa); }, - } - return .{ .tag = req.tag }; + .pty_data => { + pn.fs.pty_reader = false; + pn.fs.pty_out.clearAndFree(p.gpa); + }, + // The command runs on in the shell; nobody is waiting for it any more. + .run => if (pn.fs.run == @as(u8, @intCast(req.handle - 1))) { + pn.fs.run = null; + }, + .snapshot, .log => {}, + }; + o.deinit(p.gpa); } fn setattr(p: *Pardes, req: Req, target: Target) Reply { @@ -997,6 +1032,34 @@ test "opening /pane/new makes a pane and removing one closes it" { try testing.expectEqual(E.NOENT, th.rmdir(p, Node.of(serial, .dir)).errno()); } +test "every open that holds something is one record, named by its handle only on its own node" { + const p = try th.withTerm(testing.allocator); + defer p.deinit(); + const serial = serialOf(p); + const log = @intFromEnum(TopFile.log); + const event = Node.of(serial, .event); + const run = Node.of(serial, .pty_run); + const logged = call(p, .{ .tag = 1, .op = .open, .node = log }).reply.handle; + const reader = call(p, .{ .tag = 2, .op = .open, .node = event }).reply.handle; + const ran = call(p, .{ .tag = 3, .op = .open, .node = run }).reply.handle; + try testing.expect(logged != 0 and reader != 0 and ran != 0); + try testing.expect(logged != reader and reader != ran and logged != ran); + // An open that holds nothing takes no record. + try testing.expectEqual(@as(u32, 0), call(p, .{ .tag = 4, .op = .open, .node = Node.of(serial, .addr) }).reply.handle); + + // A handle is refused on any other node, and releasing it there gives + // nothing up. + try testing.expectEqual(E.INVAL, call(p, .{ .tag = 5, .op = .read, .node = run, .handle = logged, .size = 64 }).errno()); + try testing.expectEqual(E.INVAL, call(p, .{ .tag = 6, .op = .write, .node = log, .handle = ran, .data = "follow\n" }).errno()); + _ = call(p, .{ .tag = 7, .op = .release, .node = log, .handle = reader }); + try testing.expectEqualStrings(e_in_use, call(p, .{ .tag = 8, .op = .open, .node = event }).reply.ename); + + for ([_]u64{ log, event, run }, [_]u32{ logged, reader, ran }) |node, h| + _ = call(p, .{ .tag = 9, .op = .release, .node = node, .handle = h }); + for (p.fs.opens) |o| try testing.expect(o.node == 0); + try testing.expectEqual(@as(u16, 0), p.fs.listeners); +} + test "directories walk to their parents" { const p = try withFile(testing.allocator, "contents\n"); defer p.deinit(); -- cgit v1.3