Cancel an async request on timeout so no spurious data is sent to the tty

This commit is contained in:
Kovid Goyal 2021-10-30 14:46:48 +05:30
parent 2a637e4220
commit 6241369b6c
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
3 changed files with 50 additions and 11 deletions

View File

@ -108,6 +108,22 @@ class ParsingOfArgsFailed(ValueError):
pass
class AsyncResponder:
def __init__(self, payload_get: PayloadGetType, window: Optional[Window]) -> None:
self.async_id: str = payload_get('async_id', missing='')
self.peer_id: int = payload_get('peer_id', missing=0)
self.window_id: int = getattr(window, 'id', 0)
def send_data(self, data: Any) -> None:
from kitty.remote_control import send_response_to_client
send_response_to_client(data=data, peer_id=self.peer_id, window_id=self.window_id, async_id=self.async_id)
def send_error(self, error: str) -> None:
from kitty.remote_control import send_response_to_client
send_response_to_client(error=error, peer_id=self.peer_id, window_id=self.window_id, async_id=self.async_id)
class RemoteCommand:
name: str = ''
@ -121,6 +137,7 @@ class RemoteCommand:
args_count: Optional[int] = None
args_completion: Optional[Dict[str, Tuple[str, Union[Callable[[], Iterable[str]], Tuple[str, ...]]]]] = None
defaults: Optional[Dict[str, Any]] = None
is_asynchronous: bool = False
options_class: Type[RCOptions] = RCOptions
def __init__(self) -> None:
@ -191,6 +208,9 @@ class RemoteCommand:
windows += list(tab)
return windows
def create_async_responder(self, payload_get: PayloadGetType, window: Optional[Window]) -> AsyncResponder:
return AsyncResponder(payload_get, window)
def message_to_kitty(self, global_opts: RCOptions, opts: Any, args: ArgsType) -> PayloadType:
raise NotImplementedError()

View File

@ -1,10 +1,8 @@
#!/usr/bin/env python
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
from typing import TYPE_CHECKING, Optional
from .base import (
MATCH_TAB_OPTION, ArgsType, Boss, PayloadGetType, PayloadType, RCOptions,
RemoteCommand, ResponseType, Window, no_response
@ -39,21 +37,20 @@ type=bool-set
If specified the tab containing the window this command is run in is used
instead of the active tab.
'''
is_asynchronous = True
def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType:
ans = {'self': opts.self, 'match': opts.match}
return ans
def response_from_kitty(self, boss: Boss, window: Optional[Window], payload_get: PayloadGetType) -> ResponseType:
peer_id: int = payload_get('peer_id', missing=0)
window_id: int = getattr(window, 'id', 0)
responder = self.create_async_responder(payload_get, window)
def callback(tab: Optional['Tab'], window: Optional[Window]) -> None:
from kitty.remote_control import send_response_to_client
if window:
send_response_to_client(data=window.id, peer_id=peer_id, window_id=window_id)
responder.send_data(window.id)
else:
send_response_to_client(error='No window selected', peer_id=peer_id, window_id=window_id)
responder.send_error('No window selected')
for tab in self.tabs_for_match_payload(boss, window, payload_get):
if tab:
boss.visual_window_select_action(tab, callback, 'Choose window')

View File

@ -6,6 +6,7 @@ import os
import re
import sys
import types
from time import monotonic
from contextlib import suppress
from functools import partial
from typing import (
@ -24,6 +25,9 @@ from .typing import BossType, WindowType
from .utils import TTYIO, parse_address_spec
active_async_requests: Dict[str, float] = {}
def encode_response_for_peer(response: Any) -> bytes:
import json
return b'\x1bP@kitty-cmd' + json.dumps(response).encode('utf-8') + b'\x1b\\'
@ -40,7 +44,16 @@ def handle_cmd(boss: BossType, window: Optional[WindowType], serialized_cmd: str
c = command_for_name(cmd['cmd'])
payload = cmd.get('payload') or {}
payload['peer_id'] = peer_id
async_id = str(cmd.get('async', ''))
if async_id:
if 'cancel_async' in cmd:
active_async_requests.pop(async_id, None)
return None
active_async_requests[async_id] = monotonic()
payload['async_id'] = async_id
if len(active_async_requests) > 32:
oldest = next(iter(active_async_requests))
del active_async_requests[oldest]
try:
ans = c.response_from_kitty(boss, window, PayloadGetter(c, payload))
except Exception:
@ -159,14 +172,20 @@ def parse_rc_args(args: List[str]) -> Tuple[RCOptions, List[str]]:
return parse_args(args[1:], global_options_spec, 'command ...', msg, f'{appname} @', result_class=RCOptions)
def create_basic_command(name: str, payload: Any = None, no_response: bool = False) -> Dict[str, Any]:
def create_basic_command(name: str, payload: Any = None, no_response: bool = False, is_asynchronous: bool = False) -> Dict[str, Any]:
ans = {'cmd': name, 'version': version, 'no_response': no_response}
if payload is not None:
ans['payload'] = payload
if is_asynchronous:
from kitty.short_uuid import uuid4
ans['async'] = uuid4()
return ans
def send_response_to_client(data: Any = None, error: str = '', peer_id: int = 0, window_id: int = 0) -> None:
def send_response_to_client(data: Any = None, error: str = '', peer_id: int = 0, window_id: int = 0, async_id: str = '') -> None:
ts = active_async_requests.pop(async_id, None)
if ts is None:
return
if error:
response: Dict[str, Union[bool, int, str]] = {'ok': False, 'error': error}
else:
@ -203,13 +222,16 @@ def main(args: List[str]) -> None:
response_timeout = c.response_timeout
if hasattr(opts, 'response_timeout'):
response_timeout = opts.response_timeout
send = create_basic_command(cmd, payload=payload, no_response=no_response)
send = create_basic_command(cmd, payload=payload, no_response=no_response, is_asynchronous=c.is_asynchronous)
if not global_opts.to and 'KITTY_LISTEN_ON' in os.environ:
global_opts.to = os.environ['KITTY_LISTEN_ON']
import socket
try:
response = do_io(global_opts.to, send, no_response, response_timeout)
except (TimeoutError, socket.timeout):
send.pop('payload', None)
send['cancel_async'] = True
do_io(global_opts.to, send, True, 10)
raise SystemExit(f'Timed out after {response_timeout} seconds waiting for response form kitty')
if no_response:
return