diff --git a/docs/changelog.rst b/docs/changelog.rst index c213dc4a3..458da4a55 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -80,6 +80,9 @@ Changelog - Linux: Include a man page and the HTML docs when building the linux-package +- Remote control: Fix kitty @ sometimes failing to read the response from + kitty. (:iss:`614`) + 0.10.1 [2018-05-24] ------------------------------ diff --git a/kittens/tui/loop.py b/kittens/tui/loop.py index 062ba54d8..3f5888fd7 100644 --- a/kittens/tui/loop.py +++ b/kittens/tui/loop.py @@ -22,7 +22,7 @@ from kitty.key_encoding import ( ALT, CTRL, PRESS, RELEASE, REPEAT, SHIFT, C, D, backspace_key, decode_key_event, enter_key ) -from kitty.utils import screen_size_function +from kitty.utils import screen_size_function, write_all from .handler import Handler from .operations import init_state, reset_state @@ -41,16 +41,6 @@ def debug(*a, **kw): fobj.flush() -def write_all(fd, data): - if isinstance(data, str): - data = data.encode('utf-8') - while data: - n = os.write(fd, data) - if not n: - break - data = data[n:] - - class TermManager: def __init__(self, input_fd, output_fd): diff --git a/kitty/data-types.c b/kitty/data-types.c index 37ff33c1f..bd5134680 100644 --- a/kitty/data-types.c +++ b/kitty/data-types.c @@ -11,12 +11,15 @@ #include #undef _DARWIN_C_SOURCE #endif +#define _DEFAULT_SOURCE +#include #include "data-types.h" #include "control-codes.h" #include "modes.h" #include #include #include +#include #ifdef WITH_PROFILER #include #endif @@ -103,7 +106,34 @@ stop_profiler(PyObject UNUSED *self) { } #endif +static PyObject* +open_tty(PyObject *self UNUSED, PyObject *args UNUSED) { + int fd = open("/dev/tty", O_RDWR | O_NONBLOCK | O_CLOEXEC | O_NOCTTY); + struct termios *termios_p = calloc(1, sizeof(struct termios)); + if (!termios_p) return PyErr_NoMemory(); + if (tcgetattr(fd, termios_p) != 0) { free(termios_p); PyErr_SetFromErrno(PyExc_OSError); return NULL; } + struct termios raw_termios = *termios_p; + cfmakeraw(&raw_termios); + raw_termios.c_cc[VMIN] = 0; raw_termios.c_cc[VTIME] = 0; + if (tcsetattr(fd, TCSAFLUSH, &raw_termios) != 0) { free(termios_p); PyErr_SetFromErrno(PyExc_OSError); return NULL; } + return Py_BuildValue("iN", fd, PyLong_FromVoidPtr(termios_p)); +} + +static PyObject* +close_tty(PyObject *self UNUSED, PyObject *args) { + PyObject *tp; + int fd; + if (!PyArg_ParseTuple(args, "iO!", &fd, &PyLong_Type, &tp)) return NULL; + struct termios *termios_p = PyLong_AsVoidPtr(tp); + tcsetattr(fd, TCSAFLUSH, termios_p); + free(termios_p); + close(fd); + Py_RETURN_NONE; +} + static PyMethodDef module_methods[] = { + {"open_tty", open_tty, METH_NOARGS, ""}, + {"close_tty", close_tty, METH_VARARGS, ""}, {"set_iutf8", (PyCFunction)pyset_iutf8, METH_VARARGS, ""}, {"thread_write", (PyCFunction)cm_thread_write, METH_VARARGS, ""}, {"parse_bytes", (PyCFunction)parse_bytes, METH_VARARGS, ""}, diff --git a/kitty/remote_control.py b/kitty/remote_control.py index 77d88c9e9..7f7298530 100644 --- a/kitty/remote_control.py +++ b/kitty/remote_control.py @@ -3,15 +3,18 @@ # License: GPL v3 Copyright: 2018, Kovid Goyal import json +import os import re import sys import types from functools import partial +from io import DEFAULT_BUFFER_SIZE from .cli import emph, parse_args -from .constants import appname, version -from .utils import parse_address_spec, read_with_timeout from .cmds import cmap, parse_subcommand_cli +from .constants import appname, version +from .fast_data_types import close_tty, open_tty +from .utils import parse_address_spec, write_all def handle_cmd(boss, window, cmd): @@ -44,39 +47,85 @@ def encode_send(send): return b'\x1bP' + send + b'\x1b\\' +class SocketIO: + + def __init__(self, to): + self.family, self.address = parse_address_spec(to)[:2] + + def __enter__(self): + import socket + self.socket = socket.socket(self.family) + self.socket.setblocking(True) + self.socket.connect(self.address) + + def __exit__(self, *a): + import socket + self.socket.shutdown(socket.SHUT_RDWR) + self.socket.close() + + def send(self, data): + import socket + with self.socket.makefile('wb') as out: + out.write(data) + self.socket.shutdown(socket.SHUT_WR) + + def recv(self, more_needed, timeout): + # We dont bother with more_needed since the server will close the + # connection after transmission + self.socket.settimeout(timeout) + with self.socket.makefile('rb') as src: + data = src.read() + more_needed(data) + + +class TTYIO: + + def __enter__(self): + self.tty_fd, self.original_termios = open_tty() + + def __exit__(self, *a): + close_tty(self.tty_fd, self.original_termios) + + def send(self, data): + write_all(self.tty_fd, data) + + def recv(self, more_needed, timeout): + import select + from time import monotonic + fd = self.tty_fd + start_time = monotonic() + while timeout > monotonic() - start_time: + rd = select.select([fd], [], [], max(0, timeout - (monotonic() - start_time)))[0] + if rd: + data = os.read(fd, DEFAULT_BUFFER_SIZE) + if not data: + break # eof + if not more_needed(data): + break + else: + break + + def do_io(to, send, no_response): - import socket send = encode_send(send) - 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} + io = SocketIO(to) if to else TTYIO() + with io: + io.send(send) + if no_response: + return {'ok': True} + dcs = re.compile(br'\x1bP@kitty-cmd([^\x1b]+)\x1b\\') - received = b'' - dcs = re.compile(br'\x1bP@kitty-cmd([^\x1b]+)\x1b\\') - match = None + received = b'' + match = None - def more_needed(data): - nonlocal received, match - received += data - match = dcs.search(received) - return match is None + def more_needed(data): + nonlocal received, match + received += data + match = dcs.search(received) + return match is None + + io.recv(more_needed, timeout=10) - 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) response = json.loads(match.group(1).decode('ascii')) diff --git a/kitty/utils.py b/kitty/utils.py index a850c5113..ab29623a8 100644 --- a/kitty/utils.py +++ b/kitty/utils.py @@ -352,6 +352,16 @@ def parse_address_spec(spec): return family, address, socket_path +def write_all(fd, data): + if isinstance(data, str): + data = data.encode('utf-8') + while data: + n = os.write(fd, data) + if not n: + break + data = data[n:] + + def make_fd_non_blocking(fd): oldfl = fcntl.fcntl(fd, fcntl.F_GETFL) fcntl.fcntl(fd, fcntl.F_SETFL, oldfl | os.O_NONBLOCK)