Make the API to run background processes more powerful

Supports death notification and can redirect stdout/stderr to an fd
This commit is contained in:
Kovid Goyal
2024-05-14 13:11:04 +05:30
parent 78416acd16
commit 8eb3267fb8
2 changed files with 57 additions and 22 deletions

View File

@@ -95,6 +95,7 @@ from .fast_data_types import (
global_font_size, global_font_size,
last_focused_os_window_id, last_focused_os_window_id,
mark_os_window_for_close, mark_os_window_for_close,
monitor_pid,
monotonic, monotonic,
os_window_focus_counters, os_window_focus_counters,
os_window_font_size, os_window_font_size,
@@ -334,6 +335,7 @@ class Boss:
self.primary_selection = Clipboard(ClipboardType.primary_selection) self.primary_selection = Clipboard(ClipboardType.primary_selection)
self.update_check_started = False self.update_check_started = False
self.peer_data_map: Dict[int, Optional[Dict[str, Sequence[str]]]] = {} self.peer_data_map: Dict[int, Optional[Dict[str, Sequence[str]]]] = {}
self.background_process_death_notify_map: Dict[int, Callable[[Optional[int]], None]] = {}
self.encryption_key = EllipticCurveKey() self.encryption_key = EllipticCurveKey()
self.encryption_public_key = f'{RC_ENCRYPTION_PROTOCOL_VERSION}:{base64.b85encode(self.encryption_key.public).decode("ascii")}' self.encryption_public_key = f'{RC_ENCRYPTION_PROTOCOL_VERSION}:{base64.b85encode(self.encryption_key.public).decode("ascii")}'
self.clipboard_buffers: Dict[str, str] = {} self.clipboard_buffers: Dict[str, str] = {}
@@ -2306,6 +2308,8 @@ class Boss:
cwd_from: Optional[CwdRequest] = None, cwd_from: Optional[CwdRequest] = None,
allow_remote_control: bool = False, allow_remote_control: bool = False,
remote_control_passwords: Optional[Dict[str, Sequence[str]]] = None, remote_control_passwords: Optional[Dict[str, Sequence[str]]] = None,
notify_on_death: Optional[Callable[[Optional[int]], None]] = None,
stdout: Optional[int] = None, stderr: Optional[int] = None,
) -> None: ) -> None:
import subprocess import subprocess
env = env or None env = env or None
@@ -2325,7 +2329,8 @@ class Boss:
def doit(activation_token: str = '') -> None: def doit(activation_token: str = '') -> None:
nonlocal env nonlocal env
pass_fds: Tuple[int, ...] = () pass_fds: List[int] = []
fds_to_close_on_launch_failure: List[int] = []
if allow_remote_control: if allow_remote_control:
import socket import socket
local, remote = socket.socketpair() local, remote = socket.socketpair()
@@ -2338,35 +2343,56 @@ class Boss:
os.close(lfd) os.close(lfd)
remote.close() remote.close()
raise raise
pass_fds = (remote.fileno(),) pass_fds.append(remote.fileno())
add_env('KITTY_LISTEN_ON', f'fd:{remote.fileno()}') add_env('KITTY_LISTEN_ON', f'fd:{remote.fileno()}')
self.peer_data_map[peer_id] = remote_control_passwords self.peer_data_map[peer_id] = remote_control_passwords
if activation_token: if activation_token:
add_env('XDG_ACTIVATION_TOKEN', activation_token) add_env('XDG_ACTIVATION_TOKEN', activation_token)
try: fds_to_close_on_launch_failure = list(pass_fds)
if stdin: if stdout is not None and stdout > -1:
r, w = safe_pipe(False) pass_fds.append(stdout)
try: if stderr is not None and stderr > -1 and stderr not in pass_fds:
subprocess.Popen(cmd, env=env, stdin=r, cwd=cwd, preexec_fn=clear_handled_signals, pass_fds=pass_fds, close_fds=True) pass_fds.append(stderr)
except Exception:
os.close(w) def run(stdin: Optional[int], stdout: Optional[int], stderr: Optional[int]) -> None:
try:
p = subprocess.Popen(
cmd, env=env, cwd=cwd, preexec_fn=clear_handled_signals, pass_fds=pass_fds, stdin=stdin, stdout=stdout, stderr=stderr)
if notify_on_death:
self.background_process_death_notify_map[p.pid] = notify_on_death
monitor_pid(p.pid)
except Exception as err:
for fd in fds_to_close_on_launch_failure:
with suppress(OSError):
os.close(fd)
if notify_on_death:
try:
notify_on_death(None)
except Exception:
import traceback
traceback.print_exc()
else: else:
thread_write(w, stdin) self.show_error(_('Failed to run background process'), _('Failed to run background process with error: {}').format(err))
finally:
os.close(r) r = subprocess.DEVNULL
else: if stdin:
subprocess.Popen(cmd, env=env, cwd=cwd, preexec_fn=clear_handled_signals, pass_fds=pass_fds, close_fds=True) r, w = safe_pipe(False)
fds_to_close_on_launch_failure.append(w)
pass_fds.append(r)
try:
run(r, stdout, stderr)
if stdin:
thread_write(w, stdin)
finally: finally:
if stdin:
os.close(r)
if allow_remote_control: if allow_remote_control:
remote.close() remote.close()
try: if is_wayland():
if is_wayland(): run_with_activation_token(doit)
run_with_activation_token(doit) else:
else: doit()
doit()
except Exception as err:
self.show_error(_('Failed to run background process'), _('Failed to run background process with error: {}').format(err))
def pipe(self, source: str, dest: str, exe: str, *args: str) -> Optional[Window]: def pipe(self, source: str, dest: str, exe: str, *args: str) -> Optional[Window]:
cmd = [exe] + list(args) cmd = [exe] + list(args)
@@ -2639,6 +2665,15 @@ class Boss:
self.update_check_process = process self.update_check_process = process
def on_monitored_pid_death(self, pid: int, exit_status: int) -> None: def on_monitored_pid_death(self, pid: int, exit_status: int) -> None:
callback = self.background_process_death_notify_map.pop(pid, None)
if callback is not None:
try:
callback(exit_status)
except Exception:
import traceback
traceback.print_exc()
return
update_check_process = self.update_check_process update_check_process = self.update_check_process
if update_check_process is not None and pid == update_check_process.pid: if update_check_process is not None and pid == update_check_process.pid:
self.update_check_process = None self.update_check_process = None

View File

@@ -116,7 +116,7 @@ def update_check() -> bool:
kitty_exe(), '+runpy', kitty_exe(), '+runpy',
'from kitty.update_check import run_worker; run_worker()' 'from kitty.update_check import run_worker; run_worker()'
], stdout=subprocess.PIPE, preexec_fn=clear_handled_signals) ], stdout=subprocess.PIPE, preexec_fn=clear_handled_signals)
except OSError as e: except Exception as e:
log_error(f'Failed to run kitty for update check, with error: {e}') log_error(f'Failed to run kitty for update check, with error: {e}')
return False return False
monitor_pid(p.pid) monitor_pid(p.pid)