summaryrefslogtreecommitdiff
path: root/src/macos.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/macos.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/macos.zig')
-rw-r--r--src/macos.zig64
1 files changed, 56 insertions, 8 deletions
diff --git a/src/macos.zig b/src/macos.zig
index 97dab9a8..1327b064 100644
--- a/src/macos.zig
+++ b/src/macos.zig
@@ -126,8 +126,39 @@ const Pty = struct {
pid: posix.pid_t,
gen: u32,
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 host, which takes it (`takeExits`).
+fn wakeForExit(ctx: ?*anyopaque) void {
+ const st: *State = @ptrCast(@alignCast(ctx orelse return));
+ wake(st);
+}
+
+/// Each watched child that exited: reaped, the core told, and its pty
+/// closed if its end of file came first.
+fn takeExits(st: *State) bool {
+ var did = false;
+ while (host_io.takeExited()) |pid| for (&st.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 (&st.ptys, 0..) |*slot, id| {
+ const pt = if (slot.*) |*pt| pt else continue;
+ if (!pt.cmd.due()) continue;
+ pt.cmd.told = true;
+ st.core.update(.{ .exited = .{ .pane = @intCast(id), .status = pt.cmd.status } });
+ if (pt.cmd.eof) reap(st, @intCast(id));
+ did = true;
+ }
+ return did;
+}
+
const WatchedFile = struct {
path: []u8,
serial: u32,
@@ -783,6 +814,7 @@ fn initCore(runtime: ?*const Runtime, cols_arg: u16, rows_arg: u16) !void {
for (&st.ptys, 0..) |*slot, id| if (slot.*) |*pt| startReader(st, pt, @intCast(id));
pardes.lsp.setStatusSink(st, lspStatusSink);
+ host_io.setExitWake(st, wakeForExit);
// From here the core is touched only inside the entry points above,
// each of which takes the turn for its own duration; between them the
// 9P connections have it.
@@ -802,6 +834,7 @@ export fn pardes_deinit() void {
st.ninep = null;
}
pardes.lsp.setStatusSink(null, null);
+ host_io.setExitWake(null, null);
if (st.lsp_task) |*t| {
t.future.cancel(st.io) catch {};
st.lsp_task = null;
@@ -1269,7 +1302,7 @@ export fn pardes_watch_changed(pane: u8, generation: u32) void {
fn drainInbox(st: *State) bool {
var batch = st.inbox.take(st.io);
- var did = batch.len > 0;
+ var did = takeExits(st) or batch.len > 0;
for (batch.slice()) |msg| {
defer msg.free(st.gpa);
switch (msg) {
@@ -1280,14 +1313,28 @@ fn drainInbox(st: *State) bool {
},
.eof => |e| {
if (st.gens[e.pane] != e.gen) continue;
- // Reaped here, its status kept, it is no shell to retire.
- const status = if (st.ptys[e.pane]) |*pt| status: {
- const got = host_io.exitStatus(pt.pid, 100);
- if (got != null) pt.pid = 0;
- break :status got;
- } else null;
+ if (st.ptys[e.pane]) |*pt| {
+ // 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;
+ st.core.update(.{ .eof = .{ .pane = e.pane } });
+ _ = takeExits(st);
+ continue;
+ }
+ // Unwatched, a command's exit is read here, as its end.
+ if (st.core.panes[e.pane]) |pane| if (pane.command != null) {
+ const status = host_io.exitStatus(pt.pid, 100);
+ if (status != null) pt.pid = 0; // reaped: no shell to retire
+ reap(st, e.pane);
+ st.core.update(.{ .exited = .{ .pane = e.pane, .status = status } });
+ st.core.update(.{ .eof = .{ .pane = e.pane } });
+ continue;
+ };
+ }
reap(st, e.pane);
- st.core.update(.{ .eof = .{ .pane = e.pane, .status = status } });
+ st.core.update(.{ .eof = .{ .pane = e.pane } });
},
.lsp_done => |d| {
st.core.update(.{ .lsp_resp = .{ .id = d.id, .rows = d.rows } });
@@ -2413,6 +2460,7 @@ fn spawnShell(ctx: ?*anyopaque, pane: u8, cwd: []const u8) void {
.pid = child.pid,
.gen = gen,
.reader = .{ .any_future = null, .result = {} },
+ .cmd = .{ .watched = (if (core.panes[pane]) |pn| pn.command != null else false) and host_io.watchExit(child.pid) },
};
var lbuf: [1024]u8 = undefined;
if (host_io.shellCwd(child.pid, &lbuf)) |wd| core.setCwd(pane, wd);