Finish remote control via socket implementation
This commit is contained in:
parent
ec989a45b5
commit
15f07f57bf
@ -26,8 +26,8 @@ from .session import create_session
|
||||
from .tabs import SpecialWindow, SpecialWindowInstance, TabManager
|
||||
from .utils import (
|
||||
end_startup_notification, get_primary_selection, init_startup_notification,
|
||||
open_url, remove_socket_file, safe_print, set_primary_selection,
|
||||
single_instance
|
||||
open_url, parse_address_spec, remove_socket_file, safe_print,
|
||||
set_primary_selection, single_instance
|
||||
)
|
||||
|
||||
|
||||
@ -37,21 +37,7 @@ def initialize_renderer():
|
||||
|
||||
|
||||
def listen_on(spec):
|
||||
protocol, rest = spec.split(':', 1)
|
||||
socket_path = None
|
||||
if protocol == 'unix':
|
||||
family = socket.AF_UNIX
|
||||
address = rest
|
||||
if address.startswith('@') and len(address) > 1:
|
||||
address = '\0' + address[1:]
|
||||
else:
|
||||
socket_path = address
|
||||
elif protocol in ('tcp', 'tcp6'):
|
||||
family = socket.AF_INET if protocol == 'tcp' else socket.AF_INET6
|
||||
host, port = rest.rsplit(':', 1)
|
||||
address = host, int(port)
|
||||
else:
|
||||
raise ValueError('Unknown protocol in --listen-on value: {}'.format(spec))
|
||||
family, address, socket_path = parse_address_spec(spec)
|
||||
s = socket.socket(family)
|
||||
atexit.register(remove_socket_file, s, socket_path)
|
||||
s.bind(address)
|
||||
@ -195,20 +181,7 @@ class Boss:
|
||||
self.child_monitor.add_child(window.id, window.child.pid, window.child.child_fd, window.screen)
|
||||
self.window_id_map[window.id] = window
|
||||
|
||||
def peer_message_received(self, msg):
|
||||
import json
|
||||
msg = json.loads(msg.decode('utf-8'))
|
||||
if isinstance(msg, dict) and msg.get('cmd') == 'new_instance':
|
||||
startup_id = msg.get('startup_id')
|
||||
args, rest = parse_args(msg['args'][1:])
|
||||
args.args = rest
|
||||
opts = create_opts(args)
|
||||
session = create_session(opts, args)
|
||||
self.add_os_window(session, wclass=args.cls, wname=args.name, size=initial_window_size(opts, self.cached_values), startup_id=startup_id)
|
||||
else:
|
||||
safe_print('Unknown message received from peer, ignoring')
|
||||
|
||||
def handle_remote_cmd(self, cmd, window=None):
|
||||
def _handle_remote_command(self, cmd, window=None):
|
||||
response = None
|
||||
if self.opts.allow_remote_control:
|
||||
try:
|
||||
@ -218,6 +191,32 @@ class Boss:
|
||||
response = {'ok': False, 'error': str(err), 'tb': traceback.format_exc()}
|
||||
else:
|
||||
response = {'ok': False, 'error': 'Remote control is disabled. Add allow_remote_control yes to your kitty.conf'}
|
||||
return response
|
||||
|
||||
def peer_message_received(self, msg):
|
||||
import json
|
||||
msg = msg.decode('utf-8')
|
||||
cmd_prefix = '\x1bP@kitty-cmd'
|
||||
if msg.startswith(cmd_prefix):
|
||||
cmd = msg[len(cmd_prefix):-2]
|
||||
response = self._handle_remote_command(cmd)
|
||||
if response is not None:
|
||||
response = (cmd_prefix + json.dumps(response) + '\x1b\\').encode('utf-8')
|
||||
return response
|
||||
else:
|
||||
msg = json.loads(msg)
|
||||
if isinstance(msg, dict) and msg.get('cmd') == 'new_instance':
|
||||
startup_id = msg.get('startup_id')
|
||||
args, rest = parse_args(msg['args'][1:])
|
||||
args.args = rest
|
||||
opts = create_opts(args)
|
||||
session = create_session(opts, args)
|
||||
self.add_os_window(session, wclass=args.cls, wname=args.name, size=initial_window_size(opts, self.cached_values), startup_id=startup_id)
|
||||
else:
|
||||
safe_print('Unknown message received from peer, ignoring')
|
||||
|
||||
def handle_remote_cmd(self, cmd, window=None):
|
||||
response = self._handle_remote_command(cmd, window)
|
||||
if response is not None:
|
||||
if window is not None:
|
||||
window.send_cmd_response(response)
|
||||
|
||||
@ -354,7 +354,7 @@ parse_input(ChildMonitor *self) {
|
||||
for (Py_ssize_t i = 0; i < PyTuple_GET_SIZE(msg); i++) {
|
||||
PyObject *resp = PyObject_CallMethod(global_state.boss, "peer_message_received", "O", PyTuple_GET_ITEM(PyTuple_GET_ITEM(msg, i), 0));
|
||||
int peer_fd = (int)PyLong_AsLong(PyTuple_GET_ITEM(PyTuple_GET_ITEM(msg, i), 1));
|
||||
if (resp && PyBytes_Check(resp)) send_response(peer_fd, PyBytes_AS_STRING(PyTuple_GET_ITEM(PyTuple_GET_ITEM(msg, i), 0)), PyBytes_GET_SIZE(PyTuple_GET_ITEM(PyTuple_GET_ITEM(msg, i), 0)));
|
||||
if (resp && PyBytes_Check(resp)) send_response(peer_fd, PyBytes_AS_STRING(resp), PyBytes_GET_SIZE(resp));
|
||||
else { send_response(peer_fd, NULL, 0); if (!resp) PyErr_Print(); }
|
||||
Py_CLEAR(resp);
|
||||
}
|
||||
@ -1143,7 +1143,7 @@ wakeup_talk_loop(bool in_signal_handler) {
|
||||
static inline void
|
||||
move_queued_writes() {
|
||||
while (talk_data.num_queued_writes) {
|
||||
PeerWriteData *src = talk_data.queued_writes + talk_data.num_queued_writes--;
|
||||
PeerWriteData *src = talk_data.queued_writes + --talk_data.num_queued_writes;
|
||||
size_t fd_idx = talk_data.num_listen_fds + talk_data.num_talk_fds;
|
||||
if (fd_idx < arraysz(talk_data.fds) && talk_data.num_writes < arraysz(talk_data.writes)) {
|
||||
talk_data.fds[fd_idx].fd = src->fd; talk_data.fds[fd_idx].events = POLLOUT;
|
||||
|
||||
13
kitty/cli.py
13
kitty/cli.py
@ -85,12 +85,13 @@ in the first |_ {appname}| instance within that group
|
||||
|
||||
|
||||
--listen-on
|
||||
Tell kitty to listen on the specified UNIX socket or TCP port for control
|
||||
messages. For example, --listen-on=unix:/tmp/mykitty or
|
||||
--listen-on=tcp:localhost:12345. On Linux systems, you can also use abstract
|
||||
UNIX sockets, not associated with a file, like this: --listen-on=unix:@mykitty.
|
||||
Note that this option will be ignored, unless you set allow_remote_control to yes
|
||||
in kitty.conf
|
||||
Tell kitty to listen on the specified address for control
|
||||
messages. For example, |_ --listen-on=unix:/tmp/mykitty| or
|
||||
|_ --listen-on=tcp:localhost:12345|. On Linux systems, you can also use abstract
|
||||
UNIX sockets, not associated with a file, like this: |_ --listen-on=unix:@mykitty|.
|
||||
To control kitty, you can send it commands with |_ kitty @| using the |_ --to| option
|
||||
to specify this address. Note that this option will be ignored, unless you set
|
||||
|_ allow_remote_control| to yes in |_ kitty.conf|
|
||||
|
||||
|
||||
# Debugging options
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
|
||||
import json
|
||||
import re
|
||||
import socket
|
||||
import sys
|
||||
import types
|
||||
from functools import partial
|
||||
@ -12,7 +13,7 @@ from .cli import emph, parse_args
|
||||
from .config import parse_send_text_bytes
|
||||
from .constants import appname, version
|
||||
from .tabs import SpecialWindow
|
||||
from .utils import non_blocking_read, read_with_timeout
|
||||
from .utils import non_blocking_read, parse_address_spec, read_with_timeout
|
||||
|
||||
|
||||
def cmd(short_desc, desc=None, options_spec=None, no_response=False):
|
||||
@ -411,14 +412,28 @@ def handle_cmd(boss, window, cmd):
|
||||
|
||||
|
||||
global_options_spec = partial('''\
|
||||
|
||||
--to
|
||||
An address for the kitty instance to control. Corresponds to the address
|
||||
given to the kitty instance via the --listen-on option. If not specified,
|
||||
messages are sent to the controlling terminal for this process, i.e. they
|
||||
will only work if this process is run within an existing kitty window.
|
||||
'''.format, appname=appname)
|
||||
|
||||
|
||||
def do_io(send, no_response):
|
||||
def do_io(to, send, no_response):
|
||||
send = ('@kitty-cmd' + json.dumps(send)).encode('ascii')
|
||||
with open('/dev/tty', 'wb') as out:
|
||||
out.write(b'\x1bP' + send + b'\x1b\\')
|
||||
send = b'\x1bP' + send + b'\x1b\\'
|
||||
if to:
|
||||
family, address = parse_address_spec(to)[:2]
|
||||
s = socket.socket(family)
|
||||
s.connect(address)
|
||||
out = s.makefile('wb')
|
||||
else:
|
||||
out = open('/dev/tty', 'wb')
|
||||
with out:
|
||||
out.write(send)
|
||||
if to:
|
||||
s.shutdown(socket.SHUT_WR)
|
||||
if no_response:
|
||||
return {'ok': True}
|
||||
|
||||
@ -432,7 +447,11 @@ def do_io(send, no_response):
|
||||
match = dcs.search(received)
|
||||
return match is None
|
||||
|
||||
with open('/dev/tty', 'rb') as src:
|
||||
if to:
|
||||
src = s.makefile('rb')
|
||||
else:
|
||||
src = open('/dev/tty', 'rb')
|
||||
with src:
|
||||
read_with_timeout(more_needed, src=src)
|
||||
if match is None:
|
||||
raise SystemExit('Failed to receive response from ' + appname)
|
||||
@ -470,11 +489,11 @@ def main(args):
|
||||
if func.no_response and isinstance(payload, types.GeneratorType):
|
||||
for item in payload:
|
||||
send['payload'] = item
|
||||
do_io(send, func.no_response)
|
||||
do_io(global_opts.to, send, func.no_response)
|
||||
return
|
||||
if payload is not None:
|
||||
send['payload'] = payload
|
||||
response = do_io(send, func.no_response)
|
||||
response = do_io(global_opts.to, send, func.no_response)
|
||||
if not response.get('ok'):
|
||||
if response.get('tb'):
|
||||
print(response['tb'], file=sys.stderr)
|
||||
|
||||
@ -262,6 +262,25 @@ def single_instance(group_id=None):
|
||||
return True
|
||||
|
||||
|
||||
def parse_address_spec(spec):
|
||||
protocol, rest = spec.split(':', 1)
|
||||
socket_path = None
|
||||
if protocol == 'unix':
|
||||
family = socket.AF_UNIX
|
||||
address = rest
|
||||
if address.startswith('@') and len(address) > 1:
|
||||
address = '\0' + address[1:]
|
||||
else:
|
||||
socket_path = address
|
||||
elif protocol in ('tcp', 'tcp6'):
|
||||
family = socket.AF_INET if protocol == 'tcp' else socket.AF_INET6
|
||||
host, port = rest.rsplit(':', 1)
|
||||
address = host, int(port)
|
||||
else:
|
||||
raise ValueError('Unknown protocol in --listen-on value: {}'.format(spec))
|
||||
return family, address, socket_path
|
||||
|
||||
|
||||
@contextmanager
|
||||
def non_blocking_read(src=sys.stdin):
|
||||
import termios
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user