Use a fork() without exec() to create prewarm process
This has the advantages: 1) Even first kitten use is fast 2) Computer has to do less work overall since prewarm process is itself prewarmed
This commit is contained in:
parent
e1ad75e932
commit
da6faa656c
@ -229,7 +229,8 @@ class Boss:
|
|||||||
opts: Options,
|
opts: Options,
|
||||||
args: CLIOptions,
|
args: CLIOptions,
|
||||||
cached_values: Dict[str, Any],
|
cached_values: Dict[str, Any],
|
||||||
global_shortcuts: Dict[str, SingleKey]
|
global_shortcuts: Dict[str, SingleKey],
|
||||||
|
prewarm: PrewarmProcess,
|
||||||
):
|
):
|
||||||
set_layout_options(opts)
|
set_layout_options(opts)
|
||||||
self.update_check_started = False
|
self.update_check_started = False
|
||||||
@ -254,7 +255,7 @@ class Boss:
|
|||||||
self.allow_remote_control = opts.allow_remote_control
|
self.allow_remote_control = opts.allow_remote_control
|
||||||
if args.listen_on and (self.allow_remote_control in ('y', 'socket-only')):
|
if args.listen_on and (self.allow_remote_control in ('y', 'socket-only')):
|
||||||
listen_fd = listen_on(args.listen_on)
|
listen_fd = listen_on(args.listen_on)
|
||||||
self.prewarm = PrewarmProcess()
|
self.prewarm = prewarm
|
||||||
self.child_monitor = ChildMonitor(
|
self.child_monitor = ChildMonitor(
|
||||||
self.on_child_death,
|
self.on_child_death,
|
||||||
DumpCommands(args) if args.dump_commands or args.dump_bytes else None,
|
DumpCommands(args) if args.dump_commands or args.dump_bytes else None,
|
||||||
|
|||||||
@ -199,6 +199,15 @@ def is_prewarmable(argv: Sequence[str]) -> bool:
|
|||||||
return argv[1] != '+open'
|
return argv[1] != '+open'
|
||||||
|
|
||||||
|
|
||||||
|
@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:
|
class Child:
|
||||||
|
|
||||||
child_fd: Optional[int] = None
|
child_fd: Optional[int] = None
|
||||||
@ -339,7 +348,7 @@ class Child:
|
|||||||
ans = cmdline_of_pid(pid)
|
ans = cmdline_of_pid(pid)
|
||||||
except Exception:
|
except Exception:
|
||||||
ans = []
|
ans = []
|
||||||
if pid == self.pid and (not ans or (self.is_prewarmed and fast_data_types.get_boss().prewarm.is_prewarmed_argv(ans))):
|
if pid == self.pid and (not ans or (self.is_prewarmed and ans == cmdline_of_prewarmer())):
|
||||||
ans = list(self.argv)
|
ans = list(self.argv)
|
||||||
return ans
|
return ans
|
||||||
|
|
||||||
|
|||||||
@ -30,6 +30,7 @@ from .fonts.render import set_font_family
|
|||||||
from .options.types import Options
|
from .options.types import Options
|
||||||
from .options.utils import DELETE_ENV_VAR
|
from .options.utils import DELETE_ENV_VAR
|
||||||
from .os_window_size import initial_window_size_func
|
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 .session import create_sessions, get_os_window_sizing_data
|
||||||
from .types import SingleKey
|
from .types import SingleKey
|
||||||
from .utils import (
|
from .utils import (
|
||||||
@ -141,7 +142,7 @@ def set_x11_window_icon() -> None:
|
|||||||
set_default_window_icon(f'{path}-128{ext}')
|
set_default_window_icon(f'{path}-128{ext}')
|
||||||
|
|
||||||
|
|
||||||
def _run_app(opts: Options, args: CLIOptions, bad_lines: Sequence[BadLine] = ()) -> None:
|
def _run_app(opts: Options, args: CLIOptions, prewarm: PrewarmProcess, bad_lines: Sequence[BadLine] = ()) -> None:
|
||||||
global_shortcuts: Dict[str, SingleKey] = {}
|
global_shortcuts: Dict[str, SingleKey] = {}
|
||||||
if is_macos:
|
if is_macos:
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
@ -180,7 +181,7 @@ def _run_app(opts: Options, args: CLIOptions, bad_lines: Sequence[BadLine] = ())
|
|||||||
pre_show_callback,
|
pre_show_callback,
|
||||||
args.title or appname, args.name or args.cls or appname,
|
args.title or appname, args.name or args.cls or appname,
|
||||||
wincls, load_all_shaders, disallow_override_title=bool(args.title))
|
wincls, load_all_shaders, disallow_override_title=bool(args.title))
|
||||||
boss = Boss(opts, args, cached_values, global_shortcuts)
|
boss = Boss(opts, args, cached_values, global_shortcuts, prewarm)
|
||||||
boss.start(window_id, startup_sessions)
|
boss.start(window_id, startup_sessions)
|
||||||
if bad_lines:
|
if bad_lines:
|
||||||
boss.show_bad_config_lines(bad_lines)
|
boss.show_bad_config_lines(bad_lines)
|
||||||
@ -197,12 +198,12 @@ class AppRunner:
|
|||||||
self.first_window_callback = lambda window_handle: None
|
self.first_window_callback = lambda window_handle: None
|
||||||
self.initial_window_size_func = initial_window_size_func
|
self.initial_window_size_func = initial_window_size_func
|
||||||
|
|
||||||
def __call__(self, opts: Options, args: CLIOptions, bad_lines: Sequence[BadLine] = ()) -> None:
|
def __call__(self, opts: Options, args: CLIOptions, prewarm: PrewarmProcess, bad_lines: Sequence[BadLine] = ()) -> None:
|
||||||
set_scale(opts.box_drawing_scale)
|
set_scale(opts.box_drawing_scale)
|
||||||
set_options(opts, is_wayland(), args.debug_rendering, args.debug_font_fallback)
|
set_options(opts, is_wayland(), args.debug_rendering, args.debug_font_fallback)
|
||||||
try:
|
try:
|
||||||
set_font_family(opts, debug_font_matching=args.debug_font_fallback)
|
set_font_family(opts, debug_font_matching=args.debug_font_fallback)
|
||||||
_run_app(opts, args, bad_lines)
|
_run_app(opts, args, prewarm, bad_lines)
|
||||||
finally:
|
finally:
|
||||||
set_options(None)
|
set_options(None)
|
||||||
free_font_data() # must free font data before glfw/freetype/fontconfig/opengl etc are finalized
|
free_font_data() # must free font data before glfw/freetype/fontconfig/opengl etc are finalized
|
||||||
@ -404,8 +405,11 @@ def _main() -> None:
|
|||||||
return
|
return
|
||||||
bad_lines: List[BadLine] = []
|
bad_lines: List[BadLine] = []
|
||||||
opts = create_opts(cli_opts, accumulate_bad_lines=bad_lines)
|
opts = create_opts(cli_opts, accumulate_bad_lines=bad_lines)
|
||||||
init_glfw(opts, cli_opts.debug_keyboard, cli_opts.debug_rendering)
|
|
||||||
setup_environment(opts, cli_opts)
|
setup_environment(opts, cli_opts)
|
||||||
|
prewarm = fork_prewarm_process(opts)
|
||||||
|
if prewarm is None:
|
||||||
|
raise SystemExit(1)
|
||||||
|
init_glfw(opts, cli_opts.debug_keyboard, cli_opts.debug_rendering)
|
||||||
if cli_opts.watcher:
|
if cli_opts.watcher:
|
||||||
from .window import global_watchers
|
from .window import global_watchers
|
||||||
global_watchers.set_extra(cli_opts.watcher)
|
global_watchers.set_extra(cli_opts.watcher)
|
||||||
@ -413,7 +417,7 @@ def _main() -> None:
|
|||||||
try:
|
try:
|
||||||
with setup_profiling():
|
with setup_profiling():
|
||||||
# Avoid needing to launch threads to reap zombies
|
# Avoid needing to launch threads to reap zombies
|
||||||
run_app(opts, cli_opts, bad_lines)
|
run_app(opts, cli_opts, prewarm, bad_lines)
|
||||||
finally:
|
finally:
|
||||||
glfw_terminate()
|
glfw_terminate()
|
||||||
cleanup_ssh_control_masters()
|
cleanup_ssh_control_masters()
|
||||||
|
|||||||
206
kitty/prewarm.py
206
kitty/prewarm.py
@ -5,6 +5,7 @@ import io
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import select
|
import select
|
||||||
|
import signal
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
import warnings
|
import warnings
|
||||||
@ -13,16 +14,17 @@ from dataclasses import dataclass
|
|||||||
from importlib import import_module
|
from importlib import import_module
|
||||||
from itertools import count
|
from itertools import count
|
||||||
from typing import (
|
from typing import (
|
||||||
IO, TYPE_CHECKING, Any, Dict, List, NoReturn, Optional, Sequence, Tuple,
|
IO, TYPE_CHECKING, Any, Dict, List, NoReturn, Optional, Tuple, Union, cast
|
||||||
Union, cast
|
|
||||||
)
|
)
|
||||||
|
|
||||||
from kitty.constants import clear_handled_signals, kitty_exe
|
from kitty.constants import kitty_exe, running_in_kitty
|
||||||
from kitty.entry_points import main as main_entry_point
|
from kitty.entry_points import main as main_entry_point
|
||||||
from kitty.fast_data_types import (
|
from kitty.fast_data_types import (
|
||||||
establish_controlling_tty, get_options, safe_pipe
|
establish_controlling_tty, get_options, safe_pipe, set_options
|
||||||
)
|
)
|
||||||
|
from kitty.options.types import Options
|
||||||
from kitty.shm import SharedMemory
|
from kitty.shm import SharedMemory
|
||||||
|
from kitty.utils import log_error
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from _typeshed import ReadableBuffer, WriteableBuffer
|
from _typeshed import ReadableBuffer, WriteableBuffer
|
||||||
@ -44,67 +46,58 @@ class Child:
|
|||||||
|
|
||||||
class PrewarmProcess:
|
class PrewarmProcess:
|
||||||
|
|
||||||
def __init__(self, create_file_to_read_from_worker: bool = False) -> None:
|
def __init__(
|
||||||
self.from_worker_fd, self.in_worker_fd = safe_pipe()
|
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.children: Dict[int, Child] = {}
|
||||||
if create_file_to_read_from_worker:
|
self.worker_pid = prewarm_process_pid
|
||||||
os.set_blocking(self.from_worker_fd, True)
|
self.from_prewarm_death_notify = from_prewarm_death_notify
|
||||||
self.from_worker = open(self.from_worker_fd, mode='r', closefd=True)
|
self.write_to_process_fd = to_prewarm_stdin
|
||||||
self.from_worker_fd = -1
|
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) -> int:
|
def take_from_worker_fd(self, create_file: bool = False) -> int:
|
||||||
ans, self.from_worker_fd = self.from_worker_fd, -1
|
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
|
return ans
|
||||||
|
|
||||||
def __del__(self) -> None:
|
def __del__(self) -> None:
|
||||||
if self.from_worker_fd > -1:
|
if self.write_to_process_fd > -1:
|
||||||
os.close(self.from_worker_fd)
|
os.close(self.write_to_process_fd)
|
||||||
self.from_worker_fd = -1
|
self.write_to_process_fd = -1
|
||||||
|
if self.from_prewarm_death_notify > -1:
|
||||||
|
os.close(self.from_prewarm_death_notify)
|
||||||
|
self.from_prewarm_death_notify = -1
|
||||||
|
if self.read_from_process_fd > -1:
|
||||||
|
os.close(self.read_from_process_fd)
|
||||||
|
self.read_from_process_fd = -1
|
||||||
|
|
||||||
if hasattr(self, 'from_worker'):
|
if hasattr(self, 'from_worker'):
|
||||||
self.from_worker.close()
|
self.from_worker.close()
|
||||||
del self.from_worker
|
del self.from_worker
|
||||||
if self.worker_started:
|
if self.worker_pid > 0:
|
||||||
import subprocess
|
st = time.monotonic()
|
||||||
self.process.stdin and self.process.stdin.close()
|
while time.monotonic() - st < 1:
|
||||||
self.process.stdout and self.process.stdout.close()
|
try:
|
||||||
try:
|
pid, status = os.waitpid(self.worker_pid, os.WNOHANG)
|
||||||
self.process.wait(timeout=1.0)
|
except ChildProcessError:
|
||||||
except subprocess.TimeoutExpired:
|
return
|
||||||
self.process.kill()
|
else:
|
||||||
del self.process
|
if pid == self.worker_pid:
|
||||||
|
return
|
||||||
@property
|
time.sleep(0.01)
|
||||||
def worker_started(self) -> bool:
|
log_error('Prewarm process failed to quite gracefully, killing it')
|
||||||
return self.in_worker_fd == -1
|
os.kill(self.worker_pid, signal.SIGKILL)
|
||||||
|
os.waitpid(self.worker_pid, 0)
|
||||||
@property
|
|
||||||
def prewarm_config(self) -> str:
|
|
||||||
opts = get_options()
|
|
||||||
return json.dumps({'paths': opts.config_paths, 'overrides': opts.config_overrides})
|
|
||||||
|
|
||||||
def is_prewarmed_argv(self, argv: Sequence[str]) -> bool:
|
|
||||||
if argv[:2] != [kitty_exe(), '+runpy']:
|
|
||||||
return False
|
|
||||||
return len(argv) > 2 and argv[2].startswith('from kitty.prewarm import main; main(')
|
|
||||||
|
|
||||||
def ensure_worker(self) -> None:
|
|
||||||
if not self.worker_started:
|
|
||||||
import subprocess
|
|
||||||
env = dict(os.environ)
|
|
||||||
env['KITTY_PREWARM_CONFIG'] = self.prewarm_config
|
|
||||||
self.process = subprocess.Popen(
|
|
||||||
[kitty_exe(), '+runpy', f'from kitty.prewarm import main; main({self.in_worker_fd})'],
|
|
||||||
stdin=subprocess.PIPE, stdout=subprocess.PIPE, pass_fds=(self.in_worker_fd,), env=env,
|
|
||||||
start_new_session=True, preexec_fn=clear_handled_signals)
|
|
||||||
os.close(self.in_worker_fd)
|
|
||||||
self.in_worker_fd = -1
|
|
||||||
assert self.process.stdin is not None and self.process.stdout is not None
|
|
||||||
self.write_to_process_fd = self.process.stdin.fileno()
|
|
||||||
self.read_from_process_fd = self.process.stdout.fileno()
|
|
||||||
os.set_blocking(self.write_to_process_fd, False)
|
|
||||||
os.set_blocking(self.read_from_process_fd, False)
|
|
||||||
self.poll = select.poll()
|
|
||||||
self.poll.register(self.process.stdout.fileno(), select.POLLIN)
|
|
||||||
|
|
||||||
def poll_to_send(self, yes: bool = True) -> None:
|
def poll_to_send(self, yes: bool = True) -> None:
|
||||||
if yes:
|
if yes:
|
||||||
@ -112,9 +105,12 @@ class PrewarmProcess:
|
|||||||
else:
|
else:
|
||||||
self.poll.unregister(self.write_to_process_fd)
|
self.poll.unregister(self.write_to_process_fd)
|
||||||
|
|
||||||
def reload_kitty_config(self) -> None:
|
def reload_kitty_config(self, opts: Optional[Options] = None) -> None:
|
||||||
if self.worker_started:
|
if opts is None:
|
||||||
self.send_to_prewarm_process('reload_kitty_config:{self.prewarm_config}\n')
|
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__(
|
def __call__(
|
||||||
self,
|
self,
|
||||||
@ -125,7 +121,6 @@ class PrewarmProcess:
|
|||||||
stdin_data: Optional[Union[str, bytes]] = None,
|
stdin_data: Optional[Union[str, bytes]] = None,
|
||||||
timeout: float = TIMEOUT,
|
timeout: float = TIMEOUT,
|
||||||
) -> Child:
|
) -> Child:
|
||||||
self.ensure_worker()
|
|
||||||
tty_name = os.ttyname(tty_fd)
|
tty_name = os.ttyname(tty_fd)
|
||||||
if isinstance(stdin_data, str):
|
if isinstance(stdin_data, str):
|
||||||
stdin_data = stdin_data.encode()
|
stdin_data = stdin_data.encode()
|
||||||
@ -195,14 +190,13 @@ class PrewarmProcess:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def reload_kitty_config() -> None:
|
def reload_kitty_config(payload: str) -> None:
|
||||||
d = json.loads(os.environ.pop('KITTY_PREWARM_CONFIG'))
|
d = json.loads(payload)
|
||||||
from kittens.tui.utils import set_kitty_opts
|
from kittens.tui.utils import set_kitty_opts
|
||||||
set_kitty_opts(paths=d['paths'], overrides=d['overrides'])
|
set_kitty_opts(paths=d['paths'], overrides=d['overrides'])
|
||||||
|
|
||||||
|
|
||||||
def prewarm() -> None:
|
def prewarm() -> None:
|
||||||
reload_kitty_config()
|
|
||||||
from kittens.runner import all_kitten_names
|
from kittens.runner import all_kitten_names
|
||||||
for kitten in all_kitten_names():
|
for kitten in all_kitten_names():
|
||||||
with suppress(Exception):
|
with suppress(Exception):
|
||||||
@ -274,7 +268,7 @@ def child_main(cmd: Dict[str, Any], ready_fd: int) -> NoReturn:
|
|||||||
raise SystemExit(0)
|
raise SystemExit(0)
|
||||||
|
|
||||||
|
|
||||||
def fork(shm_address: str, ready_fd: int) -> Tuple[int, int]:
|
def fork(shm_address: str) -> Tuple[int, int, int]:
|
||||||
sz = pos = 0
|
sz = pos = 0
|
||||||
with SharedMemory(name=shm_address, unlink_on_exit=True) as shm:
|
with SharedMemory(name=shm_address, unlink_on_exit=True) as shm:
|
||||||
data = shm.read_data_with_size()
|
data = shm.read_data_with_size()
|
||||||
@ -284,8 +278,8 @@ def fork(shm_address: str, ready_fd: int) -> Tuple[int, int]:
|
|||||||
pos = shm.tell()
|
pos = shm.tell()
|
||||||
shm.unlink_on_exit = False
|
shm.unlink_on_exit = False
|
||||||
|
|
||||||
r, w = os.pipe()
|
r, w = safe_pipe()
|
||||||
os.set_inheritable(r, False)
|
ready_fd_read, ready_fd_write = safe_pipe()
|
||||||
try:
|
try:
|
||||||
child_pid = os.fork()
|
child_pid = os.fork()
|
||||||
except OSError:
|
except OSError:
|
||||||
@ -295,16 +289,18 @@ def fork(shm_address: str, ready_fd: int) -> Tuple[int, int]:
|
|||||||
if child_pid:
|
if child_pid:
|
||||||
# master process
|
# master process
|
||||||
os.close(w)
|
os.close(w)
|
||||||
|
os.close(ready_fd_read)
|
||||||
poll = select.poll()
|
poll = select.poll()
|
||||||
poll.register(r, select.POLLIN)
|
poll.register(r, select.POLLIN)
|
||||||
for (fd, event) in poll.poll():
|
for (fd, event) in poll.poll():
|
||||||
if event & select.POLLIN:
|
if event & select.POLLIN:
|
||||||
os.read(r, 1)
|
os.read(r, 1)
|
||||||
return child_pid, r
|
return child_pid, r, ready_fd_write
|
||||||
else:
|
else:
|
||||||
raise ValueError('Child process pipe failed')
|
raise ValueError('Child process pipe failed')
|
||||||
# child process
|
# child process
|
||||||
os.set_inheritable(w, False)
|
os.close(r)
|
||||||
|
os.close(ready_fd_write)
|
||||||
os.setsid()
|
os.setsid()
|
||||||
tty_name = cmd.get('tty_name')
|
tty_name = cmd.get('tty_name')
|
||||||
if tty_name:
|
if tty_name:
|
||||||
@ -313,25 +309,22 @@ def fork(shm_address: str, ready_fd: int) -> Tuple[int, int]:
|
|||||||
establish_controlling_tty(tty_name, sys.__stdin__.fileno(), sys.__stdout__.fileno(), sys.__stderr__.fileno())
|
establish_controlling_tty(tty_name, sys.__stdin__.fileno(), sys.__stdout__.fileno(), sys.__stderr__.fileno())
|
||||||
os.write(w, b'1') # this will be closed on process exit and thereby used to detect child death
|
os.write(w, b'1') # this will be closed on process exit and thereby used to detect child death
|
||||||
if shm.unlink_on_exit:
|
if shm.unlink_on_exit:
|
||||||
child_main(cmd, ready_fd)
|
child_main(cmd, ready_fd_read)
|
||||||
else:
|
else:
|
||||||
with SharedMemory(shm_address, unlink_on_exit=True) as shm:
|
with SharedMemory(shm_address, unlink_on_exit=True) as shm:
|
||||||
stdin_data = memoryview(shm.mmap)[pos:pos + sz]
|
stdin_data = memoryview(shm.mmap)[pos:pos + sz]
|
||||||
if stdin_data:
|
if stdin_data:
|
||||||
sys.stdin = MemoryViewReadWrapper(stdin_data)
|
sys.stdin = MemoryViewReadWrapper(stdin_data)
|
||||||
try:
|
try:
|
||||||
child_main(cmd, ready_fd)
|
child_main(cmd, ready_fd_read)
|
||||||
finally:
|
finally:
|
||||||
stdin_data.release()
|
stdin_data.release()
|
||||||
sys.stdin = sys.__stdin__
|
sys.stdin = sys.__stdin__
|
||||||
|
|
||||||
|
|
||||||
def main(notify_child_death_fd: int) -> None:
|
def main(stdin_fd: int, stdout_fd: int, notify_child_death_fd: int) -> None:
|
||||||
os.set_blocking(notify_child_death_fd, False)
|
os.set_blocking(notify_child_death_fd, False)
|
||||||
prewarm()
|
|
||||||
stdin_fd = sys.__stdin__.fileno()
|
|
||||||
os.set_blocking(stdin_fd, False)
|
os.set_blocking(stdin_fd, False)
|
||||||
stdout_fd = sys.__stdout__.fileno()
|
|
||||||
os.set_blocking(stdout_fd, False)
|
os.set_blocking(stdout_fd, False)
|
||||||
poll = select.poll()
|
poll = select.poll()
|
||||||
poll.register(stdin_fd, select.POLLIN)
|
poll.register(stdin_fd, select.POLLIN)
|
||||||
@ -344,6 +337,7 @@ def main(notify_child_death_fd: int) -> None:
|
|||||||
# runpy issues a warning when running modules that have already been
|
# runpy issues a warning when running modules that have already been
|
||||||
# imported. Ignore it.
|
# imported. Ignore it.
|
||||||
warnings.filterwarnings('ignore', category=RuntimeWarning, module='runpy')
|
warnings.filterwarnings('ignore', category=RuntimeWarning, module='runpy')
|
||||||
|
prewarm()
|
||||||
|
|
||||||
def check_event(event: int, err_msg: str) -> None:
|
def check_event(event: int, err_msg: str) -> None:
|
||||||
if event & select.POLLHUP:
|
if event & select.POLLHUP:
|
||||||
@ -365,19 +359,18 @@ def main(notify_child_death_fd: int) -> None:
|
|||||||
input_buf = input_buf[idx+1:]
|
input_buf = input_buf[idx+1:]
|
||||||
cmd, _, payload = line.partition(':')
|
cmd, _, payload = line.partition(':')
|
||||||
if cmd == 'reload_kitty_config':
|
if cmd == 'reload_kitty_config':
|
||||||
os.environ['KITTY_PREWARM_CONFIG'] = payload
|
reload_kitty_config(payload)
|
||||||
reload_kitty_config()
|
|
||||||
elif cmd == 'ready':
|
elif cmd == 'ready':
|
||||||
child_id = int(payload)
|
child_id = int(payload)
|
||||||
cfd = child_ready_fds.pop(child_id)
|
cfd = child_ready_fds.pop(child_id)
|
||||||
if cfd is not None:
|
if cfd is not None:
|
||||||
os.write(cfd, b'1')
|
os.write(cfd, b'1')
|
||||||
os.close(cfd)
|
os.close(cfd)
|
||||||
|
elif cmd == 'quit':
|
||||||
|
raise SystemExit(0)
|
||||||
elif cmd == 'fork':
|
elif cmd == 'fork':
|
||||||
r, w = os.pipe()
|
|
||||||
os.set_inheritable(w, False)
|
|
||||||
try:
|
try:
|
||||||
child_pid, child_death_fd = fork(payload, r)
|
child_pid, child_death_fd, ready_fd_write = fork(payload)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
es = str(e).replace('\n', ' ')
|
es = str(e).replace('\n', ' ')
|
||||||
output_buf += f'ERR:{es}\n'.encode()
|
output_buf += f'ERR:{es}\n'.encode()
|
||||||
@ -385,13 +378,10 @@ def main(notify_child_death_fd: int) -> None:
|
|||||||
if os.getpid() == self_pid:
|
if os.getpid() == self_pid:
|
||||||
child_id = next(child_id_counter)
|
child_id = next(child_id_counter)
|
||||||
child_id_map[child_id] = child_pid
|
child_id_map[child_id] = child_pid
|
||||||
child_ready_fds[child_id] = w
|
child_ready_fds[child_id] = ready_fd_write
|
||||||
child_death_fds[child_death_fd] = child_id
|
child_death_fds[child_death_fd] = child_id
|
||||||
poll.register(child_death_fd, select.POLLIN)
|
poll.register(child_death_fd, select.POLLIN)
|
||||||
output_buf += f'CHILD:{child_id}:{child_pid}\n'.encode()
|
output_buf += f'CHILD:{child_id}:{child_pid}\n'.encode()
|
||||||
finally:
|
|
||||||
if os.getpid() == self_pid:
|
|
||||||
os.close(r)
|
|
||||||
elif cmd == 'echo':
|
elif cmd == 'echo':
|
||||||
output_buf += f'{payload}\n'.encode()
|
output_buf += f'{payload}\n'.encode()
|
||||||
|
|
||||||
@ -463,3 +453,51 @@ def main(notify_child_death_fd: int) -> None:
|
|||||||
for fmd in child_ready_fds.values():
|
for fmd in child_ready_fds.values():
|
||||||
with suppress(OSError):
|
with suppress(OSError):
|
||||||
os.close(fmd)
|
os.close(fmd)
|
||||||
|
|
||||||
|
|
||||||
|
def exec_main(stdin_read: int, stdout_write: int, death_notify_write: int) -> None:
|
||||||
|
os.setsid()
|
||||||
|
# SIGUSR1 is used for reloading kitty config, we rely on the parent process
|
||||||
|
# to inform us of that
|
||||||
|
signal.signal(signal.SIGUSR1, signal.SIG_IGN)
|
||||||
|
signal.siginterrupt(signal.SIGUSR1, False)
|
||||||
|
os.set_inheritable(stdin_read, False)
|
||||||
|
os.set_inheritable(stdout_write, False)
|
||||||
|
os.set_inheritable(death_notify_write, False)
|
||||||
|
running_in_kitty(False)
|
||||||
|
|
||||||
|
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
|
||||||
|
else:
|
||||||
|
child_pid = os.fork()
|
||||||
|
if child_pid:
|
||||||
|
# master
|
||||||
|
os.close(stdin_read)
|
||||||
|
os.close(stdout_write)
|
||||||
|
os.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
|
||||||
|
os.close(stdin_write)
|
||||||
|
os.close(stdout_read)
|
||||||
|
os.close(death_notify_read)
|
||||||
|
set_options(opts)
|
||||||
|
exec_main(stdin_read, stdout_write, death_notify_write)
|
||||||
|
raise SystemExit(0)
|
||||||
|
|||||||
@ -17,9 +17,8 @@ class Prewarm(BaseTest):
|
|||||||
maxDiff = None
|
maxDiff = None
|
||||||
|
|
||||||
def test_prewarming(self):
|
def test_prewarming(self):
|
||||||
from kitty.prewarm import PrewarmProcess
|
from kitty.prewarm import fork_prewarm_process
|
||||||
|
|
||||||
p = PrewarmProcess(create_file_to_read_from_worker=True)
|
|
||||||
cwd = tempfile.gettempdir()
|
cwd = tempfile.gettempdir()
|
||||||
env = {'TEST_ENV_PASS': 'xyz'}
|
env = {'TEST_ENV_PASS': 'xyz'}
|
||||||
cols = 117
|
cols = 117
|
||||||
@ -28,6 +27,10 @@ class Prewarm(BaseTest):
|
|||||||
ttyname = os.ttyname(pty.slave_fd)
|
ttyname = os.ttyname(pty.slave_fd)
|
||||||
opts = get_options()
|
opts = get_options()
|
||||||
opts.config_overrides = 'font_family prewarm',
|
opts.config_overrides = 'font_family prewarm',
|
||||||
|
p = fork_prewarm_process(opts, use_exec=True)
|
||||||
|
if p is None:
|
||||||
|
return
|
||||||
|
p.take_from_worker_fd(create_file=True)
|
||||||
child = p(pty.slave_fd, [kitty_exe(), '+runpy', """\
|
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({
|
import os, json; from kitty.utils import *; from kitty.fast_data_types import get_options; print(json.dumps({
|
||||||
'cterm': os.ctermid(),
|
'cterm': os.ctermid(),
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user