diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-28 16:29:29 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:15 -0300 |
| commit | f30ee05b0609b8f3aa488c94f3ff21733ed37053 (patch) | |
| tree | 2129d76e4c33861ca362d22b5fb9ab02ad6f32c1 /src/host_io.zig | |
| parent | 2a842d241c462c11959102f96a3945aec493cb86 (diff) | |
| download | pardes-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/host_io.zig')
| -rw-r--r-- | src/host_io.zig | 88 |
1 files changed, 67 insertions, 21 deletions
diff --git a/src/host_io.zig b/src/host_io.zig index 68d35a9c..340513df 100644 --- a/src/host_io.zig +++ b/src/host_io.zig @@ -1426,12 +1426,6 @@ fn watchChild(pid: libc.pid_t) void { _ = libc.nanosleep(&ts, null); } wakeForExit(); - // Once more a little later: a job the command left holding its pty - // may keep its end of file from ever coming, and the host tells the - // exit then (`CommandWatch.due`). - const ts: libc.timespec = .{ .sec = 0, .nsec = (CommandWatch.grace_ms + 10) * std.time.ns_per_ms }; - _ = libc.nanosleep(&ts, null); - wakeForExit(); } fn wakeForExit() void { @@ -1440,33 +1434,85 @@ fn wakeForExit() void { if (exit_wake) |wake| wake.f(wake.ctx); } -/// Where a host's command pane is between its child's exit and its pty's -/// end of file. The exit is told once the output before it has been read, -/// at end of file -- the exit is often seen first, with output still in -/// the pty -- or after `grace_ms` without one, a job the command left in -/// the background holding the pty. +/// Where a host's command pane is between its child's exit and the end of +/// its output. The exit is told once the output before it is in: at the +/// pty's end of file; or, when something else still holds the pty (a job +/// the command left in the background), once nothing is waiting to be read +/// -- checked when the exit is seen and again after each chunk of output, +/// with no timer to miss. acme's waitthread tells an exit when it is +/// reaped (acme.c:587-700); a terminal also has output in flight. pub const CommandWatch = struct { - pub const grace_ms = 50; watched: bool = false, + /// The master, to ask whether output is still waiting. + fd: c_int = -1, eof: bool = false, exited: bool = false, told: bool = false, status: ?u8 = null, - at: i64 = 0, - - /// The child is reaped, with this status. - pub fn exit(w: *CommandWatch, status: ?u8) void { - w.exited = true; - w.status = status; - w.at = nowMs(); - } /// Whether the exit is to be told now. pub fn due(w: *const CommandWatch) bool { - return w.exited and !w.told and (w.eof or nowMs() - w.at >= grace_ms); + if (!w.exited or w.told) return false; + if (w.eof) return true; + var pfd = [1]libc.pollfd{.{ .fd = w.fd, .events = libc.POLL.IN, .revents = 0 }}; + if (libc.poll(&pfd, 1, 0) < 0) return true; + // Every holder of its terminal gone: the end of file comes, after + // the rest of the output. + if (pfd[0].revents & libc.POLL.HUP != 0) return false; + return pfd[0].revents & libc.POLL.IN == 0; } }; +/// A host's pty slot, whichever way it keeps it: an optional, or one whose +/// `fd` is -1 when empty. +fn SlotPty(comptime S: type) type { + return switch (@typeInfo(S)) { + .optional => |o| *o.child, + else => *S, + }; +} + +fn slotPty(slot: anytype) ?SlotPty(@TypeOf(slot.*)) { + return switch (@typeInfo(@TypeOf(slot.*))) { + .optional => if (slot.*) |*pt| pt else null, + else => if (slot.fd < 0) null else slot, + }; +} + +/// Each watched child that exited: reaped, and the core told once its +/// output is in, its pty closed (`close(ctx, id)`) if its end of file came +/// first. Every host runs this after an exit's wake and after each chunk +/// of a command's output; `ptys` is its slot array, each slot with a `pid` +/// and a `cmd`. +pub fn takeExits(core: *pardes.Pardes, ptys: anytype, ctx: anytype, comptime close: anytype) void { + while (takeExited()) |pid| for (ptys) |*slot| { + const pt = slotPty(slot) orelse continue; + if (!pt.cmd.watched or pt.pid != pid or pt.cmd.exited) continue; + pt.cmd.status = (reapExited(pid) orelse break).status; + pt.cmd.exited = true; + pt.pid = 0; // reaped: no signal or retire may reach whoever gets it next + break; + }; + for (ptys, 0..) |*slot, id| { + const pt = slotPty(slot) orelse continue; + if (!pt.cmd.watched or !pt.cmd.due()) continue; + pt.cmd.told = true; + const eof = pt.cmd.eof; + core.update(.{ .exited = .{ .pane = @intCast(id), .status = pt.cmd.status } }); + if (eof) close(ctx, id); + } +} + +/// A watched command's pty reached end of file: the core hears its output +/// is over; the pty is closed now if its exit was told, else when it is. +pub fn commandEof(core: *pardes.Pardes, ptys: anytype, id: usize, ctx: anytype, comptime close: anytype) void { + const pt = slotPty(&ptys[id]) orelse return; + pt.cmd.eof = true; + core.update(.{ .eof = .{ .pane = @intCast(id) } }); + if (pt.cmd.told) return close(ctx, id); + takeExits(core, ptys, ctx, close); +} + /// Reaps `pid` if it has exited: its status (null: unknown, or not ours /// to read), or null when it is still running -- a pid a watcher reported /// may since have been reaped by another and taken by a new child. |
