summaryrefslogtreecommitdiff
path: root/test
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-30 13:08:29 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:17 -0300
commitd51e1cd8d7344738ca04578beb8731ab72235a07 (patch)
tree107f768d96b98c0e625e7ead07f6b3d38e655152 /test
parentdc37b4ea777b7eb881a993e737457c18699c5b2c (diff)
downloadpardes-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.py42
1 files changed, 42 insertions, 0 deletions
diff --git a/test/fs.py b/test/fs.py
index 98f46900..10802e19 100644
--- a/test/fs.py
+++ b/test/fs.py
@@ -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()