Finish remote control via socket implementation

This commit is contained in:
Kovid Goyal 2018-03-03 10:32:49 +05:30
parent ec989a45b5
commit 15f07f57bf
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
5 changed files with 85 additions and 47 deletions

View File

@ -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)

View File

@ -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;

View File

@ -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

View File

@ -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)

View File

@ -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