Add a framework for easily and securely using remote control from the main function of a custom kitten

This commit is contained in:
Kovid Goyal
2024-09-29 20:36:12 +05:30
parent 4bb0d3dbfb
commit af83d855de
10 changed files with 219 additions and 29 deletions

View File

@@ -1948,17 +1948,32 @@ class Boss:
else:
cmd = [kitty_exe(), '+runpy', 'from kittens.runner import main; main()']
env['PYTHONWARNINGS'] = 'ignore'
overlay_window = tab.new_special_window(
SpecialWindow(
cmd + final_args,
stdin=data,
env=env,
cwd=w.cwd_of_child,
overlay_for=w.id,
overlay_behind=end_kitten.has_ready_notification,
),
copy_colors_from=w
)
remote_control_fd = -1
if end_kitten.allow_remote_control:
remote_control_passwords: Optional[dict[str, Sequence[str]]] = None
initial_data = b''
if end_kitten.remote_control_password:
from secrets import token_hex
p = token_hex(16)
remote_control_passwords = {p: end_kitten.remote_control_password if isinstance(end_kitten.remote_control_password, str) else ''}
initial_data = p.encode() + b'\n'
remote = self.add_fd_based_remote_control(remote_control_passwords, initial_data)
remote_control_fd = remote.fileno()
try:
overlay_window = tab.new_special_window(
SpecialWindow(
cmd + final_args,
stdin=data,
env=env,
cwd=w.cwd_of_child,
overlay_for=w.id,
overlay_behind=end_kitten.has_ready_notification,
),
copy_colors_from=w, remote_control_fd=remote_control_fd,
)
finally:
if end_kitten.allow_remote_control:
remote.close()
wid = w.id
overlay_window.actions_on_close.append(partial(self.on_kitten_finish, wid, custom_callback or end_kitten.handle_result, default_data=default_data))
overlay_window.open_url_handler = end_kitten.open_url_handler
@@ -2351,9 +2366,11 @@ class Boss:
overlay_for = w.id if w and as_overlay else None
return SpecialWindow(cmd, input_data, cwd_from=cwd_from, overlay_for=overlay_for, env=env)
def add_fd_based_remote_control(self, remote_control_passwords: Optional[dict[str, Sequence[str]]] = None) -> socket.socket:
def add_fd_based_remote_control(self, remote_control_passwords: Optional[dict[str, Sequence[str]]] = None, initial_data: bytes = b'') -> socket.socket:
local, remote = socket.socketpair()
os.set_inheritable(remote.fileno(), True)
if initial_data:
local.send(initial_data)
lfd = os.dup(local.fileno())
local.close()
try:

View File

@@ -7,7 +7,7 @@ from collections import defaultdict
from collections.abc import Generator, Sequence
from contextlib import contextmanager, suppress
from itertools import count
from typing import TYPE_CHECKING, DefaultDict, Optional, Protocol, Union
from typing import TYPE_CHECKING, DefaultDict, Optional
import kitty.fast_data_types as fast_data_types
@@ -23,11 +23,6 @@ if TYPE_CHECKING:
from .window import CwdRequest
class InheritableFile(Protocol):
def fileno(self) -> int: ...
if is_macos:
from kitty.fast_data_types import cmdline_of_process as cmdline_
from kitty.fast_data_types import cwd_of_process as _cwd
@@ -216,13 +211,15 @@ class Child:
is_clone_launch: str = '',
add_listen_on_env_var: bool = True,
hold: bool = False,
pass_fds: tuple[Union[int, InheritableFile], ...] = (),
pass_fds: tuple[int, ...] = (),
remote_control_fd: int = -1,
):
self.is_clone_launch = is_clone_launch
self.id = next(child_counter)
self.add_listen_on_env_var = add_listen_on_env_var
self.argv = list(argv)
self.pass_fds = pass_fds
self.remote_control_fd = remote_control_fd
if cwd_from:
try:
cwd = cwd_from.modify_argv_for_launch_with_cwd(self.argv, env) or cwd
@@ -251,7 +248,9 @@ class Child:
env['COLORTERM'] = 'truecolor'
env['KITTY_PID'] = getpid()
env['KITTY_PUBLIC_KEY'] = boss.encryption_public_key
if self.add_listen_on_env_var and boss.listening_on:
if self.remote_control_fd > -1:
env['KITTY_LISTEN_ON'] = f'fd:{self.remote_control_fd}'
elif self.add_listen_on_env_var and boss.listening_on:
env['KITTY_LISTEN_ON'] = boss.listening_on
else:
env.pop('KITTY_LISTEN_ON', None)
@@ -299,6 +298,9 @@ class Child:
self.final_env = self.get_final_env()
argv = list(self.argv)
cwd = self.cwd
pass_fds = self.pass_fds
if self.remote_control_fd > -1:
pass_fds += self.remote_control_fd,
if self.should_run_via_run_shell_kitten:
# 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
@@ -319,7 +321,7 @@ class Child:
if ksi == 'invalid':
ksi = 'enabled'
argv = [kitten_exe(), 'run-shell', '--shell', shlex.join(argv), '--shell-integration', ksi]
if is_macos and not self.pass_fds and not opts.forward_stdio:
if is_macos and not pass_fds and not opts.forward_stdio:
# In addition for getlogin() to work we need to run the shell
# via the /usr/bin/login wrapper, sigh.
# And login on macOS looks for .hushlogin in CWD instead of
@@ -339,12 +341,10 @@ class Child:
argv = cmdline_for_hold(argv)
final_exe = argv[0]
env = tuple(f'{k}={v}' for k, v in self.final_env.items())
pass_fds = tuple(sorted(x if isinstance(x, int) else x.fileno() for x in self.pass_fds))
pid = fast_data_types.spawn(
final_exe, 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)
self.pass_fds = ()
self.pid = pid
self.child_fd = master
if stdin is not None:

