summaryrefslogtreecommitdiff
path: root/src/gui/gui.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-28 14:59:17 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:15 -0300
commitd70f0029f0808e0a3724150a081015e0f943dfb5 (patch)
tree32cd92b54c52f2e6d9c6fec6d5ef42e34af7b9e4 /src/gui/gui.zig
parentada81f05127506dec0dbb7af390cb43fd0da9d40 (diff)
downloadpardes-d70f0029f0808e0a3724150a081015e0f943dfb5.tar.gz
pardes-d70f0029f0808e0a3724150a081015e0f943dfb5.zip
A command pane's command is over when its process exits, not when its pty closes
A job left in the background (sleep 100 &) held the pty open, so the pane stayed running and the child a zombie until the job ended; a command that closed its terminal and ran on got its end of file at once, the host waited 100 ms for an exit, reported exit ? and hung it up. Now each command's child is watched on a thread (waitid with WNOWAIT, so its pid stays its own until the host reaps it), and the host tells the core the exit from that: after the pty's end of file, so the output before the exit is in, or 50 ms after the exit without one, a job holding the pty. The pty stays open until both, so a command that let go of its terminal is never hung up by it. All four front ends; a host that cannot start the watcher reads the exit at end of file as before. Tests: host_io's for both cases, and cmdexit.snap end to end. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src/gui/gui.zig')
-rw-r--r--src/gui/gui.zig73
1 files changed, 63 insertions, 10 deletions
diff --git a/src/gui/gui.zig b/src/gui/gui.zig
index bfa83175..175db9e2 100644
--- a/src/gui/gui.zig
+++ b/src/gui/gui.zig
@@ -840,8 +840,17 @@ const Pty = struct {
kill_at: i64 = 0,
reader: ?std.Thread = null,
stop: [2]c_int = .{ -1, -1 },
+ /// A command pane's child, watched to its exit (host_io.watchExit);
+ /// the pty stays open until both its end of file and its exit.
+ cmd: host_io.CommandWatch = .{},
};
+/// A watched child exited: wake the loop, which takes it (`takeExits`).
+fn wakeForExit(ctx: ?*anyopaque) void {
+ const q: *Queue = @ptrCast(@alignCast(ctx.?));
+ q.wake();
+}
+
const RetiredShell = struct { pid: libc.pid_t = 0, kill_at: i64 = 0 };
const Msg = union(enum) {
@@ -2536,6 +2545,8 @@ fn localSession(
shell.threads_ok = true;
pardes.lsp.setStatusSink(&queue, lspStatusSink);
defer pardes.lsp.setStatusSink(null, null);
+ host_io.setExitWake(&queue, wakeForExit);
+ defer host_io.setExitWake(null, null);
while (!core.quit) {
pardes.turn.restoreSettled();
@@ -2824,6 +2835,8 @@ fn runGrid(init: std.process.Init, opts_in: pardes.Options) !void {
shell.threads_ok = true;
pardes.lsp.setStatusSink(&queue, lspStatusSink);
defer pardes.lsp.setStatusSink(null, null);
+ host_io.setExitWake(&queue, wakeForExit);
+ defer host_io.setExitWake(null, null);
setStdinRaw() catch {}; // stdin may be a pipe, not a pty — best effort
@@ -3865,8 +3878,29 @@ const Shell = struct {
break :vt v;
};
+ /// Each watched child that exited: reaped, the core told, and its pty
+ /// closed if its end of file came first.
+ fn takeExits(s: *Shell) void {
+ while (host_io.takeExited()) |pid| for (s.ptys) |*slot| {
+ const pt = if (slot.*) |*pt| pt else continue;
+ if (pt.pid != pid or pt.cmd.exited) continue;
+ pt.cmd.exit((host_io.reapExited(pid) orelse break).status);
+ pt.pid = 0; // reaped: no signal or retire may reach whoever gets it next
+ break;
+ };
+ for (s.ptys, 0..) |*slot, id| {
+ const pt = if (slot.*) |*pt| pt else continue;
+ if (!pt.cmd.due()) continue;
+ pt.cmd.told = true;
+ s.core.update(.{ .exited = .{ .pane = @intCast(id), .status = pt.cmd.status } });
+ if (pt.cmd.eof) s.closePty(@intCast(id));
+ s.saw_event = true;
+ }
+ }
+
fn drainQueue(s: *Shell) void {
s.reconcilePtys();
+ s.takeExits();
var msgs = s.queue.take();
var check_files = false;
for (msgs.slice()) |m| switch (m) {
@@ -3877,20 +3911,38 @@ const Shell = struct {
s.saw_event = true;
},
.eof => |e| {
- if (s.gens[e.pane] == e.gen) {
- // Reaped here, its status kept, it is no shell to retire.
- const status = if (s.ptys[e.pane]) |*pt| status: {
- const got = host_io.exitStatus(pt.pid, 100);
- if (got != null) pt.pid = 0;
- break :status got;
- } else null;
- s.closePty(e.pane);
+ if (s.gens[e.pane] == e.gen) eof: {
if (e.failure) |err| {
+ s.closePty(e.pane);
if (s.core.panes[e.pane]) |pane| {
pane.body.mode = .normal;
s.core.reportError(e.pane, "terminal reader", err);
}
- } else s.core.update(.{ .eof = .{ .pane = e.pane, .status = status } });
+ break :eof;
+ }
+ const pt = if (s.ptys[e.pane]) |*pt| pt else {
+ s.core.update(.{ .eof = .{ .pane = e.pane } });
+ break :eof;
+ };
+ // A command's pty stays open until its child has exited:
+ // closing it would hang up one that runs on without it.
+ // Its exit, if it came first, is told now its output is in.
+ if (pt.cmd.watched) {
+ pt.cmd.eof = true;
+ s.core.update(.{ .eof = .{ .pane = e.pane } });
+ s.takeExits();
+ break :eof;
+ }
+ // Unwatched, a command's exit is read here, as its end.
+ const unwatched = if (s.core.panes[e.pane]) |pane| pane.command != null else false;
+ var status: ?u8 = null;
+ if (unwatched) {
+ status = host_io.exitStatus(pt.pid, 100);
+ if (status != null) pt.pid = 0; // reaped: no shell to retire
+ }
+ s.closePty(e.pane);
+ if (unwatched) s.core.update(.{ .exited = .{ .pane = e.pane, .status = status } });
+ s.core.update(.{ .eof = .{ .pane = e.pane } });
}
s.saw_event = true;
},
@@ -4165,7 +4217,8 @@ fn spawnPane(ctx: ?*anyopaque, pane: u8, cwd: []const u8) void {
if (s.ptys[pane] != null) return s.core.reportError(pane, "shell", error.WorkersBusy);
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);
- const pt: Pty = .{ .fd = child.file.handle, .pid = child.pid, .serial = s.core.panes[pane].?.serial };
+ const command = s.core.panes[pane].?.command != null;
+ const pt: Pty = .{ .fd = child.file.handle, .pid = child.pid, .serial = s.core.panes[pane].?.serial, .cmd = .{ .watched = command and host_io.watchExit(child.pid) } };
s.ptys[pane] = pt;
var lbuf: [pardes.memory.limits.host_path_cap + 1]u8 = undefined;
if (host_io.shellCwd(pt.pid, &lbuf)) |wd| s.core.setCwd(pane, wd);