From db12ca703dd2acbeee2d7b6acc03dd8a93909755 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Tue, 29 Sep 2026 16:20:57 -0300 Subject: 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 --- docs/fs.md | 4 +++- src/ninep/pty.zig | 15 +++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) 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: : 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); +} -- cgit v1.3