ssh kitten: Send data without a roundtrip

Send data to the remote side without waiting for a data request. Avoids
an extra roundtrip during initialization.
This commit is contained in:
Kovid Goyal 2022-03-13 13:39:13 +05:30
parent 434ef97952
commit 2b06ca5e1a
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
4 changed files with 47 additions and 23 deletions

View File

@ -11,14 +11,17 @@ import re
import secrets import secrets
import shlex import shlex
import stat import stat
import subprocess
import sys import sys
import tarfile import tarfile
import tempfile import tempfile
import termios
import time import time
import traceback import traceback
from base64 import standard_b64decode, standard_b64encode from base64 import standard_b64decode, standard_b64encode
from contextlib import contextmanager, suppress from contextlib import contextmanager, suppress
from getpass import getuser from getpass import getuser
from select import select
from typing import ( from typing import (
Callable, Dict, Iterator, List, NoReturn, Optional, Sequence, Set, Tuple, Callable, Dict, Iterator, List, NoReturn, Optional, Sequence, Set, Tuple,
Union Union
@ -216,7 +219,7 @@ def prepare_exec_cmd(remote_args: Sequence[str], is_python: bool) -> str:
def bootstrap_script( def bootstrap_script(
ssh_opts: SSHOptions, script_type: str = 'sh', remote_args: Sequence[str] = (), ssh_opts: SSHOptions, script_type: str = 'sh', remote_args: Sequence[str] = (),
test_script: str = '', request_id: Optional[str] = None, cli_hostname: str = '', cli_uname: str = '', test_script: str = '', request_id: Optional[str] = None, cli_hostname: str = '', cli_uname: str = '',
request_data: str = '1', echo_on: bool = True request_data: bool = False, echo_on: bool = True
) -> Tuple[str, Dict[str, str], SharedMemory]: ) -> Tuple[str, Dict[str, str], SharedMemory]:
if request_id is None: if request_id is None:
request_id = os.environ['KITTY_PID'] + '-' + os.environ['KITTY_WINDOW_ID'] request_id = os.environ['KITTY_PID'] + '-' + os.environ['KITTY_WINDOW_ID']
@ -233,7 +236,7 @@ def bootstrap_script(
atexit.register(shm.unlink) atexit.register(shm.unlink)
replacements = { replacements = {
'DATA_PASSWORD': pw, 'PASSWORD_FILENAME': shm.name, 'EXEC_CMD': exec_cmd, 'TEST_SCRIPT': test_script, 'DATA_PASSWORD': pw, 'PASSWORD_FILENAME': shm.name, 'EXEC_CMD': exec_cmd, 'TEST_SCRIPT': test_script,
'REQUEST_ID': request_id, 'REQUEST_DATA': request_data, 'ECHO_ON': '1' if echo_on else '0', 'REQUEST_ID': request_id, 'REQUEST_DATA': '1' if request_data else '0', 'ECHO_ON': '1' if echo_on else '0',
} }
return prepare_script(ans, replacements), replacements, shm return prepare_script(ans, replacements), replacements, shm
@ -477,7 +480,6 @@ def connection_sharing_args(opts: SSHOptions, kitty_pid: int) -> List[str]:
@contextmanager @contextmanager
def restore_terminal_state() -> Iterator[bool]: def restore_terminal_state() -> Iterator[bool]:
import termios
with open(os.ctermid()) as f: with open(os.ctermid()) as f:
val = termios.tcgetattr(f.fileno()) val = termios.tcgetattr(f.fileno())
try: try:
@ -486,6 +488,34 @@ def restore_terminal_state() -> Iterator[bool]:
termios.tcsetattr(f.fileno(), termios.TCSAFLUSH, val) termios.tcsetattr(f.fileno(), termios.TCSAFLUSH, val)
def dcs_to_kitty(payload: Union[bytes, str], type: str = 'ssh') -> bytes:
if isinstance(payload, str):
payload = payload.encode('utf-8')
payload = standard_b64encode(payload)
return b'\033P@kitty-' + type.encode('ascii') + b'|' + payload + b'\033\\'
@contextmanager
def drain_potential_tty_garbage(p: 'subprocess.Popen[bytes]', data_request: str) -> Iterator[None]:
with open(os.open(os.ctermid(), os.O_CLOEXEC | os.O_RDWR | os.O_NOCTTY), 'wb') as tty:
tty.write(dcs_to_kitty(data_request))
tty.flush()
try:
yield
finally:
if p.returncode:
# discard queued data on tty in case data transmission was
# interrupted due to SSH failure, avoids spewing garbage to
# screen
termios.tcflush(tty.fileno(), termios.TCIOFLUSH)
with open(tty.fileno(), 'rb', closefd=False) as tf:
os.set_blocking(tf.fileno(), False)
from tty import setraw
setraw(tf.fileno(), termios.TCSANOW)
while select([tf], [], [], 0)[0]:
tf.read()
def run_ssh(ssh_args: List[str], server_args: List[str], found_extra_args: Tuple[str, ...], echo_on: bool) -> NoReturn: def run_ssh(ssh_args: List[str], server_args: List[str], found_extra_args: Tuple[str, ...], echo_on: bool) -> NoReturn:
cmd = ['ssh'] + ssh_args cmd = ['ssh'] + ssh_args
hostname, remote_args = server_args[0], server_args[1:] hostname, remote_args = server_args[0], server_args[1:]
@ -524,13 +554,16 @@ def run_ssh(ssh_args: List[str], server_args: List[str], found_extra_args: Tuple
os.environ['SSH_ASKPASS_REQUIRE'] = 'force' os.environ['SSH_ASKPASS_REQUIRE'] = 'force'
if not os.environ.get('SSH_ASKPASS'): if not os.environ.get('SSH_ASKPASS'):
os.environ['SSH_ASKPASS'] = os.path.join(shell_integration_dir, 'ssh', 'askpass.py') os.environ['SSH_ASKPASS'] = os.path.join(shell_integration_dir, 'ssh', 'askpass.py')
import subprocess try:
with suppress(FileNotFoundError): p = subprocess.Popen(cmd)
try: except FileNotFoundError:
raise SystemExit(subprocess.run(cmd).returncode) raise SystemExit('Could not find the ssh executable, is it in your PATH?')
except KeyboardInterrupt: else:
raise SystemExit(1) with drain_potential_tty_garbage(p, 'id={REQUEST_ID}:pwfile={PASSWORD_FILENAME}:pw={DATA_PASSWORD}'.format(**replacements)):
raise SystemExit('Could not find the ssh executable, is it in your PATH?') try:
raise SystemExit(p.wait())
except KeyboardInterrupt:
raise SystemExit(1)
def main(args: List[str]) -> NoReturn: def main(args: List[str]) -> NoReturn:

View File

@ -243,7 +243,8 @@ copy --exclude */w.* d1
test_script = f'echo "UNTAR_DONE"; {test_script}' test_script = f'echo "UNTAR_DONE"; {test_script}'
ssh_opts['shell_integration'] = SHELL_INTEGRATION_VALUE or 'disabled' ssh_opts['shell_integration'] = SHELL_INTEGRATION_VALUE or 'disabled'
script, replacements, shm = bootstrap_script( script, replacements, shm = bootstrap_script(
SSHOptions(ssh_opts), script_type='py' if 'python' in sh else 'sh', request_id="testing", test_script=test_script SSHOptions(ssh_opts), script_type='py' if 'python' in sh else 'sh', request_id="testing", test_script=test_script,
request_data=True
) )
try: try:
env = basic_shell_env(home_dir) env = basic_shell_env(home_dir)

View File

@ -27,6 +27,7 @@ with SharedMemory(
shm.flush() shm.flush()
with open(os.ctermid(), 'wb') as f: with open(os.ctermid(), 'wb') as f:
f.write(f'\x1bP@kitty-ask|{shm.name}\x1b\\'.encode('ascii')) f.write(f'\x1bP@kitty-ask|{shm.name}\x1b\\'.encode('ascii'))
f.flush()
while True: while True:
# TODO: Replace sleep() with a mutex and condition variable created in the shared memory # TODO: Replace sleep() with a mutex and condition variable created in the shared memory
time.sleep(0.05) time.sleep(0.05)

View File

@ -73,8 +73,7 @@ login_cwd=""
request_data="REQUEST_DATA" request_data="REQUEST_DATA"
trap "cleanup_on_bootstrap_exit" EXIT trap "cleanup_on_bootstrap_exit" EXIT
dcs_to_kitty "ssh" "id="REQUEST_ID":pwfile="PASSWORD_FILENAME":pw="DATA_PASSWORD"" [ "$request_data" = "1" ] && dcs_to_kitty "ssh" "id="REQUEST_ID":pwfile="PASSWORD_FILENAME":pw="DATA_PASSWORD""
record_separator=$(printf "\036")
mv_files_and_dirs() { mv_files_and_dirs() {
cwd="$PWD" cwd="$PWD"
@ -135,16 +134,6 @@ untar_and_read_env() {
tdir="" tdir=""
} }
read_record() {
record=""
while :; do
read_one_byte_from_tty || die "Reading a byte from the TTY failed"
[ "$n" = "$record_separator" ] && break
record="$record$n"
done
printf "%s" "$record"
}
get_data() { get_data() {
started="n" started="n"
while IFS= read -r line; do while IFS= read -r line; do