summaryrefslogtreecommitdiff
path: root/src/tty/tty.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-28 16:29:29 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:15 -0300
commitf30ee05b0609b8f3aa488c94f3ff21733ed37053 (patch)
tree2129d76e4c33861ca362d22b5fb9ab02ad6f32c1 /src/tty/tty.zig
parent2a842d241c462c11959102f96a3945aec493cb86 (diff)
downloadpardes-f30ee05b0609b8f3aa488c94f3ff21733ed37053.tar.gz
pardes-f30ee05b0609b8f3aa488c94f3ff21733ed37053.zip
A command's exit is told once its output is in, by the pty's state, not a timer
The watcher woke the host a second time 60 ms after the exit by its own clock while the grace was counted from the reap: a host busy for 10 ms missed it, and the tag said running for ever and Kill did nothing; and under load exit 0 could land before the last output. As decided, no timer: the exit is told once it is reaped and the pty says nothing is left (poll: no POLLIN, and no POLLHUP, which means the end of file is on its way behind the output), else at that end of file, checked after each chunk of output. The tty host checks inside its step so the frame shows it. The four hosts' copies are one host_io.takeExits/commandEof, which close a told command's pty at its end of file (the fd and the GUI's reader leaked when the exit came first). A finished command pane whose pty a job it left still holds is not reused, so that job is not hung up; and the reset before a reuse is SGR 0, not DECSTR, which ghostty's stream does not implement. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src/tty/tty.zig')
-rw-r--r--src/tty/tty.zig39
1 files changed, 15 insertions, 24 deletions
diff --git a/src/tty/tty.zig b/src/tty/tty.zig
index 14417cd0..f8f8cdc5 100644
--- a/src/tty/tty.zig
+++ b/src/tty/tty.zig
@@ -808,7 +808,6 @@ 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);
@@ -988,6 +987,9 @@ const Shell = struct {
batch += 1;
}
if (motion) |m| _ = s.apply(m);
+ // An exit's wake (a .nop) or output an exit waited behind: told
+ // here, inside the step, so the frame after it shows it.
+ s.takeExits();
s.reloadWatched();
}
@@ -1015,6 +1017,8 @@ const Shell = struct {
if (s.gens[pr.id] == pr.gen)
core.update(.{ .output = .{ .pane = @intCast(pr.id), .bytes = pr.bytes } });
s.gpa.free(pr.bytes);
+ // An exit waiting behind this output may be told now.
+ s.takeExits();
return true;
},
.pty_eof => |e| if (s.gens[e.id] == e.gen) {
@@ -1024,9 +1028,7 @@ const Shell = struct {
// 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();
+ host_io.commandEof(core, &s.ptys, e.id, s, closeWatched);
return true;
}
_ = libc.close(pt.file.handle);
@@ -1225,7 +1227,7 @@ const Shell = struct {
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 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) } };
+ s.ptys[pane] = .{ .file = child.file, .pid = child.pid, .reader = .{ .any_future = null, .result = {} }, .cmd = .{ .watched = command and host_io.watchExit(child.pid), .fd = child.file.handle } };
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) {
@@ -1274,26 +1276,15 @@ 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.
+ /// The command panes' exits, told once their output is in (host_io).
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;
- }
- }
+ host_io.takeExits(s.core, &s.ptys, s, closeWatched);
+ }
+
+ fn closeWatched(s: *@This(), id: usize) void {
+ const pt = &(s.ptys[id] orelse return);
+ _ = libc.close(pt.file.handle);
+ s.ptys[id] = null;
}
fn killJob(ctx: ?*anyopaque, pane: u8) bool {