summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-29 09:05:05 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:16 -0300
commit7b727114350030fa34281803cc12a1dbb7e6f7fc (patch)
tree032114e21cad83d2f5bf2a01e0e69470a568237b /src
parent78d6f64e72be2a22c40feaf012636bc8f04d46de (diff)
downloadpardes-7b727114350030fa34281803cc12a1dbb7e6f7fc.tar.gz
pardes-7b727114350030fa34281803cc12a1dbb7e6f7fc.zip
A command from a missing directory fails ENOENT with no pane; one whose shell never started ends exit 127
Exec made a command pane in a directory that was not there, whose shell never started: it said running for ever, a follower waited for an exit that never came, and Kill found it. The line now fails up front, as Tty does, `exec: <dir>: no such directory`; and a command pane whose shell the host could not start ends `exit 127` at once. A shell that could not start in its directory says `directory missing`, not `file not found`. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src')
-rw-r--r--src/exec.zig20
-rw-r--r--src/ninep/ctl.zig31
-rw-r--r--src/pardes.zig12
3 files changed, 60 insertions, 3 deletions
diff --git a/src/exec.zig b/src/exec.zig
index ebe90b98..79717963 100644
--- a/src/exec.zig
+++ b/src/exec.zig
@@ -351,11 +351,11 @@ fn spawnTtyWithMount(p: *Pardes, from: usize, v9fs: bool) ?*Pane {
// start in it, and a dead pane would answer no run. Asked only of a host
// that starts shells (none, as in the core's tests, starts none).
const dir = p.execDir(from);
- if (comptime hosted) if (p.host.vtable.spawn != null) if (@import("fs.zig").localPath(dir)) |local| if (!isDirectory(local)) {
+ if (missingDir(p, dir)) {
var said: [pardes.memory.limits.host_path_cap + 48]u8 = undefined;
p.reportFailure(from, std.fmt.bufPrint(&said, "Tty: {s}: no such directory", .{dir}) catch "Tty: no such directory");
return null;
- };
+ }
const free = p.freeSlot() orelse {
p.reportError(from, "Tty", error.NoPaneSlots);
return null;
@@ -375,6 +375,15 @@ fn spawnTtyWithMount(p: *Pardes, from: usize, v9fs: bool) ?*Pane {
return nt;
}
+/// Whether a shell would be started in `dir` and `dir` is not there: asked
+/// only of a host that starts shells, and only of a local path.
+pub fn missingDir(p: *Pardes, dir: []const u8) bool {
+ if (comptime !hosted) return false;
+ if (p.host.vtable.spawn == null) return false;
+ const local = @import("fs.zig").localPath(dir) orelse return false;
+ return !isDirectory(local);
+}
+
/// Whether `path` is a directory on this host, the turn out while it asks
/// (it may be inside a mount this editor serves).
pub fn isDirectory(path: []const u8) bool {
@@ -662,6 +671,13 @@ fn runCommand(p: *Pardes, from: usize, line: []const u8) ?usize {
}
const src = p.panes[from] orelse return null;
const dir = p.execDir(from);
+ // A directory that is not there runs nothing, as Tty makes nothing
+ // there: no pane, and the write fails ENOENT.
+ if (missingDir(p, dir)) {
+ var said: [pardes.memory.limits.host_path_cap + 48]u8 = undefined;
+ p.reportFailure(from, std.fmt.bufPrint(&said, "exec: {s}: no such directory", .{dir}) catch "exec: no such directory");
+ return null;
+ }
// Not one whose pty a job it left behind still prints to: reusing it
// would hang that job up.
// From a column's tag, only a finished pane in that column is reused,
diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig
index 66092451..9cf97efc 100644
--- a/src/ninep/ctl.zig
+++ b/src/ninep/ctl.zig
@@ -1918,6 +1918,37 @@ test "a terminal with text typed at its prompt is never typed into: a look at it
try testing.expect(!typed_into);
}
+test "a command line from a missing directory fails ENOENT and makes no pane; a command whose shell never started ends 127" {
+ const p = try withFile(testing.allocator, "x\n");
+ defer p.deinit();
+ const Starting = struct {
+ fn spawn(_: ?*anyopaque, _: u8, _: []const u8) void {}
+ };
+ p.host = .{ .vtable = &.{ .spawn = Starting.spawn } };
+ defer p.host = .{};
+ const serial = serialOf(p);
+ _ = wr(p, Node.of(serial, .name), "/tmp/pardes-no-such-dir-zz/f.txt\n");
+ var count: usize = 0;
+ for (p.panes) |slot| count += @intFromBool(slot != null);
+ const refused = wr(p, Node.of(serial, .exec), "echo hi\n");
+ try testing.expectEqual(E.NOENT, refused.errno());
+ try testing.expectStringStartsWith(refused.reply.ename, "exec: /tmp/pardes-no-such-dir-zz: no such directory");
+ var after: usize = 0;
+ for (p.panes) |slot| after += @intFromBool(slot != null);
+ try testing.expectEqual(count, after);
+ // A command pane whose shell the host could not start.
+ _ = wr(p, Node.of(serial, .name), "/tmp/f.txt\n");
+ try testing.expectEqual(Status.ok, wr(p, Node.of(serial, .exec), "echo hi\n").reply.status);
+ const cmd = p.paneBySerial(try std.fmt.parseInt(u32, std.mem.trimEnd(u8, rd(p, Node.of(serial, .exec), 0, 64).bytes, "\n"), 10)).?;
+ p.shellFailed(@intCast(cmd), error.FileNotFound);
+ const pane = p.panes[cmd].?;
+ try testing.expect(pane.command_done);
+ try testing.expectEqual(@as(?u8, 127), pane.command_status);
+ var rec: [32]u8 = undefined;
+ try testing.expect(th.logHas(p, try std.fmt.bufPrint(&rec, "exit {d} 127\n", .{pane.serial})));
+ try testing.expect(th.logHas(p, "shell: directory missing"));
+}
+
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/src/pardes.zig b/src/pardes.zig
index 24595045..60a5dfe3 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -5247,7 +5247,17 @@ pub const Pardes = struct {
const pane = p.panes[id] orelse return;
pane.shell_failed = true;
ctlfs.pty.shellGone(p, pane, false);
- p.reportError(id, "shell", err);
+ if (err == error.FileNotFound or err == error.NotDir)
+ p.reportFailure(id, "shell: directory missing (no such directory)")
+ else
+ p.reportError(id, "shell", err);
+ // A command that never started ended: `exit 127`, as a shell says of
+ // a command it could not run, so a follower waiting on its exit
+ // hears one, its tag stops saying it runs, and Kill finds nothing.
+ if (pane.command != null and !pane.command_done) {
+ pane.command_pty = false;
+ exec.commandDone(p, id, 127);
+ }
}
pub fn saveFailed(p: *Pardes, id: u8, path: []const u8, err: anyerror) void {