From 314fe4fe4a6653d9a321ccaaf497dc4051a8c1ef Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 18 Oct 2023 19:56:07 +0530 Subject: [PATCH] Allow launched background process to work with --allow-remote-control Use a dedicated socketpair for such processes. Fixes #6712 --- docs/changelog.rst | 2 + docs/glossary.rst | 4 +- kitty/boss.py | 121 +++++++++++++++++++++++---------- kitty/child-monitor.c | 138 +++++++++++++++++++++++++++++++++----- kitty/fast_data_types.pyi | 2 + kitty/launch.py | 11 ++- kitty/remote_control.py | 22 ++++++ kitty/window.py | 17 +---- tools/cmd/at/main.go | 20 +----- tools/cmd/at/socket_io.go | 22 +++++- tools/utils/sockets.go | 8 +++ 11 files changed, 275 insertions(+), 92 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 306a327ca..06372567a 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -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`) diff --git a/docs/glossary.rst b/docs/glossary.rst index a4fdde0c6..1ad442eda 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -180,7 +180,9 @@ Variables that kitty sets when running child programs Set when the :doc:`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 diff --git a/kitty/boss.py b/kitty/boss.py index 581ae97e5..472c8c0a4 100644 --- a/kitty/boss.py +++ b/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) diff --git a/kitty/child-monitor.c b/kitty/child-monitor.c index e99180223..2e4878295 100644 --- a/kitty/child-monitor.c +++ b/kitty/child-monitor.c @@ -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) diff --git a/kitty/fast_data_types.pyi b/kitty/fast_data_types.pyi index 7b4b7df72..a1b79a272 100644 --- a/kitty/fast_data_types.pyi +++ b/kitty/fast_data_types.pyi @@ -1271,6 +1271,8 @@ class ChildMonitor: def shutdown_monitor(self) -> None: pass + def inject_peer(self, fd: int) -> int: ... + class KeyEvent: diff --git a/kitty/launch.py b/kitty/launch.py index 10fc950a1..08a8d9c0b 100644 --- a/kitty/launch.py +++ b/kitty/launch.py @@ -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 ` 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: diff --git a/kitty/remote_control.py b/kitty/remote_control.py index 624093410..813fa9696 100644 --- a/kitty/remote_control.py +++ b/kitty/remote_control.py @@ -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: diff --git a/kitty/window.py b/kitty/window.py index f6d50222a..c618ce81b 100644 --- a/kitty/window.py +++ b/kitty/window.py @@ -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': diff --git a/tools/cmd/at/main.go b/tools/cmd/at/main.go index 81ada2c0a..7b2555664 100644 --- a/tools/cmd/at/main.go +++ b/tools/cmd/at/main.go @@ -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 diff --git a/tools/cmd/at/socket_io.go b/tools/cmd/at/socket_io.go index a98d65588..0389ad6d6 100644 --- a/tools/cmd/at/socket_io.go +++ b/tools/cmd/at/socket_io.go @@ -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) diff --git a/tools/utils/sockets.go b/tools/utils/sockets.go index fd23eadbe..8d7964ba6 100644 --- a/tools/utils/sockets.go +++ b/tools/utils/sockets.go @@ -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 }