summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-28 22:25:46 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:15 -0300
commit4073d1576876107e0750a51682a64319cf0c08c2 (patch)
treeb036d5393d862b988e9b88b3bee2f321100d4408
parent33f62509fc515c767cbef8d329d7666697e54ae7 (diff)
downloadpardes-4073d1576876107e0750a51682a64319cf0c08c2.tar.gz
pardes-4073d1576876107e0750a51682a64319cf0c08c2.zip
Kill stops a command pane's whole line again, now it runs with job control
With the line run under job control (the change before), the job running has a group of its own, and Kill's SIGTERM to the tty's foreground group stopped only that job: of `sleep 30; touch x` the touch still ran. For a command pane the hosts now signal the shell's group as well, so the whole line stops, as it did; a line typed at a prompt still loses only its foreground job. The test forks a real pty for both halves: a background job outliving its command and the pty's hangup, and a killed line not running on. Co-Authored-By: Claude Opus 5.5 <[email protected]>
-rw-r--r--docs/fs.md10
-rw-r--r--src/detached/server.zig2
-rw-r--r--src/gui/gui.zig2
-rw-r--r--src/host_io.zig51
-rw-r--r--src/macos.zig2
-rw-r--r--src/tty/tty.zig2
6 files changed, 55 insertions, 14 deletions
diff --git a/docs/fs.md b/docs/fs.md
index 90d5ad64..6419ecc6 100644
--- a/docs/fs.md
+++ b/docs/fs.md
@@ -134,16 +134,18 @@ never needed); and `Kill`, which
does not quit but stops commands, as acme's does: bare, every command pardes
started, and `Kill make ls`, those whose line begins with one of the words. A
command pardes started is a command pane's, until its child exits -- Kill
-sends its whole process group SIGTERM -- or a line it typed into a terminal (a
+sends SIGTERM to its running job and to its shell, so the whole line stops
+(of `sleep 30; echo done` the `echo` never runs) -- or a line it typed into
+a terminal (a
word written to `exec`, a middle click on one, a `pty/run`), from its shell's
start mark (C) to its end mark (D), where Kill sends its foreground job SIGTERM -- acme posts the
"kill" note, which ends a process -- and never signals the shell itself;
in a shell running without job control (`set +m`) the job shares the
shell's group, so there is none to signal: Kill says `Kill: no job to
signal`, and a write of it to `ctl` fails with that; with nothing running
-it says `Kill: nothing running`; and only the
-foreground job, so of `sleep 30; echo done` the `echo` still runs once the
-`sleep` is stopped --
+it says `Kill: nothing running`; and, of a typed line, only the
+foreground job, so of `sleep 30; echo done` typed at a prompt the `echo`
+still runs once the `sleep` is stopped --
and reads every setting there is, one a line, in the words a write of it
takes (`Verbose on`, `WindowOpacity 70`, `PanelSlide off`, `DumpDir` bare
for the default directory, `LocationsConfig ...`), so writing what it reads
diff --git a/src/detached/server.zig b/src/detached/server.zig
index 2fc165eb..857591d4 100644
--- a/src/detached/server.zig
+++ b/src/detached/server.zig
@@ -533,7 +533,7 @@ pub const Session = struct {
const pt = s.ptys[pane];
// A command whose exit is recorded is reaped: its group may be another's.
if (pt.fd < 0 or pt.cmd.exited) return;
- host_io.signalTty(pt.pid, pt.fd, sig);
+ host_io.signalTty(pt.pid, pt.fd, sig, pt.cmd.watched);
}
fn ttyTaken(ctx: ?*anyopaque, pane: u8) bool {
diff --git a/src/gui/gui.zig b/src/gui/gui.zig
index cac311e2..02f7bd9e 100644
--- a/src/gui/gui.zig
+++ b/src/gui/gui.zig
@@ -4401,7 +4401,7 @@ fn ptyResize(ctx: ?*anyopaque, pane: u8, cols: u16, rows: u16) void {
fn ptySignal(ctx: ?*anyopaque, pane: u8, sig: pardes.PtySignal) void {
const s = shellOf(ctx);
// A command whose exit is recorded is reaped: its group may be another's.
- if (s.ptys[pane]) |pt| if (pt.fd >= 0 and !pt.cmd.exited) host_io.signalTty(pt.pid, pt.fd, sig);
+ if (s.ptys[pane]) |pt| if (pt.fd >= 0 and !pt.cmd.exited) host_io.signalTty(pt.pid, pt.fd, sig, pt.cmd.watched);
}
fn ttyTaken(ctx: ?*anyopaque, pane: u8) bool {
diff --git a/src/host_io.zig b/src/host_io.zig
index 53e1628d..df78f73c 100644
--- a/src/host_io.zig
+++ b/src/host_io.zig
@@ -1670,7 +1670,11 @@ fn reapShell(pid: libc.pid_t, grace_ms: u32) void {
while (libc.waitpid(pid, null, 0) < 0 and libc.errno(-1) == .INTR) {}
}
-pub fn signalTty(shell_pid: libc.pid_t, master_fd: c_int, which: pardes.PtySignal) void {
+/// The tty's foreground job is signalled. A command pane's shell (`command`)
+/// is its command too: its line runs with job control on, so the job
+/// running is in a group of its own, and the shell's group goes as well, or
+/// the rest of the line would run on after the job it was on.
+pub fn signalTty(shell_pid: libc.pid_t, master_fd: c_int, which: pardes.PtySignal, command: bool) void {
const sig = switch (which) {
.int => libc.SIG.INT,
.term => libc.SIG.TERM,
@@ -1679,11 +1683,9 @@ pub fn signalTty(shell_pid: libc.pid_t, master_fd: c_int, which: pardes.PtySigna
.kill => libc.SIG.KILL,
};
const fg = tcgetpgrp(master_fd);
- if (fg > 0) {
- _ = libc.kill(-fg, sig);
- return;
- }
- if (shell_pid > 0) _ = libc.kill(shell_pid, sig);
+ if (fg > 0) _ = libc.kill(-fg, sig);
+ if (command and shell_pid > 0 and fg != shell_pid) _ = libc.kill(-shell_pid, sig);
+ if (fg <= 0 and !command and shell_pid > 0) _ = libc.kill(shell_pid, sig);
}
/// Kill: SIGTERM to the job in the tty's foreground, never to the shell.
@@ -2074,6 +2076,43 @@ test "a command's exit is its process's, told by its watcher, whether or not its
}
}
+test "a command's background job outlives it and its pty, and Kill stops its whole line" {
+ if (comptime !tty_probe_platform) return error.SkipZigTest;
+ if (!haveFile("/bin/sh")) return error.SkipZigTest;
+ const core = try pardes.Pardes.init(std.testing.allocator, .{ .tty_only = true });
+ defer core.deinit();
+ while (core.nextEffect()) |_| {}
+ const rcs: Shell.PromptFiles = .{};
+ var tmp = std.testing.tmpDir(.{});
+ defer tmp.cleanup();
+ var dir_buf: [4096]u8 = undefined;
+ const dir = dir_buf[0..try tmp.dir.realPath(std.testing.io, &dir_buf)];
+ const Case = struct { line: []const u8, kill: bool, made: bool };
+ for ([_]Case{
+ // The shell leads the pty's session; its exit hangs up only the
+ // foreground group, which the job, in a group of its own, is not.
+ .{ .line = "(sleep 0.4; touch left) & exit 0", .kill = false, .made = true },
+ // The job running is in its own group; the shell goes too.
+ .{ .line = "sleep 0.4; touch killed", .kill = true, .made = false },
+ }) |c| {
+ const id = core.freeSlot().?;
+ _ = try core.newCommand(id, dir, c.line);
+ while (core.nextEffect()) |_| {}
+ const child = try forkShell(core, id, &rcs, "/bin/sh", dir, 24, 80, null);
+ if (c.kill) {
+ sleepMs(150);
+ signalTty(child.pid, child.file.handle, .term, true);
+ }
+ _ = exitStatus(child.pid, 2000);
+ _ = libc.close(child.file.handle); // the pane closes: the pty hangs up
+ sleepMs(800);
+ const name = if (c.kill) "killed" else "left";
+ const made = if (tmp.dir.statFile(std.testing.io, name, .{})) |_| true else |_| false;
+ try std.testing.expectEqual(c.made, made);
+ try core.removePane(id, null);
+ }
+}
+
test "a retired shell that ignores the hangup is killed and reaped, not left a zombie" {
const pid = libc.fork();
if (pid == 0) {
diff --git a/src/macos.zig b/src/macos.zig
index a2175f3d..2b64784f 100644
--- a/src/macos.zig
+++ b/src/macos.zig
@@ -2469,7 +2469,7 @@ fn ptyResize(ctx: ?*anyopaque, pane: u8, cols: u16, rows: u16) void {
fn ptySignal(ctx: ?*anyopaque, pane: u8, sig: pardes.PtySignal) void {
const st = hostState(ctx);
// A command whose exit is recorded is reaped: its group may be another's.
- if (st.ptys[pane]) |pt| if (!pt.cmd.exited) host_io.signalTty(pt.pid, pt.file.handle, sig);
+ if (st.ptys[pane]) |pt| if (!pt.cmd.exited) host_io.signalTty(pt.pid, pt.file.handle, sig, pt.cmd.watched);
}
fn ttyTaken(ctx: ?*anyopaque, pane: u8) bool {
diff --git a/src/tty/tty.zig b/src/tty/tty.zig
index b18f4de5..96aec9a9 100644
--- a/src/tty/tty.zig
+++ b/src/tty/tty.zig
@@ -1273,7 +1273,7 @@ const Shell = struct {
const s = of(ctx);
// A command whose exit is recorded is reaped: its group may be
// another's by now.
- if (s.ptys[pane]) |pt| if (!pt.cmd.exited) host_io.signalTty(pt.pid, pt.file.handle, sig);
+ if (s.ptys[pane]) |pt| if (!pt.cmd.exited) host_io.signalTty(pt.pid, pt.file.handle, sig, pt.cmd.watched);
}
/// The pane is gone: hang its pty up, which the kernel passes on to the