summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-29 16:20:57 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:16 -0300
commitdb12ca703dd2acbeee2d7b6acc03dd8a93909755 (patch)
tree3000772520ce9189104c6c76d750878d38629126
parentd6445bb28bcb4a0d0e5f29c6308682737d253ebe (diff)
downloadpardes-db12ca703dd2acbeee2d7b6acc03dd8a93909755.tar.gz
pardes-db12ca703dd2acbeee2d7b6acc03dd8a93909755.zip
pty/ctl exec on a command pane is refused: a command pane does not restart
exec on a finished command pane ran its line again, silently, with no header, footer or records. It is now refused, EINVAL, invalid exec: a command pane does not restart. Co-Authored-By: Claude Opus 5.5 <[email protected]>
-rw-r--r--docs/fs.md4
-rw-r--r--src/ninep/pty.zig15
2 files changed, 18 insertions, 1 deletions
diff --git a/docs/fs.md b/docs/fs.md
index eab1b00d..6bf30212 100644
--- a/docs/fs.md
+++ b/docs/fs.md
@@ -993,7 +993,9 @@ 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). `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:
+its directory (a command pane's child is its command, which does not
+restart: `exec` there is refused, EINVAL, `a command pane does not
+restart`): one that is gone is refused before anything runs, `exec:
<dir>: no such directory` (ENOENT), and a shell the host cannot start (not
there, not executable, a script whose interpreter is not there) fails the
write with why -- `shell: shell not found`, or `shell: interpreter
diff --git a/src/ninep/pty.zig b/src/ninep/pty.zig
index d32c35fd..41748bdc 100644
--- a/src/ninep/pty.zig
+++ b/src/ninep/pty.zig
@@ -61,6 +61,10 @@ 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 command pane's child is its command, done or running: exec
+ // would run its line again with no header, footer or records.
+ if (!apply and std.mem.eql(u8, line, "exec")) if (p.panes[id]) |pane| if (pane.command != null)
+ return tree.failText(req.tag, E.INVAL, "invalid exec: a command pane does not restart");
// 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())) {
@@ -1087,3 +1091,14 @@ test "a run's answer longer than its room is cut, its newline kept" {
try testing.expectEqual(slot.answer.len, slot.len);
try testing.expectEqual(@as(u8, '\n'), slot.answer[slot.len - 1]);
}
+
+test "exec on a command pane is refused: a command pane does not restart" {
+ const p = try th.withTerm(testing.allocator);
+ defer p.deinit();
+ const serial = th.serialOf(p);
+ const pane = p.panes[p.paneBySerial(serial).?].?;
+ pane.command = try p.gpa.dupe(u8, "echo hi");
+ const r = th.wr(p, tree.Node.of(serial, .pty_ctl), "exec\n");
+ try testing.expectEqual(tree.E.INVAL, r.errno());
+ try testing.expectEqualStrings("invalid exec: a command pane does not restart", r.reply.ename);
+}