Skip to content

Local PTY: bound write back-pressure (deadlock fix) and coalesce read callbacks#3

Merged
arach merged 1 commit into
arach:mainfrom
robgough:fix/pty-backpressure-and-read-coalescing
Jul 12, 2026
Merged

Local PTY: bound write back-pressure (deadlock fix) and coalesce read callbacks#3
arach merged 1 commit into
arach:mainfrom
robgough:fix/pty-backpressure-and-read-coalescing

Conversation

@robgough

@robgough robgough commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Two TerminiLocalPTYProcess fixes from running Termini in production in Belfry, a tmux front-end (the contributions mentioned in #2).

Bounded write back-pressure (deadlock fix). send() retries EAGAIN/EWOULDBLOCK with usleep(5ms) in an unbounded loop. If the PTY's reader never drains — we hit this with a stalled SSH link running under the PTY — the loop runs forever and wedges the serial queue, which then deadlocks any queue.sync caller, most notably terminate(). In our case that hung app shutdown hard enough that users force-killed the app. The stall is now capped at ~1s, after which the remaining bytes are dropped.

Coalesced read callbacks (perf). drainOutput() emitted one onOutput callback — one main-thread hop plus one terminal feed — per 4 KB read(). A fast producer (build logs, cat of a big file) caused hundreds of main-thread wakeups per second. All bytes readable in a drain pass are now batched into a single callback, bounded at 128 KB per emission so a sustained firehose still yields downstream periodically.

Both changes are behavior-preserving for well-behaved peers; swift build clean on macOS.

🤖 Generated with Claude Code

https://claude.ai/code/session_01RcSW6KsDkBruL1dPD63rhX

Two robustness/efficiency fixes to TerminiLocalPTYProcess:

send() retried EAGAIN/EWOULDBLOCK with usleep(5ms) in an unbounded
loop. If the reader never drains (e.g. a stalled SSH link on the other
side of the PTY), that loop runs forever and wedges the serial queue —
which then deadlocks any queue.sync caller, notably terminate(),
hanging the host app's shutdown. The stall is now capped at ~1s, after
which the remaining bytes are dropped.

drainOutput() emitted one onOutput callback (one main-thread hop + one
terminal feed) per 4 KB read. All bytes readable in a drain pass are
now batched into a single callback, bounded at 128 KB per emission, so
output floods cost a handful of wakeups instead of hundreds per second.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RcSW6KsDkBruL1dPD63rhX
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants