summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-29 09:51:11 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:16 -0300
commitad3403adb72e3457eb76d6b3fc91cf8df3d738ae (patch)
tree1669efb3e6891b3a32a71c1fcffe8dd48428d9b2 /src
parent6e2ed2e057ba95b3a86186c255d1965e1861bdf7 (diff)
downloadpardes-ad3403adb72e3457eb76d6b3fc91cf8df3d738ae.tar.gz
pardes-ad3403adb72e3457eb76d6b3fc91cf8df3d738ae.zip
A shell that cannot start fails Tty and pty/ctl exec with why, before either answers
A shell whose exec failed (a script's missing interpreter) was reported started: Tty and pty/ctl exec succeeded, then the pane died. The child now reports a failed chdir or exec through a close-on-exec pipe the parent reads before acknowledging the shell; the host's spawn fails with the reason (`shell not found`, ENOENT), which fails the waiting write. A Tty whose first shell never ran leaves no pane; a terminal restarted by pty/ctl exec keeps its pane. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src')
-rw-r--r--src/9p_io.zig2
-rw-r--r--src/host_io.zig45
-rw-r--r--src/panes.zig3
-rw-r--r--src/pardes.zig6
4 files changed, 53 insertions, 3 deletions
diff --git a/src/9p_io.zig b/src/9p_io.zig
index 4c9c9bcf..72897979 100644
--- a/src/9p_io.zig
+++ b/src/9p_io.zig
@@ -318,7 +318,7 @@ pub const Listener = struct {
const late = core.fs.late_failure[0..core.fs.late_failure_len];
// What is not there (`no such directory`) is ENOENT, as a
// builtin's failure saying so is (ctl.failureErrno).
- const errno = if (std.mem.indexOf(u8, late, "no such") != null) pardes.ctlfs.E.NOENT else pardes.ctlfs.E.IO;
+ const errno = if (std.mem.indexOf(u8, late, "no such") != null or std.mem.indexOf(u8, late, "not found") != null) pardes.ctlfs.E.NOENT else pardes.ctlfs.E.IO;
const failed = pardes.ctlfs.failText(req.tag, errno, late);
// 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.
diff --git a/src/host_io.zig b/src/host_io.zig
index 10bcf0d7..fbdfb703 100644
--- a/src/host_io.zig
+++ b/src/host_io.zig
@@ -1156,17 +1156,58 @@ pub fn forkShell(
var env_slots: ChildEnv.Slots = undefined;
const envp = ChildEnv.build(&env_slots);
const ws = posix.winsize{ .row = rows, .col = cols, .xpixel = 0, .ypixel = 0 };
+ // The child says here why it could not become the shell (its chdir or
+ // its exec failed, as for a script whose interpreter is not there): the
+ // pipe closes on exec, so the parent reads nothing when the shell runs,
+ // and a reason when it did not, before anyone is told it started.
+ var told: [2]c_int = .{ -1, -1 };
+ // pipe and then CLOEXEC, not pipe2, which macOS has not.
+ if (libc.pipe(&told) != 0) return error.PipeFailed;
+ ninep_io.setCloexec(told[0]);
+ ninep_io.setCloexec(told[1]);
const pid = forkpty(&master, null, null, &ws);
- if (pid < 0) return error.ForkFailed;
+ if (pid < 0) {
+ _ = libc.close(told[0]);
+ _ = libc.close(told[1]);
+ return error.ForkFailed;
+ }
if (pid == 0) {
var set = posix.sigemptyset();
posix.sigaddset(&set, posix.SIG.WINCH);
posix.sigprocmask(posix.SIG.UNBLOCK, &set, null);
- if (cwd_z) |path| if (chdir(path.ptr) != 0) _exit(126);
+ if (cwd_z) |path| if (chdir(path.ptr) != 0) {
+ const why = [_]u8{ 'c', @truncate(@intFromEnum(libc.errno(@as(c_int, -1)))) };
+ _ = libc.write(told[1], &why, why.len);
+ _exit(126);
+ };
if (envp) |env| _ = execve(spawn.path, &spawn.argv, env);
_ = execv(spawn.path, &spawn.argv);
+ const why = [_]u8{ 'x', @truncate(@intFromEnum(libc.errno(@as(c_int, -1)))) };
+ _ = libc.write(told[1], &why, why.len);
_exit(127);
}
+ _ = libc.close(told[1]);
+ const failed = failed: {
+ var why: [2]u8 = undefined;
+ pardes.turn.yield();
+ defer pardes.turn.back();
+ const n = libc.read(told[0], &why, why.len);
+ _ = libc.close(told[0]);
+ if (n != 2) break :failed null;
+ break :failed why;
+ };
+ if (failed) |why| {
+ _ = libc.close(master);
+ _ = libc.waitpid(pid, null, 0);
+ const e: libc.E = @enumFromInt(why[1]);
+ if (why[0] == 'c') return if (e == .NOENT or e == .NOTDIR) error.FileNotFound else error.AccessDenied;
+ return switch (e) {
+ .NOENT => error.ShellNotFound, // itself, or its script's interpreter
+ .ACCES, .PERM => error.ShellNotExecutable,
+ .NOEXEC => error.ShellNotExecutable,
+ else => error.ShellDidNotStart,
+ };
+ }
ninep_io.setCloexec(master);
if (core) |c| c.acknowledgeShell(pane, std.mem.span(spawn.path), one_line == null and spawn.argv[1] != null);
return .{ .file = .{ .handle = master, .flags = .{ .nonblocking = false } }, .pid = pid };
diff --git a/src/panes.zig b/src/panes.zig
index f2fe5037..32ad7b6b 100644
--- a/src/panes.zig
+++ b/src/panes.zig
@@ -113,6 +113,9 @@ pub const Pane = struct {
/// then /proc says this process's directory, not the shell's
/// (Pardes.setCwd keeps the one it was started in).
shell_spoke: bool = false,
+ /// A shell has run in it (Pardes.acknowledgeShell): a Tty whose first
+ /// shell fails to start closes, one started again keeps its pane.
+ had_shell: bool = false,
pending_command: Terminal.PendingCommand = .{},
/// The last command line pardes typed into this terminal (an exec, a
/// middle click, a pty/run): its first word, which Kill matches, and
diff --git a/src/pardes.zig b/src/pardes.zig
index 26c9246a..35b44871 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -5023,6 +5023,7 @@ pub const Pardes = struct {
if (id < MAX_PANES) if (p.panes[id]) |pane| {
ctlfs.pty.shellGone(p, pane, true); // a respawn replaced whatever ran
pane.shell_failed = false;
+ pane.had_shell = true;
pane.fs.unmarked = !prompt_marks;
// The tag's Tty word names the shell the host ran, and a click
// on it (`Tty+fish`) opens another of the same.
@@ -5266,6 +5267,11 @@ pub const Pardes = struct {
if (pane.command != null and !pane.command_done) {
pane.command_pty = false;
exec.commandDone(p, id, 127);
+ } else if (pane.command == null and !pane.had_shell) {
+ // A Tty whose shell never ran: no dead pane left behind; the
+ // write that asked for it has failed with why. A terminal whose
+ // shell was started again (pty/ctl exec) keeps its pane.
+ p.unplaced.set(id);
}
}