diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-30 13:05:18 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:17 -0300 |
| commit | dc37b4ea777b7eb881a993e737457c18699c5b2c (patch) | |
| tree | 3f65229ed1e4c41090cf9fd10fbdf6d640652578 /src | |
| parent | ebcb870ac8ea962fd705068c7aaff935c5a5c15b (diff) | |
| download | pardes-dc37b4ea777b7eb881a993e737457c18699c5b2c.tar.gz pardes-dc37b4ea777b7eb881a993e737457c18699c5b2c.zip | |
pardes FILE in a pane whose session answers never falls back to a nested editor: the refusal is printed, exit 1, and no stray pane is left
When the environment named a live session that answered, several
failures broke out of forwarding and started a whole editor inside the
pane that asked: a refused name, a pane the session has not, a failed look
write. Its screen was drawn over the shell. Now, once the session answers
(Client.probe), every refusal is printed as `pardes: <file>: <why>`, in
the session's own words (the Rerror, now kept by the client), and the
launch exits 1, as acme's B does. A new name the session refuses deletes
the pane made for it, so no empty +New is left. A missing environment or a
session that does not answer still starts a separate editor. fs.py checks
a refused name and a stale PARDES_PANE.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src')
| -rw-r--r-- | src/9p_io.zig | 17 | ||||
| -rw-r--r-- | src/main.zig | 60 |
2 files changed, 58 insertions, 19 deletions
diff --git a/src/9p_io.zig b/src/9p_io.zig index d1b2099c..5a68238c 100644 --- a/src/9p_io.zig +++ b/src/9p_io.zig @@ -1808,7 +1808,17 @@ pub const Client = struct { var sock_buf: [sun_path_len]u8 = undefined; const sock = try resolve(&sock_buf, dial); var remote: RemoteError = .{}; - return fetchBytes(gpa, sock, names[0..n], &remote, null, display_path, @min(max_bytes, limits.max_file_bytes)); + return fetchBytes(gpa, sock, names[0..n], &remote, null, display_path, @min(max_bytes, limits.max_file_bytes)) catch |err| { + if (err == error.Remote) said = remote; + return err; + }; + } + + /// The peer's own words for the last error.Remote a read or write here + /// got (its Rerror), for a caller that says why. + var said: RemoteError = .{}; + pub fn lastSaid() []const u8 { + return said.buf[0..said.len]; } /// Whether a peer answers at `dial`: a version and attach, and its root @@ -1828,7 +1838,10 @@ pub const Client = struct { var sock_buf: [sun_path_len]u8 = undefined; const sock = try resolve(&sock_buf, dial); var remote: RemoteError = .{}; - const result = try fetchBytes(gpa, sock, names[0..n], &remote, bytes, path, limits.max_file_bytes); + const result = fetchBytes(gpa, sock, names[0..n], &remote, bytes, path, limits.max_file_bytes) catch |err| { + if (err == error.Remote) said = remote; + return err; + }; gpa.free(result); } diff --git a/src/main.zig b/src/main.zig index 6bdf2bfd..576efc8e 100644 --- a/src/main.zig +++ b/src/main.zig @@ -390,6 +390,19 @@ fn nativeMain(init: std.process.Init) !void { // leave a stale environment variable able to lock someone out of their // own editor. const parent = found orelse break :forwarding; + // Does it answer? One that answers takes the file or says why not, + // as acme's B does: from here on never a nested editor, whose + // screen would be drawn inside the pane that asked. + ninep_io.Client.probe(arena, parent.dial) catch break :forwarding; + const Refuse = struct { + fn with(io: std.Io, what: []const u8, err: anyerror) noreturn { + const why = if (err == error.Remote) ninep_io.Client.lastSaid() else @errorName(err); + var buf: [8192]u8 = undefined; + const text = std.fmt.bufPrint(&buf, "pardes: {s}: {s}\n", .{ what[0..@min(what.len, 4096)], why }) catch "pardes: refused\n"; + std.Io.File.stderr().writeStreamingAll(io, text) catch {}; + std.process.exit(1); + } + }; // The pane's `look` file: one line, and the line is the clicked text // itself, which is what a right click in that pane would have been. var look_buf: [64]u8 = undefined; @@ -397,15 +410,12 @@ fn nativeMain(init: std.process.Init) !void { const word = positional orelse { var tag_buf: [64]u8 = undefined; const tag = try std.fmt.bufPrint(&tag_buf, "/pane/{d}/tag", .{parent.serial}); - const contents = ninep_io.Client.read(arena, parent.dial, tag, tag) catch break :forwarding; + const contents = ninep_io.Client.read(arena, parent.dial, tag, tag) catch |err| Refuse.with(init.io, tag, err); arena.free(contents); try std.Io.File.stderr().writeStreamingAll(init.io, nested_text); std.process.exit(1); }; - // A word this launch cannot turn into a path is an argument problem - // rather than a nesting one: fall through and let the session that - // starts report it, the way `pardes typo` outside pardes does. - if (std.mem.indexOfAny(u8, word, "\r\n") != null) break :forwarding; + if (std.mem.indexOfAny(u8, word, "\r\n") != null) Refuse.with(init.io, "a file name", error.NotOneLine); const target = @import("look.zig").parsePathLine(word); var realbuf: [4096]u8 = undefined; var newbuf: [4096]u8 = undefined; @@ -415,30 +425,46 @@ fn nativeMain(init: std.process.Init) !void { // resolved and the name kept, a pane made for it that Save // creates the file from. const base = std.fs.path.basename(target.path); - if (base.len == 0 or std.mem.eql(u8, base, ".") or std.mem.eql(u8, base, "..")) break :forwarding; + if (base.len == 0 or std.mem.eql(u8, base, ".") or std.mem.eql(u8, base, "..")) Refuse.with(init.io, word, error.NotAFileName); const dir = pardes.filesystem.resolveOs(std.fs.path.dirname(target.path) orelse ".", &realbuf) orelse { // Its directory not there either: the name made absolute as - // written, and the pane's Save makes the directories (a - // name into a missing one says so, pane.zig writeName). + // written, and the pane's Save makes the directories. var cwd_buf: [4096]u8 = undefined; - if (std.c.getcwd(&cwd_buf, cwd_buf.len) == null) break :forwarding; + if (std.c.getcwd(&cwd_buf, cwd_buf.len) == null) Refuse.with(init.io, word, error.NoWorkingDirectory); const cwd = std.mem.sliceTo(&cwd_buf, 0); - break :named std.fs.path.resolvePosix(arena, &.{ cwd, target.path }) catch break :forwarding; + break :named std.fs.path.resolvePosix(arena, &.{ cwd, target.path }) catch |err| Refuse.with(init.io, word, err); }; - break :named std.fmt.bufPrint(&newbuf, "{s}/{s}", .{ std.mem.trimEnd(u8, dir.path, "/"), base }) catch break :forwarding; + break :named std.fmt.bufPrint(&newbuf, "{s}/{s}", .{ std.mem.trimEnd(u8, dir.path, "/"), base }) catch |err| Refuse.with(init.io, word, err); }; if (found_path != null) { var command_buf: [8192]u8 = undefined; - const command = std.fmt.bufPrint(&command_buf, "{s}{s}\n", .{ path, word[target.path.len..] }) catch break :forwarding; - ninep_io.Client.write(arena, parent.dial, look, command) catch break :forwarding; + const command = std.fmt.bufPrint(&command_buf, "{s}{s}\n", .{ path, word[target.path.len..] }) catch |err| Refuse.with(init.io, word, err); + ninep_io.Client.write(arena, parent.dial, look, command) catch |err| Refuse.with(init.io, word, err); } else { - const made = ninep_io.Client.read(arena, parent.dial, "/pane/new", "/pane/new") catch break :forwarding; - const serial = std.fmt.parseInt(u32, std.mem.trim(u8, made, " \n"), 10) catch break :forwarding; + const made = ninep_io.Client.read(arena, parent.dial, "/pane/new", "/pane/new") catch |err| Refuse.with(init.io, word, err); + const serial = std.fmt.parseInt(u32, std.mem.trim(u8, made, " \n"), 10) catch |err| Refuse.with(init.io, word, err); var name_buf: [64]u8 = undefined; const name = try std.fmt.bufPrint(&name_buf, "/pane/{d}/name", .{serial}); var line_buf: [4200]u8 = undefined; - const line = std.fmt.bufPrint(&line_buf, "{s}\n", .{path}) catch break :forwarding; - ninep_io.Client.write(arena, parent.dial, name, line) catch break :forwarding; + const line = std.fmt.bufPrint(&line_buf, "{s}\n", .{path}) catch |err| Refuse.with(init.io, word, err); + ninep_io.Client.write(arena, parent.dial, name, line) catch |err| { + // The name refused: the pane made for it goes too, no + // stray +New left behind. + var ctl_buf: [64]u8 = undefined; + const ctl = std.fmt.bufPrint(&ctl_buf, "/pane/{d}/ctl", .{serial}) catch unreachable; + var why_buf: [256]u8 = undefined; + const said = ninep_io.Client.lastSaid(); + const why = why_buf[0..@min(said.len, why_buf.len)]; + @memcpy(why, said[0..why.len]); + ninep_io.Client.write(arena, parent.dial, ctl, "delete\n") catch {}; + if (err == error.Remote) { + var buf: [8192]u8 = undefined; + const text = std.fmt.bufPrint(&buf, "pardes: {s}: {s}\n", .{ word[0..@min(word.len, 4096)], why }) catch "pardes: refused\n"; + std.Io.File.stderr().writeStreamingAll(init.io, text) catch {}; + std.process.exit(1); + } + Refuse.with(init.io, word, err); + }; } if (wait) waitForDel(init.io, arena, parent.dial, path); return; |
