diff --git a/kitty/boss.py b/kitty/boss.py index f6aaf6c6b..cd8e155c8 100644 --- a/kitty/boss.py +++ b/kitty/boss.py @@ -22,8 +22,9 @@ from .cli_stub import CLIOptions from .conf.utils import BadLine, KeyAction, to_cmdline from .config import common_opts_as_dict, prepare_config_file_for_editing from .constants import ( - appname, cache_dir, config_dir, is_macos, is_wayland, kitty_exe, - logo_png_file, supports_primary_selection, website_url + appname, cache_dir, clear_handled_signals, config_dir, handled_signals, + is_macos, is_wayland, kitty_exe, logo_png_file, supports_primary_selection, + website_url ) from .fast_data_types import ( CLOSE_BEING_CONFIRMED, GLFW_MOD_ALT, GLFW_MOD_CONTROL, GLFW_MOD_SHIFT, @@ -733,6 +734,8 @@ class Boss: if not getattr(self, 'io_thread_started', False): self.child_monitor.start() self.io_thread_started = True + for signum in self.child_monitor.handled_signals(): + handled_signals.add(signum) urls: List[str] = getattr(sys, 'cmdline_args_for_open', []) if urls: delattr(sys, 'cmdline_args_for_open') @@ -1737,7 +1740,7 @@ class Boss: if stdin: r, w = safe_pipe(False) try: - subprocess.Popen(cmd, env=env, stdin=r, cwd=cwd) + subprocess.Popen(cmd, env=env, stdin=r, cwd=cwd, preexec_fn=clear_handled_signals) except Exception: os.close(w) else: @@ -1745,7 +1748,7 @@ class Boss: finally: os.close(r) else: - subprocess.Popen(cmd, env=env, cwd=cwd) + subprocess.Popen(cmd, env=env, cwd=cwd, preexec_fn=clear_handled_signals) def pipe(self, source: str, dest: str, exe: str, *args: str) -> Optional[Window]: cmd = [exe] + list(args) @@ -1965,8 +1968,8 @@ class Boss: map f5 load_config_file /path/to/some/kitty.conf ''') def load_config_file(self, *paths: str, apply_overrides: bool = True) -> None: - from .config import load_config from .cli import default_config_paths + from .config import load_config old_opts = get_options() prev_paths = old_opts.config_paths or default_config_paths(self.args.config) paths = paths or prev_paths diff --git a/kitty/child-monitor.c b/kitty/child-monitor.c index 56bf1c3b2..d9faef67e 100644 --- a/kitty/child-monitor.c +++ b/kitty/child-monitor.c @@ -177,6 +177,17 @@ dealloc(ChildMonitor* self) { Py_TYPE(self)->tp_free((PyObject*)self); } +static PyObject* +handled_signals(ChildMonitor *self, PyObject *args UNUSED) { + PyObject *ans = PyTuple_New(self->io_loop_data.num_handled_signals); + if (ans) { + for (Py_ssize_t i = 0; i < PyTuple_GET_SIZE(ans); i++) { + PyTuple_SET_ITEM(ans, i, PyLong_FromLong((long)self->io_loop_data.handled_signals[i])); + } + } + return ans; +} + static void wakeup_io_loop(ChildMonitor *self, bool in_signal_handler) { wakeup_loop(&self->io_loop_data, in_signal_handler, "io_loop"); @@ -1706,6 +1717,7 @@ static PyMethodDef methods[] = { METHOD(main_loop, METH_NOARGS) METHOD(mark_for_close, METH_VARARGS) METHOD(resize_pty, METH_VARARGS) + METHODB(handled_signals, METH_NOARGS), {"set_iutf8_winid", (PyCFunction)pyset_iutf8, METH_VARARGS, ""}, {NULL} /* Sentinel */ }; diff --git a/kitty/child.c b/kitty/child.c index 7d1c6e4a1..d1b760216 100644 --- a/kitty/child.c +++ b/kitty/child.c @@ -79,14 +79,16 @@ wait_for_terminal_ready(int fd) { static PyObject* spawn(PyObject *self UNUSED, PyObject *args) { - PyObject *argv_p, *env_p; + PyObject *argv_p, *env_p, *handled_signals_p; int master, slave, stdin_read_fd, stdin_write_fd, ready_read_fd, ready_write_fd; char *cwd, *exe; - if (!PyArg_ParseTuple(args, "ssO!O!iiiiii", &exe, &cwd, &PyTuple_Type, &argv_p, &PyTuple_Type, &env_p, &master, &slave, &stdin_read_fd, &stdin_write_fd, &ready_read_fd, &ready_write_fd)) return NULL; + if (!PyArg_ParseTuple(args, "ssO!O!iiiiiiO!", &exe, &cwd, &PyTuple_Type, &argv_p, &PyTuple_Type, &env_p, &master, &slave, &stdin_read_fd, &stdin_write_fd, &ready_read_fd, &ready_write_fd, &PyTuple_Type, &handled_signals_p)) return NULL; char name[2048] = {0}; if (ttyname_r(slave, name, sizeof(name) - 1) != 0) { PyErr_SetFromErrno(PyExc_OSError); return NULL; } char **argv = serialize_string_tuple(argv_p); char **env = serialize_string_tuple(env_p); + int handled_signals[16] = {0}, num_handled_signals = MIN((int)arraysz(handled_signals), PyTuple_GET_SIZE(handled_signals_p)); + for (Py_ssize_t i = 0; i < num_handled_signals; i++) handled_signals[i] = PyLong_AsLong(PyTuple_GET_ITEM(handled_signals_p, i)); #if PY_VERSION_HEX >= 0x03070000 PyOS_BeforeFork(); @@ -99,10 +101,10 @@ spawn(PyObject *self UNUSED, PyObject *args) { PyOS_AfterFork_Child(); #endif // See _Py_RestoreSignals in signalmodule.c for a list of signals python nukes - sigset_t signals = {0}; - struct sigaction act = {.sa_handler=SIG_DFL}; -#define SA(which) { if (sigaction(which, &act, NULL) != 0) exit_on_err("sigaction() in child process failed"); } - SA(SIGINT); SA(SIGTERM); SA(SIGCHLD); SA(SIGPIPE); SA(SIGUSR1); SA(SIGUSR2); + const struct sigaction act = {.sa_handler=SIG_DFL}; + +#define SA(which) if (sigaction(which, &act, NULL) != 0) exit_on_err("sigaction() in child process failed"); + for (int si = 0; si < num_handled_signals; si++) { SA(handled_signals[si]); } #ifdef SIGXFSZ SA(SIGXFSZ); #endif @@ -110,6 +112,7 @@ spawn(PyObject *self UNUSED, PyObject *args) { SA(SIGXFZ); #endif #undef SA + sigset_t signals; sigemptyset(&signals); if (sigprocmask(SIG_SETMASK, &signals, NULL) != 0) exit_on_err("sigprocmask() in child process failed"); // Use only signal-safe functions (man 7 signal-safety) if (chdir(cwd) != 0) { if (chdir("/") != 0) {} }; // ignore failure to chdir to / diff --git a/kitty/child.py b/kitty/child.py index 143e79c41..0596fca16 100644 --- a/kitty/child.py +++ b/kitty/child.py @@ -13,7 +13,8 @@ from typing import ( import kitty.fast_data_types as fast_data_types from .constants import ( - is_freebsd, is_macos, kitty_base_dir, shell_path, terminfo_dir + handled_signals, is_freebsd, is_macos, kitty_base_dir, shell_path, + terminfo_dir ) from .types import run_once from .utils import log_error, which @@ -289,7 +290,9 @@ class Child: argv[0] = (f'-{exe.split("/")[-1]}') self.final_exe = which(exe) or exe self.final_argv0 = argv[0] - pid = fast_data_types.spawn(self.final_exe, self.cwd, tuple(argv), env, master, slave, stdin_read_fd, stdin_write_fd, ready_read_fd, ready_write_fd) + pid = fast_data_types.spawn( + self.final_exe, self.cwd, tuple(argv), env, master, slave, + stdin_read_fd, stdin_write_fd, ready_read_fd, ready_write_fd, tuple(handled_signals)) os.close(slave) self.pid = pid self.child_fd = master diff --git a/kitty/cli.py b/kitty/cli.py index 7db6d9176..825e09b12 100644 --- a/kitty/cli.py +++ b/kitty/cli.py @@ -11,7 +11,9 @@ from typing import ( from .cli_stub import CLIOptions from .conf.utils import resolve_config -from .constants import appname, defconf, is_macos, str_version, website_url +from .constants import ( + appname, clear_handled_signals, defconf, is_macos, str_version, website_url +) from .options.types import Options as KittyOpts from .typing import BadLineType, TypedDict @@ -362,7 +364,7 @@ class PrintHelpForSeq: text = '\n'.join(blocks) + '\n\n' + version() if print_help_for_seq.allow_pager and sys.stdout.isatty(): import subprocess - p = subprocess.Popen(['less', '-isRXF'], stdin=subprocess.PIPE) + p = subprocess.Popen(['less', '-isRXF'], stdin=subprocess.PIPE, preexec_fn=clear_handled_signals) try: p.communicate(text.encode('utf-8')) except KeyboardInterrupt: diff --git a/kitty/constants.py b/kitty/constants.py index fd00b753d..268b4e999 100644 --- a/kitty/constants.py +++ b/kitty/constants.py @@ -6,7 +6,7 @@ import os import pwd import sys from contextlib import suppress -from typing import TYPE_CHECKING, Iterable, NamedTuple, Optional, Set +from typing import TYPE_CHECKING, Any, Iterable, NamedTuple, Optional, Set from .types import run_once @@ -235,3 +235,16 @@ def website_url(doc_name: str = '') -> str: if doc_name: doc_name += '/' return f'https://sw.kovidgoyal.net/kitty/{doc_name}' + + +handled_signals: Set[int] = set() + + +def clear_handled_signals(*a: Any) -> None: + if not handled_signals: + return + import signal + if hasattr(signal, 'pthread_sigmask'): + signal.pthread_sigmask(signal.SIG_UNBLOCK, handled_signals) + for s in handled_signals: + signal.signal(s, signal.SIG_DFL) diff --git a/kitty/fast_data_types.pyi b/kitty/fast_data_types.pyi index ef7c9e126..6418a44fe 100644 --- a/kitty/fast_data_types.pyi +++ b/kitty/fast_data_types.pyi @@ -1200,6 +1200,9 @@ class ChildMonitor: def wakeup(self) -> None: pass + def handled_signals(self) -> Tuple[int, ...]: + pass + def main_loop(self) -> None: pass @@ -1279,7 +1282,8 @@ def spawn( stdin_read_fd: int, stdin_write_fd: int, ready_read_fd: int, - ready_write_fd: int + ready_write_fd: int, + handled_signals: Tuple[int, ...], ) -> int: pass diff --git a/kitty/main.py b/kitty/main.py index 3562edfdc..73e006a71 100644 --- a/kitty/main.py +++ b/kitty/main.py @@ -16,8 +16,9 @@ from .cli_stub import CLIOptions from .conf.utils import BadLine from .config import cached_values_for from .constants import ( - appname, beam_cursor_data_file, config_dir, glfw_path, is_macos, - is_wayland, kitty_exe, logo_png_file, running_in_kitty, website_url + appname, beam_cursor_data_file, clear_handled_signals, config_dir, + glfw_path, is_macos, is_wayland, kitty_exe, logo_png_file, + running_in_kitty, website_url ) from .fast_data_types import ( GLFW_IBEAM_CURSOR, GLFW_MOD_ALT, GLFW_MOD_SHIFT, create_os_window, @@ -249,7 +250,7 @@ def setup_profiling() -> Generator[None, None, None]: with open(cg, 'wb') as f: subprocess.call(['pprof', '--callgrind', exe, '/tmp/kitty-profile.log'], stdout=f) try: - subprocess.Popen(['kcachegrind', cg]) + subprocess.Popen(['kcachegrind', cg], preexec_fn=clear_handled_signals) except FileNotFoundError: subprocess.call(['pprof', '--text', exe, '/tmp/kitty-profile.log']) print('To view the graphical call data, use: kcachegrind', cg) diff --git a/kitty/update_check.py b/kitty/update_check.py index e61d78c54..da5bfcbe6 100644 --- a/kitty/update_check.py +++ b/kitty/update_check.py @@ -9,7 +9,9 @@ from typing import Dict, NamedTuple, Optional, Tuple from urllib.request import urlopen from .config import atomic_save -from .constants import Version, cache_dir, kitty_exe, version, website_url +from .constants import ( + Version, cache_dir, clear_handled_signals, kitty_exe, version, website_url +) from .fast_data_types import add_timer, get_boss, monitor_pid from .notify import notify from .utils import log_error, open_url @@ -115,7 +117,7 @@ def update_check() -> bool: p = subprocess.Popen([ kitty_exe(), '+runpy', 'from kitty.update_check import run_worker; run_worker()' - ], stdout=subprocess.PIPE) + ], stdout=subprocess.PIPE, preexec_fn=clear_handled_signals) except OSError as e: log_error(f'Failed to run kitty for update check, with error: {e}') return False diff --git a/kitty/utils.py b/kitty/utils.py index 3a6ebc3df..2cbb34568 100644 --- a/kitty/utils.py +++ b/kitty/utils.py @@ -18,8 +18,8 @@ from typing import ( ) from .constants import ( - appname, config_dir, is_macos, is_wayland, read_kitty_resource, - runtime_dir, shell_path, ssh_control_master_template, + appname, clear_handled_signals, config_dir, is_macos, is_wayland, + read_kitty_resource, runtime_dir, shell_path, ssh_control_master_template, supports_primary_selection ) from .fast_data_types import Color, open_tty @@ -277,7 +277,9 @@ def open_cmd(cmd: Union[Iterable[str], List[str]], arg: Union[None, Iterable[str cmd.append(arg) else: cmd.extend(arg) - return subprocess.Popen(tuple(cmd), stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, cwd=cwd or None) + return subprocess.Popen( + tuple(cmd), stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, cwd=cwd or None, + preexec_fn=clear_handled_signals) def open_url(url: str, program: Union[str, List[str]] = 'default', cwd: Optional[str] = None) -> 'PopenType[bytes]': @@ -797,7 +799,9 @@ def read_shell_environment(opts: Optional[Options] = None) -> Dict[str, str]: if '-i' not in shell and '--interactive' not in shell: shell += ['-i'] try: - p = subprocess.Popen(shell + ['-c', 'env'], stdout=slave, stdin=slave, stderr=slave, start_new_session=True, close_fds=True) + p = subprocess.Popen( + shell + ['-c', 'env'], stdout=slave, stdin=slave, stderr=slave, start_new_session=True, close_fds=True, + preexec_fn=clear_handled_signals) except FileNotFoundError: log_error('Could not find shell to read environment') return ans @@ -970,8 +974,8 @@ def cleanup_ssh_control_masters() -> None: except OSError: return workers = tuple(subprocess.Popen([ - 'ssh', '-o', f'ControlPath={x}', '-O', 'exit', 'kitty-unused-host-name'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - for x in files) + 'ssh', '-o', f'ControlPath={x}', '-O', 'exit', 'kitty-unused-host-name'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, + preexec_fn=clear_handled_signals) for x in files) for w in workers: w.wait() for x in files: diff --git a/kitty/window.py b/kitty/window.py index dc21181dc..228fd0e28 100644 --- a/kitty/window.py +++ b/kitty/window.py @@ -21,7 +21,9 @@ from typing import ( from .child import ProcessDesc from .cli_stub import CLIOptions from .config import build_ansi_color_table -from .constants import appname, config_dir, is_macos, wakeup +from .constants import ( + appname, clear_handled_signals, config_dir, is_macos, wakeup +) from .fast_data_types import ( BGIMAGE_PROGRAM, BLIT_PROGRAM, CELL_BG_PROGRAM, CELL_FG_PROGRAM, CELL_PROGRAM, CELL_SPECIAL_PROGRAM, CURSOR_BEAM, CURSOR_BLOCK, @@ -831,9 +833,10 @@ class Window: set_clipboard_string(url) def handle_remote_file(self, netloc: str, remote_path: str) -> None: - from .utils import SSHConnectionData - from kittens.ssh.main import get_connection_data from kittens.remote_file.main import is_ssh_kitten_sentinel + from kittens.ssh.main import get_connection_data + + from .utils import SSHConnectionData args = self.ssh_kitten_cmdline() conn_data: Union[None, List[str], SSHConnectionData] = None if args: @@ -903,7 +906,7 @@ class Window: import subprocess env = self.child.foreground_environ env['KITTY_CHILD_CMDLINE'] = ' '.join(map(shlex.quote, self.child.cmdline)) - subprocess.Popen(cb, env=env, cwd=self.child.foreground_cwd) + subprocess.Popen(cb, env=env, cwd=self.child.foreground_cwd, preexec_fn=clear_handled_signals) if not self.is_active: changed = not self.needs_attention self.needs_attention = True