Clear signal handlers when running processes

This commit is contained in:
Kovid Goyal 2022-06-08 17:50:42 +05:30
parent 18dd13c872
commit 47d482dca9
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
11 changed files with 82 additions and 32 deletions

View File

@ -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

View File

@ -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 */
};

View File

@ -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 /

View File

@ -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

View File

@ -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:

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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:

View File

@ -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