This commit is contained in:
Kovid Goyal
2026-07-21 15:34:38 +05:30
parent e36f6ac1b8
commit 51116e5df3

View File

@@ -9,7 +9,7 @@ from collections.abc import Generator, Iterable, Mapping, Sequence
from contextlib import contextmanager, suppress from contextlib import contextmanager, suppress
from itertools import count from itertools import count
from time import monotonic from time import monotonic
from typing import TYPE_CHECKING, DefaultDict, Optional, TypedDict from typing import TYPE_CHECKING, Any, DefaultDict, Optional, TypedDict
import kitty.fast_data_types as fast_data_types import kitty.fast_data_types as fast_data_types
@@ -51,14 +51,17 @@ else:
return list(filter(None, f.read().decode('utf-8').split('\0'))) return list(filter(None, f.read().decode('utf-8').split('\0')))
if is_freebsd: if is_freebsd:
def cwd_of_process(pid: int) -> str: def cwd_of_process(pid: int) -> str:
import subprocess import subprocess
cp = subprocess.run(['pwdx', str(pid)], capture_output=True) cp = subprocess.run(['pwdx', str(pid)], capture_output=True)
if cp.returncode != 0: if cp.returncode != 0:
raise ValueError(f'Failed to find cwd of process with pid: {pid}') raise ValueError(f'Failed to find cwd of process with pid: {pid}')
ans = cp.stdout.decode('utf-8', 'replace').split()[1] ans = cp.stdout.decode('utf-8', 'replace').split()[1]
return os.path.realpath(ans, strict=True) return os.path.realpath(ans, strict=True)
else: else:
def cwd_of_process(pid: int) -> str: def cwd_of_process(pid: int) -> str:
# We use realpath instead of readlink to match macOS behavior where # We use realpath instead of readlink to match macOS behavior where
# the underlying OS API returns real paths. # the underlying OS API returns real paths.
@@ -98,7 +101,6 @@ def checked_terminfo_dir() -> str | None:
class CachedProcessData: class CachedProcessData:
cached_result: DefaultDict[int, list[int]] | None = None cached_result: DefaultDict[int, list[int]] | None = None
cache_active: bool = False cache_active: bool = False
cache_at: float = 0 cache_at: float = 0
@@ -150,6 +152,7 @@ def session_id(pids: Iterable[int]) -> int:
return sid return sid
return -1 return -1
def parse_environ_block(data: str) -> dict[str, str]: def parse_environ_block(data: str) -> dict[str, str]:
"""Parse a C environ block of environment variables into a dictionary.""" """Parse a C environ block of environment variables into a dictionary."""
# The block is usually raw data from the target process. It might contain # The block is usually raw data from the target process. It might contain
@@ -158,15 +161,15 @@ def parse_environ_block(data: str) -> dict[str, str]:
pos = 0 pos = 0
while True: while True:
next_pos = data.find("\0", pos) next_pos = data.find('\0', pos)
# nul byte at the beginning or double nul byte means finish # nul byte at the beginning or double nul byte means finish
if next_pos <= pos: if next_pos <= pos:
break break
# there might not be an equals sign # there might not be an equals sign
equal_pos = data.find("=", pos, next_pos) equal_pos = data.find('=', pos, next_pos)
if equal_pos > pos: if equal_pos > pos:
key = data[pos:equal_pos] key = data[pos:equal_pos]
value = data[equal_pos + 1:next_pos] value = data[equal_pos + 1 : next_pos]
ret[key] = value ret[key] = value
pos = next_pos + 1 pos = next_pos + 1
@@ -237,9 +240,9 @@ child_counter = count()
class Child: class Child:
child_fd: int | None = None child_fd: int | None = None
pid: int | None = None pid: int | None = None
initial_termios_state: list[Any] | None = None
forked = False forked = False
def __init__( def __init__(
@@ -274,18 +277,23 @@ class Child:
self.stdin = stdin self.stdin = stdin
self.env = env or {} self.env = env or {}
self.startup_command_via_shell_integration = startup_command_via_shell_integration self.startup_command_via_shell_integration = startup_command_via_shell_integration
self.final_env:dict[str, str] = {} self.final_env: dict[str, str] = {}
self.is_default_shell = bool(self.argv and self.argv[0] == shell_path) self.is_default_shell = bool(self.argv and self.argv[0] == shell_path)
self.should_run_via_run_shell_kitten = is_macos and self.is_default_shell self.should_run_via_run_shell_kitten = is_macos and self.is_default_shell
self.hold = hold self.hold = hold
def get_final_env(self) -> tuple[dict[str, str], bool]: def get_final_env(self) -> tuple[dict[str, str], bool]:
from kitty.options.utils import DELETE_ENV_VAR from kitty.options.utils import DELETE_ENV_VAR
env = default_env().copy() env = default_env().copy()
opts = fast_data_types.get_options() opts = fast_data_types.get_options()
boss = fast_data_types.get_boss() boss = fast_data_types.get_boss()
if is_macos and env.get('LC_CTYPE') == 'UTF-8' and not getattr(sys, 'kitty_run_data').get( if (
'lc_ctype_before_python') and not getattr(default_env, 'lc_ctype_set_by_user', False): is_macos
and env.get('LC_CTYPE') == 'UTF-8'
and not getattr(sys, 'kitty_run_data').get('lc_ctype_before_python')
and not getattr(default_env, 'lc_ctype_set_by_user', False)
):
del env['LC_CTYPE'] del env['LC_CTYPE']
env.update(self.env) env.update(self.env)
env['TERM'] = opts.term env['TERM'] = opts.term
@@ -314,6 +322,7 @@ class Child:
self.unmodified_argv = list(self.argv) self.unmodified_argv = list(self.argv)
if not self.should_run_via_run_shell_kitten and 'disabled' not in opts.shell_integration: if not self.should_run_via_run_shell_kitten and 'disabled' not in opts.shell_integration:
from .shell_integration import modify_shell_environ from .shell_integration import modify_shell_environ
modify_shell_environ(opts, env, self.argv) modify_shell_environ(opts, env, self.argv)
env = {k: v for k, v in env.items() if v is not DELETE_ENV_VAR} env = {k: v for k, v in env.items() if v is not DELETE_ENV_VAR}
if self.is_clone_launch: if self.is_clone_launch:
@@ -327,6 +336,7 @@ class Child:
env['KITTY_SI_RUN_COMMAND_AT_STARTUP'] = self.startup_command_via_shell_integration env['KITTY_SI_RUN_COMMAND_AT_STARTUP'] = self.startup_command_via_shell_integration
else: else:
from .shell_integration import join from .shell_integration import join
scmd = self.argv or resolved_shell(fast_data_types.get_options()) scmd = self.argv or resolved_shell(fast_data_types.get_options())
try: try:
env['KITTY_SI_RUN_COMMAND_AT_STARTUP'] = join(scmd[0], self.startup_command_via_shell_integration) env['KITTY_SI_RUN_COMMAND_AT_STARTUP'] = join(scmd[0], self.startup_command_via_shell_integration)
@@ -356,7 +366,7 @@ class Child:
cwd = self.cwd cwd = self.cwd
pass_fds = self.pass_fds pass_fds = self.pass_fds
if self.remote_control_fd > -1: if self.remote_control_fd > -1:
pass_fds += self.remote_control_fd, pass_fds += (self.remote_control_fd,)
if self.should_run_via_run_shell_kitten or must_run_startup_command_via_kitten: if self.should_run_via_run_shell_kitten or must_run_startup_command_via_kitten:
# bash will only source ~/.bash_profile if it detects it is a login # bash will only source ~/.bash_profile if it detects it is a login
# shell (see the invocation section of the bash man page), which it # shell (see the invocation section of the bash man page), which it
@@ -373,6 +383,7 @@ class Child:
# xterm, urxvt, konsole and gnome-terminal do not do it in my # xterm, urxvt, konsole and gnome-terminal do not do it in my
# testing. # testing.
import shlex import shlex
ksi = ' '.join(opts.shell_integration) ksi = ' '.join(opts.shell_integration)
if ksi == 'invalid': if ksi == 'invalid':
ksi = 'enabled' ksi = 'enabled'
@@ -388,6 +399,7 @@ class Child:
# login closes inherited file descriptors so dont use it when # login closes inherited file descriptors so dont use it when
# forward_stdio or pass_fds are used. # forward_stdio or pass_fds are used.
import pwd import pwd
user = pwd.getpwuid(os.geteuid()).pw_name user = pwd.getpwuid(os.geteuid()).pw_name
if cwd: if cwd:
argv.append('--cwd=' + cwd) argv.append('--cwd=' + cwd)
@@ -400,8 +412,21 @@ class Child:
final_exe = argv[0] final_exe = argv[0]
env = tuple(f'{k}={v}' for k, v in self.final_env.items()) env = tuple(f'{k}={v}' for k, v in self.final_env.items())
pid = fast_data_types.spawn( pid = fast_data_types.spawn(
final_exe, cwd, tuple(argv), env, master, slave, stdin_read_fd, stdin_write_fd, final_exe,
ready_read_fd, ready_write_fd, tuple(handled_signals), kitten_exe(), opts.forward_stdio, pass_fds) cwd,
tuple(argv),
env,
master,
slave,
stdin_read_fd,
stdin_write_fd,
ready_read_fd,
ready_write_fd,
tuple(handled_signals),
kitten_exe(),
opts.forward_stdio,
pass_fds,
)
os.close(slave) os.close(slave)
self.pid = pid self.pid = pid
self.child_fd = master self.child_fd = master
@@ -419,7 +444,7 @@ class Child:
except NotImplementedError: except NotImplementedError:
pass pass
except OSError as err: except OSError as err:
log_error("Could not move child process into a systemd scope: " + str(err)) log_error('Could not move child process into a systemd scope: ' + str(err))
return pid return pid
def __del__(self) -> None: def __del__(self) -> None:
@@ -569,6 +594,7 @@ class Child:
def send_signal_for_key(self, key_num: bytes) -> bool: def send_signal_for_key(self, key_num: bytes) -> bool:
import signal import signal
if self.child_fd is None: if self.child_fd is None:
return False return False
t = termios.tcgetattr(self.child_fd) t = termios.tcgetattr(self.child_fd)
@@ -588,8 +614,8 @@ class Child:
return True return True
def reset_termios_state(self, when: int = termios.TCSANOW) -> None: def reset_termios_state(self, when: int = termios.TCSANOW) -> None:
if (s := getattr(self, 'initial_termios_state', None)) and self.child_fd is not None: if self.initial_termios_state is not None and self.child_fd is not None:
try: try:
termios.tcsetattr(self.child_fd, when, s) termios.tcsetattr(self.child_fd, when, self.initial_termios_state)
except OSError: except OSError:
pass pass