summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
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();