summaryrefslogtreecommitdiff
path: root/src/ninep
diff options
context:
space:
mode:
Diffstat (limited to 'src/ninep')
-rw-r--r--src/ninep/cols.zig4
-rw-r--r--src/ninep/ctl.zig133
-rw-r--r--src/ninep/events.zig79
-rw-r--r--src/ninep/tree.zig24
4 files changed, 183 insertions, 57 deletions
diff --git a/src/ninep/cols.zig b/src/ninep/cols.zig
index b2e62d28..e3b456ba 100644
--- a/src/ninep/cols.zig
+++ b/src/ninep/cols.zig
@@ -167,10 +167,10 @@ pub fn writeCtl(p: *Pardes, req: Req, serial: u32) Reply {
} else false;
// Plan 9's words for it, so a mount's errno is EINVAL as the root's
// and a pane's ctl give (9ns reads "control message").
- if (!takes) return tree.failText(req.tag, E.INVAL, std.fmt.bufPrint(&p.fs.ename, "unknown control message \"{s}\": a column's ctl takes Delcol, Joincol, New or Tty", .{line[0..@min(line.len, 64)]}) catch "unknown control message");
+ if (!takes) return tree.failText(req.tag, E.INVAL, std.fmt.bufPrint(&p.fs.ename, "unknown control message \"{s}\": a column's ctl takes Delcol, Joincol, New or Tty", .{pardes.ctlfs.ctl.firstWord(line)[0..@min(pardes.ctlfs.ctl.firstWord(line).len, 64)]}) catch "unknown control message");
// Only Tty takes a word (its shell); `New extra` is no New.
if (!std.mem.eql(u8, word, "Tty") and word.len != line.len)
- return tree.failText(req.tag, E.INVAL, std.fmt.bufPrint(&p.fs.ename, "wrong #args in control message \"{s}\"", .{line[0..@min(line.len, 64)]}) catch "wrong #args in control message");
+ return tree.failText(req.tag, E.INVAL, std.fmt.bufPrint(&p.fs.ename, "wrong #args in control message \"{s}\"", .{pardes.ctlfs.ctl.firstWord(line)[0..@min(pardes.ctlfs.ctl.firstWord(line).len, 64)]}) catch "wrong #args in control message");
const col = layout.columnBySerial(p, serial) orelse return Reply.fail(req.tag, E.NOENT);
p.exec_column = col;
defer p.exec_column = null;
diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig
index 31265a94..87339b70 100644
--- a/src/ninep/ctl.zig
+++ b/src/ninep/ctl.zig
@@ -296,7 +296,7 @@ pub fn captured(p: *Pardes, req: Req, id: usize, exec: bool, text: []const u8, i
/// The errno a builtin's failure answers with: ENOENT for what is not
/// there (`no such`, `not found`, the words 9ns reads as ENOENT too), else
/// EIO.
-fn failureErrno(failure: []const u8) u16 {
+pub 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 ...`).
@@ -422,23 +422,39 @@ const Builtin = builtins.registry.Builtin();
/// Refuses a control message, quoting it the way Plan 9's cmderror does
/// (kernel/misc/parse.c:82): `unknown control message "Bogus 3"`.
fn refuse(p: *Pardes, req: Req, why: []const u8, line: []const u8) Reply {
- const room = p.fs.ename.len -| (why.len + 3);
- var shown: [320]u8 = undefined;
- const text = std.fmt.bufPrint(&p.fs.ename, "{s} \"{s}\"", .{ why, blanked(line[0..@min(line.len, room)], &shown) }) catch why;
- return tree.failText(req.tag, E.INVAL, text);
+ const word = firstWord(line);
+ // A word too long to quote whole gives up its middle (fitErr), so the
+ // refusal keeps both its ends.
+ var shown: [4096]u8 = undefined;
+ var whole: [4096 + 320]u8 = undefined;
+ const full = std.fmt.bufPrint(&whole, "{s} \"{s}\"", .{ why, blanked(word[0..@min(word.len, shown.len)], &shown) }) catch why;
+ // Into the session's own buffer: `whole` is this frame's.
+ if (full.len <= p.fs.ename.len) {
+ @memcpy(p.fs.ename[0..full.len], full);
+ return tree.failText(req.tag, E.INVAL, p.fs.ename[0..full.len]);
+ }
+ return tree.failText(req.tag, E.INVAL, tree.fitErr(full, &p.fs.ename));
+}
+
+/// The word a refusal quotes: a control message's first, its offending
+/// word, never the line with its arguments (one rule everywhere: `wrong
+/// #args in control message "Newcol"`).
+pub fn firstWord(line: []const u8) []const u8 {
+ const trimmed = std.mem.trim(u8, line, " \t\r\n");
+ return trimmed[0 .. std.mem.indexOfAny(u8, trimmed, " \t") orelse trimmed.len];
}
/// `refuse`, saying which ctl takes the message instead.
pub fn refuseTo(p: *Pardes, req: Req, why: []const u8, line: []const u8, ctl: []const u8) Reply {
const room = p.fs.ename.len -| (why.len + ctl.len + 18);
var shown: [320]u8 = undefined;
- const text = std.fmt.bufPrint(&p.fs.ename, "{s} \"{s}\": write it to {s}", .{ why, blanked(line[0..@min(line.len, room)], &shown), ctl }) catch why;
+ const text = std.fmt.bufPrint(&p.fs.ename, "{s} \"{s}\": write it to {s}", .{ why, blanked(firstWord(line)[0..@min(firstWord(line).len, room)], &shown), ctl }) catch why;
return tree.failText(req.tag, E.INVAL, text);
}
/// A quoted line as a refusal shows it: a control byte (a tab, the one a
/// line may hold) is a blank, so the reason reads as one line of words.
-fn blanked(line: []const u8, buf: *[320]u8) []const u8 {
+fn blanked(line: []const u8, buf: []u8) []const u8 {
const n = @min(line.len, buf.len);
for (line[0..n], buf[0..n]) |c, *o| o.* = if (c < ' ' or c == 0x7f) ' ' else c;
return buf[0..n];
@@ -1087,7 +1103,8 @@ test "a control character in a write to any ctl refuses the whole write, and a q
}
const quoted = wr(p, Node.of(serialOf(p), .ctl), "bogus\tword\n");
try testing.expectEqual(E.INVAL, quoted.errno());
- try testing.expect(std.mem.indexOf(u8, quoted.reply.ename, "bogus word") != null);
+ // The offending word alone is quoted: a tab ends it.
+ try testing.expect(std.mem.indexOf(u8, quoted.reply.ename, "\"bogus\"") != null);
try testing.expect(std.mem.indexOfScalar(u8, quoted.reply.ename, '\t') == null);
}
@@ -1209,11 +1226,11 @@ test "the pane ctl takes acme's verbs and the pane's builtins, and refuses the r
const refused = wr(p, ctl_node, bad);
try testing.expectEqual(E.INVAL, refused.errno());
var want: [64]u8 = undefined;
- try testing.expectEqualStrings(try std.fmt.bufPrint(&want, "unknown control message \"{s}\"", .{bad}), refused.reply.ename);
+ try testing.expectEqualStrings(try std.fmt.bufPrint(&want, "unknown control message \"{s}\"", .{firstWord(bad)}), refused.reply.ename);
}
try testing.expectEqualStrings("not a window control message \"Newcol\": write it to /ctl", wr(p, ctl_node, "Newcol").reply.ename);
- try testing.expectEqualStrings("not a window control message \"Verbose off\": write it to /ctl", wr(p, ctl_node, "Verbose off").reply.ename);
- try testing.expectEqualStrings("wrong #args in control message \"Collapse now\"", wr(p, ctl_node, "Collapse now").reply.ename);
+ try testing.expectEqualStrings("not a window control message \"Verbose\": write it to /ctl", wr(p, ctl_node, "Verbose off").reply.ename);
+ try testing.expectEqualStrings("wrong #args in control message \"Collapse\"", wr(p, ctl_node, "Collapse now").reply.ename);
// Checked whole first: a bad line anywhere and nothing ran.
try testing.expectEqual(E.INVAL, wr(p, ctl_node, "Msg first\nbogus").errno());
try testing.expect(p.panes[0].?.msg_len == 0 or !std.mem.eql(u8, p.panes[0].?.msg[0..p.panes[0].?.msg_len], "first"));
@@ -1265,18 +1282,18 @@ test "the root ctl reads the settings as a write takes them, and takes the sessi
try testing.expectEqual(columns + 1, p.ncol);
try testing.expectEqualStrings("not a session control message \"Del\": write it to pane/<n>/ctl", wr(p, root_ctl, "Del").reply.ename);
- try testing.expectEqualStrings("unknown control message \"Nonsense 1\"", wr(p, root_ctl, "Nonsense 1").reply.ename);
- try testing.expectEqualStrings("bad value in control message; takes on, off \"Verbose maybe\"", wr(p, root_ctl, "Verbose maybe").reply.ename);
- try testing.expectEqualStrings("bad value in control message; takes acme, pardes \"Placement east\"", wr(p, root_ctl, "Placement east").reply.ename);
+ try testing.expectEqualStrings("unknown control message \"Nonsense\"", wr(p, root_ctl, "Nonsense 1").reply.ename);
+ try testing.expectEqualStrings("bad value in control message; takes on, off \"Verbose\"", wr(p, root_ctl, "Verbose maybe").reply.ename);
+ try testing.expectEqualStrings("bad value in control message; takes acme, pardes \"Placement\"", wr(p, root_ctl, "Placement east").reply.ename);
// A number's refusal names its range, a path's what it takes.
- try testing.expectEqualStrings("bad value in control message; takes 0-100 (a percentage) \"InactiveDim 200\"", wr(p, root_ctl, "InactiveDim 200").reply.ename);
- try testing.expectEqualStrings("bad value in control message; takes 0-60000 (milliseconds) \"MessageLinger x\"", wr(p, root_ctl, "MessageLinger x").reply.ename);
+ try testing.expectEqualStrings("bad value in control message; takes 0-100 (a percentage) \"InactiveDim\"", wr(p, root_ctl, "InactiveDim 200").reply.ename);
+ try testing.expectEqualStrings("bad value in control message; takes 0-60000 (milliseconds) \"MessageLinger\"", wr(p, root_ctl, "MessageLinger x").reply.ename);
for (config.Runtime.settings) |setting| if (setting.action != .theme)
try testing.expect(config.Runtime.takes(setting.action) != null);
const no_theme = wr(p, root_ctl, "Theme no-such-theme").reply.ename;
try testing.expectStringStartsWith(no_theme, "bad value in control message; like it: n");
- try testing.expect(std.mem.endsWith(u8, no_theme, "; Themes lists all \"Theme no-such-theme\""));
- try testing.expectEqualStrings("wrong #args in control message \"Newcol 2\"", wr(p, root_ctl, "Newcol 2").reply.ename);
+ try testing.expect(std.mem.endsWith(u8, no_theme, "; Themes lists all \"Theme\""));
+ try testing.expectEqualStrings("wrong #args in control message \"Newcol\"", wr(p, root_ctl, "Newcol 2").reply.ename);
try testing.expectEqualStrings("wrong #args in control message \"Theme\"", wr(p, root_ctl, "Theme").reply.ename);
// A bare required word fails the check, so the line before never runs.
try testing.expectEqual(E.INVAL, wr(p, root_ctl, "Verbose on\nMount").errno());
@@ -1760,6 +1777,71 @@ test "exec Save writes the named file through the host" {
try testing.expectEqualStrings(pane.file.?.content, p.fallback.get("/tmp/renamed.txt").?);
}
+test "a refusal's +Unsaved goes in the session's directory when the active pane's directory is not there" {
+ if (comptime !pardes.hosted) return;
+ const p = try withFile(testing.allocator, "x\n");
+ defer p.deinit();
+ const serial = serialOf(p);
+ try testing.expectEqual(Status.ok, wr(p, Node.of(serial, .name), "/tmp/pardes-no-such-dir-zz/x.txt\n").reply.status);
+ try testing.expectEqual(Status.ok, wr(p, Node.of(serial, .body), "edited\n").reply.status);
+ try testing.expectEqual(Status.err, wr(p, @intFromEnum(tree.TopFile.ctl), "Exit\n").reply.status);
+ const unsaved = for (p.panes) |slot| {
+ const pane = slot orelse continue;
+ const f = pane.file orelse continue;
+ if (f.output) |o| if (o.from == .unsaved) break f.path;
+ } else return error.NoUnsavedPane;
+ try testing.expectEqualStrings(p.sessionDir(), std.fs.path.dirname(unsaved).?);
+}
+
+test "a look at a file it may not read fails the write naming the file" {
+ if (comptime !pardes.hosted) return;
+ 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 = "locked.txt", .data = "secret\n", .flags = .{ .permissions = .fromMode(0o000) } });
+ var dir_buf: [4096]u8 = undefined;
+ const dir = dir_buf[0..try tmp.dir.realPath(testing.io, &dir_buf)];
+ var line: [4200]u8 = undefined;
+ const path = try std.fmt.bufPrint(&line, "{s}/locked.txt", .{dir});
+ var want: [4200]u8 = undefined;
+ const expected = try std.fmt.bufPrint(&want, "look: {s}: permission denied", .{path});
+ var with_newline: [4300]u8 = undefined;
+ const refused = wr(p, root_look, try std.fmt.bufPrint(&with_newline, "{s}\n", .{path}));
+ if (refused.reply.status == .ok) return error.SkipZigTest; // root reads anything
+ try testing.expectEqualStrings(expected, refused.reply.ename);
+}
+
+test "Pager is a setting of the root ctl: pardes by default, off taken, anything else refused with its choices" {
+ const p = try withFile(testing.allocator, "x\n");
+ defer p.deinit();
+ const root_ctl = @intFromEnum(tree.TopFile.ctl);
+ try testing.expect(std.mem.indexOf(u8, rd(p, root_ctl, 0, 1 << 16).bytes, "Pager pardes\n") != null);
+ try testing.expectEqual(Status.ok, wr(p, root_ctl, "Pager off\n").reply.status);
+ try testing.expectEqual(config.Runtime.Pager.off, p.settings.pager);
+ try testing.expect(std.mem.indexOf(u8, rd(p, root_ctl, 0, 1 << 16).bytes, "Pager off\n") != null);
+ try testing.expectEqualStrings("bad value in control message; takes pardes, off \"Pager\"", wr(p, root_ctl, "Pager less\n").reply.ename);
+}
+
+test "a refused Del written back through event fails that write, EIO, with its err" {
+ const gpa = testing.allocator;
+ const p = try withFile(gpa, "text\n");
+ defer p.deinit();
+ const serial = serialOf(p);
+ try testing.expectEqual(Status.ok, wr(p, Node.of(serial, .body), "more\n").reply.status); // unsaved
+ const event = Node.of(serial, .event);
+ const h = call(p, .{ .tag = 1, .op = .open, .node = event }).reply.handle;
+ defer _ = call(p, .{ .tag = 9, .op = .release, .node = event, .handle = h });
+ try testing.expectEqual(Status.ok, wr(p, Node.of(serial, .exec), "Del\n").reply.status);
+ const record = call(p, .{ .tag = 2, .op = .read, .node = event, .handle = h, .size = 256 }).bytes;
+ var copy: [256]u8 = undefined;
+ @memcpy(copy[0..record.len], record);
+ const back = wr(p, event, copy[0..record.len]);
+ try testing.expectEqual(E.IO, back.errno());
+ try testing.expect(std.mem.indexOf(u8, back.reply.ename, "Modified") != null);
+ try testing.expect(p.paneBySerial(serial) != null);
+}
+
test "a pane's own look and exec go to the event reader holding it, which writes them back to act" {
const gpa = testing.allocator;
const p = try withFile(gpa, "Msg routed\n");
@@ -1951,7 +2033,7 @@ test "Joincol with no column to the right and Theme with no such theme say so" {
try testing.expectEqual(E.INVAL, themed.errno());
try testing.expectStringStartsWith(themed.reply.ename, "bad value in control message; like it: ");
try testing.expect(std.mem.indexOf(u8, themed.reply.ename, ": dr") != null);
- try testing.expect(std.mem.endsWith(u8, themed.reply.ename, "\"Theme drak\""));
+ try testing.expect(std.mem.endsWith(u8, themed.reply.ename, "\"Theme\""));
// A click on the word says it on the message row.
_ = wr(p, Node.of(serialOf(p), .exec), "Theme drak\n");
const pane = p.panes[p.active].?;
@@ -2089,8 +2171,8 @@ test "a setting this frontend cannot show says GUI-only, and DumpDir reads back
defer p.deinit();
const root_ctl = @intFromEnum(tree.TopFile.ctl);
if (!builtins.capabilities.lift) {
- try testing.expectEqualStrings("Lift is GUI-only, invalid here \"Lift on\"", wr(p, root_ctl, "Lift on\n").reply.ename);
- try testing.expectEqualStrings("GripWidth is GUI-only, invalid here \"GripWidth 120\"", wr(p, root_ctl, "GripWidth 120\n").reply.ename);
+ try testing.expectEqualStrings("Lift is GUI-only, invalid here \"Lift\"", wr(p, root_ctl, "Lift on\n").reply.ename);
+ try testing.expectEqualStrings("GripWidth is GUI-only, invalid here \"GripWidth\"", wr(p, root_ctl, "GripWidth 120\n").reply.ename);
// A builtin the GUI alone has, written to a ctl or an exec, never
// runs as a shell command.
if (comptime !pardes.font_picker) {
@@ -2100,7 +2182,7 @@ test "a setting this frontend cannot show says GUI-only, and DumpDir reads back
try testing.expectEqualStrings("Fonts is GUI-only, invalid here", ran.reply.ename);
}
}
- try testing.expectEqualStrings("unknown control message \"Liftt on\"", wr(p, root_ctl, "Liftt on\n").reply.ename);
+ try testing.expectEqualStrings("unknown control message \"Liftt\"", wr(p, root_ctl, "Liftt on\n").reply.ename);
var buf: [900]u8 = undefined;
if (pardes.dump.directory(&buf, "")) |dir| {
var want: [920]u8 = undefined;
@@ -2945,13 +3027,14 @@ test "every EINVAL a write gets says why, in its err record too; DEL is a contro
try testing.expect(th.logHas(p, "an empty name"));
}
-test "/index shows a name as the log does: a newline in it is \\n, controls spaces, bytes not UTF-8 \\xNN" {
+test "/index shows a name as the log does: a newline in it is \\n, a control byte, C1 or byte not UTF-8 \\xNN" {
const p = try th.withTerm(testing.allocator);
defer p.deinit();
const term = p.paneBySerial(serialOf(p)).?;
- p.setCwd(term, "/tmp/two\nlines\x7f\xff");
+ p.setCwd(term, "/tmp/two\nlines\x07\x7f\xc2\x85\xff");
const index = rd(p, @intFromEnum(tree.TopFile.index), 0, 4096).bytes;
- try testing.expect(std.mem.indexOf(u8, index, "/tmp/two\\nlines \\xff ") != null);
+ // Each byte as itself, so the name decodes to what it is.
+ try testing.expect(std.mem.indexOf(u8, index, "/tmp/two\\nlines\\x07\\x7f\\xc2\\x85\\xff ") != null);
try testing.expectEqual(@as(usize, 1), std.mem.count(u8, index, "\n"));
try testing.expect(index.len > 0);
try testing.expectEqual(@as(u64, 0), call(p, .{ .tag = 1, .op = .getattr, .node = @intFromEnum(tree.TopFile.index) }).reply.attr.size);
diff --git a/src/ninep/events.zig b/src/ninep/events.zig
index bacf3ad6..c0b52d03 100644
--- a/src/ninep/events.zig
+++ b/src/ninep/events.zig
@@ -6,6 +6,7 @@ const exec = @import("../exec.zig");
const look = @import("../look.zig");
const cloud9 = @import("cloud9");
const tree = @import("tree.zig");
+const ctl = @import("ctl.zig");
const pane_files = @import("pane.zig");
const dump = @import("../dump.zig");
@@ -336,9 +337,10 @@ fn sanitize(record: []const u8, out: []u8) []u8 {
/// `text` as one line of UTF-8 a reader can split and decode, as /log and
/// /index show names: a backslash is `\\`, a newline `\n` (a name with one
-/// stays readable as what it is, not run into the next), any other control
-/// character, DEL or C1 control (U+0080-U+009F) a space, and a byte that
-/// is not UTF-8 `\xNN`. `out` of 4 bytes a byte of `text` holds it all.
+/// stays readable as what it is, not run into the next), and any other
+/// control character, DEL, C1 control (U+0080-U+009F) or byte that is not
+/// UTF-8 `\xNN`, so a name read there decodes to the bytes it is. `out` of
+/// 4 bytes a byte of `text` holds it all.
pub fn shown(text: []const u8, out: []u8) []u8 {
return shownAs(text, out, true);
}
@@ -364,8 +366,15 @@ fn shownAs(text: []const u8, out: []u8, escape_newline: bool) []u8 {
continue;
}
if (c < ' ' or c == 0x7f) {
- out[w] = ' ';
- w += 1;
+ // A name's (escape_newline) is `\xNN`, so it reads back as the
+ // byte it was; a message's a space.
+ if (escape_newline) {
+ _ = std.fmt.bufPrint(out[w..], "\\x{x:0>2}", .{c}) catch break;
+ w += 4;
+ } else {
+ out[w] = ' ';
+ w += 1;
+ }
i += 1;
continue;
}
@@ -377,6 +386,13 @@ fn shownAs(text: []const u8, out: []u8, escape_newline: bool) []u8 {
continue;
}
if (n == 2 and c == 0xC2 and body[i + 1] <= 0x9F) {
+ if (escape_newline) {
+ if (w + 8 > out.len) break;
+ _ = std.fmt.bufPrint(out[w..], "\\xc2\\x{x:0>2}", .{body[i + 1]}) catch break;
+ w += 8;
+ i += 2;
+ continue;
+ }
out[w] = ' ';
w += 1;
i += 2;
@@ -494,18 +510,6 @@ pub fn writeLog(p: *Pardes, req: Req) Reply {
return .{ .tag = req.tag, .written = @intCast(req.data.len) };
}
-/// The length a stat reports: the text an open would freeze now.
-pub fn logLen(q: *const Queue) u64 {
- var n: u64 = 0;
- var at = q.head;
- while (at + 4 <= q.buf.items.len) {
- const len = std.mem.readInt(u32, q.buf.items[at..][0..4], .little);
- n += len;
- at += 4 + len;
- }
- return n;
-}
-
// ---- per-pane event records ----
pub const max_record_text = 256;
@@ -697,11 +701,27 @@ fn scanNumber(data: []const u8, i: *usize) ?u32 {
/// Writing a Look or Exec record back performs the action it names.
/// The word a click at `at` expands to: the file name or word run under it
/// on its line (look.expandedWord), else nothing.
-fn expandAt(whole: []const u8, at: usize) []const u8 {
+/// An empty range written back: what a click at `at` takes, as the mouse's
+/// does (exec.pointerOperand). In a diff pane's body that is look.diffClick's
+/// answer, the line's address or its code's words.
+pub fn expandAt(p: *Pardes, pane: *Pane, whole: []const u8, at: usize, in_body: bool) []const u8 {
const start = if (std.mem.lastIndexOfScalar(u8, whole[0..at], '\n')) |nl| nl + 1 else 0;
const end = std.mem.indexOfScalarPos(u8, whole, at, '\n') orelse whole.len;
- const b = look.expandedWord(whole[start..end], at - start) orelse return "";
- return whole[start + b.lo .. start + b.hi];
+ var word_from: usize = 0;
+ if (in_body) diff: {
+ const row = std.mem.count(u8, whole[0..start], "\n");
+ // Only when the served line is the one the diff reads there.
+ const lines = look.diffLines(p, pane) orelse break :diff;
+ if (row >= lines.len or !std.mem.eql(u8, std.mem.trimEnd(u8, lines[row], "\r"), std.mem.trimEnd(u8, whole[start..end], "\r"))) break :diff;
+ var buf: [4096 + 32]u8 = undefined;
+ const click = look.diffClick(p, pane, row, at - start, &buf) orelse break :diff;
+ if (click.address) |address| return p.scratch.allocator().dupe(u8, address) catch "";
+ word_from = click.word_from;
+ }
+ const line = whole[start..end];
+ if (at - start < word_from or word_from > line.len) return "";
+ const b = look.expandedWord(line[word_from..], at - start - word_from) orelse return "";
+ return whole[start + word_from + b.lo .. start + word_from + b.hi];
}
pub fn writeEvent(p: *Pardes, asked: Req, id: usize) Reply {
@@ -732,6 +752,11 @@ pub fn writeEvent(p: *Pardes, asked: Req, id: usize) Reply {
}
if (check.i != req.data.len) return tree.failText(req.tag, E.INVAL, tree.e_bad_event ++ ": a record is <origin><action><q0> <q1> <flag> <count> <text>, a line each");
}
+ // What a record written back runs fails the write as a click written
+ // to exec would (a refused Del: EIO and its err), not said and lost.
+ p.fs.capturing = true;
+ p.fs.failure_len = 0;
+ defer p.fs.capturing = false;
var run: EventReader = .{ .data = req.data };
while (run.next()) |r| {
const live = p.paneBySerial(serial) orelse break;
@@ -742,7 +767,7 @@ pub fn writeEvent(p: *Pardes, asked: Req, id: usize) Reply {
// An empty range with no text is a click at that point: expanded as
// the click would be, to the word or file name under it (acme's
// expand, look.c).
- const said = if (r.q0 == r.q1) r.text orelse expandAt(whole, lo) else whole[lo..hi];
+ const said = if (r.q0 == r.q1) r.text orelse expandAt(p, pane, whole, lo, !r.action.onTag()) else whole[lo..hi];
const text = p.scratch.allocator().dupe(u8, said) catch continue;
switch (r.action) {
// A body's click written back is done as the click would have
@@ -811,6 +836,10 @@ pub fn writeEvent(p: *Pardes, asked: Req, id: usize) Reply {
else => unreachable,
}
}
+ if (p.fs.failure_len > 0) {
+ const failure = p.fs.failure[0..p.fs.failure_len];
+ return tree.failText(req.tag, ctl.failureErrno(failure), std.fmt.bufPrint(&p.fs.ename, "{s}", .{failure}) catch failure);
+ }
return .{ .tag = req.tag, .written = @intCast(asked.data.len) };
}
@@ -981,10 +1010,10 @@ test "a pane deleted while its event file is open leaves no suppression behind"
_ = wr(p, Node.of(serial, .ctl), "Del\n");
try testing.expect(p.paneBySerial(serial) == null);
try testing.expectEqual(@as(u16, 0), p.fs.listeners);
- // The reader's next read hears what acme says of a window gone under it.
+ // The reader's next read is its end: no more records, no error.
const shut = call(p, .{ .tag = 19, .op = .read, .node = event, .handle = a.reply.handle, .size = 64 });
- try testing.expectEqual(E.NOENT, shut.errno());
- try testing.expectEqualStrings(tree.e_shut_down, shut.reply.ename);
+ try testing.expectEqual(tree.Status.ok, shut.reply.status);
+ try testing.expectEqualStrings("", shut.bytes);
_ = call(p, .{ .tag = 20, .op = .release, .node = event, .handle = a.reply.handle });
_ = call(p, .{ .tag = 21, .op = .release, .node = event, .handle = b.reply.handle });
@@ -1388,7 +1417,7 @@ test "a record is one line of UTF-8: DEL and C1 are spaces, bytes not UTF-8 are
test "a long err record keeps its end, its middle given up to an ellipsis" {
const p = try withFile(testing.allocator, "x\n");
defer p.deinit();
- _ = wr(p, @intFromEnum(tree.TopFile.ctl), "Bogus " ++ "abcdefgh " ** 40 ++ "\n");
+ _ = wr(p, @intFromEnum(tree.TopFile.ctl), "Bogus" ++ "abcdefgh" ** 40 ++ "\n");
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;
diff --git a/src/ninep/tree.zig b/src/ninep/tree.zig
index be03e8f0..c71a706f 100644
--- a/src/ninep/tree.zig
+++ b/src/ninep/tree.zig
@@ -646,7 +646,9 @@ fn topSize(p: *Pardes, f: TopFile) u64 {
.README => fs.help.len,
.status => ctl.statusLen(p),
.look, .exec, .tagexec => ctl.resultsLen(p),
- .log => events.logLen(&p.fs.log),
+ // A stream: a length would read as its end to a kernel that caches
+ // one (a 9ns mount's splice), and a follower's reads go past it.
+ .log => 0,
.ctl => ctl.rootLen(p),
.commands => ctl.commandsLen(),
.focus => focus: {
@@ -1092,10 +1094,14 @@ fn readFile(p: *Pardes, req: Req, target: Target) Reply {
// A run's answer outlives the pane it ran in.
if (t.file == .pty_run and req.handle != 0) return pty.readRun(p, req);
const id = p.paneBySerial(t.serial) orelse {
- // An event or pty/data read the pane closed under: acme's
- // answer to the same (editors/acme/xfid.c:1005).
- if (openOf(p, req)) |o| if (o.what == .event or o.what == .pty_data)
- return failText(req.tag, E.NOENT, e_shut_down);
+ // An event reader whose pane closed reads its end: there
+ // are no more records, as a pipe's writer gone is EOF, so
+ // `while read` and `cat` end cleanly. A pty/data read the
+ // pane closed under keeps acme's answer (xfid.c:1005).
+ if (openOf(p, req)) |o| {
+ if (o.what == .event) return .{ .tag = req.tag };
+ if (o.what == .pty_data) return failText(req.tag, E.NOENT, e_shut_down);
+ }
return Reply.fail(req.tag, E.NOENT);
};
const pn = p.panes[id].?;
@@ -1335,6 +1341,14 @@ test "filesystem inspection preserves pending and displayed Look hover" {
try testing.expect(!p.raw_hover_intent);
}
+test "log stats 0 however much it holds, a stream as acme's generated files are" {
+ const p = try th.withFile(testing.allocator, "x\n");
+ defer p.deinit();
+ _ = th.wr(p, Node.of(th.serialOf(p), .exec), "Msg a record for the log\n");
+ try testing.expect(th.logHas(p, "a record for the log"));
+ try testing.expectEqual(@as(u64, 0), call(p, .{ .tag = 1, .op = .getattr, .node = @intFromEnum(TopFile.log) }).reply.attr.size);
+}
+
test "filesystem pane creation and truncation cancel Look hover" {
const requests = [_]Req{
// Opening `new` is what makes a pane now; walking to it makes none,