diff options
Diffstat (limited to 'src/ninep/tree.zig')
| -rw-r--r-- | src/ninep/tree.zig | 164 |
1 files changed, 154 insertions, 10 deletions
diff --git a/src/ninep/tree.zig b/src/ninep/tree.zig index 085d03a3..da9c37f0 100644 --- a/src/ninep/tree.zig +++ b/src/ninep/tree.zig @@ -82,21 +82,21 @@ pub const features: cloud9.fs.Features = .{ .remove = true, .create = true }; /// the mutation of its own step, so the layout and the surface are whole /// under it -- and a step out there is never waiting on a write of its own, /// so what parks here is never the syscall's own request. -pub fn needsQuiet(req: Req) bool { +pub fn needsQuiet(p: *Pardes, req: Req) bool { return switch (req.op) { .write, .setattr => true, - .release => req.remove, + .release => req.remove or holdsLines(p, req), else => false, }; } /// `needsQuiet`, plus the open that makes a pane: what costs a frame and a /// wake of the editor. -pub fn changesPane(req: Req) bool { +pub fn changesPane(p: *Pardes, req: Req) bool { return switch (req.op) { .write, .setattr => true, .open => req.node == @intFromEnum(TopFile.new), - .release => req.remove, + .release => req.remove or holdsLines(p, req), .lookup, .getattr, .read, .readdir => false, }; } @@ -125,7 +125,20 @@ pub const Open = struct { pty_data, /// An open that may write a pane's ctl, and so hold its lock. ctl, + /// An open of a file that takes lines of commands (look, exec, a + /// ctl, tagexec): its `pending` and `results`. + lines, } = .{ .snapshot = null }, + /// What a write to a file of command lines left unfinished: a last line + /// with no newline yet, or an Edit block still open. A write through a + /// mount is cut at the 9P message size, so one `echo` of many lines may + /// come as several writes, cut anywhere; the rest waits for the open's + /// next write, or its release, and never runs as a fragment. + pending: std.ArrayList(u8) = .empty, + /// The panes this open's last write touched, which a read of look or + /// exec on it answers, as /net/tcp/clone answers on the open that + /// asked; a fresh open holds the session's last, from before it opened. + results: Results = .{}, /// 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/ @@ -142,6 +155,16 @@ pub const Open = struct { pub const Replay = struct { off: u64, bytes: []u8 }; + pub const Results = struct { + list: [pardes.MAX_PANES]u32 = undefined, + len: u8 = 0, + /// Written on: reads are a stream from the write on, whatever + /// offset they come at, since a shell's `exec 3<>look` shares one + /// offset between the write and the read (as pty/run does). + wrote: bool = false, + read: usize = 0, + }; + /// `asker` is the connection the read came on, which only the listener /// knows. pub const Held = struct { asker: *anyopaque, req: Req, ticket: cloud9.fs.Ticket }; @@ -154,8 +177,9 @@ pub const Open = struct { gpa.free(run.output); gpa.free(run.line); }, - .event, .pty_data, .ctl => {}, + .event, .pty_data, .ctl, .lines => {}, } + o.pending.deinit(gpa); if (o.replay) |r| gpa.free(r.bytes); o.* = .{}; } @@ -692,9 +716,11 @@ fn open(p: *Pardes, req: Req, target: Target) Reply { }, .screen => .{ .snapshot = null }, .log => .{ .log = .{} }, - else => return .{ .tag = req.tag }, + else => if (linesOpen(target, req)) .lines else return .{ .tag = req.tag }, }, - .col => |c| return if (layout.columnBySerial(p, c.serial) != null) .{ .tag = req.tag } else Reply.fail(req.tag, E.NOENT), + .col => |c| if (layout.columnBySerial(p, c.serial) == null) + return Reply.fail(req.tag, E.NOENT) + else if (linesOpen(target, req)) .lines else return .{ .tag = req.tag }, .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); @@ -711,7 +737,7 @@ fn open(p: *Pardes, req: Req, target: Target) Reply { 1, 2 => .ctl, // OWRITE, ORDWR else => return .{ .tag = req.tag }, }, - else => return .{ .tag = req.tag }, + else => if (linesOpen(target, req)) .lines else return .{ .tag = req.tag }, }; }, }; @@ -720,6 +746,10 @@ fn open(p: *Pardes, req: Req, target: Target) Reply { } else return Reply.fail(req.tag, E.NFILE); const o = &p.fs.opens[i]; o.* = .{ .node = req.node, .what = what }; + if (resultsFile(target)) { + @memcpy(o.results.list[0..p.fs.results_len], p.fs.results[0..p.fs.results_len]); + o.results.len = p.fs.results_len; + } switch (o.what) { // A screen freezes at open; a terminal body at its first read. .snapshot => |*bytes| if (pane_of == null) { @@ -739,7 +769,7 @@ fn open(p: *Pardes, req: Req, target: Target) Reply { pn.fs.event_reader = pn.fs.event_reader or reader; }, .pty_data => pane_of.?.fs.pty_reader = true, - .run, .ctl => {}, + .run, .ctl, .lines => {}, } return .{ .tag = req.tag, .handle = @intCast(i + 1) }; } @@ -779,6 +809,14 @@ fn releaseHandle(p: *Pardes, req: Req) void { if (t == .col and t.col.file == .tag) cols.released(p, t.col.serial); if (t == .top and t.top == .tag) cols.released(p, null); const o = openOf(p, req) orelse return; + // The last line an open wrote with no newline after it runs now. + if (o.pending.items.len > 0) { + const rest = p.scratch.allocator().dupe(u8, o.pending.items) catch ""; + o.pending.clearRetainingCapacity(); + const w: Req = .{ .tag = req.tag, .op = .write, .node = req.node, .handle = req.handle, .data = rest }; + const reply = writeNow(p, w, t); + if (reply.status == .err) events.noteError(p, w, reply); + } 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| { @@ -800,7 +838,7 @@ fn releaseHandle(p: *Pardes, req: Req) void { .ctl => if (pn.fs.lock == req.handle) { pn.fs.lock = null; }, - .snapshot, .log => {}, + .snapshot, .log, .lines => {}, }; o.deinit(p.gpa); } @@ -912,7 +950,69 @@ fn readFile(p: *Pardes, req: Req, target: Target) Reply { } } +/// The files whose writes are lines of commands, run as each line is whole. +fn linesFile(target: Target) bool { + return switch (target) { + .top => |f| f == .look or f == .exec or f == .ctl or f == .tagexec, + .col => |c| c.file == .ctl or c.file == .exec, + .pane => |t| t.file == .look or t.file == .exec or t.file == .ctl, + }; +} + +/// The ones of those a read answers with the panes a write touched. +fn resultsFile(target: Target) bool { + return switch (target) { + .top => |f| f == .look or f == .exec or f == .tagexec, + .col => |c| c.file == .exec, + .pane => |t| t.file == .look or t.file == .exec, + }; +} + +/// An open that keeps lines or results: any that may write one of those +/// files, and any that may read a look or exec. +fn linesOpen(target: Target, req: Req) bool { + if (!linesFile(target)) return false; + return req.omode & 3 != 0 or resultsFile(target); +} + +/// A release that will run a line its open held back. +fn holdsLines(p: *Pardes, req: Req) bool { + const o = openOf(p, req) orelse return false; + return o.pending.items.len > 0; +} + +/// A line held back longer than this is refused rather than kept growing. +const pending_cap = 1 << 20; + fn write(p: *Pardes, req: Req, target: Target) Reply { + const o = (if (linesFile(target)) openOf(p, req) else null) orelse return writeNow(p, req, target); + if (o.what != .lines and o.what != .ctl) return writeNow(p, req, target); + // A clicked line holds no control character: refused at once, not held + // to fail unseen at the close. + if (resultsFile(target)) for (req.data) |c| if (c < ' ' and c != '\t' and c != '\n' and c != '\r') return Reply.fail(req.tag, E.INVAL); + o.pending.appendSlice(p.gpa, req.data) catch return Reply.fail(req.tag, E.NOMEM); + const end = ctl.completeEnd(p, o.pending.items); + if (end == 0) { + if (o.pending.items.len <= pending_cap) return .{ .tag = req.tag, .written = @intCast(req.data.len) }; + o.pending.clearRetainingCapacity(); + return failText(req.tag, E.INVAL, "a line or Edit block over 1 MiB"); + } + const whole = p.scratch.allocator().dupe(u8, o.pending.items[0..end]) catch return Reply.fail(req.tag, E.NOMEM); + const rest = o.pending.items.len - end; + std.mem.copyForwards(u8, o.pending.items[0..rest], o.pending.items[end..]); + o.pending.shrinkRetainingCapacity(rest); + var w = req; + w.data = whole; + var reply = writeNow(p, w, target); + if (resultsFile(target)) { + @memcpy(o.results.list[0..p.fs.results_len], p.fs.results[0..p.fs.results_len]); + o.results = .{ .list = o.results.list, .len = p.fs.results_len, .wrote = true }; + } + if (reply.status == .ok) reply.written = @intCast(req.data.len); + return reply; +} + +fn writeNow(p: *Pardes, req: Req, target: Target) Reply { switch (target) { .top => |f| return switch (f) { .look, .exec => ctl.command(p, req, null, f == .exec), @@ -1345,3 +1445,47 @@ test "at the pane cap, pane/new, look and New each say so, and look reads back e const made = th.wr(p, ctl_node, "New\n"); try testing.expect(std.mem.indexOf(u8, made.reply.ename, said) != null); } + +test "a command line cut across writes runs once whole, and the last runs at release; an Edit block waits for its end" { + const p = try th.withFile(testing.allocator, "x\n"); + defer p.deinit(); + const exec = @intFromEnum(TopFile.exec); + const h = call(p, .{ .tag = 1, .op = .open, .node = exec, .omode = 1 }).reply.handle; + try testing.expect(h != 0); + for ([_][]const u8{ "Msg he", "llo\nMsg wor", "ld\nMsg tail" }) |piece| + try testing.expectEqual(Status.ok, call(p, .{ .tag = 2, .op = .write, .node = exec, .handle = h, .data = piece }).reply.status); + try testing.expect(th.logHas(p, " hello\n") and th.logHas(p, " world\n")); + try testing.expect(!th.logHas(p, " he\n") and !th.logHas(p, " tail\n")); + _ = call(p, .{ .tag = 3, .op = .release, .node = exec, .handle = h, .opened = true }); + try testing.expect(th.logHas(p, " tail\n")); + // An Edit block's text lines come in later writes on the same open. + const serial = serialOf(p); + const ctl_node = Node.of(serial, .ctl); + const c = call(p, .{ .tag = 4, .op = .open, .node = ctl_node, .omode = 1 }).reply.handle; + for ([_][]const u8{ "Edit ,a\n", "one\ntw", "o\n", ".\n" }) |piece| + try testing.expectEqual(Status.ok, call(p, .{ .tag = 5, .op = .write, .node = ctl_node, .handle = c, .data = piece }).reply.status); + _ = call(p, .{ .tag = 6, .op = .release, .node = ctl_node, .handle = c, .opened = true }); + try testing.expectEqualStrings("x\none\ntwo\n", p.panes[0].?.file.?.content); +} + +test "look and exec read back what their own open's write touched; a fresh open reads the session's last" { + const p = try th.withFile(testing.allocator, "x\n"); + defer p.deinit(); + const exec = @intFromEnum(TopFile.exec); + const first = p.panes[0].?.serial; + const mine = call(p, .{ .tag = 1, .op = .open, .node = exec, .omode = 2 }).reply.handle; + const theirs = call(p, .{ .tag = 2, .op = .open, .node = exec, .omode = 2 }).reply.handle; + _ = call(p, .{ .tag = 3, .op = .write, .node = exec, .handle = mine, .data = "Tty\n" }); + const made = p.panes[p.active].?.serial; + try testing.expect(made != first); + _ = call(p, .{ .tag = 4, .op = .write, .node = exec, .handle = theirs, .data = "Msg hi\n" }); + var want: [16]u8 = undefined; + // Read at the offset the write left, as a shell's `exec 3<>` does. + try testing.expectEqualStrings(try std.fmt.bufPrint(&want, "{d}\n", .{made}), call(p, .{ .tag = 5, .op = .read, .node = exec, .handle = mine, .off = 4, .size = 64 }).bytes); + try testing.expectEqualStrings("", call(p, .{ .tag = 6, .op = .read, .node = exec, .handle = mine, .off = 4, .size = 64 }).bytes); + const said = p.panes[p.active].?.serial; + try testing.expectEqualStrings(try std.fmt.bufPrint(&want, "{d}\n", .{said}), call(p, .{ .tag = 7, .op = .read, .node = exec, .handle = theirs, .size = 64 }).bytes); + const fresh = call(p, .{ .tag = 8, .op = .open, .node = exec }).reply.handle; + try testing.expectEqualStrings(try std.fmt.bufPrint(&want, "{d}\n", .{said}), call(p, .{ .tag = 9, .op = .read, .node = exec, .handle = fresh, .size = 64 }).bytes); + for ([_]u32{ mine, theirs, fresh }) |hh| _ = call(p, .{ .tag = 10, .op = .release, .node = exec, .handle = hh, .opened = true }); +} |
