summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-07-07 23:06:14 -0300
committerGabriel Schneider <[email protected]>2026-08-01 15:02:07 -0300
commitacb5be9e71f0faf3781c7d57cdb0ba829ab3e5c4 (patch)
tree339c871ad72b967e4e97789d6f4f4559ed12da02
parentc4cb26fe6f061dd4bcfb9e9df7b7d4e4d434d10a (diff)
downloadpardes-acb5be9e71f0faf3781c7d57cdb0ba829ab3e5c4.tar.gz
pardes-acb5be9e71f0faf3781c7d57cdb0ba829ab3e5c4.zip
fix tty crash on outer-terminal resize (font-size change): SIGWINCH is blocked in every thread and consumed on a dedicated sigwait thread instead of vaxis's signal handler, which locked std.Io mutexes from signal context and panicked the Io.Threaded machinery; pane shells unblock it after fork so bash/vim still see resizes. winch.snap storms WINCH from a pane (crashed pre-fix) and checks a real resize still reflows.
-rw-r--r--src/tty.zig39
-rw-r--r--test/snapshots/winch.golden66
-rw-r--r--test/snapshots/winch.snap19
3 files changed, 123 insertions, 1 deletions
diff --git a/src/tty.zig b/src/tty.zig
index 7de76f4d..a2ea6462 100644
--- a/src/tty.zig
+++ b/src/tty.zig
@@ -47,6 +47,20 @@ pub fn run(init: std.process.Init, opts: pardes.Options) !void {
const io = init.io;
const gpa = init.gpa;
+ // SIGWINCH must never run vaxis's signal handler: it posts the winsize
+ // event through std.Io.Mutex/Condition, and when the signal lands on a
+ // thread blocked inside an Io.Threaded syscall region (pty readers in
+ // read(2), the main thread parked in queue.pop) a contended lock re-enters
+ // the Io machinery and Syscall.start hits `unreachable` — panic, then the
+ // panic-time terminal restore used to write through the same Io and
+ // recurse until stack overflow. Reproduced by resizing the outer terminal
+ // (e.g. a font-size change) while shells run. Block it here, before any
+ // thread exists (threads inherit the mask, so vaxis's handler never
+ // fires), and take it synchronously on the sigwait thread below instead.
+ var winch_set = posix.sigemptyset();
+ posix.sigaddset(&winch_set, posix.SIG.WINCH);
+ posix.sigprocmask(posix.SIG.BLOCK, &winch_set, null);
+
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
@@ -107,7 +121,9 @@ pub fn run(init: std.process.Init, opts: pardes.Options) !void {
try loop.start();
defer loop.stop();
- try loop.installResizeHandler();
+ // resize watcher: plain detached thread (not io.concurrent — teardown
+ // joins those, and sigwait never returns); dies with the process
+ (try std.Thread.spawn(.{}, winchWatch, .{ &loop, &vx, &tty })).detach();
vx.queryTerminal(tty.writer(), std.Io.Duration.fromMilliseconds(2)) catch {};
// now threads are fine: start a reader task per pty
@@ -354,11 +370,32 @@ fn drainEffects(
};
}
+/// Consume SIGWINCH synchronously (it is blocked in every thread) and post
+/// the new size as a winsize event from normal thread context — the one place
+/// vaxis's Io-backed queue is safe to touch on a resize.
+fn winchWatch(loop: *Loop, vx: *vaxis.Vaxis, tty: *vaxis.Tty) void {
+ var set = posix.sigemptyset();
+ posix.sigaddset(&set, posix.SIG.WINCH);
+ while (true) {
+ var sig: c_int = 0;
+ if (libc.sigwait(&set, &sig) != 0) continue;
+ if (vx.state.in_band_resize) continue; // terminal reports via CSI 48
+ const ws = tty.getWinsize() catch continue;
+ loop.postEvent(.{ .winsize = ws }) catch {};
+ }
+}
+
fn forkShell(cwd: ?[*:0]const u8, rows: u16, cols: u16) struct { file: std.Io.File, pid: posix.pid_t } {
var master: c_int = undefined;
const ws = posix.winsize{ .row = rows, .col = cols, .xpixel = 0, .ypixel = 0 };
const pid = forkpty(&master, null, null, &ws);
if (pid == 0) {
+ // the blocked-SIGWINCH mask survives fork AND exec — unblock it or
+ // bash/vim in the pane would never see resizes (sigprocmask is
+ // async-signal-safe)
+ var set = posix.sigemptyset();
+ posix.sigaddset(&set, posix.SIG.WINCH);
+ posix.sigprocmask(posix.SIG.UNBLOCK, &set, null);
if (cwd) |c| _ = chdir(c);
const argv: [4:null]?[*:0]const u8 = .{ bash_path, "--rcfile", "/tmp/pardes-osc133.bash", null };
_ = execv(bash_path, &argv);
diff --git a/test/snapshots/winch.golden b/test/snapshots/winch.golden
new file mode 100644
index 00000000..f0b1277b
--- /dev/null
+++ b/test/snapshots/winch.golden
@@ -0,0 +1,66 @@
+== snap storm grid=100x30 cursor=4,6
+|Kill Newcol Tutor Debug Colors NextColor Dump
+| TTY /tmp/pardes-snap/winch/cwd Del NOR /tmp/pardes-snap/winch/cwd Del
+| $ ls
+| $ for i in $(seq 600); do kill -WINCH $PPID; don
+| e; echo SUR''VIVED
+| SURVIVED
+| $
+|
+|
+|
+|
+|
+|
+|
+|
+|
+| NOR /tmp/pardes-snap/winch/cwd Del
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+== snap resized grid=106x34 cursor=4,6
+|Kill Newcol Tutor Debug Colors NextColor Dump
+| TTY /tmp/pardes-snap/winch/cwd Del NOR /tmp/pardes-snap/winch/cwd Del
+| $ ls
+| $ for i in $(seq 600); do kill -WINCH $PPID; done;
+| echo SUR''VIVED
+| SURVIVED
+| $
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+| NOR /tmp/pardes-snap/winch/cwd Del
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
diff --git a/test/snapshots/winch.snap b/test/snapshots/winch.snap
new file mode 100644
index 00000000..b1376ab3
--- /dev/null
+++ b/test/snapshots/winch.snap
@@ -0,0 +1,19 @@
+# SIGWINCH crash (reported as "crash on font-size change"): resizing the outer
+# terminal delivers SIGWINCH, and vaxis's default handler locks std.Io mutexes
+# from signal context — landing on a thread inside an Io.Threaded syscall
+# region (pty readers in read(2), main parked in queue.pop) panicked the Io
+# machinery and killed pardes. Now every thread blocks SIGWINCH and a sigwait
+# watcher consumes it. The in-pane kill storm below crashed pardes before the
+# fix; the resize after proves the watcher still reflows the grid.
+start 30 100 -n 3
+stable 700 20000
+key c-b
+stable 600 8000
+text for i in $(seq 600); do kill -WINCH $PPID; done; echo SUR''VIVED
+key enter
+wait 15000 SURVIVED
+stable 700 15000
+snap storm
+resize 34 106
+stable 700 15000
+snap resized