diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-30 13:08:29 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:17 -0300 |
| commit | d51e1cd8d7344738ca04578beb8731ab72235a07 (patch) | |
| tree | 107f768d96b98c0e625e7ead07f6b3d38e655152 /test | |
| parent | dc37b4ea777b7eb881a993e737457c18699c5b2c (diff) | |
| download | pardes-d51e1cd8d7344738ca04578beb8731ab72235a07.tar.gz pardes-d51e1cd8d7344738ca04578beb8731ab72235a07.zip | |
A tty editor killed with SIGTERM or SIGHUP puts its terminal back before it dies
A nested pardes killed in a pane died with the default action. That left
the pane's terminal on the alternate screen, in raw mode, with mouse and
paste reporting on, so the shell under it was unusable until a reset. The
tty frontend now handles both signals: it writes the resets a clean exit
writes (keyboard protocol, mouse modes, focus, bracketed paste, SGR,
cursor, main screen) and restores the cooked termios, all
async-signal-safe. Then it dies of the same signal, since SA_RESETHAND put
the default back. fs.py kills a tty session with each signal and checks
for the main-screen switch and cooked mode; it fails without the handler.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'test')
| -rw-r--r-- | test/fs.py | 42 |
1 files changed, 42 insertions, 0 deletions
@@ -4,6 +4,7 @@ import fcntl import json import os import shutil +import select import signal from pathlib import Path import subprocess @@ -1329,6 +1330,47 @@ def test(binary, quic=False): assert orphan.poll() is None assert orphan.wait(timeout=5) == 1 + # Killed with SIGTERM or SIGHUP, a tty editor puts its terminal + # back (main screen, cooked mode) before it dies: a nested one + # killed in a pane leaves that pane usable. + for sig in (signal.SIGTERM, signal.SIGHUP): + master, slave = os.openpty() + fcntl.ioctl(slave, termios.TIOCSWINSZ, struct.pack('HHHH', 24, 80, 0, 0)) + cooked = termios.tcgetattr(slave) + env = {k: v for k, v in os.environ.items() if not k.startswith('PARDES_')} + env.update(HOME=str(root), XDG_RUNTIME_DIR=str(root), TERM='xterm-256color', PARDES_NOTIME='1') + killed = subprocess.Popen([binary, '--tty', '--9p=killed', 'x.txt'], cwd=root, env=env, + stdin=slave, stdout=slave, stderr=subprocess.DEVNULL, + start_new_session=True, + preexec_fn=lambda: fcntl.ioctl(0, termios.TIOCSCTTY, 0)) + seen = b'' + try: + deadline = time.monotonic() + 10 + while b'\x1b[?1049h' not in seen: + assert time.monotonic() < deadline, seen[-200:] + if select.select([master], [], [], .1)[0]: + seen += os.read(master, 65536) + time.sleep(.3) + killed.send_signal(sig) + assert killed.wait(timeout=5) == -sig + while select.select([master], [], [], .2)[0]: + try: + chunk = os.read(master, 65536) + except OSError: + break + if not chunk: + break + seen += chunk + tail = seen[seen.rindex(b'\x1b[?1049h'):] + assert b'\x1b[?1049l' in tail, tail[-200:] + assert termios.tcgetattr(slave)[3] & termios.ICANON == cooked[3] & termios.ICANON + finally: + if killed.poll() is None: + killed.kill() + killed.wait() + os.close(master) + os.close(slave) + # A launch with no terminal to draw on (a pty that is no one's # controlling terminal) says so and exits 1, no error trace. master, slave = os.openpty() |
