diff options
Diffstat (limited to 'src/gui/gui.zig')
| -rw-r--r-- | src/gui/gui.zig | 67 |
1 files changed, 58 insertions, 9 deletions
diff --git a/src/gui/gui.zig b/src/gui/gui.zig index aa39cf32..5bc1ea6d 100644 --- a/src/gui/gui.zig +++ b/src/gui/gui.zig @@ -816,8 +816,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) { @@ -1298,7 +1307,7 @@ test "GUI PTY Restore joins a real reader waiting for queue space" { try std.testing.expect(host_io.writeFd(child.file.handle, "printf 'after-full'; exit\n")); try PtyTests.waitBlocked(&queue); const old_serial = core.panes[0].?.serial; - const replacement = try dump.restore(core, core.dump_out.?); + const replacement = try dump.restore(core, core.dump_out.?, "test"); shell.stopPtys(); core.deinit(); core = replacement; @@ -1627,7 +1636,7 @@ test "GUI LSP worker limit rejects before allocation and recovers after owned wo try std.testing.expect(core.lsp_wait == null); try std.testing.expectEqualStrings("abc", pane.file.?.content); try std.testing.expectEqual(@as(i32, 1), pane.body.cur_col); - try std.testing.expect(std.mem.indexOf(u8, pane.msg[0..pane.msg_len], "WorkersBusy") != null); + try std.testing.expect(std.mem.indexOf(u8, pane.msg[0..pane.msg_len], "workers busy") != null); try std.testing.expectEqual(@as(usize, 0), failing.alloc_index); try std.testing.expect(!failing.has_induced_failure); try std.testing.expectEqual(rejected, queue.lsp_id.?); @@ -2617,6 +2626,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(); @@ -2642,7 +2653,7 @@ fn localSession( break :blk; }; defer gpa.free(bytes); - const nc = dump.restore(core, bytes) catch |err| { + const nc = dump.restore(core, bytes, rp) catch |err| { core.reportError(core.active, "Restore", err); break :blk; }; @@ -2942,6 +2953,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 @@ -3999,8 +4012,20 @@ const Shell = struct { break :vt v; }; + /// The command panes' exits, told once their output is in (host_io). + fn takeExits(s: *Shell) void { + host_io.takeExits(s.core, s.ptys, s, closeWatched); + } + + fn closeWatched(s: *Shell, id: usize) void { + s.closePty(@intCast(id)); + s.saw_event = true; + } + fn drainQueue(s: *Shell) void { s.reconcilePtys(); + // After this batch's output, which an exit is told behind. + defer s.takeExits(); var msgs = s.queue.take(); var check_files = false; for (msgs.slice()) |m| switch (m) { @@ -4011,14 +4036,36 @@ const Shell = struct { s.saw_event = true; }, .eof => |e| { - if (s.gens[e.pane] == e.gen) { - 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 } }); + 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) { + host_io.commandEof(s.core, s.ptys, e.pane, s, closeWatched); + 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; }, @@ -4325,7 +4372,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), .fd = child.file.handle } }; 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); @@ -4353,7 +4401,8 @@ fn ptyResize(ctx: ?*anyopaque, pane: u8, cols: u16, rows: u16) void { fn ptySignal(ctx: ?*anyopaque, pane: u8, sig: pardes.PtySignal) void { const s = shellOf(ctx); - if (s.ptys[pane]) |pt| if (pt.fd >= 0) host_io.signalTty(pt.pid, pt.fd, sig); + // A command whose exit is recorded is reaped: its group may be another's. + if (s.ptys[pane]) |pt| if (pt.fd >= 0 and !pt.cmd.exited) host_io.signalTty(pt.pid, pt.fd, sig); } fn ttyTaken(ctx: ?*anyopaque, pane: u8) bool { @@ -4491,7 +4540,7 @@ test "the headless grid host round-trips a yank back as a paste" { core.update(.{ .key = .{ .cp = ' ' } }); core.update(.{ .key = .{ .cp = 'y' } }); while (core.nextEffect()) |e| core.perform(e); - const yanked = core.yank orelse return error.MissingYank; + const yanked = core.registers.text(core.gpa, '+') orelse return error.MissingYank; try std.testing.expect(yanked.len > 0); const before = pane.file.?.content.len; |
