summaryrefslogtreecommitdiff
path: root/src/host_io.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/host_io.zig')
-rw-r--r--src/host_io.zig82
1 files changed, 78 insertions, 4 deletions
diff --git a/src/host_io.zig b/src/host_io.zig
index 1db0dac9..913930ac 100644
--- a/src/host_io.zig
+++ b/src/host_io.zig
@@ -867,7 +867,8 @@ pub const Shell = struct {
buf[bin.len] = 0;
if (isDirectory(@ptrCast(&buf))) return std.fmt.bufPrint(said, "not a shell: {s} is a directory", .{bin}) catch "not a shell: a directory";
}
- return std.fmt.bufPrint(said, "no shell \"{s}\" (a name on the usual paths, or a path to one)", .{bin}) catch "no such shell";
+ // "not found": the words a mount reads as ENOENT.
+ return std.fmt.bufPrint(said, "no shell \"{s}\", not found (a name on the usual paths, or a path to one)", .{bin}) catch "no such shell";
}
fn fallback(buf: *[std.fs.max_path_bytes]u8) [*:0]const u8 {
@@ -1156,17 +1157,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 };
@@ -1286,7 +1328,14 @@ pub fn shellCwd(pid: libc.pid_t, buf: []u8) ?[]const u8 {
const path = std.fmt.bufPrintSentinel(&pbuf, "/proc/{d}/cwd", .{pid}, 0) catch return null;
const n = libc.readlink(path, buf.ptr, buf.len);
if (n <= 0 or n >= buf.len) return null;
- return buf[0..@intCast(n)];
+ const link = buf[0..@intCast(n)];
+ // A directory removed under the shell reads `<path> (deleted)`:
+ // no directory to be named by, so the name it had stays, and a
+ // restart works there once it is back.
+ if (std.mem.endsWith(u8, link, " (deleted)")) {
+ _ = std.Io.Dir.cwd().statFile(std.Io.Threaded.global_single_threaded.io(), link, .{}) catch return null;
+ }
+ return link;
},
.macos, .ios, .tvos, .watchos, .visionos => {
var info: proc_vnodepathinfo = undefined;
@@ -1330,6 +1379,31 @@ test "shell cwd rejects truncation and preserves an owned child path longer than
try std.testing.expectEqualStrings(directory, shellCwd(child.pid, &result) orelse return error.MissingCwd);
}
+test "a shell whose directory was removed is not named `... (deleted)`" {
+ if (comptime builtin.os.tag != .linux) return error.SkipZigTest;
+ if (!haveFile("/bin/sh")) return error.SkipZigTest;
+ const io = std.testing.io;
+ var tmp = std.testing.tmpDir(.{});
+ defer tmp.cleanup();
+ try tmp.dir.createDirPath(io, "gone");
+ var path_buf: [4096]u8 = undefined;
+ const directory = path_buf[0..try tmp.dir.realPathFile(io, "gone", &path_buf)];
+ const rcs: Shell.PromptFiles = .{};
+ const child = try forkShell(null, 0, &rcs, "/bin/sh", directory, 24, 80, null);
+ defer {
+ _ = libc.kill(child.pid, libc.SIG.KILL);
+ _ = libc.waitpid(child.pid, null, 0);
+ _ = libc.close(child.file.handle);
+ }
+ var sh: TestShell = .{ .master = child.file.handle, .pid = child.pid };
+ try std.testing.expect(writeFd(child.file.handle, "printf '\\160ardes-cwd-ready\\n'\n"));
+ try std.testing.expect(sh.waitText("pardes-cwd-ready", 5_000));
+ var result: [4096]u8 = undefined;
+ try std.testing.expectEqualStrings(directory, shellCwd(child.pid, &result) orelse return error.MissingCwd);
+ try tmp.dir.deleteDir(io, "gone");
+ try std.testing.expect(shellCwd(child.pid, &result) == null);
+}
+
extern "c" fn tcgetpgrp(fd: c_int) libc.pid_t;
const occ_max_depth: u8 = 8;