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 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: class RemoteCommand:
name: str = '' name: str = ''
@ -121,6 +137,7 @@ class RemoteCommand:
args_count: Optional[int] = None args_count: Optional[int] = None
args_completion: Optional[Dict[str, Tuple[str, Union[Callable[[], Iterable[str]], Tuple[str, ...]]]]] = None args_completion: Optional[Dict[str, Tuple[str, Union[Callable[[], Iterable[str]], Tuple[str, ...]]]]] = None
defaults: Optional[Dict[str, Any]] = None defaults: Optional[Dict[str, Any]] = None
is_asynchronous: bool = False
options_class: Type[RCOptions] = RCOptions options_class: Type[RCOptions] = RCOptions
def __init__(self) -> None: def __init__(self) -> None:
@ -191,6 +208,9 @@ class RemoteCommand:
windows += list(tab) windows += list(tab)
return windows 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: def message_to_kitty(self, global_opts: RCOptions, opts: Any, args: ArgsType) -> PayloadType:
raise NotImplementedError() raise NotImplementedError()

View File

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

View File

@ -6,6 +6,7 @@ import os
import re import re
import sys import sys
import types import types
from time import monotonic
from contextlib import suppress from contextlib import suppress
from functools import partial from functools import partial
from typing import ( from typing import (
@ -24,6 +25,9 @@ from .typing import BossType, WindowType
from .utils import TTYIO, parse_address_spec from .utils import TTYIO, parse_address_spec
active_async_requests: Dict[str, float] = {}
def encode_response_for_peer(response: Any) -> bytes: def encode_response_for_peer(response: Any) -> bytes:
import json import json
return b'\x1bP@kitty-cmd' + json.dumps(response).encode('utf-8') + b'\x1b\\' 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']) c = command_for_name(cmd['cmd'])
payload = cmd.get('payload') or {} payload = cmd.get('payload') or {}
payload['peer_id'] = peer_id 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: try:
ans = c.response_from_kitty(boss, window, PayloadGetter(c, payload)) ans = c.response_from_kitty(boss, window, PayloadGetter(c, payload))
except Exception: 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) 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} ans = {'cmd': name, 'version': version, 'no_response': no_response}
if payload is not None: if payload is not None:
ans['payload'] = payload ans['payload'] = payload
if is_asynchronous:
from kitty.short_uuid import uuid4
ans['async'] = uuid4()
return ans 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: if error:
response: Dict[str, Union[bool, int, str]] = {'ok': False, 'error': error} response: Dict[str, Union[bool, int, str]] = {'ok': False, 'error': error}
else: else:
@ -203,13 +222,16 @@ def main(args: List[str]) -> None:
response_timeout = c.response_timeout response_timeout = c.response_timeout
if hasattr(opts, 'response_timeout'): if hasattr(opts, 'response_timeout'):
response_timeout = 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: if not global_opts.to and 'KITTY_LISTEN_ON' in os.environ:
global_opts.to = os.environ['KITTY_LISTEN_ON'] global_opts.to = os.environ['KITTY_LISTEN_ON']
import socket import socket
try: try:
response = do_io(global_opts.to, send, no_response, response_timeout) response = do_io(global_opts.to, send, no_response, response_timeout)
except (TimeoutError, socket.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') raise SystemExit(f'Timed out after {response_timeout} seconds waiting for response form kitty')
if no_response: if no_response:
return return