diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/9p_io.zig | 22 | ||||
| -rw-r--r-- | src/fs.zig | 17 | ||||
| -rw-r--r-- | src/ninep/events.zig | 67 | ||||
| -rw-r--r-- | src/ninep/pane.zig | 7 | ||||
| -rw-r--r-- | src/ninep/pty.zig | 63 | ||||
| -rw-r--r-- | src/ninep/screen.zig | 79 | ||||
| -rw-r--r-- | src/ninep/tree.zig | 229 |
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); @@ -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(); |
