diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-28 14:59:17 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:15 -0300 |
| commit | d70f0029f0808e0a3724150a081015e0f943dfb5 (patch) | |
| tree | 32cd92b54c52f2e6d9c6fec6d5ef42e34af7b9e4 /src/tty | |
| parent | ada81f05127506dec0dbb7af390cb43fd0da9d40 (diff) | |
| download | pardes-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/tty')
| -rw-r--r-- | src/tty/tty.zig | 54 |
1 files changed, 50 insertions, 4 deletions
diff --git a/src/tty/tty.zig b/src/tty/tty.zig index 490958ee..8e3809b0 100644 --- a/src/tty/tty.zig +++ b/src/tty/tty.zig @@ -317,8 +317,17 @@ const Pty = struct { file: std.Io.File, pid: posix.pid_t, reader: std.Io.Future(anyerror!void), + /// 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 loop: *Loop = @ptrCast(@alignCast(ctx.?)); + _ = loop.tryPostEvent(.nop) catch {}; +} + const KittyPlacement = struct { cell_x: u16, cell_y: u16, @@ -730,7 +739,9 @@ fn localSession( .fs = fs, }; pardes.lsp.setStatusSink(&sh, lspStatusSink); + host_io.setExitWake(loop, wakeForExit); defer { + host_io.setExitWake(null, null); pardes.lsp.setStatusSink(null, null); for (&sh.ptys) |*slot| if (slot.*) |*pt| { pt.reader.cancel(io) catch {}; @@ -797,6 +808,7 @@ fn localSession( frames: while (!core.quit) { pardes.turn.restoreSettled(); try core.pump(host); + sh.takeExits(); if (core.takeRestore()) |rp| blk: { const bytes = filesystem.readRestore(gpa, rp, core.settings.dump_dir.get()) catch |err| { core.reportError(core.active, "Restore", err); @@ -1006,15 +1018,26 @@ const Shell = struct { return true; }, .pty_eof => |e| if (s.gens[e.id] == e.gen) { - var status: ?u8 = null; if (s.ptys[e.id]) |*pt| { pt.reader.await(s.io) catch {}; // reader just finished; join it or its future leaks + // 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; + core.update(.{ .eof = .{ .pane = @intCast(e.id) } }); + s.takeExits(); + return true; + } _ = libc.close(pt.file.handle); - status = host_io.exitStatus(pt.pid, 100); + // Unwatched, a command's exit is read here, as its end. + const command = if (core.panes[e.id]) |pn| pn.command != null else false; + const status = if (command) host_io.exitStatus(pt.pid, 100) else null; if (status == null) host_io.retireShell(pt.pid); s.ptys[e.id] = null; + if (command) core.update(.{ .exited = .{ .pane = @intCast(e.id), .status = status } }); } - core.update(.{ .eof = .{ .pane = @intCast(e.id), .status = status } }); + core.update(.{ .eof = .{ .pane = @intCast(e.id) } }); }, .key_press => |key| if (s.in_paste) { const bytes = pasteBytes(key); @@ -1201,7 +1224,8 @@ const Shell = struct { 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 = {} } }; + const command = if (s.core.panes[pane]) |pn| pn.command != null else false; + s.ptys[pane] = .{ .file = child.file, .pid = child.pid, .reader = .{ .any_future = null, .result = {} }, .cmd = .{ .watched = command and host_io.watchExit(child.pid) } }; var lbuf: [pardes.memory.limits.host_path_cap + 1]u8 = undefined; if (host_io.shellCwd(child.pid, &lbuf)) |wd| s.core.setCwd(pane, wd); if (s.threads_ok) { @@ -1248,6 +1272,28 @@ const Shell = struct { return host_io.ttyTaken(pt.pid, pt.file.handle); } + /// Each watched child that exited: reaped, the core told, and its pty + /// closed if its end of file came first. + fn takeExits(s: *@This()) 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) { + _ = libc.close(pt.file.handle); + slot.* = null; + } + } + } + fn killJob(ctx: ?*anyopaque, pane: u8) bool { const s = of(ctx); const pt = s.ptys[pane] orelse return false; |
