summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/gui/gui.zig4
-rw-r--r--src/host_io.zig47
-rw-r--r--src/macos.zig5
-rw-r--r--src/pardes.zig26
-rw-r--r--src/tty/tty.zig14
5 files changed, 84 insertions, 12 deletions
diff --git a/src/gui/gui.zig b/src/gui/gui.zig
index 1d5d3c05..2369dd28 100644
--- a/src/gui/gui.zig
+++ b/src/gui/gui.zig
@@ -3786,6 +3786,10 @@ const Shell = struct {
s.ptys[pane] = null;
return;
};
+ // No room to wait for it here: a slot kept for it would refuse the
+ // next shell spawned into this pane until it was reaped.
+ host_io.retireShell(pt.pid);
+ s.ptys[pane] = null;
}
fn stopPtys(s: *Shell) void {
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;
diff --git a/src/macos.zig b/src/macos.zig
index a0abc119..ebe8193b 100644
--- a/src/macos.zig
+++ b/src/macos.zig
@@ -2505,8 +2505,7 @@ fn startReader(st: *State, pt: *Pty, id: u8) void {
/// The pane is gone: its shell goes with it.
fn closePty(ctx: ?*anyopaque, pane: u8) void {
const st = hostState(ctx);
- const pt = st.ptys[pane] orelse return;
- _ = libc.kill(pt.pid, posix.SIG.HUP);
+ if (st.ptys[pane] == null) return;
reap(st, pane);
st.gens[pane] +%= 1;
}
@@ -2516,7 +2515,7 @@ fn reap(st: *State, pane: u8) void {
st.ptys[pane] = null;
pt.reader.cancel(st.io) catch {};
_ = libc.close(pt.file.handle);
- _ = libc.waitpid(pt.pid, null, posix.W.NOHANG);
+ host_io.retireShell(pt.pid);
}
fn readPty(st: *State, io: std.Io, pty: std.Io.File, id: u8, gen: u32) anyerror!void {
diff --git a/src/pardes.zig b/src/pardes.zig
index f3348ea5..59556649 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -1125,6 +1125,32 @@ test "Tty spawns a raw shell in the caller's directory" {
try std.testing.expect(found);
}
+test "a terminal respawned into a closed pane's slot hangs the old shell up first" {
+ // Hosts keep a shell per slot: were the spawn first, the close would
+ // end the new shell and leave the old one running.
+ const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 100, .rows = 30 });
+ defer p.deinit();
+ try std.testing.expect(p.executeBuiltinLine(0, "Tty"));
+ const slot = p.active;
+ while (p.nextEffect()) |_| {}
+ try p.removePane(slot, null);
+ try std.testing.expect(p.executeBuiltinLine(0, "Tty"));
+ try std.testing.expectEqual(slot, p.active);
+ var closed = false;
+ var spawned = false;
+ while (p.nextEffect()) |effect| switch (effect) {
+ .close_pty => |c| if (c.pane == slot) {
+ try std.testing.expect(!spawned);
+ closed = true;
+ },
+ .spawn => |s| if (s.pane == slot) {
+ spawned = true;
+ },
+ else => {},
+ };
+ try std.testing.expect(closed and spawned);
+}
+
test "Tty9p marks only the new Linux terminal for a mounted shell" {
if (comptime !hosted or @import("builtin").os.tag != .linux) return error.SkipZigTest;
const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true });
diff --git a/src/tty/tty.zig b/src/tty/tty.zig
index b2768ed6..128a7ead 100644
--- a/src/tty/tty.zig
+++ b/src/tty/tty.zig
@@ -1007,6 +1007,7 @@ const Shell = struct {
if (s.ptys[e.id]) |*pt| {
pt.reader.await(s.io) catch {}; // reader just finished; join it or its future leaks
_ = libc.close(pt.file.handle);
+ host_io.retireShell(pt.pid);
s.ptys[e.id] = null;
}
core.update(.{ .eof = .{ .pane = @intCast(e.id) } });
@@ -1193,11 +1194,7 @@ const Shell = struct {
fn spawn(ctx: ?*anyopaque, pane: u8, cwd: []const u8) void {
const s = of(ctx);
- if (s.ptys[pane]) |*old| {
- old.reader.cancel(s.io) catch {};
- _ = libc.close(old.file.handle);
- s.ptys[pane] = null;
- }
+ closePty(ctx, pane); // a shell still in the slot goes first, reaped
s.gens[pane] +%= 1;
const child = host_io.forkShell(s.core, pane, s.prompt_rcs, s.core.shellBin(), cwd, s.core.screen_h, s.core.screen_w, s.fs) catch |err| return s.core.reportError(pane, "shell", err);
s.ptys[pane] = .{ .file = child.file, .pid = child.pid, .reader = .{ .any_future = null, .result = {} } };
@@ -1229,8 +1226,8 @@ const Shell = struct {
}
/// The pane is gone: hang its pty up, which the kernel passes on to the
- /// shell as SIGHUP, and say it too for a shell that ignores the tty's.
- /// Output still in flight carries the old generation and is dropped.
+ /// shell as SIGHUP, and retire the shell. Output still in flight
+ /// carries the old generation and is dropped.
fn closePty(ctx: ?*anyopaque, pane: u8) void {
const s = of(ctx);
var pt = s.ptys[pane] orelse return;
@@ -1238,8 +1235,7 @@ const Shell = struct {
s.gens[pane] +%= 1;
pt.reader.cancel(s.io) catch {};
_ = libc.close(pt.file.handle);
- _ = libc.kill(pt.pid, posix.SIG.HUP);
- _ = libc.waitpid(pt.pid, null, posix.W.NOHANG);
+ host_io.retireShell(pt.pid);
}
fn ttyTaken(ctx: ?*anyopaque, pane: u8) bool {