summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-27 20:20:27 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:14 -0300
commit7dc3891ed574d6e96548f592b09b0f8abc87ae2a (patch)
tree70cddb9ce4a7e9039f8167e806b7e996f94eefb0 /src
parentacd9e032a147f4aa7ad49110dc2f4fb945e7a9bb (diff)
downloadpardes-7dc3891ed574d6e96548f592b09b0f8abc87ae2a.tar.gz
pardes-7dc3891ed574d6e96548f592b09b0f8abc87ae2a.zip
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 <[email protected]>
Diffstat (limited to 'src')
-rw-r--r--src/9p_io.zig22
-rw-r--r--src/fs.zig17
-rw-r--r--src/ninep/events.zig67
-rw-r--r--src/ninep/pane.zig7
-rw-r--r--src/ninep/pty.zig63
-rw-r--r--src/ninep/screen.zig79
-rw-r--r--src/ninep/tree.zig229
7 files changed, 237 insertions, 247 deletions
diff --git a/src/9p_io.zig b/src/9p_io.zig
index 6182ca75..7c366aa8 100644
--- a/src/9p_io.zig
+++ b/src/9p_io.zig
@@ -229,7 +229,9 @@ pub const Listener = struct {
if (req.op == .release and quiet) l.collectOs();
// A read with nothing yet stays parked in the engine, and the core
// keeps it to answer when what it waits on has something.
- if (reply.status == .again and req.op == .read) pardes.ctlfs.hold(core, req, conn);
+ if (reply.status == .again and req.op == .read) if (pardes.ctlfs.openOf(core, req)) |o| {
+ o.held = .{ .asker = conn, .req = req };
+ };
if (!pardes.ctlfs.changesPane(req)) return conn.reply(&reply, core.fsPayload(reply));
// The editor draws the change and performs what it asked for; when
// it asked for something -- a save, a shell, a watch -- the answer
@@ -262,8 +264,8 @@ pub const Listener = struct {
const core = l.core;
if (!core.fs.news) return;
core.fs.news = false;
- for (&core.fs.held) |*slot| {
- const held = slot.* orelse continue;
+ for (&core.fs.opens) |*o| {
+ const held = o.held orelse continue;
const conn: *Runner.Conn = @ptrCast(@alignCast(held.asker));
conn.lock();
const parked: ?bool = for (conn.engine.slots) |sl| {
@@ -272,13 +274,13 @@ pub const Listener = struct {
// Gone (flushed, answered, hung up) it is forgotten; out being
// asked again, the asking answers it.
if (parked != true) {
- if (parked == null) slot.* = null;
+ if (parked == null) o.held = null;
conn.unlock();
continue;
}
const reply = core.serveFs(held.req);
if (reply.status != .again) {
- slot.* = null;
+ o.held = null;
conn.engine.reply(&reply, core.fsPayload(reply));
}
conn.unlock();
@@ -1280,7 +1282,7 @@ test "Unix TCP and QUIC share one listener through reads writes reconnects and r
l.reset(p);
try testing.expectEqual(@as(usize, 0), l.runner.count());
for (&l.conns, 0..) |conn, i| try testing.expect(!l.live(@intCast(i)) and !conn.draining);
- for (p.fs.snapshots) |snapshot| try testing.expect(snapshot.node == 0);
+ for (p.fs.opens) |o| try testing.expect(o.node == 0);
}
};
}
@@ -1409,8 +1411,8 @@ test "a held read is answered when the log has news, and a flushed one spends no
const deadline = Client.nowMs() + Client.budget_ms;
while (Client.nowMs() < deadline) {
pardes.turn.wake();
- const any = for (core.fs.held) |slot| {
- if (slot != null) break true;
+ const any = for (core.fs.opens) |o| {
+ if (o.held != null) break true;
} else false;
pardes.turn.rest();
if (any) return;
@@ -1450,8 +1452,8 @@ test "a held read is answered when the log has news, and a flushed one spends no
try testing.expect(std.mem.indexOf(u8, answered.result.read, "while held") != null);
s.drop(1);
pardes.turn.wake();
- const left = for (p.fs.held) |slot| {
- if (slot != null) break true;
+ const left = for (p.fs.opens) |o| {
+ if (o.held != null) break true;
} else false;
pardes.turn.rest();
try testing.expect(!left);
diff --git a/src/fs.zig b/src/fs.zig
index c2e88b10..4f395b1c 100644
--- a/src/fs.zig
+++ b/src/fs.zig
@@ -321,7 +321,7 @@ test "virtual writes enforce permissions even when contents are empty" {
try std.testing.expectError(error.IsDirectory, write(p, "/virtual/src", bytes));
} else try std.testing.expectError(error.FileNotFound, write(p, "/virtual/src/fs.zig", bytes));
}
- for (p.fs.snapshots) |snapshot| try std.testing.expect(snapshot.node == 0);
+ for (p.fs.opens) |o| try std.testing.expect(o.node == 0);
}
test "direct self reads and writes use the same dot paths as Look" {
@@ -480,10 +480,7 @@ test "bounded reads release terminal snapshots after success and size refusal" {
defer gpa.free(bytes);
try std.testing.expectEqualStrings(expected, bytes);
try std.testing.expectError(error.FileTooLarge, readLimit(p, path, expected.len - 1));
- for (p.fs.snapshots) |snapshot| {
- try std.testing.expectEqual(@as(u64, 0), snapshot.node);
- try std.testing.expect(snapshot.bytes == null);
- }
+ for (p.fs.opens) |o| try std.testing.expectEqual(@as(u64, 0), o.node);
}
test "bounded reads probe size-unknown proc files at the exact limit" {
@@ -1237,14 +1234,11 @@ pub const Namespace = struct {
mounts: std.ArrayList(Mount) = .empty,
node_name: [16]u8 = undefined,
os_paths: std.ArrayList(OsPath) = .empty,
- snapshots: [tree.screen.snapshot_slots]tree.screen.Snapshot = @splat(.{}),
- runs: [tree.pty.run_slots]tree.pty.Run = @splat(.{}),
+ /// What each open holds between its open and its release.
+ opens: [tree.open_slots]tree.Open = @splat(.{}),
out: std.ArrayList(u8) = .empty,
listeners: u16 = 0,
origin: u8 = 'K',
- /// Reads that found nothing yet, one per open that can wait: the log's
- /// and a run's opens, and one event and one pty/data reader a pane.
- held: [tree.screen.snapshot_slots + tree.pty.run_slots + 2 * MAX_PANES]?tree.Held = @splat(null),
/// Something a held read may be waiting on changed since they were last
/// answered: a record queued, a run answered, a pane gone.
news: bool = false,
@@ -1266,8 +1260,7 @@ pub const Namespace = struct {
st.mounts.deinit(gpa);
for (st.os_paths.items) |entry| gpa.free(entry.path);
st.os_paths.deinit(gpa);
- for (st.snapshots) |snapshot| if (snapshot.bytes) |bytes| gpa.free(bytes);
- for (&st.runs) |*slot| slot.reset(gpa, 0);
+ for (&st.opens) |*o| o.deinit(gpa);
st.log.deinit(gpa);
st.out.deinit(gpa);
}
diff --git a/src/ninep/events.zig b/src/ninep/events.zig
index 78945c5c..8f66cf3a 100644
--- a/src/ninep/events.zig
+++ b/src/ninep/events.zig
@@ -169,37 +169,42 @@ fn pushLog(p: *Pardes, record: []u8) void {
p.fs.news = true;
}
+/// What an open of /log holds.
+pub const LogOpen = struct {
+ /// The ring's text when it was opened.
+ bytes: []const u8 = &.{},
+ /// This open waits for records after `bytes`, from `next`.
+ follow: bool = false,
+ next: u64 = 0,
+ /// How much of record `next` a short read already took.
+ part: u32 = 0,
+ /// How much of `bytes` this open has read. Reads walk it as a stream once
+ /// following and ignore their offset, since `exec 3<>log; echo follow
+ /// >&3` leaves the shared offset past the word it wrote.
+ pos: u64 = 0,
+};
+
/// An open freezes the ring's text, so `cat log` answers what happened lately
/// and ends; `next` is where a follower picks up after it.
-pub fn openLog(p: *Pardes, req: Req) Reply {
- for (&p.fs.snapshots, 0..) |*slot, i| {
- if (slot.node != 0) continue;
- var text: std.ArrayList(u8) = .empty;
- var at = p.fs.log.head;
- const items = p.fs.log.buf.items;
- var n: u64 = 0;
- while (at + 4 <= items.len) : (n += 1) {
- const len = std.mem.readInt(u32, items[at..][0..4], .little);
- text.appendSlice(p.gpa, items[at + 4 ..][0..len]) catch {
- text.deinit(p.gpa);
- return Reply.fail(req.tag, E.NOMEM);
- };
- at += 4 + len;
- }
- const bytes = text.toOwnedSlice(p.gpa) catch return Reply.fail(req.tag, E.NOMEM);
- slot.* = .{ .node = req.node, .bytes = bytes, .next = p.fs.log.dropped + n };
- return .{ .tag = req.tag, .handle = @intCast(i + 1) };
+pub fn freezeLog(p: *Pardes) !LogOpen {
+ var text: std.ArrayList(u8) = .empty;
+ errdefer text.deinit(p.gpa);
+ var at = p.fs.log.head;
+ const items = p.fs.log.buf.items;
+ var n: u64 = 0;
+ while (at + 4 <= items.len) : (n += 1) {
+ const len = std.mem.readInt(u32, items[at..][0..4], .little);
+ try text.appendSlice(p.gpa, items[at + 4 ..][0..len]);
+ at += 4 + len;
}
- return Reply.fail(req.tag, E.NFILE);
+ return .{ .bytes = try text.toOwnedSlice(p.gpa), .next = p.fs.log.dropped + n };
}
/// Past the frozen text, a following open reads newer records one per read,
-/// parked until one arrives; one that fell behind the ring hears `lost N`.
+/// held until one arrives; one that fell behind the ring hears `lost N`.
pub fn readLog(p: *Pardes, req: Req) Reply {
- if (req.handle == 0 or req.handle > p.fs.snapshots.len) return Reply.fail(req.tag, E.INVAL);
- const slot = &p.fs.snapshots[req.handle - 1];
- if (slot.node != req.node) return Reply.fail(req.tag, E.INVAL);
- const frozen = slot.bytes orelse &.{};
+ const slot = &(tree.openOf(p, req) orelse return Reply.fail(req.tag, E.INVAL)).what.log;
+ const frozen = slot.bytes;
const out = p.fs.stage(p.gpa);
if (!slot.follow or slot.pos < frozen.len) {
// Until `follow`, the frozen text is a file like any other: `tail -n`
@@ -243,9 +248,7 @@ pub fn readLog(p: *Pardes, req: Req) Reply {
/// `follow`, written to an open of the log, makes that open wait for newer
/// records instead of ending; closing it is the way back.
pub fn writeLog(p: *Pardes, req: Req) Reply {
- if (req.handle == 0 or req.handle > p.fs.snapshots.len) return Reply.fail(req.tag, E.INVAL);
- const slot = &p.fs.snapshots[req.handle - 1];
- if (slot.node != req.node) return Reply.fail(req.tag, E.INVAL);
+ const slot = &(tree.openOf(p, req) orelse return Reply.fail(req.tag, E.INVAL)).what.log;
if (!std.mem.eql(u8, std.mem.trim(u8, req.data, " \t\r\n"), "follow")) return Reply.fail(req.tag, E.INVAL);
slot.follow = true;
return .{ .tag = req.tag, .written = @intCast(req.data.len) };
@@ -386,7 +389,7 @@ pub fn noteAction(
pub fn notePtyOutput(p: *Pardes, id: usize, bytes: []const u8) void {
if (id >= MAX_PANES or bytes.len == 0) return;
const pf = &(p.panes[id] orelse return).fs;
- if (pf.pty_readers == 0) return;
+ if (!pf.pty_reader) return;
var off: usize = 0;
while (off < bytes.len) {
const n = @min(bytes.len - off, queue_cap / 2);
@@ -745,7 +748,7 @@ test "the log records whether or not anyone reads, and an open that follows wait
try testing.expect(std.mem.startsWith(u8, lost, "lost "));
try testing.expect(std.mem.startsWith(u8, rdf.next(p, log, fh, frozen).bytes, "msg "));
_ = call(p, .{ .tag = 10, .op = .release, .node = log, .handle = fh });
- for (p.fs.snapshots) |slot| try testing.expect(slot.node == 0);
+ for (p.fs.opens) |o| try testing.expect(o.node == 0);
// A record bigger than the whole ring is cut to fit, not dropped with
// everything else pushed out ahead of it.
@@ -759,12 +762,12 @@ test "the log records whether or not anyone reads, and an open that follows wait
_ = call(p, .{ .tag = 13, .op = .release, .node = log, .handle = cut.reply.handle });
}
-test "opens of the log share the snapshot slots, and a closed one frees its slot" {
+test "opens of the log share the open records, and a closed one frees its record" {
const gpa = testing.allocator;
const p = try withFile(gpa, "x\n");
defer p.deinit();
const log = @intFromEnum(tree.TopFile.log);
- var handles: [tree.screen.snapshot_slots]u32 = undefined;
+ var handles: [tree.open_slots]u32 = undefined;
for (&handles) |*h| h.* = call(p, .{ .tag = 1, .op = .open, .node = log }).reply.handle;
try testing.expectEqual(E.NFILE, call(p, .{ .tag = 2, .op = .open, .node = log }).errno());
_ = call(p, .{ .tag = 3, .op = .release, .node = log, .handle = handles[5] });
@@ -772,5 +775,5 @@ test "opens of the log share the snapshot slots, and a closed one frees its slot
try testing.expectEqual(Status.ok, again.reply.status);
handles[5] = again.reply.handle;
for (handles) |h| _ = call(p, .{ .tag = 5, .op = .release, .node = log, .handle = h });
- for (p.fs.snapshots) |slot| try testing.expect(slot.node == 0);
+ for (p.fs.opens) |o| try testing.expect(o.node == 0);
}
diff --git a/src/ninep/pane.zig b/src/ninep/pane.zig
index b07ee038..c03cd776 100644
--- a/src/ninep/pane.zig
+++ b/src/ninep/pane.zig
@@ -32,6 +32,7 @@ pub const State = struct {
/// `cat addr` answer what was written.
addr: Range = .{},
limit: ?Range = null,
+ /// Opens of `event`, which hold the pane scripted.
readers: u16 = 0,
/// One of `readers` reads `event`; a second reading open is refused.
event_reader: bool = false,
@@ -39,9 +40,11 @@ pub const State = struct {
nomark: bool = false,
noscroll: bool = false,
tag_snap: std.ArrayList(u8) = .empty,
- pty_readers: u16 = 0,
+ /// An open reads pty/data, so output queues for it; a second is refused.
+ pty_reader: bool = false,
pty_out: events.Queue = .{},
- /// The pty/run open waiting on this shell's current command.
+ /// The open record (tree.zig) of the pty/run waiting on this shell's
+ /// current command.
run: ?u8 = null,
/// The host started a shell it could not teach to mark its prompts.
unmarked: bool = false,
diff --git a/src/ninep/pty.zig b/src/ninep/pty.zig
index ddb7bcd1..706163e5 100644
--- a/src/ninep/pty.zig
+++ b/src/ninep/pty.zig
@@ -109,15 +109,12 @@ pub fn readStatus(p: *Pardes, req: Req, id: usize, pane: *Pane) Reply {
// command. The shell's own OSC 133 marks say when the command started (C)
// and ended (D, with its status); pardes injects them into bash and fish.
-pub const run_slots = 32;
-
/// The most of a command's output an answer carries: its last this-many
/// bytes, since that is where a failure says what went wrong.
pub const output_cap = 64 * 1024;
+/// What an open of run holds (tree.zig's open records).
pub const Run = struct {
- /// 0 is a free slot.
- node: u64 = 0,
phase: enum { idle, sent, done } = .idle,
/// The shell's command count its C must reach for the end to be ours.
want: u32 = 0,
@@ -139,29 +136,8 @@ pub const Run = struct {
len: u8 = 0,
/// Then what the command printed, gpa-owned.
output: []u8 = &.{},
-
- pub fn reset(slot: *Run, gpa: std.mem.Allocator, node: u64) void {
- gpa.free(slot.output);
- slot.* = .{ .node = node };
- }
};
-fn runOf(p: *Pardes, req: Req) ?*Run {
- if (req.handle == 0 or req.handle > run_slots) return null;
- const slot = &p.fs.runs[req.handle - 1];
- return if (slot.node == req.node) slot else null;
-}
-
-pub fn openRun(p: *Pardes, req: Req, serial: u32) Reply {
- _ = serial;
- for (&p.fs.runs, 0..) |*slot, i| {
- if (slot.node != 0) continue;
- slot.* = .{ .node = req.node };
- return .{ .tag = req.tag, .handle = @intCast(i + 1) };
- }
- return Reply.fail(req.tag, E.NFILE);
-}
-
fn answer(p: *Pardes, slot: *Run, comptime fmt: []const u8, args: anytype) void {
slot.len = @intCast((std.fmt.bufPrint(&slot.answer, fmt ++ "\n", args) catch unreachable).len);
slot.phase = .done;
@@ -169,9 +145,11 @@ fn answer(p: *Pardes, slot: *Run, comptime fmt: []const u8, args: anytype) void
}
pub fn writeRun(p: *Pardes, req: Req, id: usize, pane: *Pane) Reply {
- const slot = runOf(p, req) orelse return Reply.fail(req.tag, E.INVAL);
+ const slot = &(tree.openOf(p, req) orelse return Reply.fail(req.tag, E.INVAL)).what.run;
if (slot.phase == .sent) return tree.failText(req.tag, E.BUSY, tree.e_in_use);
- slot.reset(p.gpa, slot.node); // a new line on this open: a new answer
+ // A new line on this open: a new answer.
+ p.gpa.free(slot.output);
+ slot.* = .{};
const line = std.mem.trimEnd(u8, req.data, "\r\n");
if (std.mem.trim(u8, line, " \t").len == 0) return tree.failText(req.tag, E.INVAL, e_bad_line);
if (line.len == 0) return tree.failText(req.tag, E.INVAL, e_bad_line);
@@ -197,13 +175,13 @@ pub fn writeRun(p: *Pardes, req: Req, id: usize, pane: *Pane) Reply {
p.emitWrite(id, line);
p.emitWrite(id, "\r");
slot.phase = .sent;
- pf.run = @intCast(slot - &p.fs.runs[0]);
+ pf.run = @intCast(req.handle - 1);
}
return .{ .tag = req.tag, .written = @intCast(req.data.len) };
}
pub fn readRun(p: *Pardes, req: Req) Reply {
- const slot = runOf(p, req) orelse return Reply.fail(req.tag, E.INVAL);
+ const slot = &(tree.openOf(p, req) orelse return Reply.fail(req.tag, E.INVAL)).what.run;
switch (slot.phase) {
.idle => return .{ .tag = req.tag, .payload = .{ .staged = 0 } },
.sent => return .{ .tag = req.tag, .status = .again },
@@ -226,17 +204,6 @@ pub fn readRun(p: *Pardes, req: Req) Reply {
return .{ .tag = req.tag, .payload = .{ .staged = @intCast(out.items.len) } };
}
-pub fn releaseRun(p: *Pardes, req: Req) Reply {
- const slot = runOf(p, req) orelse return .{ .tag = req.tag };
- const idx: u8 = @intCast(slot - &p.fs.runs[0]);
- // The command runs on in the shell; nobody is waiting for it any more.
- for (p.panes) |live| if (live) |pane| if (pane.fs.run == idx) {
- pane.fs.run = null;
- };
- slot.reset(p.gpa, 0);
- return .{ .tag = req.tag };
-}
-
/// After each chunk of a terminal's output: once the command the run sent
/// has ended and the shell is back at a prompt, the run answers how. Waiting
/// for the prompt means whoever reads the answer can send the next command
@@ -245,7 +212,7 @@ pub fn noteMarks(p: *Pardes, id: usize, pane: *Pane) void {
if (comptime !pardes.panes.Terminal.enabled) return;
const state = pane.terminal orelse return;
const pf = &pane.fs;
- const slot = &p.fs.runs[pf.run orelse return];
+ const slot = &p.fs.opens[pf.run orelse return].what.run;
const marks = &state.stream.handler;
if (marks.phase != .input) return;
// ponytail: a prompt redrawn before the command starts (a resize in that
@@ -325,7 +292,7 @@ pub fn noteMarks(p: *Pardes, id: usize, pane: *Pane) void {
/// be reported, so the run says so instead of waiting forever.
pub fn shellGone(p: *Pardes, pane: *Pane) void {
const idx = pane.fs.run orelse return;
- answer(p, &p.fs.runs[idx], "error shell gone", .{});
+ answer(p, &p.fs.opens[idx].what.run, "error shell gone", .{});
pane.fs.run = null;
}
@@ -387,7 +354,7 @@ test "a pane that is not a terminal has no pty/ at all" {
.op = .open,
.node = Node.of(serial, .pty_data),
}).errno());
- try testing.expectEqual(@as(u16, 0), p.panes[0].?.fs.pty_readers);
+ try testing.expect(!p.panes[0].?.fs.pty_reader);
var buf: [32]th.Dirent = undefined;
const files = th.dirents(rdir(p, dir, 0).bytes, &buf);
@@ -578,11 +545,11 @@ test "pty/data writes at the shell and reads the raw stream" {
// 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.panes[0].?.fs.pty_readers);
+ try testing.expect(!p.panes[0].?.fs.pty_reader);
_ = 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.panes[0].?.fs.pty_readers);
+ try testing.expect(p.panes[0].?.fs.pty_reader);
try testing.expectEqual(@as(u16, 0), p.fs.listeners);
try testing.expect(p.panes[0].?.fs.readers == 0);
@@ -602,7 +569,7 @@ 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, .handle = reader.reply.handle });
- try testing.expectEqual(@as(u16, 0), p.panes[0].?.fs.pty_readers);
+ try testing.expect(!p.panes[0].?.fs.pty_reader);
try testing.expectEqual(@as(usize, 0), p.panes[0].?.fs.pty_out.buf.capacity);
try testing.expectEqual(Status.again, rd(p, data, 0, 64).reply.status);
@@ -611,7 +578,7 @@ test "pty/data writes at the shell and reads the raw stream" {
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.panes[0].?.fs.pty_readers);
+ try testing.expect(p.panes[0].?.fs.pty_reader);
p.update(.{ .output = .{ .pane = 0, .bytes = "still" } });
while (p.nextEffect()) |_| {}
try testing.expectEqualStrings("still", rd(p, data, 0, 64).bytes);
@@ -717,7 +684,7 @@ test "run answers how its command ended once the shell is back at a prompt" {
try p.removePane(0, null);
try testing.expectEqualStrings("error shell gone\n", call(p, .{ .tag = 19, .op = .read, .node = node, .handle = gone.reply.handle, .size = 64 }).bytes);
_ = call(p, .{ .tag = 20, .op = .release, .node = node, .handle = gone.reply.handle });
- for (p.fs.runs) |slot| try testing.expect(slot.node == 0);
+ for (p.fs.opens) |o| try testing.expect(o.node == 0);
}
test "the pty queue drops the oldest at its cap" {
diff --git a/src/ninep/screen.zig b/src/ninep/screen.zig
index 89ca369b..f72043d4 100644
--- a/src/ninep/screen.zig
+++ b/src/ninep/screen.zig
@@ -1,5 +1,5 @@
-//! /screen, /listeners and the snapshot handles that freeze a screen or a
-//! terminal history between open and close.
+//! /screen, /listeners and the snapshots that freeze a screen or a terminal
+//! history between open and close (tree.zig's open records hold them).
const std = @import("std");
const pardes = @import("../pardes.zig");
const panes = @import("../panes.zig");
@@ -11,21 +11,6 @@ const Req = tree.Req;
const Reply = tree.Reply;
const E = tree.E;
-pub const Snapshot = struct {
- node: u64 = 0,
- bytes: ?[]const u8 = null,
- /// /log only: this open waits for records after `bytes`, from `next`.
- follow: bool = false,
- next: u64 = 0,
- /// How much of record `next` a short read already took.
- part: u32 = 0,
- /// /log only: how much of `bytes` this open has read. Reads walk it as a
- /// stream and ignore their offset, since `exec 3<>log; echo follow >&3`
- /// leaves the shared offset past the word it wrote.
- pos: u64 = 0,
-};
-pub const snapshot_slots = 32;
-
pub fn render(p: *Pardes) ![]u8 {
var arena: std.heap.ArenaAllocator = .init(p.gpa);
defer arena.deinit();
@@ -58,26 +43,13 @@ pub fn render(p: *Pardes) ![]u8 {
return out.toOwnedSlice();
}
-/// A screen freezes at open; a terminal body freezes at its first read.
-pub fn openSnapshot(p: *Pardes, req: Req, of_screen: bool) Reply {
- for (&p.fs.snapshots, 0..) |*slot, i| {
- if (slot.node != 0) continue;
- const bytes = if (of_screen) render(p) catch return Reply.fail(req.tag, E.NOMEM) else null;
- slot.* = .{ .node = req.node, .bytes = bytes };
- return .{ .tag = req.tag, .handle = @intCast(i + 1) };
- }
- return Reply.fail(req.tag, E.NFILE);
-}
-
pub fn readSnapshot(p: *Pardes, req: Req, pane: ?*Pane) Reply {
- if (req.handle == 0 or req.handle > p.fs.snapshots.len) return Reply.fail(req.tag, E.INVAL);
- const snapshot = &p.fs.snapshots[req.handle - 1];
- if (snapshot.node == 0 or snapshot.node != req.node) return Reply.fail(req.tag, E.INVAL);
- if (snapshot.bytes == null) {
+ const snapshot = &(tree.openOf(p, req) orelse return Reply.fail(req.tag, E.INVAL)).what.snapshot;
+ if (snapshot.* == null) {
const terminal = pane orelse return Reply.fail(req.tag, E.INVAL);
- snapshot.bytes = panes.Terminal.screenTextAlloc(terminal, p.gpa) catch return Reply.fail(req.tag, E.NOMEM);
+ snapshot.* = panes.Terminal.screenTextAlloc(terminal, p.gpa) catch return Reply.fail(req.tag, E.NOMEM);
}
- const bytes = snapshot.bytes.?;
+ const bytes = snapshot.*.?;
const off = @min(req.off, bytes.len);
const len = @min(bytes.len - off, req.size);
const out = p.fs.stage(p.gpa);
@@ -85,16 +57,6 @@ pub fn readSnapshot(p: *Pardes, req: Req, pane: ?*Pane) Reply {
return .{ .tag = req.tag, .payload = .{ .staged = @intCast(len) } };
}
-/// True when the handle named a snapshot, which is now released.
-pub fn releaseSnapshot(p: *Pardes, req: Req) bool {
- if (req.handle == 0 or req.handle > p.fs.snapshots.len) return false;
- const snapshot = &p.fs.snapshots[req.handle - 1];
- if (snapshot.node != req.node) return false;
- if (snapshot.bytes) |bytes| p.gpa.free(bytes);
- snapshot.* = .{};
- return true;
-}
-
const listeners_cap = 512;
fn listenersText(p: *Pardes, buf: []u8) []const u8 {
@@ -199,7 +161,7 @@ test "an empty terminal body snapshot stays empty while output continues" {
const node = Node.of(serialOf(p), .body);
const opened = call(p, .{ .tag = 1, .op = .open, .node = node });
try testing.expectEqualStrings("", call(p, .{ .tag = 2, .op = .read, .node = node, .handle = opened.reply.handle, .size = 32 }).bytes);
- try testing.expect(p.fs.snapshots[opened.reply.handle - 1].bytes != null);
+ try testing.expect(p.fs.opens[opened.reply.handle - 1].what.snapshot != null);
p.update(.{ .output = .{ .pane = 0, .bytes = "new output" } });
while (p.nextEffect()) |_| {}
try testing.expectEqualStrings("", call(p, .{ .tag = 3, .op = .read, .node = node, .handle = opened.reply.handle, .size = 32 }).bytes);
@@ -214,15 +176,15 @@ test "terminal body snapshots are lazy bounded and released after the pane close
const p = try withTerm(gpa);
defer p.deinit();
const node = Node.of(serialOf(p), .body);
- var handles: [snapshot_slots]u32 = undefined;
+ var handles: [tree.open_slots]u32 = undefined;
for (&handles) |*opened_handle| {
const opened = call(p, .{ .tag = 1, .op = .open, .node = node });
try testing.expectEqual(Status.ok, opened.reply.status);
opened_handle.* = opened.reply.handle;
}
- for (p.fs.snapshots) |snapshot| {
- try testing.expectEqual(node, snapshot.node);
- try testing.expect(snapshot.bytes == null);
+ for (p.fs.opens) |o| {
+ try testing.expectEqual(node, o.node);
+ try testing.expect(o.what.snapshot == null);
}
try testing.expectEqual(E.NFILE, call(p, .{ .tag = 2, .op = .open, .node = node }).errno());
const scratch = try th.newPane(p);
@@ -235,26 +197,23 @@ test "terminal body snapshots are lazy bounded and released after the pane close
const failed = call(p, .{ .tag = 3, .op = .read, .node = node, .handle = handles[0], .size = 32 });
p.gpa = gpa;
try testing.expectEqual(E.NOMEM, failed.errno());
- try testing.expect(p.fs.snapshots[handles[0] - 1].bytes == null);
+ try testing.expect(p.fs.opens[handles[0] - 1].what.snapshot == null);
const first = call(p, .{ .tag = 4, .op = .read, .node = node, .handle = handles[0], .size = 32 });
try testing.expectEqualStrings("latest output", first.bytes);
- const saved = p.fs.snapshots[handles[0] - 1].bytes.?;
+ const saved = p.fs.opens[handles[0] - 1].what.snapshot.?;
failing = std.testing.FailingAllocator.init(gpa, .{ .fail_index = 0 });
p.gpa = failing.allocator();
const second = call(p, .{ .tag = 5, .op = .read, .node = node, .handle = handles[0], .off = 7, .size = 32 });
p.gpa = gpa;
try testing.expectEqualStrings("output", second.bytes);
try testing.expect(!failing.has_induced_failure);
- try testing.expectEqual(saved.ptr, p.fs.snapshots[handles[0] - 1].bytes.?.ptr);
+ try testing.expectEqual(saved.ptr, p.fs.opens[handles[0] - 1].what.snapshot.?.ptr);
try testing.expectEqual(E.INVAL, call(p, .{ .tag = 6, .op = .read, .node = screen_node, .handle = handles[0], .size = 32 }).errno());
_ = call(p, .{ .tag = 7, .op = .release, .node = screen_node, .handle = handles[0] });
- try testing.expectEqual(node, p.fs.snapshots[handles[0] - 1].node);
+ try testing.expectEqual(node, p.fs.opens[handles[0] - 1].node);
try p.removePane(0, null);
for (handles) |opened_handle| _ = call(p, .{ .tag = 8, .op = .release, .node = node, .handle = opened_handle });
- for (p.fs.snapshots) |snapshot| {
- try testing.expectEqual(@as(u64, 0), snapshot.node);
- try testing.expect(snapshot.bytes == null);
- }
+ for (p.fs.opens) |o| try testing.expectEqual(@as(u64, 0), o.node);
}
test "screen snapshots preserve rendered cells and styles until their handle is released" {
@@ -263,7 +222,7 @@ test "screen snapshots preserve rendered cells and styles until their handle is
defer p.deinit();
const opened = call(p, .{ .tag = 1, .op = .open, .node = screen_node });
try testing.expectEqual(Status.ok, opened.reply.status);
- const snapshot = p.fs.snapshots[opened.reply.handle - 1].bytes.?;
+ const snapshot = p.fs.opens[opened.reply.handle - 1].what.snapshot.?;
const original = try gpa.dupe(u8, snapshot);
defer gpa.free(original);
const parsed = try std.json.parseFromSlice(std.json.Value, gpa, original, .{});
@@ -276,7 +235,7 @@ test "screen snapshots preserve rendered cells and styles until their handle is
_ = wr(p, Node.of(serialOf(p), .body), "changed\n");
const newer = call(p, .{ .tag = 2, .op = .open, .node = screen_node });
try testing.expectEqual(Status.ok, newer.reply.status);
- try testing.expect(!std.mem.eql(u8, original, p.fs.snapshots[newer.reply.handle - 1].bytes.?));
+ try testing.expect(!std.mem.eql(u8, original, p.fs.opens[newer.reply.handle - 1].what.snapshot.?));
var off: usize = 0;
while (off < original.len) {
const result = call(p, .{ .tag = 3, .op = .read, .node = screen_node, .handle = opened.reply.handle, .off = off, .size = 13 });
@@ -286,7 +245,7 @@ test "screen snapshots preserve rendered cells and styles until their handle is
}
_ = call(p, .{ .tag = 4, .op = .release, .node = screen_node, .handle = opened.reply.handle });
_ = call(p, .{ .tag = 5, .op = .release, .node = screen_node, .handle = newer.reply.handle });
- for (p.fs.snapshots) |slot| try testing.expect(slot.node == 0);
+ for (p.fs.opens) |o| try testing.expect(o.node == 0);
}
test "screen inspection preserves acknowledged presentation and the next real frame" {
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();