summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/ninep/events.zig61
-rw-r--r--src/pardes.zig1
2 files changed, 62 insertions, 0 deletions
diff --git a/src/ninep/events.zig b/src/ninep/events.zig
index 172f5c00..8ee25b74 100644
--- a/src/ninep/events.zig
+++ b/src/ninep/events.zig
@@ -4,6 +4,7 @@ const std = @import("std");
const pardes = @import("../pardes.zig");
const exec = @import("../exec.zig");
const look = @import("../look.zig");
+const cloud9 = @import("cloud9");
const tree = @import("tree.zig");
const pane_files = @import("pane.zig");
@@ -149,6 +150,36 @@ pub fn noteMessage(p: *Pardes, serial: u32, text: []const u8) void {
std.fmt.bufPrint(&buf, "msg {d} {s}\n", .{ serial, text })) catch return);
}
+/// Records `err <serial|-> <file>: <why>` for a write or truncation that was
+/// refused or failed: through a mount a shell sees only the errno the
+/// kernel mapped the reply to (`Invalid argument`), and here is the reason.
+/// The log is the one place for it, as acme's `errors` file takes text and
+/// answers nothing: a per-pane readable error file would be a second.
+pub fn noteError(p: *Pardes, req: Req, reply: Reply) void {
+ const why = if (reply.ename.len > 0) reply.ename else cloud9.fs.errString(reply.errno);
+ var name: [16]u8 = undefined;
+ var serial: []const u8 = "-";
+ var file: []const u8 = "os";
+ if (req.node & tree.sources.archive_node != 0) file = "src";
+ if (req.node != pardes.filesystem.os_root and req.node & pardes.filesystem.os_node == 0 and req.node & tree.sources.archive_node == 0) {
+ if (tree.Node.target(req.node)) |t| switch (t) {
+ .top => |f| file = f.fileName(),
+ .pane => |pt| {
+ serial = std.fmt.bufPrint(&name, "{d}", .{pt.serial}) catch "-";
+ file = switch (pt.file) {
+ .pty_ctl => "pty/ctl",
+ .pty_status => "pty/status",
+ .pty_data => "pty/data",
+ .pty_run => "pty/run",
+ else => pt.file.fileName(),
+ };
+ },
+ };
+ }
+ var buf: [256]u8 = undefined;
+ pushLog(p, std.fmt.bufPrint(&buf, "err {s} {s}: {s}\n", .{ serial, file, why[0..@min(why.len, 200)] }) catch return);
+}
+
/// The log is one ring that records whether or not anyone reads it. A record
/// is one line: a newline in a message or a name would read as two records.
fn pushLog(p: *Pardes, record: []u8) void {
@@ -690,6 +721,8 @@ test "the log records whether or not anyone reads, and an open that follows wait
return call(pp, .{ .tag = 9, .op = .read, .node = node, .handle = h, .off = off, .size = 8192 });
}
};
+ // The word it refused is on record, and then there is nothing new.
+ try testing.expect(std.mem.startsWith(u8, rdf.next(p, log, fh, frozen).bytes, "err - log: "));
try testing.expectEqual(Status.again, rdf.next(p, log, fh, frozen).reply.status);
const serial = try th.newPane(p);
@@ -761,6 +794,34 @@ 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 "a refused or failed write is an err record in the log, saying which file and why" {
+ const p = try withFile(testing.allocator, "one\n");
+ defer p.deinit();
+ p.update(.tick); // the pane's `new` is logged before the log is opened
+ while (p.nextEffect()) |_| {}
+ const serial = serialOf(p);
+ const log = @intFromEnum(tree.TopFile.log);
+ const f = call(p, .{ .tag = 1, .op = .open, .node = log }).reply.handle;
+ const frozen = call(p, .{ .tag = 2, .op = .read, .node = log, .handle = f, .size = 1 << 16 }).bytes.len;
+ _ = call(p, .{ .tag = 3, .op = .write, .node = log, .handle = f, .data = "follow" });
+ var want: [128]u8 = undefined;
+ const next = struct {
+ fn record(pp: *Pardes, h: u32, off: usize) []const u8 {
+ return call(pp, .{ .tag = 4, .op = .read, .node = @intFromEnum(tree.TopFile.log), .handle = h, .off = off, .size = 4096 }).bytes;
+ }
+ };
+ _ = wr(p, Node.of(serial, .addr), "/nowhere/");
+ try testing.expectEqualStrings(try std.fmt.bufPrint(&want, "err {d} addr: no match for regexp\n", .{serial}), next.record(p, f, frozen));
+ _ = wr(p, @intFromEnum(tree.TopFile.ctl), "Bogus");
+ try testing.expectEqualStrings("err - ctl: unknown control message \"Bogus\"\n", next.record(p, f, frozen));
+ _ = wr(p, @intFromEnum(tree.TopFile.focus), "99999");
+ try testing.expectEqualStrings("err - focus: no such window\n", next.record(p, f, frozen));
+ // A write that succeeds says nothing.
+ _ = wr(p, Node.of(serial, .addr), "#0");
+ try testing.expectEqual(Status.again, call(p, .{ .tag = 5, .op = .read, .node = log, .handle = f, .off = frozen, .size = 4096 }).reply.status);
+ _ = call(p, .{ .tag = 6, .op = .release, .node = log, .handle = f });
+}
+
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");
diff --git a/src/pardes.zig b/src/pardes.zig
index 084b2216..9d336db3 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -4642,6 +4642,7 @@ pub const Pardes = struct {
p.raw_hover_intent = false;
look.cancelLookHover(p);
const reply = ctlfs.handle(p, req);
+ if (reply.status == .err and (req.op == .write or req.op == .setattr)) ctlfs.events.noteError(p, req, reply);
// The request was a whole step of its own, so it settles the way a
// step does: the cursor and scroll reconciled, the scripted panes
// told, and the panes it made announced to /log now rather than at