Allow launched background process to work with --allow-remote-control
Use a dedicated socketpair for such processes. Fixes #6712
This commit is contained in:
parent
a9b412baba
commit
314fe4fe4a
11 changed files with 275 additions and 92 deletions
|
|
@ -52,6 +52,8 @@ Detailed list of changes
|
|||
|
||||
- A new mouse action ``mouse_selection word_and_line_from_point`` to select the current word under the mouse cursor and extend to end of line (:pull:`6663`)
|
||||
|
||||
- Allow :option:`kitty @ launch --allow-remote-control` to work even with background processes, by using a dedicated socketpair for the process (:iss:`6712`)
|
||||
|
||||
- Allow using the full range of standard mouse cursor shapes when customizing the mouse cursor
|
||||
|
||||
- macOS: When running the default shell with the login program fix :file:`~/.hushlogin` not being respected when opening windows not in the home directory (:iss:`6689`)
|
||||
|
|
|
|||
|
|
@ -180,7 +180,9 @@ Variables that kitty sets when running child programs
|
|||
Set when the :doc:`remote control <remote-control>` facility is enabled and
|
||||
the a socket is used for control via :option:`kitty --listen-on` or :opt:`listen_on`.
|
||||
Contains the path to the socket. Avoid the need to use :option:`kitty @ --to` when
|
||||
issuing remote control commands.
|
||||
issuing remote control commands. Can also be a file descriptor of the form
|
||||
fd:num instead of a socket address, in which case, remote control
|
||||
communication should proceed over the specified file descriptor.
|
||||
|
||||
.. envvar:: KITTY_PIPE_DATA
|
||||
|
||||
|
|
|
|||
121
kitty/boss.py
121
kitty/boss.py
|
|
@ -23,6 +23,7 @@
|
|||
Iterator,
|
||||
List,
|
||||
Optional,
|
||||
Sequence,
|
||||
Set,
|
||||
Tuple,
|
||||
Union,
|
||||
|
|
@ -333,6 +334,7 @@ def __init__(
|
|||
self.clipboard = Clipboard()
|
||||
self.primary_selection = Clipboard(ClipboardType.primary_selection)
|
||||
self.update_check_started = False
|
||||
self.peer_data_map: Dict[int, Optional[Dict[str, Sequence[str]]]] = {}
|
||||
self.encryption_key = EllipticCurveKey()
|
||||
self.encryption_public_key = f'{RC_ENCRYPTION_PROTOCOL_VERSION}:{base64.b85encode(self.encryption_key.public).decode("ascii")}'
|
||||
self.clipboard_buffers: Dict[str, str] = {}
|
||||
|
|
@ -597,14 +599,16 @@ def add_child(self, window: Window) -> None:
|
|||
self.window_id_map[window.id] = window
|
||||
|
||||
def _handle_remote_command(self, cmd: str, window: Optional[Window] = None, peer_id: int = 0) -> RCResponse:
|
||||
from .remote_control import is_cmd_allowed, parse_cmd
|
||||
from .remote_control import is_cmd_allowed, parse_cmd, remote_control_allowed
|
||||
response = None
|
||||
window = window or None
|
||||
from_socket = peer_id > 0
|
||||
is_fd_peer = from_socket and peer_id in self.peer_data_map
|
||||
window_has_remote_control = bool(window and window.allow_remote_control)
|
||||
if not window_has_remote_control:
|
||||
if not window_has_remote_control and not is_fd_peer:
|
||||
if self.allow_remote_control == 'n':
|
||||
return {'ok': False, 'error': 'Remote control is disabled'}
|
||||
if self.allow_remote_control == 'socket-only' and peer_id == 0:
|
||||
if self.allow_remote_control == 'socket-only' and not from_socket:
|
||||
return {'ok': False, 'error': 'Remote control is allowed over a socket only'}
|
||||
try:
|
||||
pcmd = parse_cmd(cmd, self.encryption_key)
|
||||
|
|
@ -628,13 +632,16 @@ def _handle_remote_command(self, cmd: str, window: Optional[Window] = None, peer
|
|||
extra_data: Dict[str, Any] = {}
|
||||
try:
|
||||
allowed_unconditionally = (
|
||||
self.allow_remote_control == 'y' or (peer_id > 0 and self.allow_remote_control in ('socket-only', 'socket')) or
|
||||
(window and window.remote_control_allowed(pcmd, extra_data)))
|
||||
self.allow_remote_control == 'y' or
|
||||
(from_socket and not is_fd_peer and self.allow_remote_control in ('socket-only', 'socket')) or
|
||||
(window and window.remote_control_allowed(pcmd, extra_data)) or
|
||||
(is_fd_peer and remote_control_allowed(pcmd, self.peer_data_map.get(peer_id), None, extra_data))
|
||||
)
|
||||
except PermissionError:
|
||||
return {'ok': False, 'error': 'Remote control disallowed by window specific password'}
|
||||
if allowed_unconditionally:
|
||||
return self._execute_remote_command(pcmd, window, peer_id, self_window)
|
||||
q = is_cmd_allowed(pcmd, window, peer_id > 0, extra_data)
|
||||
q = is_cmd_allowed(pcmd, window, from_socket, extra_data)
|
||||
if q is True:
|
||||
return self._execute_remote_command(pcmd, window, peer_id, self_window)
|
||||
if q is None:
|
||||
|
|
@ -761,20 +768,30 @@ def call_remote_control(self, self_window: Optional[Window], args: Tuple[str, ..
|
|||
return None
|
||||
raise
|
||||
|
||||
def peer_message_received(self, msg_bytes: bytes, peer_id: int) -> Union[bytes, bool, None]:
|
||||
cmd_prefix = b'\x1bP@kitty-cmd'
|
||||
terminator = b'\x1b\\'
|
||||
if msg_bytes.startswith(cmd_prefix) and msg_bytes.endswith(terminator):
|
||||
cmd = msg_bytes[len(cmd_prefix):-len(terminator)].decode('utf-8')
|
||||
response = self._handle_remote_command(cmd, peer_id=peer_id)
|
||||
if response is None:
|
||||
return None
|
||||
if isinstance(response, AsyncResponse):
|
||||
return True
|
||||
from kitty.remote_control import encode_response_for_peer
|
||||
return encode_response_for_peer(response)
|
||||
def peer_message_received(self, msg_bytes: bytes, peer_id: int, is_remote_control: bool) -> Union[bytes, bool, None]:
|
||||
if peer_id > 0 and msg_bytes == b'peer_death':
|
||||
self.peer_data_map.pop(peer_id, None)
|
||||
return False
|
||||
if is_remote_control:
|
||||
cmd_prefix = b'\x1bP@kitty-cmd'
|
||||
terminator = b'\x1b\\'
|
||||
if msg_bytes.startswith(cmd_prefix) and msg_bytes.endswith(terminator):
|
||||
cmd = msg_bytes[len(cmd_prefix):-len(terminator)].decode('utf-8')
|
||||
response = self._handle_remote_command(cmd, peer_id=peer_id)
|
||||
if response is None:
|
||||
return None
|
||||
if isinstance(response, AsyncResponse):
|
||||
return True
|
||||
from kitty.remote_control import encode_response_for_peer
|
||||
return encode_response_for_peer(response)
|
||||
log_error('Malformatted remote control message received from peer, ignoring')
|
||||
return None
|
||||
|
||||
data:SingleInstanceData = json.loads(msg_bytes.decode('utf-8'))
|
||||
try:
|
||||
data:SingleInstanceData = json.loads(msg_bytes.decode('utf-8'))
|
||||
except Exception:
|
||||
log_error('Malformed command received over single instance socket, ignoring')
|
||||
return None
|
||||
if isinstance(data, dict) and data.get('cmd') == 'new_instance':
|
||||
from .cli_stub import CLIOptions
|
||||
startup_id = data['environ'].get('DESKTOP_STARTUP_ID', '')
|
||||
|
|
@ -812,7 +829,7 @@ def peer_message_received(self, msg_bytes: bytes, peer_id: int) -> Union[bytes,
|
|||
elif activation_token and is_wayland() and os_window_id:
|
||||
focus_os_window(os_window_id, True, activation_token)
|
||||
else:
|
||||
log_error('Unknown message received from peer, ignoring')
|
||||
log_error('Unknown message received over single instance socket, ignoring')
|
||||
return None
|
||||
|
||||
def handle_remote_cmd(self, cmd: str, window: Optional[Window] = None) -> None:
|
||||
|
|
@ -2232,7 +2249,9 @@ def run_background_process(
|
|||
cwd: Optional[str] = None,
|
||||
env: Optional[Dict[str, str]] = None,
|
||||
stdin: Optional[bytes] = None,
|
||||
cwd_from: Optional[CwdRequest] = None
|
||||
cwd_from: Optional[CwdRequest] = None,
|
||||
allow_remote_control: bool = False,
|
||||
remote_control_passwords: Optional[Dict[str, Sequence[str]]] = None,
|
||||
) -> None:
|
||||
import subprocess
|
||||
env = env or None
|
||||
|
|
@ -2244,28 +2263,56 @@ def run_background_process(
|
|||
with suppress(Exception):
|
||||
cwd = cwd_from.cwd_of_child
|
||||
|
||||
def add_env(key: str, val: str) -> None:
|
||||
nonlocal env
|
||||
if env is None:
|
||||
env = default_env().copy()
|
||||
env[key] = val
|
||||
|
||||
def doit(activation_token: str = '') -> None:
|
||||
nonlocal env
|
||||
if activation_token:
|
||||
if env is None:
|
||||
env = default_env().copy()
|
||||
env['XDG_ACTIVATION_TOKEN'] = activation_token
|
||||
if stdin:
|
||||
r, w = safe_pipe(False)
|
||||
pass_fds: Tuple[int, ...] = ()
|
||||
if allow_remote_control:
|
||||
import socket
|
||||
local, remote = socket.socketpair()
|
||||
os.set_inheritable(remote.fileno(), True)
|
||||
lfd = os.dup(local.fileno())
|
||||
local.close()
|
||||
try:
|
||||
subprocess.Popen(cmd, env=env, stdin=r, cwd=cwd, preexec_fn=clear_handled_signals)
|
||||
peer_id = self.child_monitor.inject_peer(lfd)
|
||||
except Exception:
|
||||
os.close(w)
|
||||
os.close(lfd)
|
||||
remote.close()
|
||||
raise
|
||||
pass_fds = (remote.fileno(),)
|
||||
add_env('KITTY_LISTEN_ON', f'fd:{remote.fileno()}')
|
||||
self.peer_data_map[peer_id] = remote_control_passwords
|
||||
if activation_token:
|
||||
add_env('XDG_ACTIVATION_TOKEN', activation_token)
|
||||
try:
|
||||
if stdin:
|
||||
r, w = safe_pipe(False)
|
||||
try:
|
||||
subprocess.Popen(cmd, env=env, stdin=r, cwd=cwd, preexec_fn=clear_handled_signals, pass_fds=pass_fds, close_fds=True)
|
||||
except Exception:
|
||||
os.close(w)
|
||||
else:
|
||||
thread_write(w, stdin)
|
||||
finally:
|
||||
os.close(r)
|
||||
else:
|
||||
thread_write(w, stdin)
|
||||
finally:
|
||||
os.close(r)
|
||||
subprocess.Popen(cmd, env=env, cwd=cwd, preexec_fn=clear_handled_signals, pass_fds=pass_fds, close_fds=True)
|
||||
finally:
|
||||
if allow_remote_control:
|
||||
remote.close()
|
||||
|
||||
try:
|
||||
if is_wayland():
|
||||
run_with_activation_token(doit)
|
||||
else:
|
||||
subprocess.Popen(cmd, env=env, cwd=cwd, preexec_fn=clear_handled_signals)
|
||||
if is_wayland():
|
||||
run_with_activation_token(doit)
|
||||
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]:
|
||||
cmd = [exe] + list(args)
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ typedef struct {
|
|||
char *data;
|
||||
size_t sz;
|
||||
id_type peer_id;
|
||||
bool is_remote_control_peer;
|
||||
} Message;
|
||||
|
||||
typedef struct {
|
||||
|
|
@ -232,8 +233,49 @@ static void* io_loop(void *data);
|
|||
static void* talk_loop(void *data);
|
||||
static void send_response_to_peer(id_type peer_id, const char *msg, size_t msg_sz);
|
||||
static void wakeup_talk_loop(bool);
|
||||
static bool add_peer_to_injection_queue(int peer_fd, int pipe_fd);
|
||||
static bool talk_thread_started = false;
|
||||
|
||||
static bool
|
||||
simple_read_from_pipe(int fd, void *data, size_t sz) {
|
||||
// read a small amount of data to a pipe handling only EINTR
|
||||
while (true) {
|
||||
ssize_t ret = read(fd, data, sz);
|
||||
if (ret == -1 && errno == EINTR) continue;
|
||||
return ret == (ssize_t)sz;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static PyObject*
|
||||
inject_peer(PyObject *s, PyObject *a) {
|
||||
#define inject_peer_doc "inject_peer(fd) -> Start communication with a peer over the specified file descriptor"
|
||||
ChildMonitor *self = (ChildMonitor*)s;
|
||||
if (!PyLong_Check(a)) { PyErr_SetString(PyExc_TypeError, "peer fd must be an int"); return NULL; }
|
||||
long fd = PyLong_AsLong(a);
|
||||
if (fd < 0) { PyErr_Format(PyExc_ValueError, "Invalid peer fd: %ld", fd); return NULL; }
|
||||
if (!talk_thread_started) {
|
||||
int ret;
|
||||
if ((ret = pthread_create(&self->talk_thread, NULL, talk_loop, self)) != 0) {
|
||||
return PyErr_Format(PyExc_OSError, "Failed to start talk thread with error: %s", strerror(ret));
|
||||
}
|
||||
talk_thread_started = true;
|
||||
}
|
||||
int fds[2] = {0};
|
||||
if (!self_pipe(fds, false)) return PyErr_SetFromErrno(PyExc_OSError);
|
||||
if (!add_peer_to_injection_queue(fd, fds[1])) {
|
||||
safe_close(fds[0], __FILE__, __LINE__); safe_close(fds[1], __FILE__, __LINE__);
|
||||
PyErr_SetString(PyExc_RuntimeError, "Too many peers waiting to be injected");
|
||||
return NULL;
|
||||
}
|
||||
wakeup_talk_loop(false);
|
||||
id_type peer_id = 0;
|
||||
bool ok = simple_read_from_pipe(fds[0], &peer_id, sizeof(peer_id));
|
||||
safe_close(fds[0], __FILE__, __LINE__);
|
||||
if (!ok) { PyErr_SetString(PyExc_RuntimeError, "Failed to read peer id from self pipe"); return NULL; }
|
||||
return PyLong_FromUnsignedLongLong(peer_id);
|
||||
}
|
||||
|
||||
static PyObject *
|
||||
start(PyObject *s, PyObject *a UNUSED) {
|
||||
#define start_doc "start() -> Start the I/O thread"
|
||||
|
|
@ -467,7 +509,7 @@ parse_input(ChildMonitor *self) {
|
|||
Message *msg = msgs + i;
|
||||
PyObject *resp = NULL;
|
||||
if (msg->data) {
|
||||
resp = PyObject_CallMethod(global_state.boss, "peer_message_received", "y#K", msg->data, (int)msg->sz, msg->peer_id);
|
||||
resp = PyObject_CallMethod(global_state.boss, "peer_message_received", "y#KO", msg->data, (int)msg->sz, msg->peer_id, msg->is_remote_control_peer ? Py_True : Py_False);
|
||||
free(msg->data);
|
||||
if (!resp) PyErr_Print();
|
||||
}
|
||||
|
|
@ -1563,6 +1605,7 @@ typedef struct {
|
|||
size_t capacity, used;
|
||||
bool failed;
|
||||
} write;
|
||||
bool is_remote_control_peer;
|
||||
} Peer;
|
||||
static id_type peer_id_counter = 0;
|
||||
|
||||
|
|
@ -1577,24 +1620,33 @@ typedef struct pollfd PollFD;
|
|||
#define PEER_LIMIT 256
|
||||
#define nuke_socket(s) { shutdown(s, SHUT_RDWR); safe_close(s, __FILE__, __LINE__); }
|
||||
|
||||
static bool
|
||||
accept_peer(int listen_fd, bool shutting_down) {
|
||||
int peer = accept(listen_fd, NULL, NULL);
|
||||
if (UNLIKELY(peer == -1)) {
|
||||
if (errno == EINTR) return true;
|
||||
if (!shutting_down) perror("accept() on talk socket failed!");
|
||||
return false;
|
||||
}
|
||||
static id_type
|
||||
add_peer(int peer, bool is_remote_control_peer) {
|
||||
id_type ans = 0;
|
||||
if (talk_data.num_peers < PEER_LIMIT) {
|
||||
ensure_space_for(&talk_data, peers, Peer, talk_data.num_peers + 8, peers_capacity, 8, false);
|
||||
Peer *p = talk_data.peers + talk_data.num_peers++;
|
||||
memset(p, 0, sizeof(Peer));
|
||||
p->fd = peer; p->id = ++peer_id_counter;
|
||||
if (!p->id) p->id = ++peer_id_counter;
|
||||
ans = p->id;
|
||||
p->is_remote_control_peer = is_remote_control_peer;
|
||||
} else {
|
||||
log_error("Too many peers want to talk, ignoring one.");
|
||||
nuke_socket(peer);
|
||||
}
|
||||
return ans;
|
||||
}
|
||||
|
||||
static bool
|
||||
accept_peer(int listen_fd, bool shutting_down, bool is_remote_control_peer) {
|
||||
int peer = accept(listen_fd, NULL, NULL);
|
||||
if (UNLIKELY(peer == -1)) {
|
||||
if (errno == EINTR) return true;
|
||||
if (!shutting_down) perror("accept() on talk socket failed!");
|
||||
return false;
|
||||
}
|
||||
add_peer(peer, is_remote_control_peer);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -1621,11 +1673,23 @@ queue_peer_message(ChildMonitor *self, Peer *peer) {
|
|||
}
|
||||
}
|
||||
m->peer_id = peer->id;
|
||||
m->is_remote_control_peer = peer->is_remote_control_peer;
|
||||
peer->num_of_unresponded_messages_sent_to_main_thread++;
|
||||
talk_mutex(unlock);
|
||||
wakeup_main_loop();
|
||||
}
|
||||
|
||||
static void
|
||||
notify_on_peer_removal(ChildMonitor *self, const Peer *p) {
|
||||
ensure_space_for(self, messages, Message, self->messages_count + 16, messages_capacity, 16, true);
|
||||
Message *m = self->messages + self->messages_count++;
|
||||
memset(m, 0, sizeof(Message));
|
||||
m->data = strdup("peer_death");
|
||||
if (m->data) m->sz = strlen("peer_death");
|
||||
m->peer_id = p->id;
|
||||
m->is_remote_control_peer = p->id;
|
||||
}
|
||||
|
||||
static bool
|
||||
has_complete_peer_command(Peer *peer) {
|
||||
peer->read.command_end = 0;
|
||||
|
|
@ -1702,17 +1766,52 @@ wakeup_talk_loop(bool in_signal_handler) {
|
|||
}
|
||||
|
||||
|
||||
static void
|
||||
prune_peers(void) {
|
||||
static bool
|
||||
prune_peers(ChildMonitor *self) {
|
||||
bool pruned = false;
|
||||
for (size_t idx = talk_data.num_peers; idx-- > 0;) {
|
||||
Peer *p = talk_data.peers + idx;
|
||||
if (p->read.finished && !p->num_of_unresponded_messages_sent_to_main_thread && !p->write.used) {
|
||||
notify_on_peer_removal(self, p);
|
||||
free_peer(p);
|
||||
remove_i_from_array(talk_data.peers, idx, talk_data.num_peers);
|
||||
pruned = true;
|
||||
}
|
||||
}
|
||||
if (pruned) wakeup_main_loop();
|
||||
return pruned;
|
||||
}
|
||||
|
||||
static struct {
|
||||
size_t num;
|
||||
struct { int peer_fd, pipe_fd; } fds[16];
|
||||
} peers_to_inject = {0};
|
||||
|
||||
static bool
|
||||
add_peer_to_injection_queue(int peer_fd, int pipe_fd) {
|
||||
bool added = false;
|
||||
talk_mutex(lock);
|
||||
if (peers_to_inject.num < arraysz(peers_to_inject.fds)) {
|
||||
peers_to_inject.fds[peers_to_inject.num].peer_fd = peer_fd;
|
||||
peers_to_inject.fds[peers_to_inject.num].pipe_fd = pipe_fd;
|
||||
peers_to_inject.num++;
|
||||
added = true;
|
||||
}
|
||||
talk_mutex(unlock);
|
||||
return added;
|
||||
}
|
||||
|
||||
static void
|
||||
simple_write_to_pipe(int fd, void *data, size_t sz) {
|
||||
// write a small amount of data to a pipe handling only EINTR
|
||||
while (true) {
|
||||
ssize_t ret = write(fd, data, sz);
|
||||
if (ret == -1 && errno == EINTR) continue;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static void*
|
||||
talk_loop(void *data) {
|
||||
// The talk thread loop
|
||||
|
|
@ -1731,9 +1830,17 @@ talk_loop(void *data) {
|
|||
|
||||
while (LIKELY(!self->shutting_down)) {
|
||||
num_peer_fds = 0;
|
||||
talk_mutex(lock);
|
||||
if (peers_to_inject.num) {
|
||||
for (size_t i = 0; i < peers_to_inject.num; i++) {
|
||||
id_type added_peer_id = add_peer(peers_to_inject.fds[i].peer_fd, true);
|
||||
simple_write_to_pipe(peers_to_inject.fds[i].pipe_fd, &added_peer_id, sizeof(id_type));
|
||||
safe_close(peers_to_inject.fds[i].pipe_fd, __FILE__, __LINE__);
|
||||
}
|
||||
peers_to_inject.num = 0;
|
||||
}
|
||||
if (talk_data.num_peers > 0) {
|
||||
talk_mutex(lock);
|
||||
prune_peers();
|
||||
prune_peers(self);
|
||||
for (size_t i = 0; i < talk_data.num_peers; i++) {
|
||||
Peer *p = talk_data.peers + i;
|
||||
if (!p->read.finished || p->write.used) {
|
||||
|
|
@ -1746,14 +1853,14 @@ talk_loop(void *data) {
|
|||
fds[p->fd_array_idx].events = flags;
|
||||
} else p->fd_array_idx = 0;
|
||||
}
|
||||
talk_mutex(unlock);
|
||||
}
|
||||
talk_mutex(unlock);
|
||||
for (size_t i = 0; i < num_listen_fds; i++) fds[i].revents = 0;
|
||||
int ret = poll(fds, num_listen_fds + num_peer_fds, -1);
|
||||
if (ret > 0) {
|
||||
for (size_t i = 0; i < num_listen_fds - 1; i++) {
|
||||
if (fds[i].revents & POLLIN) {
|
||||
if (!accept_peer(fds[i].fd, self->shutting_down)) goto end;
|
||||
if (!accept_peer(fds[i].fd, self->shutting_down, fds[i].fd == self->listen_fd)) goto end;
|
||||
}
|
||||
}
|
||||
if (fds[num_listen_fds - 1].revents & POLLIN) {
|
||||
|
|
@ -1814,6 +1921,7 @@ send_response_to_peer(id_type peer_id, const char *msg, size_t msg_sz) {
|
|||
// Boilerplate {{{
|
||||
static PyMethodDef methods[] = {
|
||||
METHOD(add_child, METH_VARARGS)
|
||||
METHOD(inject_peer, METH_O)
|
||||
METHOD(needs_write, METH_VARARGS)
|
||||
METHOD(start, METH_NOARGS)
|
||||
METHOD(wakeup, METH_NOARGS)
|
||||
|
|
|
|||
|
|
@ -1271,6 +1271,8 @@ class ChildMonitor:
|
|||
def shutdown_monitor(self) -> None:
|
||||
pass
|
||||
|
||||
def inject_peer(self, fd: int) -> int: ...
|
||||
|
||||
|
||||
class KeyEvent:
|
||||
|
||||
|
|
|
|||
|
|
@ -71,7 +71,11 @@ def options_spec() -> str:
|
|||
intended to run for a long time as a primary window.
|
||||
|
||||
:code:`background`
|
||||
The process will be run in the :italic:`background`, without a kitty window.
|
||||
The process will be run in the :italic:`background`, without a kitty
|
||||
window. Note that if :option:`kitty @ launch --allow-remote-control` is
|
||||
specified the :envvar:`KITTY_LISTEN_ON` environment variable will be set to
|
||||
a dedicated socket pair file descriptor that the process can use for remote
|
||||
control.
|
||||
|
||||
:code:`clipboard`, :code:`primary`
|
||||
These two are meant to work with :option:`--stdin-source <launch --stdin-source>` to copy
|
||||
|
|
@ -583,7 +587,10 @@ def _launch(
|
|||
cmd = kw['cmd']
|
||||
if not cmd:
|
||||
raise ValueError('The cmd to run must be specified when running a background process')
|
||||
boss.run_background_process(cmd, cwd=kw['cwd'], cwd_from=kw['cwd_from'], env=env or None, stdin=kw['stdin'])
|
||||
boss.run_background_process(
|
||||
cmd, cwd=kw['cwd'], cwd_from=kw['cwd_from'], env=env or None, stdin=kw['stdin'],
|
||||
allow_remote_control=kw['allow_remote_control'], remote_control_passwords=kw['remote_control_passwords']
|
||||
)
|
||||
elif opts.type in ('clipboard', 'primary'):
|
||||
stdin = kw.get('stdin')
|
||||
if stdin is not None:
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
Iterator,
|
||||
List,
|
||||
Optional,
|
||||
Sequence,
|
||||
Tuple,
|
||||
Union,
|
||||
cast,
|
||||
|
|
@ -104,6 +105,27 @@ def fnmatch_pattern(pat: str) -> 're.Pattern[str]':
|
|||
return re.compile(translate(pat))
|
||||
|
||||
|
||||
def remote_control_allowed(
|
||||
pcmd: Dict[str, Any], remote_control_passwords: Optional[Dict[str, Sequence[str]]],
|
||||
window: Optional['Window'], extra_data: Dict[str, Any]
|
||||
) -> bool:
|
||||
if not remote_control_passwords:
|
||||
return True
|
||||
pw = pcmd.get('password', '')
|
||||
auth_items = remote_control_passwords.get(pw)
|
||||
if pw == '!':
|
||||
auth_items = None
|
||||
if auth_items is None:
|
||||
if '!' in remote_control_passwords:
|
||||
raise PermissionError()
|
||||
return False
|
||||
from .remote_control import password_authorizer
|
||||
pa = password_authorizer(auth_items)
|
||||
if not pa.is_cmd_allowed(pcmd, window, False, extra_data):
|
||||
raise PermissionError()
|
||||
return True
|
||||
|
||||
|
||||
class PasswordAuthorizer:
|
||||
|
||||
def __init__(self, auth_items: FrozenSet[str]) -> None:
|
||||
|
|
|
|||
|
|
@ -557,21 +557,8 @@ def __init__(
|
|||
def remote_control_allowed(self, pcmd: Dict[str, Any], extra_data: Dict[str, Any]) -> bool:
|
||||
if not self.allow_remote_control:
|
||||
return False
|
||||
if not self.remote_control_passwords:
|
||||
return True
|
||||
pw = pcmd.get('password', '')
|
||||
auth_items = self.remote_control_passwords.get(pw)
|
||||
if pw == '!':
|
||||
auth_items = None
|
||||
if auth_items is None:
|
||||
if '!' in self.remote_control_passwords:
|
||||
raise PermissionError()
|
||||
return False
|
||||
from .remote_control import password_authorizer
|
||||
pa = password_authorizer(auth_items)
|
||||
if not pa.is_cmd_allowed(pcmd, self, False, extra_data):
|
||||
raise PermissionError()
|
||||
return True
|
||||
from .remote_control import remote_control_allowed
|
||||
return remote_control_allowed(pcmd, self.remote_control_passwords, self, extra_data)
|
||||
|
||||
@property
|
||||
def file_transmission_control(self) -> 'FileTransmission':
|
||||
|
|
|
|||
|
|
@ -55,14 +55,6 @@ func escape_list_of_strings(args []string) []escaped_string {
|
|||
return ans
|
||||
}
|
||||
|
||||
func escape_dict_of_strings(args map[string]string) map[escaped_string]escaped_string {
|
||||
ans := make(map[escaped_string]escaped_string, len(args))
|
||||
for k, v := range args {
|
||||
ans[escaped_string(k)] = escaped_string(v)
|
||||
}
|
||||
return ans
|
||||
}
|
||||
|
||||
func set_payload_string_field(io_data *rc_io_data, field, data string) {
|
||||
payload_interface := reflect.ValueOf(&io_data.rc.Payload).Elem()
|
||||
struct_in_interface := reflect.New(payload_interface.Elem().Type()).Elem()
|
||||
|
|
@ -143,16 +135,6 @@ func simple_serializer(rc *utils.RemoteControlCmd) (ans []byte, err error) {
|
|||
|
||||
type serializer_func func(rc *utils.RemoteControlCmd) ([]byte, error)
|
||||
|
||||
func debug_to_log(args ...any) {
|
||||
f, err := os.OpenFile("/tmp/kdlog", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0666)
|
||||
if err == nil {
|
||||
fmt.Fprintln(f, args...)
|
||||
f.Close()
|
||||
}
|
||||
}
|
||||
|
||||
var serializer serializer_func = simple_serializer
|
||||
|
||||
func create_serializer(password string, encoded_pubkey string, io_data *rc_io_data) (err error) {
|
||||
io_data.serializer = simple_serializer
|
||||
if password != "" {
|
||||
|
|
@ -240,7 +222,7 @@ func get_response(do_io func(io_data *rc_io_data) ([]byte, error), io_data *rc_i
|
|||
io_data.multiple_payload_generator = nil
|
||||
io_data.rc.NoResponse = true
|
||||
io_data.chunks_done = false
|
||||
do_io(io_data)
|
||||
_, _ = do_io(io_data)
|
||||
err = fmt.Errorf("Timed out waiting for a response from kitty")
|
||||
}
|
||||
return
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ import (
|
|||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"kitty/tools/tui/loop"
|
||||
|
|
@ -151,9 +153,23 @@ func simple_socket_io(conn *net.Conn, io_data *rc_io_data) (serialized_response
|
|||
}
|
||||
|
||||
func do_socket_io(io_data *rc_io_data) (serialized_response []byte, err error) {
|
||||
conn, err := net.Dial(global_options.to_network, global_options.to_address)
|
||||
if err != nil {
|
||||
return
|
||||
var conn net.Conn
|
||||
if global_options.to_network == "fd" {
|
||||
fd, _ := strconv.Atoi(global_options.to_address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
f := os.NewFile(uintptr(fd), "fd:"+global_options.to_address)
|
||||
conn, err = net.FileConn(f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
} else {
|
||||
conn, err = net.Dial(global_options.to_network, global_options.to_address)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
defer conn.Close()
|
||||
return simple_socket_io(&conn, io_data)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ package utils
|
|||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/seancfoley/ipaddress-go/ipaddr"
|
||||
|
|
@ -37,6 +38,13 @@ func ParseSocketAddress(spec string) (network string, addr string, err error) {
|
|||
}
|
||||
return
|
||||
}
|
||||
if network == "fd" {
|
||||
fd := -1
|
||||
if fd, err = strconv.Atoi(addr); err != nil || fd < 0 {
|
||||
err = fmt.Errorf("Not a valid file descriptor number: %#v. Cannot use: %s", addr, spec)
|
||||
}
|
||||
return
|
||||
}
|
||||
err = fmt.Errorf("Unknown network type: %#v in socket address: %s", network, spec)
|
||||
return
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue