summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-29 07:27:13 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:16 -0300
commit22fb8b6147f7fb72407af80c36d5e99e4664fea2 (patch)
tree0f5543812af5421a55d21e3bd95d929a19b48d82
parentb3557640d6a0a4c2c41b6e1cae3c8182a32937ca (diff)
downloadpardes-22fb8b6147f7fb72407af80c36d5e99e4664fea2.tar.gz
pardes-22fb8b6147f7fb72407af80c36d5e99e4664fea2.zip
Mount dials its peer at once and fails if none answers; a peer out of reach is said as `dial failed: <reason>`
Mount took any well-formed dial and nothing was found out until the first look through it, which then said `look: look: dial`. Mount now probes the peer (version, attach, its root) with the turn out, and fails its write, mounting nothing, when none answers; the dial errors read as words, `no answer`, `timed out`, `hung up`, and a look through a peer gone since names its path: `look: /n/peer/f: dial failed: no answer`. Co-Authored-By: Claude Opus 5.5 <[email protected]>
-rw-r--r--docs/fs.md7
-rw-r--r--src/9p_io.zig10
-rw-r--r--src/Messages.zig17
-rw-r--r--src/builtins.zig15
-rw-r--r--src/look.zig4
-rw-r--r--src/ninep/ctl.zig16
-rw-r--r--test/fs_namespace.zig12
7 files changed, 74 insertions, 7 deletions
diff --git a/docs/fs.md b/docs/fs.md
index 2f97661e..92f2876a 100644
--- a/docs/fs.md
+++ b/docs/fs.md
@@ -32,7 +32,10 @@ and `/virtual/X` both name the served `/X`.
`--mount=peer=work` mounts the named session `work`; the dial can also be an
absolute socket path, `unix!/path`, `tcp!IP!port`, or `quic!IP!port`.
At runtime, use `Mount peer dial` and
-`Unmount peer`. There are eight named mounts; `os` and `self` are reserved.
+`Unmount peer`. Mount dials the peer when it mounts it and fails its write
+if nothing answers, `Mount peer /tmp/s: dial failed: no answer` (or `timed
+out`, `hung up`), mounting nothing; a peer that goes away later is found out
+by the next use, as `look: /n/peer/f: dial failed: no answer`. There are eight named mounts; `os` and `self` are reserved.
Unmount refuses mounts still used by a pane, its working directory, or a
pending Save. Mounts are saved in dumps. Save uses the file's original mount.
@@ -257,7 +260,7 @@ and `not a session control message "X": write it to pane/<n>/ctl` or `not
a window control message "X": write it to /ctl` for a word of the other ctl. 9ns maps them all to EINVAL, and a write
refused here has done nothing. A line that then fails as it runs fails the
write with the error the editor reports for it and the line, e.g. `Mount:
-AlreadyMounted "Mount peer /tmp/s"` (EIO), and `control message needs its
+already mounted "Mount peer /tmp/s"` (EIO), and `control message needs its
argument "Save"` (EINVAL), for a builtin that would have asked at a prompt
(a `Save` on a scratch) rather than open one nobody is there to answer. The
lines before a failing one have taken effect and those after it never run,
diff --git a/src/9p_io.zig b/src/9p_io.zig
index bda1ad36..2130db27 100644
--- a/src/9p_io.zig
+++ b/src/9p_io.zig
@@ -1761,6 +1761,16 @@ pub const Client = struct {
return fetchBytes(gpa, sock, names[0..n], &remote, null, display_path, @min(max_bytes, limits.max_file_bytes));
}
+ /// Whether a peer answers at `dial`: a version and attach, and its root
+ /// read. Any answer from the peer, an error of its own included, is one.
+ pub fn probe(gpa: std.mem.Allocator, dial: []const u8) !void {
+ const got = readLimit(gpa, dial, "", "", 0) catch |err| switch (err) {
+ error.IsDirectory, error.NotFound, error.Remote, error.FileTooLarge => return,
+ else => return err,
+ };
+ gpa.free(got);
+ }
+
pub fn write(gpa: std.mem.Allocator, dial: []const u8, path: []const u8, bytes: []const u8) !void {
if (comptime !supported) return error.Unsupported;
var names: [max_depth][]const u8 = undefined;
diff --git a/src/Messages.zig b/src/Messages.zig
index c0838fb2..482ad64d 100644
--- a/src/Messages.zig
+++ b/src/Messages.zig
@@ -450,10 +450,27 @@ pub const no_pane_slots = "no space for a pane: {d} max";
pub const no_pane_room = "no space for a pane in that column: each keeps its tag and {d} rows";
/// `<operation>: <the error in words>`: FileNotFound reads "file not found".
+/// Why a peer could not be reached, in words, for an error of a 9P dial
+/// (src/9p_io.zig, Client); null for any other error.
+pub fn dialReason(err: anyerror) ?[]const u8 {
+ return switch (err) {
+ error.Dial => "no answer",
+ error.Timeout => "timed out",
+ error.Hangup => "hung up",
+ error.Botch => "protocol botch",
+ else => null,
+ };
+}
+
pub fn reportError(p: *Pardes, id: usize, operation: []const u8, err: anyerror) void {
var buf: [256]u8 = undefined;
var w = std.Io.Writer.fixed(&buf);
w.print("{s}: ", .{operation}) catch {};
+ // A peer out of reach says so, not the error's name (`dial`).
+ if (dialReason(err)) |why| {
+ w.print("dial failed: {s}", .{why}) catch {};
+ return reportFailure(p, id, w.buffered());
+ }
// Every route that opens a pane says the cap the same way.
if (err == error.NoPaneSlots or err == error.NoPaneRoom) {
if (err == error.NoPaneSlots)
diff --git a/src/builtins.zig b/src/builtins.zig
index 404e78f4..3f4a680c 100644
--- a/src/builtins.zig
+++ b/src/builtins.zig
@@ -676,6 +676,21 @@ pub const Mount = struct {
const dial = std.mem.trim(u8, args.rest(), " \t");
if (dial.len == 0) return c.p.reportError(c.id, "Mount name dial", error.MissingArgument);
c.p.fs.mount(c.p.gpa, name, dial) catch |err| return c.p.reportError(c.id, "Mount", err);
+ // A mount that cannot be reached is said now, and taken back, not
+ // found out by the first look through it.
+ const reached = reached: {
+ pardes.turn.yield();
+ defer pardes.turn.back();
+ break :reached @import("9p_io.zig").Client.probe(c.p.gpa, dial);
+ };
+ reached catch |err| {
+ if (c.p.fs.mounts.pop()) |gone| {
+ c.p.gpa.free(gone.name);
+ c.p.gpa.free(gone.dial);
+ }
+ var what: [160]u8 = undefined;
+ return c.p.reportError(c.id, std.fmt.bufPrint(&what, "Mount {s} {s}", .{ name, dial[0..@min(dial.len, 100)] }) catch "Mount", err);
+ };
}
};
diff --git a/src/look.zig b/src/look.zig
index 2f712fae..4ee5a638 100644
--- a/src/look.zig
+++ b/src/look.zig
@@ -1458,7 +1458,7 @@ pub fn lookAt(p: *Pardes, id: usize, operand: []const u8) void {
if (!focusPaneByPath(p, target.path, target.at)) {
const free = p.freeSlot() orelse return p.reportError(id, "look", error.NoPaneSlots);
const nt = panes.File.open(p, free, target.path, target.at.line) catch |err|
- return p.reportError(id, "look", err);
+ return p.reportError(id, if (@import("Messages.zig").dialReason(err) != null) target.path else "look", err);
if (target.at.col > 0) nt.body.cur_col = @intCast(target.at.col - 1);
exec.placeNew(p, id, id, free, .doc);
layout.compute(p);
@@ -1476,7 +1476,7 @@ pub fn lookAt(p: *Pardes, id: usize, operand: []const u8) void {
if (focusPaneByPath(p, target.path, .{})) return;
const free = p.freeSlot() orelse return p.reportError(id, "look", error.NoPaneSlots);
_ = panes.Image.create(p, free, target.path, &.{}) catch |err|
- return p.reportError(id, "look", err);
+ return p.reportError(id, if (@import("Messages.zig").dialReason(err) != null) target.path else "look", err);
exec.placeNew(p, id, id, free, .doc);
},
}
diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig
index b8f73c6f..7c68210a 100644
--- a/src/ninep/ctl.zig
+++ b/src/ninep/ctl.zig
@@ -1712,6 +1712,22 @@ test "size sets the screen of a session no frontend is attached to, and only of
try testing.expectEqual(@as(u16, 40), p.screen_h);
}
+test "Mount dials its peer and fails, mounting nothing, when none answers; a look through a peer gone says dial failed" {
+ const p = try withFile(testing.allocator, "x\n");
+ defer p.deinit();
+ const root_ctl = @intFromEnum(tree.TopFile.ctl);
+ const dead = "/tmp/pardes-test-no-such-peer.sock";
+ const refused = wr(p, root_ctl, "Mount dead " ++ dead ++ "\n");
+ try testing.expectEqual(E.IO, refused.errno());
+ try testing.expectEqualStrings("Mount dead " ++ dead ++ ": dial failed: no answer \"Mount dead " ++ dead ++ "\"", refused.reply.ename);
+ try testing.expectEqual(@as(usize, 0), p.fs.mounts.items.len);
+ // Mounted when it answered, gone since: a look through it says so.
+ try p.fs.mounts.append(p.gpa, .{ .name = try p.gpa.dupe(u8, "gone"), .dial = try p.gpa.dupe(u8, dead) });
+ const looked = wr(p, @intFromEnum(tree.TopFile.look), "/n/gone/f.txt\n");
+ try testing.expect(looked.errno() != 0);
+ try testing.expect(th.logHas(p, "look: /n/gone/f.txt: dial failed: no answer"));
+}
+
test "size is monotonic: growing is never refused, and a size once taken is taken again" {
const p = try Pardes.init(testing.allocator, .{ .tty_only = true, .cols = 160, .rows = 60 });
defer p.deinit();
diff --git a/test/fs_namespace.zig b/test/fs_namespace.zig
index 2f78ca1a..bd620c2d 100644
--- a/test/fs_namespace.zig
+++ b/test/fs_namespace.zig
@@ -6,9 +6,15 @@ test "runtime Mount and Unmount copy command arguments and change Look resolutio
const gpa = std.testing.allocator;
const p = try pardes.Pardes.init(gpa, .{ .tty_only = true });
defer p.deinit();
- var command = "Mount peer /tmp/a socket.sock".*;
- try std.testing.expect(p.executeBuiltinLine(0, &command));
- @memset(&command, 'x');
+ // No peer answers at the dial: Mount mounts nothing (it dials first).
+ try std.testing.expect(p.executeBuiltinLine(0, "Mount peer /tmp/a socket.sock"));
+ try std.testing.expectEqual(@as(usize, 0), p.fs.mounts.items.len);
+ // Mounted (as one that answered is), its arguments are copies.
+ var name = "peer".*;
+ var dial = "/tmp/a socket.sock".*;
+ try p.fs.mount(gpa, &name, &dial);
+ @memset(&name, 'x');
+ @memset(&dial, 'x');
try std.testing.expectEqual(@as(usize, 1), p.fs.mounts.items.len);
try std.testing.expectEqualStrings("peer", p.fs.mounts.items[0].name);
try std.testing.expectEqualStrings("/tmp/a socket.sock", p.fs.mounts.items[0].dial);