View File

@@ -279,6 +279,7 @@ completion=type:file relative:conf kwds:-
default=rc-pass
A file from which to read the password. Trailing whitespace is ignored. Relative
paths are resolved from the kitty configuration directory. Use - to read from STDIN.
Use :code:`fd:num` to read from the file descriptor :code:`num`.
Used if no :option:`kitten @ --password` is supplied. Defaults to checking for the
:file:`rc-pass` file in the kitty configuration directory.

View File

@@ -453,6 +453,8 @@ class Tab: # {{{
is_clone_launch: str = '',
add_listen_on_env_var: bool = True,
hold: bool = False,
pass_fds: tuple[int, ...] = (),
remote_control_fd: int = -1,
) -> Child:
check_for_suitability = True
if cmd is None:
@@ -500,7 +502,9 @@ class Tab: # {{{
pwid = platform_window_id(self.os_window_id)
if pwid is not None:
fenv['WINDOWID'] = str(pwid)
ans = Child(cmd, cwd or self.cwd, stdin, fenv, cwd_from, is_clone_launch=is_clone_launch, add_listen_on_env_var=add_listen_on_env_var, hold=hold)
ans = Child(
cmd, cwd or self.cwd, stdin, fenv, cwd_from, is_clone_launch=is_clone_launch,
add_listen_on_env_var=add_listen_on_env_var, hold=hold, pass_fds=pass_fds, remote_control_fd=remote_control_fd)
ans.fork()
return ans
@@ -532,11 +536,13 @@ class Tab: # {{{
remote_control_passwords: Optional[dict[str, Sequence[str]]] = None,
hold: bool = False,
bias: Optional[float] = None,
pass_fds: tuple[int, ...] = (),
remote_control_fd: int = -1,
) -> Window:
child = self.launch_child(
use_shell=use_shell, cmd=cmd, stdin=stdin, cwd_from=cwd_from, cwd=cwd, env=env,
is_clone_launch=is_clone_launch, add_listen_on_env_var=False if allow_remote_control and remote_control_passwords else True,
hold=hold,
hold=hold, pass_fds=pass_fds, remote_control_fd=remote_control_fd,
)
window = Window(
self, child, self.args, override_title=override_title,
@@ -561,6 +567,8 @@ class Tab: # {{{
copy_colors_from: Optional[Window] = None,
allow_remote_control: bool = False,
remote_control_passwords: Optional[dict[str, Sequence[str]]] = None,
pass_fds: tuple[int, ...] = (),
remote_control_fd: int = -1,
) -> Window:
return self.new_window(
use_shell=False, cmd=special_window.cmd, stdin=special_window.stdin,
@@ -568,7 +576,7 @@ class Tab: # {{{
cwd_from=special_window.cwd_from, cwd=special_window.cwd, overlay_for=special_window.overlay_for,
env=special_window.env, location=location, copy_colors_from=copy_colors_from,
allow_remote_control=allow_remote_control, watchers=special_window.watchers, overlay_behind=special_window.overlay_behind,
hold=special_window.hold, remote_control_passwords=remote_control_passwords,
hold=special_window.hold, remote_control_passwords=remote_control_passwords, pass_fds=pass_fds, remote_control_fd=remote_control_fd,
)
@ac('win', 'Close all windows in the tab other than the currently active window')