Dont change tty state to raw

Just turn off echo. Makes the code much simpler, can just use shell
builtin POSIX read function.
This commit is contained in:
Kovid Goyal
2022-03-13 08:51:59 +05:30
parent 7cd74cb852
commit 74f0057ec8
3 changed files with 76 additions and 144 deletions

View File

@@ -17,18 +17,20 @@ import tempfile
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 suppress, contextmanager from contextlib import contextmanager, suppress
from getpass import getuser from getpass import getuser
from typing import ( from typing import (
Any, Callable, Dict, Iterator, List, NoReturn, Optional, Sequence, Set, Callable, Dict, Iterator, List, NoReturn, Optional, Sequence, Set, Tuple,
Tuple, Union Union
) )
from kitty.constants import ( from kitty.constants import (
runtime_dir, shell_integration_dir, ssh_control_master_template, runtime_dir, shell_integration_dir, ssh_control_master_template,
terminfo_dir terminfo_dir
) )
from kitty.options.types import Options
from kitty.shm import SharedMemory from kitty.shm import SharedMemory
from kitty.types import run_once
from kitty.utils import SSHConnectionData from kitty.utils import SSHConnectionData
from .completion import complete, ssh_options from .completion import complete, ssh_options
@@ -66,6 +68,12 @@ def serialize_env(env: Dict[str, str], base_env: Dict[str, str]) -> bytes:
return '\n'.join(lines).encode('utf-8') return '\n'.join(lines).encode('utf-8')
@run_once
def kitty_opts() -> Options:
from kitty.cli import create_default_opts
return create_default_opts()
def make_tarfile(ssh_opts: SSHOptions, base_env: Dict[str, str], compression: str = 'gz') -> bytes: def make_tarfile(ssh_opts: SSHOptions, base_env: Dict[str, str], compression: str = 'gz') -> bytes:
def normalize_tarinfo(tarinfo: tarfile.TarInfo) -> tarfile.TarInfo: def normalize_tarinfo(tarinfo: tarfile.TarInfo) -> tarfile.TarInfo:
@@ -98,15 +106,13 @@ def make_tarfile(ssh_opts: SSHOptions, base_env: Dict[str, str], compression: st
from kitty.shell_integration import get_effective_ksi_env_var from kitty.shell_integration import get_effective_ksi_env_var
if ssh_opts.shell_integration == 'inherited': if ssh_opts.shell_integration == 'inherited':
from kitty.cli import create_default_opts ksi = get_effective_ksi_env_var(kitty_opts())
ksi = get_effective_ksi_env_var(create_default_opts())
else: else:
from kitty.options.types import Options
from kitty.options.utils import shell_integration from kitty.options.utils import shell_integration
ksi = get_effective_ksi_env_var(Options({'shell_integration': shell_integration(ssh_opts.shell_integration)})) ksi = get_effective_ksi_env_var(Options({'shell_integration': shell_integration(ssh_opts.shell_integration)}))
env = { env = {
'TERM': os.environ['TERM'], 'TERM': os.environ.get('TERM') or kitty_opts().term,
'COLORTERM': 'truecolor', 'COLORTERM': 'truecolor',
} }
for q in ('KITTY_WINDOW_ID', 'WINDOWID'): for q in ('KITTY_WINDOW_ID', 'WINDOWID'):
@@ -139,12 +145,7 @@ def make_tarfile(ssh_opts: SSHOptions, base_env: Dict[str, str], compression: st
def get_ssh_data(msg: str, request_id: str) -> Iterator[bytes]: def get_ssh_data(msg: str, request_id: str) -> Iterator[bytes]:
record_sep = b'\036' yield b'\nKITTY_DATA_START\n' # to discard leading data
def fmt_prefix(msg: Any) -> bytes:
return str(msg).encode('ascii') + record_sep
yield record_sep # to discard leading data
try: try:
msg = standard_b64decode(msg).decode('utf-8') msg = standard_b64decode(msg).decode('utf-8')
md = dict(x.split('=', 1) for x in msg.split(':')) md = dict(x.split('=', 1) for x in msg.split(':'))
@@ -153,7 +154,7 @@ def get_ssh_data(msg: str, request_id: str) -> Iterator[bytes]:
rq_id = md['id'] rq_id = md['id']
except Exception: except Exception:
traceback.print_exc() traceback.print_exc()
yield fmt_prefix('!invalid ssh data request message') yield b'invalid ssh data request message\n'
else: else:
try: try:
with SharedMemory(pwfilename, readonly=True) as shm: with SharedMemory(pwfilename, readonly=True) as shm:
@@ -170,13 +171,21 @@ def get_ssh_data(msg: str, request_id: str) -> Iterator[bytes]:
raise ValueError('Incorrect request id') raise ValueError('Incorrect request id')
except Exception as e: except Exception as e:
traceback.print_exc() traceback.print_exc()
yield fmt_prefix(f'!{e}') yield f'{e}\n'.encode('utf-8')
else: else:
yield b'OK\n'
ssh_opts = SSHOptions(env_data['opts']) ssh_opts = SSHOptions(env_data['opts'])
ssh_opts.copy = {k: CopyInstruction(*v) for k, v in ssh_opts.copy.items()} ssh_opts.copy = {k: CopyInstruction(*v) for k, v in ssh_opts.copy.items()}
encoded_data = env_data['tarfile'].encode('ascii') encoded_data = memoryview(env_data['tarfile'].encode('ascii'))
yield fmt_prefix(len(encoded_data)) # macOS has a 255 byte limit on its input queue as per man stty.
yield encoded_data # Not clear if that applies to canonical mode input as well, but
# better to be safe.
line_sz = 254
while encoded_data:
yield encoded_data[:line_sz]
yield b'\n'
encoded_data = encoded_data[line_sz:]
yield b'KITTY_DATA_END\n'
def safe_remove(x: str) -> None: def safe_remove(x: str) -> None:

View File

@@ -8,14 +8,12 @@ import io
import os import os
import pwd import pwd
import re import re
import select
import shutil import shutil
import subprocess import subprocess
import sys import sys
import tarfile import tarfile
import tempfile import tempfile
import termios import termios
import tty
tty_fd = -1 tty_fd = -1
original_termios_state = None original_termios_state = None
@@ -126,26 +124,32 @@ def compile_terminfo(base):
os.symlink('../x/xterm-kitty', q) os.symlink('../x/xterm-kitty', q)
def iter_base64_data(f):
global leading_data
started = 0
for line in f:
line = line.rstrip()
if started == 0:
if line == b'KITTY_DATA_START':
started = 1
else:
leading_data += line
elif started == 1:
if line == b'OK':
started = 2
else:
raise SystemExit(line.decode('utf-8', 'replace').rstrip())
else:
if line == b'KITTY_DATA_END':
break
yield line
def get_data(): def get_data():
global data_dir, shell_integration_dir, leading_data global data_dir, shell_integration_dir, leading_data
data = b'' data = []
with open(tty_fd, 'rb', closefd=False) as f:
while data.count(b'\036') < 2: data = b''.join(iter_base64_data(f))
select.select([tty_fd], [], [])
n = os.read(tty_fd, 64)
if not n:
raise SystemExit('Unexpected EOF while reading data from terminal')
data += n
leading_data, size, data = data.split(b'\036', 2)
if size.startswith(b'!'):
raise SystemExit(size[1:].decode('utf-8', 'replace'))
size = int(size)
while len(data) < size:
select.select([tty_fd], [], [])
n = os.read(tty_fd, size - len(data))
if not n:
raise SystemExit('Unexpected EOF while reading data from terminal')
data += n
cleanup() cleanup()
if leading_data: if leading_data:
# clear current line as it might have things echoed on it from leading_data # clear current line as it might have things echoed on it from leading_data
@@ -212,7 +216,7 @@ def exec_with_shell_integration():
def main(): def main():
global tty_fd, original_termios_state, login_shell global tty_fd, original_termios_state, login_shell
try: try:
tty_fd = os.open(os.ctermid(), os.O_RDWR | os.O_NONBLOCK | os.O_CLOEXEC) tty_fd = os.open(os.ctermid(), os.O_RDWR | os.O_CLOEXEC)
except OSError: except OSError:
pass pass
else: else:
@@ -222,11 +226,8 @@ def main():
except OSError: except OSError:
pass pass
else: else:
tty.setraw(tty_fd, termios.TCSANOW)
new_state = termios.tcgetattr(tty_fd) new_state = termios.tcgetattr(tty_fd)
new_state[3] &= ~termios.ECHO new_state[3] &= ~termios.ECHO
new_state[-1][termios.VMIN] = 1
new_state[-1][termios.VTIME] = 0
termios.tcsetattr(tty_fd, termios.TCSANOW, new_state) termios.tcsetattr(tty_fd, termios.TCSANOW, new_state)
try: try:
if original_termios_state is not None: if original_termios_state is not None:

View File

@@ -64,103 +64,13 @@ init_tty() {
[ -n "$saved_tty_settings" ] && tty_ok="y" [ -n "$saved_tty_settings" ] && tty_ok="y"
if [ "$tty_ok" = "y" ]; then if [ "$tty_ok" = "y" ]; then
command stty raw min 1 time 0 -echo 2> /dev/null < /dev/tty || die "stty failed to set raw mode" command stty -echo 2> /dev/null < /dev/tty || die "stty failed to set raw mode"
return 0 return 0
fi fi
return 1 return 1
} }
# try to use zsh's builtin sysread function for reading to TTY dcs_to_kitty() { printf "\033P@kitty-$1|%s\033\134" "$(printf "%s" "$2" | base64_encode)" > /dev/tty; }
# as it is superior to the POSIX variants. The builtin read function doesn't work
# as it hangs reading N bytes on macOS
tty_fd=-1
if [ -n "$ZSH_VERSION" ] && builtin zmodload zsh/system 2> /dev/null; then
builtin sysopen -o cloexec -rwu tty_fd -- "$TTY" 2> /dev/null
[ $tty_fd = -1 ] && builtin sysopen -o cloexec -rwu tty_fd -- /dev/tty 2> /dev/null
fi
if [ $tty_fd -gt -1 ]; then
dcs_to_kitty() {
builtin local b64data
b64data=$(builtin printf "%s" "$2" | base64_encode)
builtin print -nu "$tty_fd" '\eP@kitty-'"${1}|${b64data//[[:space:]]}"'\e\\'
}
read_one_byte_from_tty() {
builtin sysread -s "1" -i "$tty_fd" n 2> /dev/null
return $?
}
read_n_bytes_from_tty() {
builtin let num_left=$1
while [ $num_left -gt 0 ]; do
builtin sysread -c num_read -s "$num_left" -i "$tty_fd" -o "1" 2> /dev/null || die "Failed to read $num_left bytes from TTY using sysread"
builtin let num_left=$num_left-$num_read
done
}
else
dcs_to_kitty() { printf "\033P@kitty-$1|%s\033\134" "$(printf "%s" "$2" | base64_encode)" > /dev/tty; }
read_one_byte_from_tty() {
# We need a way to read a single byte at a time and to read a specified number of bytes in one invocation.
# The options are head -c, read -N and dd
#
# read -N is not in POSIX and dash/posh dont implement it. Also bash seems to read beyond
# the specified number of bytes into an internal buffer.
#
# head -c reads beyond the specified number of bytes into an internal buffer on macOS
#
# POSIX dd works for one byte at a time but for reading X bytes it needs the GNU iflag=count_bytes
# extension, and is anyway unsafe as it can lead to corrupt output when the read syscall is interrupted.
n=$(command dd bs=1 count=1 2> /dev/null < /dev/tty)
return $?
}
# using dd with bs=1 is very slow, so use head. On non GNU coreutils head
# does not limit itself to reading -c bytes only from the pipe so we can potentially lose
# some trailing data, for instance if the user starts typing. Cant be helped.
if [ "$(printf "%s" "test" | command ghead -c 3 2> /dev/null)" = "tes" ]; then
# Some BSD based systems use ghead for GNU head which is strictly superior to
# Broken System Design head, so prefer it.
read_n_bytes_from_tty() {
command ghead -c "$1" < /dev/tty
}
elif [ "$(printf "%s" "test" | command head -c 3 2> /dev/null)" = "tes" ]; then
read_n_bytes_from_tty() {
command head -c "$1" < /dev/tty
}
elif detect_python; then
read_n_bytes_from_tty() {
command "$python" "-c" "
import sys, os, errno
def eintr_retry(func, *args):
while True:
try:
return func(*args)
except EnvironmentError as e:
if e.errno != errno.EINTR:
raise
n = $1
in_fd = sys.stdin.fileno()
out_fd = sys.stdout.fileno()
while n > 0:
d = memoryview(eintr_retry(os.read, in_fd, n))
n -= len(d)
while d:
nw = eintr_retry(os.write, out_fd, d)
d = d[nw:]
" < /dev/tty
}
elif detect_perl; then
read_n_bytes_from_tty() {
command "$perl" -MList::Util=min -e '
open(my $fh,"<","/dev/tty");binmode($fh);binmode(STDOUT);my ($n,$buf)=(@ARGV[0],"");
while($n>0){my $rv=sysread($fh,$buf,min(65536,$n));unless($rv){exit(1);};$n-=$rv;print STDOUT $buf;}' "$1" 2> /dev/null
}
else
read_n_bytes_from_tty() {
command dd bs=1 count="$1" 2> /dev/null < /dev/tty
}
fi
fi
debug() { dcs_to_kitty "print" "debug: $1"; } debug() { dcs_to_kitty "print" "debug: $1"; }
echo_via_kitty() { dcs_to_kitty "echo" "$1"; } echo_via_kitty() { dcs_to_kitty "echo" "$1"; }
@@ -212,13 +122,19 @@ compile_terminfo() {
fi fi
} }
read_base64_from_tty() {
while IFS= read -r line; do
[ "$line" = "KITTY_DATA_END" ] && return 0
printf "%s" "$line"
done
}
untar_and_read_env() { untar_and_read_env() {
# extract the tar file atomically, in the sense that any file from the # extract the tar file atomically, in the sense that any file from the
# tarfile is only put into place after it has been fully written to disk # tarfile is only put into place after it has been fully written to disk
tdir=$(command mktemp -d "$HOME/.kitty-ssh-kitten-untar-XXXXXXXXXXXX") tdir=$(command mktemp -d "$HOME/.kitty-ssh-kitten-untar-XXXXXXXXXXXX")
[ $? = 0 ] || die "Creating temp directory failed" [ $? = 0 ] || die "Creating temp directory failed"
read_n_bytes_from_tty "$1" | base64_decode | command tar "xpzf" "-" "-C" "$tdir" read_base64_from_tty | base64_decode | command tar "xpzf" "-" "-C" "$tdir"
data_file="$tdir/data.sh" data_file="$tdir/data.sh"
[ -f "$data_file" ] && . "$data_file" [ -f "$data_file" ] && . "$data_file"
[ -z "$KITTY_SSH_KITTEN_DATA_DIR" ] && die "Failed to read SSH data from tty" [ -z "$KITTY_SSH_KITTEN_DATA_DIR" ] && die "Failed to read SSH data from tty"
@@ -245,14 +161,20 @@ read_record() {
} }
get_data() { get_data() {
leading_data=$(read_record) started="n"
size=$(read_record) while IFS= read -r line; do
case "$size" in if [ "$started" = "y" ]; then
("!"*) [ "$line" = "OK" ] && break
die "$size" die "$line"
;; else
esac if [ "$line" = "KITTY_DATA_START" ]; then
untar_and_read_env "$size" started="y"
else
leading_data="$leading_data$line"
fi
fi
done
untar_and_read_env
} }
if [ "$tty_ok" = "y" ]; then if [ "$tty_ok" = "y" ]; then