fix(dashboard): clamp PTY resize dimensions for WSL2 winsize garbage (#38200)
* fix(dashboard): clamp PTY resize dimensions for WSL2 winsize garbage WSL2 reports columns=131072, rows=1 from a broken winsize probe. The dashboard /chat tab forwards xterm.js dimensions through PtyBridge.resize(), which packs them as unsigned short via struct.pack. 131072 > 65535 raised struct.error — uncaught (only OSError was handled) — breaking the resize path and leaving the TUI laid out for a one-row, absurdly-wide screen, which surfaces as blank/disappearing text. Clamp cols/rows to a sane [1, 2000]x[1, 1000] range before packing. Non-finite/non-integer probes fall back to the minimum so nothing can reach struct.pack and raise. * test(dashboard): de-flake pub/events broadcast test test_pub_broadcasts_to_events_subscribers round-tripped a frame through two nested Starlette TestClient WebSocket portals within a 10s wall-clock budget. Under heavy parallel CI load a starved ASGI thread occasionally blew that budget even though the server logic is correct, producing intermittent 'broadcast not received within 10s' failures. Drive _broadcast_event directly under asyncio with fake subscribers instead. Same fan-out contract (verbatim delivery to every subscriber on the channel, nothing to other channels), zero scheduling surface. Runs in ~0.3s, deterministic across 10 consecutive runs.
This commit is contained in:
@@ -50,6 +50,33 @@ except ImportError: # pragma: no cover - dev env without ptyprocess
|
||||
__all__ = ["PtyBridge", "PtyUnavailableError"]
|
||||
|
||||
|
||||
# ``struct winsize`` packs rows/cols as unsigned short (0..65535). We clamp
|
||||
# well below that ceiling: real terminals never exceed a couple thousand
|
||||
# columns, and a value above this is a broken probe (WSL2 reports
|
||||
# columns=131072) rather than a genuine ultrawide. Lower bound is 1 — a
|
||||
# zero/negative dimension is the classic "no size yet" signal.
|
||||
_MIN_DIMENSION = 1
|
||||
_MAX_COLS = 2000
|
||||
_MAX_ROWS = 1000
|
||||
|
||||
|
||||
def _clamp_dimension(value: int, maximum: int) -> int:
|
||||
"""Clamp a reported terminal dimension into ``[_MIN_DIMENSION, maximum]``.
|
||||
|
||||
Non-integer / non-finite values fall back to ``_MIN_DIMENSION`` so a bad
|
||||
probe can never reach ``struct.pack`` and raise ``struct.error``.
|
||||
"""
|
||||
try:
|
||||
n = int(value)
|
||||
except (TypeError, ValueError, OverflowError):
|
||||
return _MIN_DIMENSION
|
||||
if n < _MIN_DIMENSION:
|
||||
return _MIN_DIMENSION
|
||||
if n > maximum:
|
||||
return maximum
|
||||
return n
|
||||
|
||||
|
||||
class PtyUnavailableError(RuntimeError):
|
||||
"""Raised when a PTY cannot be created on this platform.
|
||||
|
||||
@@ -189,11 +216,23 @@ class PtyBridge:
|
||||
view = view[n:]
|
||||
|
||||
def resize(self, cols: int, rows: int) -> None:
|
||||
"""Forward a terminal resize to the child via ``TIOCSWINSZ``."""
|
||||
"""Forward a terminal resize to the child via ``TIOCSWINSZ``.
|
||||
|
||||
Dimensions are clamped to a sane range first. Some hosts report
|
||||
garbage window sizes — the motivating case is WSL2, where xterm.js
|
||||
in the dashboard ``/chat`` tab can pick up ``columns=131072,
|
||||
rows=1`` from a broken winsize probe. ``struct winsize`` packs each
|
||||
field as an unsigned short (max 65535), so an unclamped 131072 would
|
||||
raise ``struct.error`` (not ``OSError``) and break the resize path,
|
||||
leaving the TUI laid out for a one-row / absurdly-wide screen —
|
||||
which is what shows up as blank / disappearing text.
|
||||
"""
|
||||
if self._closed:
|
||||
return
|
||||
cols = _clamp_dimension(cols, _MAX_COLS)
|
||||
rows = _clamp_dimension(rows, _MAX_ROWS)
|
||||
# struct winsize: rows, cols, xpixel, ypixel (all unsigned short)
|
||||
winsize = struct.pack("HHHH", max(1, rows), max(1, cols), 0, 0)
|
||||
winsize = struct.pack("HHHH", rows, cols, 0, 0)
|
||||
try:
|
||||
fcntl.ioctl(self._fd, termios.TIOCSWINSZ, winsize)
|
||||
except OSError:
|
||||
|
||||
Reference in New Issue
Block a user