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