Get rid of prewarming

Don't need it anymore since all major UI kittens are ported to Go
and so don't have startup latency.
This commit is contained in:
Kovid Goyal 2023-03-10 13:22:10 +05:30
parent 48e7ebb838
commit 34cbf5ceac
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
7 changed files with 31 additions and 888 deletions

View File

@ -120,7 +120,6 @@ from .notify import notification_activated
from .options.types import Options
from .options.utils import MINIMUM_FONT_SIZE, KeyMap, SubSequenceMap
from .os_window_size import initial_window_size_func
from .prewarm import PrewarmProcess
from .rgb import color_from_int
from .session import Session, create_sessions, get_os_window_sizing_data
from .tabs import SpecialWindow, SpecialWindowInstance, Tab, TabDict, TabManager
@ -320,7 +319,6 @@ class Boss:
args: CLIOptions,
cached_values: Dict[str, Any],
global_shortcuts: Dict[str, SingleKey],
prewarm: PrewarmProcess,
):
set_layout_options(opts)
self.clipboard = Clipboard()
@ -357,11 +355,10 @@ class Boss:
if args.listen_on and self.allow_remote_control in ('y', 'socket', 'socket-only', 'password'):
listen_fd = listen_on(args.listen_on)
self.listening_on = args.listen_on
self.prewarm = prewarm
self.child_monitor = ChildMonitor(
self.on_child_death,
DumpCommands(args) if args.dump_commands or args.dump_bytes else None,
talk_fd, listen_fd, self.prewarm.take_from_worker_fd()
talk_fd, listen_fd,
)
set_boss(self)
self.args = args
@ -2402,7 +2399,6 @@ class Boss:
for w in self.all_windows:
self.default_bg_changed_for(w.id)
w.refresh(reload_all_gpu_data=True)
self.prewarm.reload_kitty_config()
@ac('misc', '''
Reload the config file

View File

@ -57,7 +57,7 @@ typedef struct {
bool shutting_down;
pthread_t io_thread, talk_thread;
int talk_fd, listen_fd, prewarm_fd;
int talk_fd, listen_fd;
Message *messages;
size_t messages_capacity, messages_count;
LoopData io_loop_data;
@ -158,11 +158,11 @@ static PyObject *
new(PyTypeObject *type, PyObject *args, PyObject UNUSED *kwds) {
ChildMonitor *self;
PyObject *dump_callback, *death_notify;
int talk_fd = -1, listen_fd = -1, prewarm_fd = -1;
int talk_fd = -1, listen_fd = -1;
int ret;
if (the_monitor) { PyErr_SetString(PyExc_RuntimeError, "Can have only a single ChildMonitor instance"); return NULL; }
if (!PyArg_ParseTuple(args, "OO|iii", &death_notify, &dump_callback, &talk_fd, &listen_fd, &prewarm_fd)) return NULL;
if (!PyArg_ParseTuple(args, "OO|ii", &death_notify, &dump_callback, &talk_fd, &listen_fd)) return NULL;
if ((ret = pthread_mutex_init(&children_lock, NULL)) != 0) {
PyErr_Format(PyExc_RuntimeError, "Failed to create children_lock mutex: %s", strerror(ret));
return NULL;
@ -175,7 +175,6 @@ new(PyTypeObject *type, PyObject *args, PyObject UNUSED *kwds) {
if (!init_loop_data(&self->io_loop_data, KITTY_HANDLED_SIGNALS)) return PyErr_SetFromErrno(PyExc_OSError);
self->talk_fd = talk_fd;
self->listen_fd = listen_fd;
self->prewarm_fd = prewarm_fd;
if (self == NULL) return PyErr_NoMemory();
self->death_notify = death_notify; Py_INCREF(death_notify);
if (dump_callback != Py_None) {
@ -184,7 +183,6 @@ new(PyTypeObject *type, PyObject *args, PyObject UNUSED *kwds) {
} else parse_func = parse_worker;
self->count = 0;
children_fds[0].fd = self->io_loop_data.wakeup_read_fd; children_fds[1].fd = self->io_loop_data.signal_read_fd;
children_fds[2].fd = self->prewarm_fd;
children_fds[0].events = POLLIN; children_fds[1].events = POLLIN; children_fds[2].events = POLLIN;
the_monitor = self;
@ -211,7 +209,6 @@ dealloc(ChildMonitor* self) {
FREE_CHILD(add_queue[add_queue_count]);
}
free_loop_data(&self->io_loop_data);
safe_close(self->prewarm_fd, __FILE__, __LINE__); self->prewarm_fd = -1;
Py_TYPE(self)->tp_free((PyObject*)self);
}
@ -1365,34 +1362,6 @@ mark_monitored_pids(pid_t pid, int status) {
children_mutex(unlock);
}
static void
reap_prewarmed_children(ChildMonitor *self, int fd, bool enable_close_on_child_death) {
static char buf[256];
static size_t buf_pos = 0;
while(true) {
ssize_t len = read(fd, buf + buf_pos, sizeof(buf) - buf_pos);
if (len < 0) {
if (errno == EINTR) continue;
if (errno != EIO && errno != EAGAIN) log_error("Call to read() from reap_prewarmed_children() failed with error: %s", strerror(errno));
break;
}
buf_pos += len;
char *nl;
while (buf_pos > 1 && (nl = memchr(buf, '\n', buf_pos)) != NULL) {
size_t sz = nl - buf + 1;
if (enable_close_on_child_death) {
*nl = 0;
int pid = atoi(buf);
if (pid) mark_child_for_removal(self, pid);
}
memmove(buf, buf + sz, sz);
buf_pos -= sz;
}
if (len == 0) break;
}
}
static void
reap_children(ChildMonitor *self, bool enable_close_on_child_death) {
int status;
@ -1508,9 +1477,6 @@ io_loop(void *data) {
}
if (ss.child_died) reap_children(self, OPT(close_on_child_death));
}
if (children_fds[2].revents && POLLIN) {
reap_prewarmed_children(self, children_fds[2].fd, OPT(close_on_child_death));
}
for (i = 0; i < self->count; i++) {
if (children_fds[EXTRA_FDS + i].revents & (POLLIN | POLLHUP)) {
data_received = true;

View File

@ -9,7 +9,7 @@ from typing import TYPE_CHECKING, DefaultDict, Dict, Generator, List, Optional,
import kitty.fast_data_types as fast_data_types
from .constants import handled_signals, is_freebsd, is_macos, kitten_exe, kitty_base_dir, shell_path, terminfo_dir, wrapped_kitten_names
from .constants import handled_signals, is_freebsd, is_macos, kitten_exe, kitty_base_dir, shell_path, terminfo_dir
from .types import run_once
from .utils import log_error, which
@ -185,48 +185,11 @@ class ProcessDesc(TypedDict):
cmdline: Optional[Sequence[str]]
def is_prewarmable(argv: List[str]) -> Tuple[bool, List[str]]:
if len(argv) < 3 or os.path.basename(argv[0]) != 'kitty':
return False, argv
if argv[1][:1] != '+':
return False, argv
sw = ''
if argv[1] == '+':
which = argv[2]
if len(argv) > 3:
sw = argv[3]
else:
which = argv[1][1:]
if len(argv) > 2:
sw = argv[2]
if which == 'open':
return False, argv
if which == 'kitten' and sw in wrapped_kitten_names():
argv = list(argv)
argv[0] = kitten_exe()
if argv[1] == '+':
del argv[1:3]
else:
del argv[1]
return False, argv
return True, argv
@run_once
def cmdline_of_prewarmer() -> List[str]:
# we need this check in case the prewarmed process has done an exec and
# changed its cmdline
with suppress(Exception):
return cmdline_of_pid(fast_data_types.get_boss().prewarm.worker_pid)
return ['']
class Child:
child_fd: Optional[int] = None
pid: Optional[int] = None
forked = False
is_prewarmed = False
def __init__(
self,
@ -296,18 +259,16 @@ class Child:
self.forked = True
master, slave = openpty()
stdin, self.stdin = self.stdin, None
self.is_prewarmed, self.argv = is_prewarmable(self.argv)
if not self.is_prewarmed:
ready_read_fd, ready_write_fd = os.pipe()
os.set_inheritable(ready_write_fd, False)
os.set_inheritable(ready_read_fd, True)
if stdin is not None:
stdin_read_fd, stdin_write_fd = os.pipe()
os.set_inheritable(stdin_write_fd, False)
os.set_inheritable(stdin_read_fd, True)
else:
stdin_read_fd = stdin_write_fd = -1
env = tuple(f'{k}={v}' for k, v in self.final_env().items())
ready_read_fd, ready_write_fd = os.pipe()
os.set_inheritable(ready_write_fd, False)
os.set_inheritable(ready_read_fd, True)
if stdin is not None:
stdin_read_fd, stdin_write_fd = os.pipe()
os.set_inheritable(stdin_write_fd, False)
os.set_inheritable(stdin_read_fd, True)
else:
stdin_read_fd = stdin_write_fd = -1
env = tuple(f'{k}={v}' for k, v in self.final_env().items())
argv = list(self.argv)
exe = argv[0]
if is_macos and exe == shell_path:
@ -328,23 +289,17 @@ class Child:
argv[0] = (f'-{exe.split("/")[-1]}')
self.final_exe = which(exe) or exe
self.final_argv0 = argv[0]
if self.is_prewarmed:
fe = self.final_env()
self.prewarmed_child = fast_data_types.get_boss().prewarm(slave, self.argv, self.cwd, fe, stdin)
pid = self.prewarmed_child.child_process_pid
else:
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), kitten_exe())
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), kitten_exe())
os.close(slave)
self.pid = pid
self.child_fd = master
if not self.is_prewarmed:
if stdin is not None:
os.close(stdin_read_fd)
fast_data_types.thread_write(stdin_write_fd, stdin)
os.close(ready_read_fd)
self.terminal_ready_fd = ready_write_fd
if stdin is not None:
os.close(stdin_read_fd)
fast_data_types.thread_write(stdin_write_fd, stdin)
os.close(ready_read_fd)
self.terminal_ready_fd = ready_write_fd
if self.child_fd is not None:
os.set_blocking(self.child_fd, False)
return pid
@ -356,18 +311,15 @@ class Child:
self.terminal_ready_fd = -1
def mark_terminal_ready(self) -> None:
if self.is_prewarmed:
fast_data_types.get_boss().prewarm.mark_child_as_ready(self.prewarmed_child.child_id)
else:
os.close(self.terminal_ready_fd)
self.terminal_ready_fd = -1
os.close(self.terminal_ready_fd)
self.terminal_ready_fd = -1
def cmdline_of_pid(self, pid: int) -> List[str]:
try:
ans = cmdline_of_pid(pid)
except Exception:
ans = []
if pid == self.pid and (not ans or (self.is_prewarmed and ans == cmdline_of_prewarmer())):
if pid == self.pid and (not ans):
ans = list(self.argv)
return ans

View File

@ -1232,7 +1232,6 @@ class ChildMonitor:
dump_callback: Optional[Callable[[bytes], None]],
talk_fd: int = -1,
listen_fd: int = -1,
prewarm_fd: int = -1,
):
pass

View File

@ -49,7 +49,6 @@ from .fonts.render import set_font_family
from .options.types import Options
from .options.utils import DELETE_ENV_VAR
from .os_window_size import initial_window_size_func
from .prewarm import PrewarmProcess, fork_prewarm_process
from .session import create_sessions, get_os_window_sizing_data
from .types import SingleInstanceData
from .utils import (
@ -202,7 +201,7 @@ def set_x11_window_icon() -> None:
set_default_window_icon(f'{path}-128{ext}')
def _run_app(opts: Options, args: CLIOptions, prewarm: PrewarmProcess, bad_lines: Sequence[BadLine] = ()) -> None:
def _run_app(opts: Options, args: CLIOptions, bad_lines: Sequence[BadLine] = ()) -> None:
global_shortcuts: Dict[str, SingleKey] = {}
if is_macos:
from collections import defaultdict
@ -249,7 +248,7 @@ def _run_app(opts: Options, args: CLIOptions, prewarm: PrewarmProcess, bad_lines
pre_show_callback,
args.title or appname, args.name or args.cls or appname,
wincls, wstate, load_all_shaders, disallow_override_title=bool(args.title))
boss = Boss(opts, args, cached_values, global_shortcuts, prewarm)
boss = Boss(opts, args, cached_values, global_shortcuts)
boss.start(window_id, startup_sessions)
if bad_lines:
boss.show_bad_config_lines(bad_lines)
@ -266,12 +265,12 @@ class AppRunner:
self.first_window_callback = lambda window_handle: None
self.initial_window_size_func = initial_window_size_func
def __call__(self, opts: Options, args: CLIOptions, prewarm: PrewarmProcess, bad_lines: Sequence[BadLine] = ()) -> None:
def __call__(self, opts: Options, args: CLIOptions, bad_lines: Sequence[BadLine] = ()) -> None:
set_scale(opts.box_drawing_scale)
set_options(opts, is_wayland(), args.debug_rendering, args.debug_font_fallback)
try:
set_font_family(opts, debug_font_matching=args.debug_font_fallback)
_run_app(opts, args, prewarm, bad_lines)
_run_app(opts, args, bad_lines)
finally:
set_options(None)
free_font_data() # must free font data before glfw/freetype/fontconfig/opengl etc are finalized
@ -497,9 +496,6 @@ def _main() -> None:
bad_lines: List[BadLine] = []
opts = create_opts(cli_opts, accumulate_bad_lines=bad_lines)
setup_environment(opts, cli_opts)
prewarm = fork_prewarm_process(opts)
if prewarm is None:
raise SystemExit(1)
# set_locale on macOS uses cocoa APIs when LANG is not set, so we have to
# call it after the fork
@ -522,7 +518,7 @@ def _main() -> None:
try:
with setup_profiling():
# Avoid needing to launch threads to reap zombies
run_app(opts, cli_opts, prewarm, bad_lines)
run_app(opts, cli_opts, bad_lines)
finally:
glfw_terminate()
cleanup_ssh_control_masters()

View File

@ -1,621 +0,0 @@
#!/usr/bin/env python
# License: GPLv3 Copyright: 2022, Kovid Goyal <kovid at kovidgoyal.net>
import fcntl
import io
import json
import os
import select
import signal
import sys
import termios
import time
import traceback
import warnings
from contextlib import suppress
from dataclasses import dataclass
from importlib import import_module
from itertools import count
from typing import IO, TYPE_CHECKING, Any, Callable, Dict, Iterator, List, NoReturn, Optional, Tuple, TypeVar, Union, cast
from kitty.constants import kitty_exe, running_in_kitty
from kitty.entry_points import main as main_entry_point
from kitty.fast_data_types import (
CLD_EXITED,
CLD_KILLED,
CLD_STOPPED,
clearenv,
get_options,
install_signal_handlers,
read_signals,
remove_signal_handlers,
safe_pipe,
set_options,
set_use_os_log,
)
from kitty.options.types import Options
from kitty.shm import SharedMemory
from kitty.types import SignalInfo
from kitty.utils import log_error, safer_fork
if TYPE_CHECKING:
from _typeshed import ReadableBuffer, WriteableBuffer
error_events = select.POLLERR | select.POLLNVAL | select.POLLHUP
TIMEOUT = 5.0
def restore_python_signal_handlers() -> None:
remove_signal_handlers()
signal.signal(signal.SIGINT, signal.default_int_handler)
signal.signal(signal.SIGPIPE, signal.SIG_IGN)
signal.signal(signal.SIGUSR1, signal.SIG_DFL)
signal.signal(signal.SIGCHLD, signal.SIG_DFL)
def print_error(*a: Any) -> None:
log_error('Prewarm zygote:', *a)
class PrewarmProcessFailed(Exception):
pass
@dataclass
class Child:
child_id: int
child_process_pid: int
def wait_for_child_death(child_pid: int, timeout: float = 1, options: int = 0) -> Optional[int]:
st = time.monotonic()
while not timeout or time.monotonic() - st < timeout:
try:
pid, status = os.waitpid(child_pid, options | os.WNOHANG)
except ChildProcessError:
return 0
else:
if pid == child_pid:
return status
if not timeout:
break
time.sleep(0.01)
return None
class PrewarmProcess:
def __init__(
self,
prewarm_process_pid: int,
to_prewarm_stdin: int,
from_prewarm_stdout: int,
from_prewarm_death_notify: int,
) -> None:
self.children: Dict[int, Child] = {}
self.worker_pid = prewarm_process_pid
self.from_prewarm_death_notify = from_prewarm_death_notify
self.write_to_process_fd = to_prewarm_stdin
self.read_from_process_fd = from_prewarm_stdout
self.poll = select.poll()
self.poll.register(self.read_from_process_fd, select.POLLIN)
def take_from_worker_fd(self, create_file: bool = False) -> int:
if create_file:
os.set_blocking(self.from_prewarm_death_notify, True)
self.from_worker = open(self.from_prewarm_death_notify, mode='r', closefd=True)
self.from_prewarm_death_notify = -1
return -1
ans, self.from_prewarm_death_notify = self.from_prewarm_death_notify, -1
return ans
def __del__(self) -> None:
if self.write_to_process_fd > -1:
safe_close(self.write_to_process_fd)
self.write_to_process_fd = -1
if self.from_prewarm_death_notify > -1:
safe_close(self.from_prewarm_death_notify)
self.from_prewarm_death_notify = -1
if self.read_from_process_fd > -1:
safe_close(self.read_from_process_fd)
self.read_from_process_fd = -1
if hasattr(self, 'from_worker'):
self.from_worker.close()
del self.from_worker
if self.worker_pid > 0:
if wait_for_child_death(self.worker_pid) is None:
log_error('Prewarm process failed to quit gracefully, killing it')
os.kill(self.worker_pid, signal.SIGKILL)
os.waitpid(self.worker_pid, 0)
def poll_to_send(self, yes: bool = True) -> None:
if yes:
self.poll.register(self.write_to_process_fd, select.POLLOUT)
else:
self.poll.unregister(self.write_to_process_fd)
def reload_kitty_config(self, opts: Optional[Options] = None) -> None:
if opts is None:
opts = get_options()
data = json.dumps({'paths': opts.config_paths, 'overrides': opts.config_overrides})
if self.write_to_process_fd > -1:
self.send_to_prewarm_process(f'reload_kitty_config:{data}\n')
def __call__(
self,
tty_fd: int,
argv: List[str],
cwd: str = '',
env: Optional[Dict[str, str]] = None,
stdin_data: Optional[Union[str, bytes]] = None,
timeout: float = TIMEOUT,
) -> Child:
tty_name = os.ttyname(tty_fd)
if isinstance(stdin_data, str):
stdin_data = stdin_data.encode()
if env is None:
env = dict(os.environ)
cmd: Dict[str, Union[int, List[str], str, Dict[str, str]]] = {
'tty_name': tty_name, 'cwd': cwd or os.getcwd(), 'argv': argv, 'env': env,
}
total_size = 0
if stdin_data is not None:
cmd['stdin_size'] = len(stdin_data)
total_size += len(stdin_data)
data = json.dumps(cmd).encode()
total_size += len(data) + SharedMemory.num_bytes_for_size
with SharedMemory(size=total_size, unlink_on_exit=True) as shm:
shm.write_data_with_size(data)
if stdin_data:
shm.write(stdin_data)
shm.flush()
self.send_to_prewarm_process(f'fork:{shm.name}\n')
input_buf = b''
st = time.monotonic()
while time.monotonic() - st < timeout:
for (fd, event) in self.poll.poll(2):
if event & error_events:
raise PrewarmProcessFailed('Failed doing I/O with prewarm process')
if fd == self.read_from_process_fd and event & select.POLLIN:
d = os.read(self.read_from_process_fd, io.DEFAULT_BUFFER_SIZE)
input_buf += d
while (idx := input_buf.find(b'\n')) > -1:
line = input_buf[:idx].decode()
input_buf = input_buf[idx+1:]
if line.startswith('CHILD:'):
_, cid, pid = line.split(':')
child = self.add_child(int(cid), int(pid))
shm.unlink_on_exit = False
return child
if line.startswith('ERR:'):
raise PrewarmProcessFailed(line.split(':', 1)[-1])
raise PrewarmProcessFailed('Timed out waiting for I/O with prewarm process')
def add_child(self, child_id: int, pid: int) -> Child:
self.children[child_id] = c = Child(child_id, pid)
return c
def send_to_prewarm_process(self, output_buf: Union[str, bytes] = b'', timeout: float = TIMEOUT) -> None:
if isinstance(output_buf, str):
output_buf = output_buf.encode()
st = time.monotonic()
while time.monotonic() - st < timeout and output_buf:
self.poll_to_send(bool(output_buf))
for (fd, event) in self.poll.poll(2):
if event & error_events:
raise PrewarmProcessFailed(f'Failed doing I/O with prewarm process: {event}')
if fd == self.write_to_process_fd and event & select.POLLOUT:
n = os.write(self.write_to_process_fd, output_buf)
output_buf = output_buf[n:]
self.poll_to_send(False)
if output_buf:
raise PrewarmProcessFailed('Timed out waiting to write to prewarm process')
def mark_child_as_ready(self, child_id: int) -> bool:
c = self.children.pop(child_id, None)
if c is None:
return False
self.send_to_prewarm_process(f'ready:{child_id}\n')
return True
def reload_kitty_config(payload: str) -> None:
d = json.loads(payload)
from kittens.tui.utils import set_kitty_opts
set_kitty_opts(paths=d['paths'], overrides=d['overrides'])
def prewarm() -> None:
from kittens.runner import all_kitten_names
for kitten in all_kitten_names():
with suppress(Exception):
import_module(f'kittens.{kitten}.main')
class MemoryViewReadWrapperBytes(io.BufferedIOBase):
def __init__(self, mw: memoryview):
self.mw = mw
self.pos = 0
def detach(self) -> io.RawIOBase:
raise io.UnsupportedOperation('detach() not supported')
def read(self, size: Optional[int] = -1) -> bytes:
if size is None or size < 0:
size = max(0, len(self.mw) - self.pos)
oldpos = self.pos
self.pos = min(len(self.mw), self.pos + size)
if self.pos <= oldpos:
return b''
return bytes(self.mw[oldpos:self.pos])
def readinto(self, b: 'WriteableBuffer') -> int:
if not isinstance(b, memoryview):
b = memoryview(b)
b = b.cast('B')
data = self.read(len(b))
n = len(data)
b[:n] = data
return n
readinto1 = readinto
def readall(self) -> bytes:
return self.read()
def write(self, b: 'ReadableBuffer') -> int:
raise io.UnsupportedOperation('readonly stream')
def readable(self) -> bool:
return True
class MemoryViewReadWrapper(io.TextIOWrapper):
def __init__(self, mw: memoryview):
super().__init__(cast(IO[bytes], MemoryViewReadWrapperBytes(mw)), encoding='utf-8', errors='replace')
parent_tty_name = ''
is_zygote = True
def debug(*a: Any) -> None:
if parent_tty_name:
with open(parent_tty_name, 'w') as f:
print(*a, file=f)
def child_main(cmd: Dict[str, Any], ready_fd: int = -1, prewarm_type: str = 'direct') -> NoReturn:
getattr(sys, 'kitty_run_data')['prewarmed'] = prewarm_type
cwd = cmd.get('cwd')
if cwd:
with suppress(OSError):
os.chdir(cwd)
env = cmd.get('env')
if env is not None:
os.environ.clear()
# os.environ.clear() does not delete all existing env vars from the
# libc environ pointer in some circumstances, I havent figured out
# which exactly. Presumably there is something that alters the
# libc environ pointer?? The environ pointer is used by os.exec and
# therefore by subprocess and friends, so we need to ensure it is
# cleared.
clearenv()
os.environ.update(env)
argv = cmd.get('argv')
if argv:
sys.argv = list(argv)
if ready_fd > -1:
poll = select.poll()
poll.register(ready_fd, select.POLLIN)
tuple(poll.poll())
safe_close(ready_fd)
main_entry_point()
raise SystemExit(0)
def fork(shm_address: str, free_non_child_resources: Callable[[], None]) -> Tuple[int, int]:
global is_zygote
sz = pos = 0
with SharedMemory(name=shm_address, unlink_on_exit=True) as shm:
data = shm.read_data_with_size()
cmd = json.loads(data)
sz = cmd.get('stdin_size', 0)
if sz:
pos = shm.tell()
shm.unlink_on_exit = False
r, w = safe_pipe()
ready_fd_read, ready_fd_write = safe_pipe()
try:
child_pid = safer_fork()
except OSError:
safe_close(r)
safe_close(w)
safe_close(ready_fd_read)
safe_close(ready_fd_write)
if sz:
with SharedMemory(shm_address, unlink_on_exit=True):
pass
raise
if child_pid:
# master process
safe_close(w)
safe_close(ready_fd_read)
poll = select.poll()
poll.register(r, select.POLLIN)
tuple(poll.poll())
safe_close(r)
return child_pid, ready_fd_write
# child process
is_zygote = False
restore_python_signal_handlers()
safe_close(r)
safe_close(ready_fd_write)
free_non_child_resources()
os.setsid()
tty_name = cmd.get('tty_name')
if tty_name:
sys.__stdout__.flush()
sys.__stderr__.flush()
establish_controlling_tty(tty_name, sys.__stdin__.fileno(), sys.__stdout__.fileno(), sys.__stderr__.fileno())
safe_close(w)
if shm.unlink_on_exit:
child_main(cmd, ready_fd_read)
else:
with SharedMemory(shm_address, unlink_on_exit=True) as shm:
stdin_data = memoryview(shm.mmap)[pos:pos + sz]
if stdin_data:
sys.stdin = MemoryViewReadWrapper(stdin_data)
try:
child_main(cmd, ready_fd_read)
finally:
stdin_data.release()
sys.stdin = sys.__stdin__
return 0, -1 # type: ignore
Funtion = TypeVar('Funtion', bound=Callable[..., Any])
def eintr_retry(func: Funtion) -> Funtion:
def ret(*a: Any, **kw: Any) -> Any:
while True:
with suppress(InterruptedError):
return func(*a, **kw)
return cast(Funtion, ret)
safe_close = eintr_retry(os.close)
safe_open = eintr_retry(os.open)
safe_ioctl = eintr_retry(fcntl.ioctl)
safe_dup2 = eintr_retry(os.dup2)
def establish_controlling_tty(fd_or_tty_name: Union[str, int], *dups: int, closefd: bool = True) -> int:
tty_name = os.ttyname(fd_or_tty_name) if isinstance(fd_or_tty_name, int) else fd_or_tty_name
with open(safe_open(tty_name, os.O_RDWR | os.O_CLOEXEC), 'w', closefd=closefd) as f:
tty_fd = f.fileno()
safe_ioctl(tty_fd, termios.TIOCSCTTY, 0)
for fd in dups:
safe_dup2(tty_fd, fd)
return -1 if closefd else tty_fd
interactive_and_job_control_signals = (
signal.SIGINT, signal.SIGQUIT, signal.SIGTSTP, signal.SIGTTIN, signal.SIGTTOU
)
def main(stdin_fd: int, stdout_fd: int, notify_child_death_fd: int) -> None:
global parent_tty_name
with suppress(OSError):
parent_tty_name = os.ttyname(sys.stdout.fileno())
os.set_blocking(notify_child_death_fd, False)
os.set_blocking(stdin_fd, False)
os.set_blocking(stdout_fd, False)
signal_read_fd = install_signal_handlers(signal.SIGCHLD, signal.SIGUSR1)[0]
poll = select.poll()
poll.register(stdin_fd, select.POLLIN)
poll.register(signal_read_fd, select.POLLIN)
input_buf = output_buf = child_death_buf = b''
child_ready_fds: Dict[int, int] = {}
child_pid_map: Dict[int, int] = {}
child_id_counter = count()
# runpy issues a warning when running modules that have already been
# imported. Ignore it.
warnings.filterwarnings('ignore', category=RuntimeWarning, module='runpy')
prewarm()
def get_all_non_child_fds() -> Iterator[int]:
yield notify_child_death_fd
yield stdin_fd
yield stdout_fd
# the signal fds are closed by remove_signal_handlers()
yield from child_ready_fds.values()
def free_non_child_resources() -> None:
for fd in get_all_non_child_fds():
if fd > -1:
safe_close(fd)
def check_event(event: int, err_msg: str) -> None:
if event & select.POLLHUP:
raise SystemExit(0)
if event & error_events:
print_error(err_msg)
raise SystemExit(1)
def handle_input(event: int) -> None:
nonlocal input_buf, output_buf
check_event(event, 'Polling of input pipe failed')
if not (event & select.POLLIN):
return
d = os.read(stdin_fd, io.DEFAULT_BUFFER_SIZE)
if not d:
raise SystemExit(0)
input_buf += d
while (idx := input_buf.find(b'\n')) > -1:
line = input_buf[:idx].decode()
input_buf = input_buf[idx+1:]
cmd, _, payload = line.partition(':')
if cmd == 'reload_kitty_config':
reload_kitty_config(payload)
elif cmd == 'ready':
child_id = int(payload)
cfd = child_ready_fds.pop(child_id, None)
if cfd is not None:
safe_close(cfd)
elif cmd == 'quit':
raise SystemExit(0)
elif cmd == 'fork':
try:
child_pid, ready_fd_write = fork(payload, free_non_child_resources)
except Exception as e:
es = str(e).replace('\n', ' ')
output_buf += f'ERR:{es}\n'.encode()
else:
if is_zygote:
child_id = next(child_id_counter)
child_pid_map[child_pid] = child_id
child_ready_fds[child_id] = ready_fd_write
output_buf += f'CHILD:{child_id}:{child_pid}\n'.encode()
elif cmd == 'echo':
output_buf += f'{payload}\n'.encode()
def handle_output(event: int) -> None:
nonlocal output_buf
check_event(event, 'Polling of output pipe failed')
if not (event & select.POLLOUT):
return
if output_buf:
n = os.write(stdout_fd, output_buf)
if not n:
raise SystemExit(0)
output_buf = output_buf[n:]
if not output_buf:
poll.unregister(stdout_fd)
def handle_notify_child_death(event: int) -> None:
nonlocal child_death_buf
check_event(event, 'Polling of notify child death pipe failed')
if not (event & select.POLLOUT):
return
if child_death_buf:
n = os.write(notify_child_death_fd, child_death_buf)
if not n:
raise SystemExit(0)
child_death_buf = child_death_buf[n:]
if not child_death_buf:
poll.unregister(notify_child_death_fd)
def handle_child_death(dead_child_id: int, dead_child_pid: int) -> None:
nonlocal child_death_buf
xfd = child_ready_fds.pop(dead_child_id, None)
if xfd is not None:
safe_close(xfd)
child_death_buf += f'{dead_child_pid}\n'.encode()
def handle_signals(event: int) -> None:
check_event(event, 'Polling of signal pipe failed')
if not event & select.POLLIN:
return
def handle_signal(siginfo: SignalInfo) -> None:
if siginfo.si_signo != signal.SIGCHLD or siginfo.si_code not in (CLD_KILLED, CLD_EXITED, CLD_STOPPED):
return
while True:
try:
pid, status = os.waitpid(-1, os.WNOHANG | os.WUNTRACED)
except ChildProcessError:
pid = 0
if not pid:
break
child_id = child_pid_map.pop(pid, None)
if child_id is not None:
handle_child_death(child_id, pid)
read_signals(signal_read_fd, handle_signal)
keep_type_checker_happy = True
try:
while is_zygote and keep_type_checker_happy:
if output_buf:
poll.register(stdout_fd, select.POLLOUT)
if child_death_buf:
poll.register(notify_child_death_fd, select.POLLOUT)
for (q, event) in poll.poll():
if q == stdin_fd:
handle_input(event)
elif q == stdout_fd:
handle_output(event)
elif q == signal_read_fd:
handle_signals(event)
elif q == notify_child_death_fd:
handle_notify_child_death(event)
except (KeyboardInterrupt, EOFError, BrokenPipeError):
if is_zygote:
raise SystemExit(1)
raise
except Exception:
if is_zygote:
traceback.print_exc()
raise
finally:
if is_zygote:
restore_python_signal_handlers()
for fmd in child_ready_fds.values():
with suppress(OSError):
safe_close(fmd)
def exec_main(stdin_read: int, stdout_write: int, death_notify_write: int) -> None:
os.setsid()
os.set_inheritable(stdin_read, False)
os.set_inheritable(stdout_write, False)
os.set_inheritable(death_notify_write, False)
running_in_kitty(False)
for x in (sys.stdout, sys.stdin, sys.stderr):
if not x.line_buffering: # happens if the parent kitty instance has stdout not pointing to a terminal
x.reconfigure(line_buffering=True) # type: ignore
try:
main(stdin_read, stdout_write, death_notify_write)
finally:
set_options(None)
def fork_prewarm_process(opts: Options, use_exec: bool = False) -> Optional[PrewarmProcess]:
stdin_read, stdin_write = safe_pipe()
stdout_read, stdout_write = safe_pipe()
death_notify_read, death_notify_write = safe_pipe()
if use_exec:
import subprocess
tp = subprocess.Popen(
[kitty_exe(), '+runpy', f'from kitty.prewarm import exec_main; exec_main({stdin_read}, {stdout_write}, {death_notify_write})'],
pass_fds=(stdin_read, stdout_write, death_notify_write))
child_pid = tp.pid
tp.returncode = 0 # prevent a warning when the popen object is deleted with the process still running
os.set_blocking(stdout_read, True)
os.set_blocking(stdout_read, False)
else:
child_pid = safer_fork()
if child_pid:
# master
safe_close(stdin_read)
safe_close(stdout_write)
safe_close(death_notify_write)
p = PrewarmProcess(child_pid, stdin_write, stdout_read, death_notify_read)
if use_exec:
p.reload_kitty_config()
return p
# child
set_use_os_log(False)
safe_close(stdin_write)
safe_close(stdout_read)
safe_close(death_notify_read)
set_options(opts)
exec_main(stdin_read, stdout_write, death_notify_write)
raise SystemExit(0)

View File

@ -1,145 +0,0 @@
#!/usr/bin/env python
# License: GPLv3 Copyright: 2022, Kovid Goyal <kovid at kovidgoyal.net>
import json
import os
import select
import signal
import subprocess
import tempfile
import time
from kitty.constants import kitty_exe
from kitty.fast_data_types import CLD_EXITED, CLD_KILLED, CLD_STOPPED, get_options, has_sigqueue, install_signal_handlers, read_signals, sigqueue
from . import BaseTest
class Prewarm(BaseTest):
maxDiff = None
def test_prewarming(self):
from kitty.prewarm import fork_prewarm_process
cwd = tempfile.gettempdir()
env = {'TEST_ENV_PASS': 'xyz'}
cols = 317
stdin_data = 'from_stdin'
pty = self.create_pty(cols=cols)
ttyname = os.ttyname(pty.slave_fd)
opts = get_options()
opts.config_overrides = 'font_family prewarm',
os.environ['SHOULD_NOT_BE_PRESENT'] = '1'
p = fork_prewarm_process(opts, use_exec=True)
del os.environ['SHOULD_NOT_BE_PRESENT']
if p is None:
return
p.take_from_worker_fd(create_file=True)
child = p(pty.slave_fd, [kitty_exe(), '+runpy', """\
import os, json; from kitty.utils import *; from kitty.fast_data_types import get_options; print(json.dumps({
'cterm': os.ctermid(),
'ttyname': os.ttyname(sys.stdout.fileno()),
'cols': read_screen_size().cols,
'cwd': os.getcwd(),
'env': os.environ.copy(),
'pid': os.getpid(),
'font_family': get_options().font_family,
'stdin': sys.stdin.read(),
}, indent=2), "ALL_OUTPUT_PRESENT", sep="")"""], cwd=cwd, env=env, stdin_data=stdin_data, timeout=15.0)
self.assertFalse(pty.screen_contents().strip())
p.mark_child_as_ready(child.child_id)
pty.wait_till(lambda: 'ALL_OUTPUT_PRESENT' in pty.screen_contents())
data = json.JSONDecoder().raw_decode(pty.screen_contents())[0]
self.ae(data['cols'], cols)
self.assertTrue(data['cterm'])
self.ae(data['ttyname'], ttyname)
self.ae(os.path.realpath(data['cwd']), os.path.realpath(cwd))
self.ae(data['env']['TEST_ENV_PASS'], env['TEST_ENV_PASS'])
self.assertNotIn('SHOULD_NOT_BE_PRESENT', data['env'])
self.ae(data['font_family'], 'prewarm')
self.ae(int(p.from_worker.readline()), data['pid'])
def test_signal_handling(self):
from kitty.prewarm import restore_python_signal_handlers, wait_for_child_death
expecting_code = 0
expecting_signal = signal.SIGCHLD
expecting_value = 0
found_signal = False
def handle_signals(signals):
nonlocal found_signal
for siginfo in signals:
if siginfo.si_signo != expecting_signal.value:
continue
if expecting_code is not None:
self.ae(siginfo.si_code, expecting_code)
self.ae(siginfo.sival_int, expecting_value)
if expecting_code in (CLD_EXITED, CLD_KILLED):
p.wait(1)
p.stdin.close()
found_signal = True
def assert_signal():
nonlocal found_signal
found_signal = False
st = time.monotonic()
while time.monotonic() - st < 30:
for (fd, event) in poll.poll(10):
if fd == signal_read_fd:
signals = []
read_signals(signal_read_fd, signals.append)
handle_signals(signals)
if found_signal:
break
self.assertTrue(found_signal, f'Failed to get signal: {expecting_signal!r}')
def t(signal, q, expecting_sig=signal.SIGCHLD):
nonlocal expecting_code, found_signal, expecting_signal
expecting_code = q
expecting_signal = expecting_sig
if signal is not None:
p.send_signal(signal)
assert_signal()
poll = select.poll()
def run():
return subprocess.Popen([kitty_exe(), '+runpy', 'import sys; sys.stdin.read()'], stderr=subprocess.DEVNULL, stdin=subprocess.PIPE)
p = run()
orig_mask = signal.pthread_sigmask(signal.SIG_BLOCK, ())
signal_read_fd = install_signal_handlers(signal.SIGCHLD, signal.SIGUSR1)[0]
try:
poll.register(signal_read_fd, select.POLLIN)
t(signal.SIGINT, CLD_KILLED)
p = run()
p.stdin.close()
t(None, CLD_EXITED)
expecting_code = None
expecting_signal = signal.SIGUSR1
os.kill(os.getpid(), signal.SIGUSR1)
assert_signal()
expecting_value = 17 if has_sigqueue else 0
sigqueue(os.getpid(), signal.SIGUSR1.value, expecting_value)
assert_signal()
expecting_code = None
expecting_value = 0
p = run()
p.send_signal(signal.SIGSTOP)
s = wait_for_child_death(p.pid, options=os.WUNTRACED, timeout=5)
self.assertTrue(os.WIFSTOPPED(s))
t(None, CLD_STOPPED)
p.send_signal(signal.SIGCONT)
s = wait_for_child_death(p.pid, options=os.WCONTINUED, timeout=5)
self.assertTrue(os.WIFCONTINUED(s))
# macOS does not send SIGCHLD when child is continued
# https://stackoverflow.com/questions/48487935/sigchld-is-sent-on-sigcont-on-linux-but-not-on-macos
p.stdin.close()
p.wait(3)
for fd, event in poll.poll(0):
read_signals(signal_read_fd, lambda si: None)
finally:
restore_python_signal_handlers()
signal.pthread_sigmask(signal.SIG_SETMASK, orig_mask)