diff options
| author | Gabriel Schneider <[email protected]> | 2026-07-06 12:45:47 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-08-01 15:02:07 -0300 |
| commit | bca5d6a52500e72ec4de69c38f6fb2324df4b503 (patch) | |
| tree | bc59bd97a668b4780cb8e591f66162ead4b2ac3d /src | |
| parent | de4def3548a6729b0dfd2120495a61beef8c8c2c (diff) | |
| download | pardes-bca5d6a52500e72ec4de69c38f6fb2324df4b503.tar.gz pardes-bca5d6a52500e72ec4de69c38f6fb2324df4b503.zip | |
tty shell: leave the terminal clean on every exit path. Root causes: vaxis.Tty.init makes the tty raw but run() never called tty.deinit (termios stayed raw -> no echo, staircased output in the spawner shell); the pty reader tasks were never joined (Kill-quit hung the exit in the runtime thread-join with readers blocked in read(2), and finished readers dumped DebugAllocator leak traces onto the tty). Fix: defer tty.deinit() (runs last, after vx.deinit flushes its resets); cancel each pty reader at teardown (cancel interrupts the blocked read) then close masters and drain the loop queue (queued pty_read/paste bytes are gpa-owned); await the reader on pty_eof before closing; readPty posts its eof via tryPostEvent so a post-cancel full queue can't re-block; root panic handler wraps vaxis.recover() so a panic restores cooked mode/main screen/mouse before the trace prints (vaxis.Panic itself is stale: references std.debug.FormattedPanic which 0.16 dropped). Verified with a scripted pty harness (bash spawner, DSR replies): both quit paths (exit-EOF of last pane, middle-click Kill) end with rmcup+cursor+sgr+mouse-off and identical stty -a before/after, no leak spew, prompt back in 0.5s; snap suite 18/18.
Diffstat (limited to 'src')
| -rw-r--r-- | src/main.zig | 10 | ||||
| -rw-r--r-- | src/tty.zig | 28 |
2 files changed, 32 insertions, 6 deletions
diff --git a/src/main.zig b/src/main.zig index ff151323..9a8cd6da 100644 --- a/src/main.zig +++ b/src/main.zig @@ -18,6 +18,16 @@ pub const std_options_debug_io: std.Io = if (is_emscripten) else std_options_debug_threaded_io.?.io(); +// A panic must restore the terminal (cooked mode, main screen, mouse off) +// before the trace prints, or it lands garbled in a raw alt screen. recover() +// no-ops unless the vaxis tty is live, so gui/tty share the handler. +pub const panic = if (is_emscripten) std.debug.FullPanic(std.debug.defaultPanic) else std.debug.FullPanic(struct { + fn call(msg: []const u8, ret_addr: ?usize) noreturn { + @import("vaxis").recover(); + std.debug.defaultPanic(msg, ret_addr); + } +}.call); + const help_text = \\Usage: pardes [options] \\ diff --git a/src/tty.zig b/src/tty.zig index d9821343..f2b36ed4 100644 --- a/src/tty.zig +++ b/src/tty.zig @@ -40,6 +40,7 @@ pub fn run(init: std.process.Init, opts: pardes.Options) !void { var tty_buf: [0x10000]u8 = undefined; var tty = try vaxis.Tty.init(io, &tty_buf); + defer tty.deinit(); // restore cooked termios LAST, after vx flushed its resets var vx = try vaxis.init(io, gpa, init.environ_map, .{ .system_clipboard_allocator = gpa }); defer vx.deinit(gpa, tty.writer()); try vx.enterAltScreen(tty.writer()); @@ -65,14 +66,26 @@ pub fn run(init: std.process.Init, opts: pardes.Options) !void { var kitty_handles: [pardes.MAX_PANES]?vaxis.Image = @splat(null); var ptys: [pardes.MAX_PANES]?Pty = @splat(null); - defer for (&ptys) |*slot| if (slot.*) |*pt| { - _ = linux.close(pt.file.handle); - slot.* = null; - }; + var loop: Loop = .init(io, &tty, &vx); + defer { + // reap the reader tasks (cancel interrupts a blocked read) before + // closing the masters — the runtime joins those threads on exit and a + // reader stuck in read(2) would hang the process — then drain the + // queue: leftover events own gpa bytes and would dump as leaks. + for (&ptys) |*slot| if (slot.*) |*pt| { + pt.reader.cancel(io) catch {}; + _ = linux.close(pt.file.handle); + slot.* = null; + }; + while (loop.tryEvent() catch null) |ev| switch (ev) { + .pty_read => |pr| gpa.free(pr.bytes), + .paste => |b| gpa.free(@constCast(b)), + else => {}, + }; + } // Perform the initial spawns BEFORE any worker thread exists: forkpty from // a multithreaded process can wedge the child before exec. - var loop: Loop = .init(io, &tty, &vx); drainEffects(core, &ptys, io, gpa, &loop, &vx, &tty, false); try loop.start(); @@ -104,6 +117,7 @@ pub fn run(init: std.process.Init, opts: pardes.Options) !void { }, .pty_eof => |id| { if (ptys[id]) |*pt| { + pt.reader.await(io) catch {}; // reader just finished; join it or its future leaks _ = linux.close(pt.file.handle); ptys[id] = null; } @@ -301,7 +315,9 @@ fn readPty(io: std.Io, gpa: std.mem.Allocator, pty: std.Io.File, id: usize, loop break; }; } - loop.postEvent(.{ .pty_eof = id }) catch {}; + // non-blocking: a teardown cancel only unblocks one wait, so a blocking + // post into a full queue here could hang the exit + _ = loop.tryPostEvent(.{ .pty_eof = id }) catch {}; } /// The effective codepoint the way vaxis Key.matches sees it: a single-char |
