summaryrefslogtreecommitdiff
path: root/src/ninep/tree.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/ninep/tree.zig')
-rw-r--r--src/ninep/tree.zig229
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();