summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/tty/tty.zig36
-rw-r--r--test/fs.py42
2 files changed, 78 insertions, 0 deletions
diff --git a/src/tty/tty.zig b/src/tty/tty.zig
index ef7f2761..1d7ff415 100644
--- a/src/tty/tty.zig
+++ b/src/tty/tty.zig
@@ -1083,6 +1083,8 @@ pub fn run(init: std.process.Init, opts: pardes.Options, attach: ?[]const u8) !v
defer vx.exitAltScreen(tty.writer()) catch {};
try vx.setMouseMode(tty.writer(), true);
try vx.setBracketedPaste(tty.writer(), true);
+ Killed.arm(tty.fd.handle, tty.termios);
+ defer Killed.disarm();
if (attach != null) {
attach_end = attachSession(init, attach_name, &tty, &vx);
@@ -1119,6 +1121,40 @@ pub fn run(init: std.process.Init, opts: pardes.Options, attach: ?[]const u8) !v
}
}
+/// SIGTERM or SIGHUP: the terminal put back as a clean exit leaves it --
+/// the main screen, no mouse, paste or keyboard modes, the cursor shown,
+/// cooked termios -- and then the signal's own death. A nested editor
+/// killed in a pane must not leave that pane's terminal wedged.
+/// Async-signal-safe: write, tcsetattr, sigaction, raise.
+const Killed = struct {
+ var fd: posix.fd_t = -1;
+ var cooked: posix.termios = undefined;
+ const reset = "\x1b[<u\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?1006l\x1b[?1004l\x1b[?2004l\x1b[0m\x1b[?25h\x1b[?1049l";
+ const signals = [_]posix.SIG{ .TERM, .HUP };
+
+ fn arm(tty_fd: posix.fd_t, termios: posix.termios) void {
+ cooked = termios;
+ fd = tty_fd;
+ const on: posix.Sigaction = .{ .handler = .{ .handler = handle }, .mask = posix.sigemptyset(), .flags = posix.SA.RESETHAND };
+ for (signals) |sig| posix.sigaction(sig, &on, null);
+ }
+
+ fn disarm() void {
+ fd = -1;
+ const default: posix.Sigaction = .{ .handler = .{ .handler = posix.SIG.DFL }, .mask = posix.sigemptyset(), .flags = 0 };
+ for (signals) |sig| posix.sigaction(sig, &default, null);
+ }
+
+ fn handle(sig: posix.SIG) callconv(.c) void {
+ if (fd >= 0) {
+ _ = std.c.write(fd, reset, reset.len);
+ _ = std.c.tcsetattr(fd, .FLUSH, &cooked);
+ }
+ // SA_RESETHAND put the default back: the same signal, now fatal.
+ _ = std.c.raise(sig);
+ }
+};
+
fn localSession(
init: std.process.Init,
opts: pardes.Options,
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()