summaryrefslogtreecommitdiff
path: root/src/ninep/ctl.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/ninep/ctl.zig')
-rw-r--r--src/ninep/ctl.zig120
1 files changed, 109 insertions, 11 deletions
diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig
index 0bbc0319..9a95a10f 100644
--- a/src/ninep/ctl.zig
+++ b/src/ninep/ctl.zig
@@ -192,8 +192,30 @@ pub fn command(p: *Pardes, req: Req, serial: ?u32, exec: bool, in_tag: bool) Rep
for (text) |c| if ((c < ' ' and c != '\t' and c != '\n') or c == 0x7f) return tree.failText(req.tag, E.INVAL, e_control);
if (exec) if (tooLong(req, text)) |refusal| return refusal;
if (!apply) continue;
- const id = if (serial) |s| p.paneBySerial(s) orelse break else p.active;
+ var id = if (serial) |s| p.paneBySerial(s) orelse break else p.active;
+ // No pane at all: the root's look and exec act in the session's
+ // directory. An exec runs as the workspace tag's; a look from a
+ // scratch made as New makes one (and its column), dropped
+ // unannounced when the look went elsewhere.
+ var scratch: ?usize = null;
+ if (p.panes[id] == null and serial == null and p.anyPane() == null) {
+ if (exec) {
+ p.exec_header = true;
+ defer p.exec_header = false;
+ if (captured(p, req, id, exec, text, true)) |refusal| return refusal;
+ continue;
+ }
+ const free = p.freeSlot() orelse return Reply.fail(req.tag, E.NOSPC);
+ p.newScratchFor(id, .scratch);
+ if (p.panes[free] == null) return Reply.fail(req.tag, E.NOSPC);
+ p.active = free;
+ id = free;
+ scratch = free;
+ }
if (p.panes[id] == null) return Reply.fail(req.tag, E.NOENT);
+ defer if (scratch) |s| if (p.active != s) if (p.panes[s]) |sp| if (sp.file) |f| {
+ if (f.content.len == 0) p.removePane(s, null) catch {};
+ };
p.fs.ran_at = p.panes[id].?.serial;
p.fs.no_pane_slot = false;
if (captured(p, req, id, exec, text, in_tag)) |refusal| return refusal;
@@ -267,7 +289,8 @@ fn failureErrno(failure: []const u8) u16 {
if (std.mem.indexOf(u8, failure, "no such") != null or std.mem.indexOf(u8, failure, "not found") != null) return E.NOENT;
// A pattern refused (Edit's too) is malformed input, as 9ns reads it;
// so is a word this build has not (`invalid: acme's Put ...`).
- if (std.mem.indexOf(u8, failure, "bad regular expression") != null or std.mem.startsWith(u8, failure, "invalid")) return E.INVAL;
+ if (std.mem.indexOf(u8, failure, "bad regular expression") != null or std.mem.startsWith(u8, failure, "invalid") or
+ std.mem.startsWith(u8, failure, "address out of range")) return E.INVAL;
return E.IO;
}
@@ -586,7 +609,7 @@ pub fn writeRoot(p: *Pardes, req: Req) Reply {
if (apply) p.fs.results_len = 0;
var it = Messages.init(p, req.data);
while (it.next()) |line| {
- if (line.len == 0) continue;
+ if (line.len == 0 or exec_line.isComment(line)) continue;
if (std.mem.startsWith(u8, line, "size ") or std.mem.eql(u8, line, "size")) {
const size = sizeOf(line) orelse return refuse(p, req, e_size, line);
// A frontend's window is the size; only a session none is
@@ -812,7 +835,7 @@ pub fn writePane(p: *Pardes, req: Req, pane: *Pane) Reply {
var held = !other and pane.fs.lock != null;
var it = Messages.init(p, req.data);
while (it.next()) |line_in| {
- if (line_in.len == 0) continue;
+ if (line_in.len == 0 or exec_line.isComment(line_in)) continue;
// A line before this one may have closed the pane (`Del`).
if (apply and p.paneBySerial(serial) == null) return Reply.fail(req.tag, E.NOENT);
// acme's ctl words: done by the file that replaces each, or the
@@ -1929,12 +1952,12 @@ test "a builtin that fails a ctl write logs only its err, so the same failure ag
p.update(.tick); // the pane's `new` is logged first
while (p.nextEffect()) |_| {}
const ctl_node = @intFromEnum(tree.TopFile.ctl);
- for (0..3) |_| try testing.expectEqual(Status.err, wr(p, ctl_node, "Kill zzz\n").reply.status);
+ for (0..3) |_| try testing.expectEqual(Status.err, wr(p, ctl_node, "Unmount zzz\n").reply.status);
const log = @intFromEnum(tree.TopFile.log);
const h = call(p, .{ .tag = 1, .op = .open, .node = log }).reply.handle;
const text = call(p, .{ .tag = 2, .op = .read, .node = log, .handle = h, .size = 1 << 16 }).bytes;
- try testing.expect(std.mem.endsWith(u8, text, "err - ctl: Kill: no running command has that first word (x3)\n"));
- try testing.expect(std.mem.indexOf(u8, text, "msg - Kill") == null);
+ try testing.expect(std.mem.endsWith(u8, text, "err - ctl: Unmount: no such mount zzz (x3)\n"));
+ try testing.expect(std.mem.indexOf(u8, text, "msg - Unmount") == null);
_ = call(p, .{ .tag = 3, .op = .release, .node = log, .handle = h });
}
@@ -2445,6 +2468,81 @@ test "a look that misses file:/re/ names the file, and is logged as its pane's"
try testing.expect(th.logHas(p, try std.fmt.bufPrint(&want, "err {d} look: {s}/f.txt: no match for regexp", .{ f_serial, dir })));
}
+test "with no pane at all, the root look opens its file and the root exec runs in the session's directory" {
+ const p = try withFile(testing.allocator, "x\n");
+ defer p.deinit();
+ var tmp = testing.tmpDir(.{});
+ defer tmp.cleanup();
+ try tmp.dir.writeFile(testing.io, .{ .sub_path = "f.txt", .data = "file\n" });
+ var buf: [4096]u8 = undefined;
+ const session = buf[0..try tmp.dir.realPath(testing.io, &buf)];
+ @memcpy(p.session_dir[0..session.len], session);
+ p.session_dir_len = @intCast(session.len);
+ const Empty = struct {
+ fn all(q: *Pardes) !void {
+ while (q.anyPane()) |k| _ = wr(q, Node.of(q.panes[k].?.serial, .ctl), "delete\n");
+ }
+ };
+ try Empty.all(p);
+ const look_node = @intFromEnum(tree.TopFile.look);
+ try testing.expectEqual(Status.ok, wr(p, look_node, "f.txt\n").reply.status);
+ var want: [4200]u8 = undefined;
+ const path = try std.fmt.bufPrint(&want, "{s}/f.txt", .{session});
+ var opened: usize = 0;
+ var scratches: usize = 0;
+ for (p.panes) |slot| {
+ const f = (slot orelse continue).file orelse continue;
+ opened += @intFromBool(std.mem.eql(u8, f.path, path));
+ scratches += @intFromBool(std.mem.endsWith(u8, f.path, "/+New"));
+ }
+ try testing.expectEqual(@as(usize, 1), opened);
+ try testing.expectEqual(@as(usize, 0), scratches);
+
+ try Empty.all(p);
+ try testing.expectEqual(Status.ok, wr(p, @intFromEnum(tree.TopFile.exec), "echo hi\n").reply.status);
+ const made = p.panes[p.anyPane().?].?;
+ try testing.expect(made.command != null);
+ try testing.expectEqualStrings(session, made.cwdSlice());
+}
+
+test "a look at file:3:0 is refused as addr 3:0 is, a column counting from 1" {
+ const p = try withFile(testing.allocator, "one\ntwo\nthree\n");
+ defer p.deinit();
+ const serial = serialOf(p);
+ const by_addr = wr(p, Node.of(serial, .addr), "3:0");
+ const by_look = wr(p, Node.of(serial, .look), "/test.txt:3:0\n");
+ try testing.expectEqual(E.INVAL, by_addr.errno());
+ try testing.expectEqual(E.INVAL, by_look.errno());
+ try testing.expectEqualStrings(by_addr.reply.ename, by_look.reply.ename);
+ try testing.expectEqual(Status.ok, wr(p, Node.of(serial, .look), "/test.txt:3:1\n").reply.status);
+}
+
+test "Kill with a name and nothing running says so and does not fail, as bare Kill does" {
+ const p = try withFile(testing.allocator, "x\n");
+ defer p.deinit();
+ const r = wr(p, @intFromEnum(tree.TopFile.ctl), "Kill zzz\n");
+ try testing.expectEqual(Status.ok, r.reply.status);
+ try testing.expect(th.logHas(p, "Kill: nothing running"));
+}
+
+test "a line starting with # runs as nothing at an exec or a ctl, silently" {
+ const p = try withFile(testing.allocator, "x\n");
+ defer p.deinit();
+ const serial = serialOf(p);
+ // The setup's own announcements are made by the first write's update.
+ _ = wr(p, @intFromEnum(tree.TopFile.ctl), "# settle\n");
+ const before = p.next_serial;
+ const log_before = try pardes.ctlfs.events.freezeLog(p);
+ defer p.gpa.free(log_before.bytes);
+ for ([_]u64{ @intFromEnum(tree.TopFile.exec), Node.of(serial, .exec), @intFromEnum(tree.TopFile.ctl), Node.of(serial, .ctl), Node.ofCol(layout.columnSerial(p, 0), .ctl) }) |node| {
+ try testing.expectEqual(Status.ok, wr(p, node, "# Platform: tty\n # indented too\n").reply.status);
+ }
+ try testing.expectEqual(before, p.next_serial);
+ const log_after = try pardes.ctlfs.events.freezeLog(p);
+ defer p.gpa.free(log_after.bytes);
+ try testing.expectEqualStrings(log_before.bytes, log_after.bytes);
+}
+
test "the ctl word name refuses a leading blank, as the name file does" {
const p = try withFile(testing.allocator, "x\n");
defer p.deinit();
@@ -3225,9 +3323,9 @@ test "a builtin that fails through exec, tagexec or a column's exec fails the wr
while (p.nextEffect()) |_| {}
const serial = serialOf(p);
for ([_]u64{ root_exec, Node.of(serial, .exec), @intFromEnum(tree.TopFile.tagexec), Node.ofCol(pardes.layout.columnSerial(p, 0), .exec) }) |node| {
- const failed = wr(p, node, "Kill zzz\n");
+ const failed = wr(p, node, "Unmount zzz\n");
try testing.expectEqual(Status.err, failed.reply.status);
- try testing.expect(std.mem.indexOf(u8, failed.reply.ename, "Kill: no running command has that first word") != null);
+ try testing.expect(std.mem.indexOf(u8, failed.reply.ename, "Unmount: no such mount zzz") != null);
}
const log = @intFromEnum(tree.TopFile.log);
const h = call(p, .{ .tag = 1, .op = .open, .node = log }).reply.handle;
@@ -3235,8 +3333,8 @@ test "a builtin that fails through exec, tagexec or a column's exec fails the wr
try testing.expect(std.mem.indexOf(u8, text, "msg ") == null);
// The root's exec ran at the pane, so its err and the pane's exec's are
// the same record, counted.
- try testing.expectEqual(@as(usize, 3), std.mem.count(u8, text, "Kill: no running command has that first word"));
- try testing.expect(std.mem.indexOf(u8, text, "first word (x2)") != null);
+ try testing.expectEqual(@as(usize, 3), std.mem.count(u8, text, "Unmount: no such mount zzz"));
+ try testing.expect(std.mem.indexOf(u8, text, "mount zzz (x2)") != null);
_ = call(p, .{ .tag = 3, .op = .release, .node = log, .handle = h });
// A control character is refused with its reason.
try testing.expectEqualStrings(e_control, wr(p, root_exec, "Msg a\x01b\n").reply.ename);