summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.agents/skills/pardes-9p/SKILL.md2
-rw-r--r--docs/fs.md8
-rw-r--r--src/host_io.zig34
-rw-r--r--src/ninep/ctl.zig19
-rw-r--r--src/ninep/pty.zig6
-rw-r--r--src/pardes.zig6
6 files changed, 72 insertions, 3 deletions
diff --git a/.agents/skills/pardes-9p/SKILL.md b/.agents/skills/pardes-9p/SKILL.md
index afdf3a4b..d89a3ac3 100644
--- a/.agents/skills/pardes-9p/SKILL.md
+++ b/.agents/skills/pardes-9p/SKILL.md
@@ -399,7 +399,7 @@ Only terminal panes have `pty/`. Write keystroke bytes to `pty/data`, not
command. For an interactive application, send its actual input bytes; `b'\x03'`
is Ctrl-C, and Ctrl-U is `b'\x15'` where that application supports it. These go
to the child terminal, not to Pardes key bindings. `pty/ctl` takes `winsize C R`,
-`sig INT` and `exec`, one per line. `pty/status` reads one line: the pty's cols,
+`sig INT` and `exec` (restart the shell; a directory that is gone fails ENOENT), one per line. `pty/status` reads one line: the pty's cols,
rows and busy (1 while a command runs or text is typed at the prompt).
`client.screen()` returns `cols`, `rows`, `cursor`, `styles` and row-major
diff --git a/docs/fs.md b/docs/fs.md
index d94a1954..0b255390 100644
--- a/docs/fs.md
+++ b/docs/fs.md
@@ -908,7 +908,13 @@ where the host cannot name the program, and when text is typed at the prompt)
terminal (a REPL, `less`) takes none: write to `pty/data` for it --
which is also when the third field of `pty/status` reads 1. `pty/status` is
one line, three right-aligned fields and a newline: the pty's columns and
-rows, then busy (0 or 1). The size, and `pty/ctl`'s `winsize` read back, is
+rows, then busy (0 or 1). `pty/ctl` takes `winsize C R`, `sig
+INT|TERM|HUP|QUIT|KILL` and `exec`, which starts the pane's shell again in
+its directory: one that is gone is refused before anything runs, `exec:
+<dir>: no such directory` (ENOENT), and a shell the host cannot start fails
+the write with why, as any builtin's failure does. A directory removed
+under a running shell leaves the pane its name (never `... (deleted)`), so
+an `exec` works there once the directory is back. The size, and `pty/ctl`'s `winsize` read back, is
what `winsize C R` last set (R of 1 is taken as 2: a one-row pty loses
its prompt's mark and would read busy for ever), until the pane itself
resizes and gives the pty its grid again. A line written
diff --git a/src/host_io.zig b/src/host_io.zig
index 1db0dac9..10bcf0d7 100644
--- a/src/host_io.zig
+++ b/src/host_io.zig
@@ -1286,7 +1286,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 +1337,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;
diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig
index eea033ed..2f94dd3c 100644
--- a/src/ninep/ctl.zig
+++ b/src/ninep/ctl.zig
@@ -1949,6 +1949,25 @@ test "a command line from a missing directory fails ENOENT and makes no pane; a
try testing.expect(th.logHas(p, "shell: directory missing"));
}
+test "pty/ctl exec in a directory that is gone fails ENOENT; a shell that cannot start fails the write" {
+ const p = try th.withTerm(testing.allocator);
+ 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);
+ p.setCwd(p.paneBySerial(serial).?, "/tmp/pardes-no-such-dir-zz");
+ const refused = wr(p, Node.of(serial, .pty_ctl), "exec\n");
+ try testing.expectEqual(E.NOENT, refused.errno());
+ try testing.expectStringStartsWith(refused.reply.ename, "exec: /tmp/pardes-no-such-dir-zz: no such directory");
+ // The host's own failure is the waiting write's (late_failure).
+ p.fs.late_failure_len = 0;
+ p.shellFailed(@intCast(p.paneBySerial(serial).?), error.AccessDenied);
+ try testing.expectEqualStrings("shell: access denied", p.fs.late_failure[0..p.fs.late_failure_len]);
+}
+
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/ninep/pty.zig b/src/ninep/pty.zig
index 68f98554..aa0f499a 100644
--- a/src/ninep/pty.zig
+++ b/src/ninep/pty.zig
@@ -61,6 +61,12 @@ pub fn writeCtl(p: *Pardes, req: Req, id: usize) Reply {
while (it.next()) |raw| {
const line = std.mem.trim(u8, raw, " \t\r");
if (line.len == 0) continue;
+ // A shell restarted where the directory is gone would not start:
+ // refused before anything runs, as Tty there is.
+ if (!apply and std.mem.eql(u8, line, "exec")) if (p.panes[id]) |pane| if (pardes.exec.missingDir(p, pane.cwdSlice())) {
+ const dir = pane.cwdSlice();
+ return tree.failText(req.tag, E.NOENT, std.fmt.bufPrint(&p.fs.ename, "exec: {s}: no such directory", .{dir[0..@min(dir.len, 256)]}) catch "exec: no such directory");
+ };
if (!verb(p, id, line, apply)) return tree.failText(req.tag, E.INVAL, if (outOfRange(line)) e_winsize_range else e_bad_pty_ctl);
}
}
diff --git a/src/pardes.zig b/src/pardes.zig
index 60a5dfe3..1afb1e24 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -5251,6 +5251,12 @@ pub const Pardes = struct {
p.reportFailure(id, "shell: directory missing (no such directory)")
else
p.reportError(id, "shell", err);
+ // A 9P write that asked for the shell (pty/ctl's exec), waiting on
+ // it, fails with what was said, as a failed Save's does.
+ const said = pane.msg[0..pane.msg_len];
+ const kept = @import("Messages.zig").clip(said, p.fs.late_failure.len);
+ @memcpy(p.fs.late_failure[0..kept.len], kept);
+ p.fs.late_failure_len = @intCast(kept.len);
// 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.