diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-28 12:01:34 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:14 -0300 |
| commit | 990c2e1b184e9cb1fddd8eb1b05a472cfb113dde (patch) | |
| tree | 0d3327598ff2522900c33e4018ca187a89fdbeb2 /src/host_io.zig | |
| parent | 78c60342022bc44307ad789aaef163ab96ebd08b (diff) | |
| download | pardes-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.zig | 47 |
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; |
