summaryrefslogtreecommitdiff
path: root/src/host_io.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-28 12:01:34 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:14 -0300
commit990c2e1b184e9cb1fddd8eb1b05a472cfb113dde (patch)
tree0d3327598ff2522900c33e4018ca187a89fdbeb2 /src/host_io.zig
parent78c60342022bc44307ad789aaef163ab96ebd08b (diff)
downloadpardes-990c2e1b184e9cb1fddd8eb1b05a472cfb113dde.tar.gz
pardes-990c2e1b184e9cb1fddd8eb1b05a472cfb113dde.zip
A closed terminal's shell is reaped on tty and macOS, killed if it ignores the hangup
tty and macOS hung the pty up and called waitpid once without waiting, so a shell still exiting, or one that ignores SIGHUP, stayed a zombie or ran on with nobody reading it; tty also never reaped a shell that exited by itself, and its spawn into an occupied slot closed the old pty without ending the shell. host_io's retireShell says hangup, waits 100 ms on a thread (macOS has no host timer to poll from), then kills and reaps. The gui's own retired list, when full, left the slot holding the old shell and refused the next spawn into that pane; it now hands that shell to retireShell instead. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src/host_io.zig')
-rw-r--r--src/host_io.zig47
1 files changed, 47 insertions, 0 deletions
diff --git a/src/host_io.zig b/src/host_io.zig
index 5d9dd413..81d784c5 100644
--- a/src/host_io.zig
+++ b/src/host_io.zig
@@ -1330,6 +1330,35 @@ const darwin = struct {
}
};
+/// A terminal pane's shell is done with (its pane gone, its pty closed or at
+/// end of file): say hangup, for a shell that ignores the tty's, and see it
+/// reaped, killed if it is still there 100 ms on. A thread does the waiting
+/// so that no host loop needs a timer for it (macOS's has none); a thread
+/// that cannot start kills and reaps it here.
+/// ponytail: a shell still inside its 100 ms when the editor exits is left
+/// to init; the gui and detached hosts keep their own retired lists, which
+/// this could replace.
+pub fn retireShell(pid: libc.pid_t) void {
+ if (pid <= 0) return;
+ _ = libc.kill(pid, libc.SIG.HUP);
+ if (libc.waitpid(pid, null, libc.W.NOHANG) != 0) return;
+ const thread = std.Thread.spawn(.{}, reapShell, .{ pid, 100 }) catch return reapShell(pid, 0);
+ thread.detach();
+}
+
+fn reapShell(pid: libc.pid_t, grace_ms: u32) void {
+ var waited: u32 = 0;
+ while (waited < grace_ms) : (waited += 5) {
+ // Not 0 is reaped, or someone else reaped it: either way not ours.
+ if (libc.waitpid(pid, null, libc.W.NOHANG) != 0) return;
+ const ts: libc.timespec = .{ .sec = 0, .nsec = 5 * std.time.ns_per_ms };
+ _ = libc.nanosleep(&ts, null);
+ }
+ if (libc.waitpid(pid, null, libc.W.NOHANG) != 0) return;
+ _ = libc.kill(pid, libc.SIG.KILL);
+ 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 {
const sig = switch (which) {
.int => libc.SIG.INT,
@@ -1665,6 +1694,24 @@ test "Kill's signal stops the foreground job and never the shell" {
try std.testing.expectEqual(@as(libc.pid_t, 0), libc.waitpid(sh.pid, null, libc.W.NOHANG));
}
+test "a retired shell that ignores the hangup is killed and reaped, not left a zombie" {
+ const pid = libc.fork();
+ if (pid == 0) {
+ const argv = [_:null]?[*:0]const u8{ "/bin/sh", "-c", "trap '' HUP; exec sleep 30" };
+ const envp = [_:null]?[*:0]const u8{};
+ _ = libc.execve("/bin/sh", &argv, &envp);
+ libc._exit(127);
+ }
+ try std.testing.expect(pid > 0);
+ sleepMs(100); // past the trap
+ retireShell(pid);
+ // A zombie still answers kill(pid, 0); only a reaped pid is gone.
+ var waited: i64 = 0;
+ while (libc.kill(pid, @enumFromInt(0)) == 0 and waited < 2000) : (waited += 10) sleepMs(10);
+ try std.testing.expect(libc.kill(pid, @enumFromInt(0)) != 0);
+ try std.testing.expect(waited >= 90); // it did ignore the hangup
+}
+
test "a background job is not the tty's owner" {
if (comptime !tty_probe_platform) return error.SkipZigTest;
var sh = TestShell.start() orelse return error.SkipZigTest;