Skip to content

Prevent terminal query replies from blocking Emacs - #690

Merged
emil-e merged 1 commit into
dakra:mainfrom
emil-e:fix/nonblocking-terminal-replies
Sep 15, 2026
Merged

emil-e merged 1 commit into
dakra:mainfrom
emil-e:fix/nonblocking-terminal-replies

Conversation

@emil-e

@emil-e emil-e commented Sep 15, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • make terminal-generated PTY replies abort immediately when the child input queue is full
  • drop blocked replies instead of waiting with the terminal lock held
  • retain serialization with cancellable Emacs-originated input

Fixes #689.

Verification

  • zig build --prefix .
  • zig build test
  • native-process ERT tests (5/5)
  • reproduction-style smoke test with 20,000 DSR queries

Use a zero-timeout cancellation token for terminal-generated PTY writes so full child input queues drop replies instead of freezing Emacs.
@srijan

srijan commented Sep 15, 2026

Copy link
Copy Markdown

Tested this locally. It fixes the freeze, and I found one residual issue worth a follow-up.

Setup

Built both b46898e (PR head) and f1b03e5 (its base) from source with zig 0.16.0, -Doptimize=ReleaseFast, and ran each in a throwaway emacs -Q with ghostel-module-directory pointed at the respective build and load-path at the matching lisp/, so the elisp and the module never skew. macOS 25.6, Emacs 31.1, ghostel-use-native-pty t.

Wedge detection is an emacsclient --eval probe against a server-start in the test Emacs, with timeout; exit 124 means Emacs never got back to the command loop.

Results

The original reproduction from #689 (20,000 DSR queries interleaved with output, child never reads stdin):

base f1b03e5 PR b46898e
emacsclient probe exit 124 (wedged) exit 0
threads parked in __ulock_wait 2 0

The real-world shape that originally hit this — ssh as ghostel's direct pty child, with a full-screen TUI started on the far end:

base f1b03e5 PR b46898e
probes over 60s 5/5 timed out 6/6 answered
threads parked in __ulock_wait 2 0

Under the PR the remote TUI rendered correctly and Emacs stayed responsive throughout.

Regression checks

Both pass on the PR, so the zero-timeout give-up does not fire for programs that actually read their input:

  • Plain DSR handshake, a program that queries and reads the reply: 5/5 replies received, identical on both builds.
  • Busy-but-healthy TUI: 2,000 DSR queries interleaved with 400-byte output bursts, child draining stdin promptly. 2,000/2,000 replies received on both builds, zero dropped.

zig build test passes.

One residual: the drop is not all-or-nothing

ptyWriteFromTerminal advances offset across partial writes, so when the input queue fills mid-reply it abandons a half-written escape sequence in the child's input. The old code never did this, because it blocked until the whole reply landed — which is of course the freeze being fixed here.

Repro: fill the queue from a child that is not reading, then drain it and look at what is actually sitting there.

import sys, os, re, time, tty
fd = sys.stdin.fileno()
tty.setraw(fd)
w = sys.stdout.write
for _ in range(4000):                 # stuff the queue while never reading
    w('\x1b[6n'); w('z' * 100 + '\r\n')
sys.stdout.flush()
time.sleep(3)
os.set_blocking(fd, False)
buf = b''
end = time.time() + 5
while time.time() < end:
    try: buf += os.read(fd, 65536)
    except Exception: time.sleep(0.05)
whole = re.findall(rb'\x1b\[\d+;\d+R', buf)
leftover = re.sub(rb'\x1b\[\d+;\d+R', b'', buf)
print('bytes=%d well_formed=%d non_reply_bytes=%d\nleftover=%r'
      % (len(buf), len(whole), len(leftover), leftover[:64]))

On the PR build:

bytes=1022 well_formed=146 non_reply_bytes=4
leftover=b'\x1b[34'

That trailing ESC [ 3 4 is a DSR reply cut in half at the 1024-byte queue boundary. A TUI that recovers and resumes reading will treat it as the start of a sequence and swallow the next keystrokes into it.

Suggested narrowing: give up only at offset == 0, so a terminal reply is either written whole or not written at all. Partial delivery of a reply is never useful to the child.

None of this is a reason to hold the PR — a mangled escape sequence is a far better failure than a frozen editor. Happy to test a follow-up.

@emil-e

emil-e commented Sep 15, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks for testing. On the residual, I would decline the narrowing since that reintroduces the original bug. For a pathological child, this would again make it possible for it to block forever. I will merge this as is.

@emil-e
emil-e merged commit 378320a into dakra:main Sep 15, 2026
30 checks passed
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.

Native PTY: terminal query reply blocks the reader thread with the terminal lock held, freezing Emacs

2 participants