From 32198b59020e295efbc48de343a9de69c952dcfc Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Tue, 29 Sep 2026 05:15:57 -0300 Subject: A Save that fails logs its err, not a save record The save record was logged as soon as the host was asked to write, so a Save into a read-only directory logged save and then failed. It is now logged only once the write is done; a failure's err record (the write's, naming the path) stands alone, the msg it was also said as dropped. Co-Authored-By: Claude Opus 5.5 --- src/9p_io.zig | 3 +++ src/ninep/events.zig | 10 ++++++++++ src/pardes.zig | 31 +++++++++++++++++++++++++++++-- 3 files changed, 42 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/9p_io.zig b/src/9p_io.zig index 72963e08..bda1ad36 100644 --- a/src/9p_io.zig +++ b/src/9p_io.zig @@ -316,6 +316,9 @@ pub const Listener = struct { // settle in the same wait is told to this write too. if (l.core == core and core.fs.late_failure_len != 0) { const failed = pardes.ctlfs.failText(req.tag, pardes.ctlfs.E.IO, core.fs.late_failure[0..core.fs.late_failure_len]); + // Its err record says it (the path in it); the msg the failure + // was also said as goes, as a builtin's failing a write does. + pardes.ctlfs.events.dropMessage(core, core.fs.late_failure[0..core.fs.late_failure_len]); pardes.ctlfs.events.noteError(core, req, failed); return conn.reply(&failed, ""); } diff --git a/src/ninep/events.zig b/src/ninep/events.zig index 7a06bd04..d2beecd1 100644 --- a/src/ninep/events.zig +++ b/src/ninep/events.zig @@ -226,6 +226,16 @@ pub fn dropAnnouncement(p: *Pardes, serial: u32, text: []const u8) void { p.fs.log.buf.shrinkRetainingCapacity(last.at); } +/// Drops the newest record when it is a msg saying `text` (of any pane) +/// that no follower has read: a failure its write's err record will say. +pub fn dropMessage(p: *Pardes, text: []const u8) void { + const last = newest(p) orelse return; + if (followerRead(p, last.seq) or !std.mem.startsWith(u8, last.text, "msg ")) return; + const rest = last.text["msg ".len..]; + const sp = std.mem.indexOfScalar(u8, rest, ' ') orelse return; + if (std.mem.eql(u8, rest[sp + 1 ..], text)) p.fs.log.buf.shrinkRetainingCapacity(last.at); +} + /// Pushes `record`, or, when it repeats the newest record word for word, /// counts that one instead: ` (x2)`, the count being every time it /// was said. One a follower has already read is not rewritten: the repeat diff --git a/src/pardes.zig b/src/pardes.zig index 5d709679..b488ec1c 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -5239,9 +5239,12 @@ pub const Pardes = struct { p.hostWriteFile(sf.pane, f.path, f.content); const saved_pane = p.panes[sf.pane] orelse return; if (saved_pane.serial != serial) return; - ctlfs.events.noteLog(p, .save, saved_pane); const saved = if (saved_pane.file) |*file| file else return; - if (saved.saved_revision != saved.revision or !saved.watch_after_save) return; + // A save the host could not do (saveFailed) is its err + // record alone, not a `save`. + if (saved.saved_revision != saved.revision) return; + ctlfs.events.noteLog(p, .save, saved_pane); + if (!saved.watch_after_save) return; saved.watch_after_save = false; if (filesystem.localPath(saved.path) != null) p.emit(.{ .watch = .{ .pane = sf.pane, .on = true } }); @@ -7983,3 +7986,27 @@ test "a pane closed while its question stands ends the question in the log" { var want: [64]u8 = undefined; try std.testing.expect(th.logHas(p, try std.fmt.bufPrint(&want, "answer {d} -\ndel {d}", .{ serial, serial }))); } + +test "a Save the host could not do logs no save record" { + const th = @import("ninep/testing.zig"); + const p = try th.withFile(std.testing.allocator, "x\n"); + defer p.deinit(); + const Failing = struct { + core: *Pardes, + const vtable: Host.VTable = .{ .write_file = write }; + fn write(ctx: ?*anyopaque, pane: u8, path: []const u8, _: []const u8) void { + const f: *@This() = @ptrCast(@alignCast(ctx.?)); + f.core.saveFailed(pane, path, error.AccessDenied); + } + }; + var host: Failing = .{ .core = p }; + p.host = .{ .ctx = &host, .vtable = &Failing.vtable }; + _ = th.wr(p, ctlfs.Node.of(p.panes[0].?.serial, .body), "more\n"); + p.perform(.{ .save_file = .{ .pane = 0 } }); + try std.testing.expect(!th.logHas(p, "\nsave ")); + try std.testing.expect(p.panes[0].?.file.?.saved_revision != p.panes[0].?.file.?.revision); + // With the host's write done, it is logged. + p.host = .{}; + p.perform(.{ .save_file = .{ .pane = 0 } }); + try std.testing.expect(th.logHas(p, "\nsave ")); +} -- cgit v1.3