diff options
Diffstat (limited to 'src/ninep/tree.zig')
| -rw-r--r-- | src/ninep/tree.zig | 229 |
1 files changed, 146 insertions, 83 deletions
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 }; +// ---- opens ---- -/// 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; +/// 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(); |
