Pass the value of shell_integration in the tarfile

Allows per host overrides and also avoids needing to read kitty.conf in
the ssh kitten
This commit is contained in:
Kovid Goyal 2022-02-24 22:06:15 +05:30
parent 6e5dbc5285
commit 02a68e7541
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
4 changed files with 58 additions and 155 deletions

View File

@ -6,14 +6,15 @@ import io
import os
import re
import shlex
import subprocess
import sys
import tarfile
import tempfile
import time
from contextlib import suppress
from typing import Dict, Iterator, List, NoReturn, Optional, Set, Tuple
from typing import Dict, Iterator, List, NoReturn, Optional, Set, Tuple, Union
from kitty.constants import cache_dir, shell_integration_dir, terminfo_dir
from kitty.shell_integration import get_effective_ksi_env_var
from kitty.short_uuid import uuid4
from kitty.utils import SSHConnectionData
@ -24,17 +25,32 @@ DEFAULT_SHELL_INTEGRATION_DEST = '.local/share/kitty-ssh-kitten/shell-integratio
def make_tarfile(hostname: str = '', shell_integration_dest: str = DEFAULT_SHELL_INTEGRATION_DEST) -> bytes:
def filter_files(tarinfo: tarfile.TarInfo) -> Optional[tarfile.TarInfo]:
if tarinfo.name.endswith('ssh/bootstrap.sh') or tarinfo.name.endswith('ssh/bootstrap.py'):
return None
def normalize_tarinfo(tarinfo: tarfile.TarInfo) -> tarfile.TarInfo:
tarinfo.uname = tarinfo.gname = 'kitty'
tarinfo.uid = tarinfo.gid = 0
return tarinfo
def add_data_as_file(tf: tarfile.TarFile, arcname: str, data: Union[str, bytes]) -> tarfile.TarInfo:
ans = tarfile.TarInfo(arcname)
ans.mtime = int(time.time())
ans.type = tarfile.REGTYPE
if isinstance(data, str):
data = data.encode('utf-8')
ans.size = len(data)
normalize_tarinfo(ans)
tf.addfile(ans, io.BytesIO(data))
return ans
def filter_files(tarinfo: tarfile.TarInfo) -> Optional[tarfile.TarInfo]:
if tarinfo.name.endswith('ssh/bootstrap.sh') or tarinfo.name.endswith('ssh/bootstrap.py'):
return None
return normalize_tarinfo(tarinfo)
buf = io.BytesIO()
with tarfile.open(mode='w:bz2', fileobj=buf, encoding='utf-8') as tf:
tf.add(shell_integration_dir, arcname=shell_integration_dest, filter=filter_files)
tf.add(terminfo_dir, arcname='.terminfo', filter=filter_files)
add_data_as_file(tf, shell_integration_dest.rstrip('/') + '/settings/ksi_env_var', get_effective_ksi_env_var())
return buf.getvalue()
@ -77,7 +93,6 @@ def prepare_script(ans: str, replacements: Dict[str, str]) -> str:
for k in ('EXEC_CMD', 'OVERRIDE_LOGIN_SHELL'):
replacements[k] = replacements.get(k, '')
replacements['SHELL_INTEGRATION_DIR'] = replacements.get('SHELL_INTEGRATION_DIR', DEFAULT_SHELL_INTEGRATION_DEST)
replacements['SHELL_INTEGRATION_VALUE'] = replacements.get('SHELL_INTEGRATION_VALUE', 'enabled')
def sub(m: 're.Match[str]') -> str:
return replacements[m.group()]
@ -91,129 +106,8 @@ def bootstrap_script(script_type: str = 'sh', **replacements: str) -> str:
return prepare_script(ans, replacements)
SHELL_SCRIPT = '''\
#!/bin/sh
# macOS ships with an ancient version of tic that cannot read from stdin, so we
# create a temp file for it
tmp=$(mktemp)
cat >$tmp << 'TERMEOF'
TERMINFO
TERMEOF
tname=.terminfo
if [ -e "/usr/share/misc/terminfo.cdb" ]; then
# NetBSD requires this see https://github.com/kovidgoyal/kitty/issues/4622
tname=".terminfo.cdb"
fi
tic_out=$(tic -x -o $HOME/$tname $tmp 2>&1)
rc=$?
rm $tmp
if [ "$rc" != "0" ]; then echo "$tic_out"; exit 1; fi
if [ -z "$USER" ]; then export USER=$(whoami); fi
export TERMINFO="$HOME/$tname"
login_shell=""
python=""
login_shell_is_ok() {
if [ -z "$login_shell" ] || [ ! -x "$login_shell" ]; then return 1; fi
case "$login_shell" in
*sh) return 0;
esac
return 1;
}
detect_python() {
python=$(command -v python3)
if [ -z "$python" ]; then python=$(command -v python2); fi
if [ -z "$python" ]; then python=python; fi
}
using_getent() {
cmd=$(command -v getent)
if [ -z "$cmd" ]; then return; fi
output=$($cmd passwd $USER 2>/dev/null)
if [ $? = 0 ]; then login_shell=$(echo $output | cut -d: -f7); fi
}
using_id() {
cmd=$(command -v id)
if [ -z "$cmd" ]; then return; fi
output=$($cmd -P $USER 2>/dev/null)
if [ $? = 0 ]; then login_shell=$(echo $output | cut -d: -f7); fi
}
using_passwd() {
cmd=$(command -v grep)
if [ -z "$cmd" ]; then return; fi
output=$($cmd "^$USER:" /etc/passwd 2>/dev/null)
if [ $? = 0 ]; then login_shell=$(echo $output | cut -d: -f7); fi
}
using_python() {
detect_python
if [ ! -x "$python" ]; then return; fi
output=$($python -c "import pwd, os; print(pwd.getpwuid(os.geteuid()).pw_shell)")
if [ $? = 0 ]; then login_shell=$output; fi
}
execute_with_python() {
detect_python
exec $python -c "import os; os.execl('$login_shell', '-' '$shell_name')"
}
die() { echo "$*" 1>&2 ; exit 1; }
using_getent
if ! login_shell_is_ok; then using_id; fi
if ! login_shell_is_ok; then using_python; fi
if ! login_shell_is_ok; then using_passwd; fi
if ! login_shell_is_ok; then die "Could not detect login shell"; fi
# If a command was passed to SSH execute it here
EXEC_CMD
# We need to pass the first argument to the executed program with a leading -
# to make sure the shell executes as a login shell. Note that not all shells
# support exec -a so we use the below to try to detect such shells
shell_name=$(basename $login_shell)
if [ -z "$PIPESTATUS" ]; then
# the dash shell does not support exec -a and also does not define PIPESTATUS
execute_with_python
fi
exec -a "-$shell_name" $login_shell
'''
PYTHON_SCRIPT = '''\
#!/usr/bin/env python
from __future__ import print_function
from tempfile import NamedTemporaryFile
import subprocess, os, sys, pwd, binascii, json
# macOS ships with an ancient version of tic that cannot read from stdin, so we
# create a temp file for it
with NamedTemporaryFile() as tmp:
tname = '.terminfo'
if os.path.exists('/usr/share/misc/terminfo.cdb'):
tname += '.cdb'
tmp.write(binascii.unhexlify('{terminfo}'))
tmp.flush()
p = subprocess.Popen(['tic', '-x', '-o', os.path.expanduser('~/' + tname), tmp.name], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdout, stderr = p.communicate()
if p.wait() != 0:
getattr(sys.stderr, 'buffer', sys.stderr).write(stdout + stderr)
raise SystemExit('Failed to compile terminfo using tic')
command_to_execute = json.loads(binascii.unhexlify('{command_to_execute}'))
try:
shell_path = pwd.getpwuid(os.geteuid()).pw_shell or '/bin/sh'
except KeyError:
shell_path = '/bin/sh'
shell_name = '-' + os.path.basename(shell_path)
if command_to_execute:
os.execlp(shell_path, shell_path, '-c', command_to_execute)
os.execlp(shell_path, shell_name)
'''
def load_script(script_type: str = 'sh', exec_cmd: str = '') -> str:
return bootstrap_script(script_type, EXEC_CMD=exec_cmd)
def get_ssh_cli() -> Tuple[Set[str], Set[str]]:
@ -345,8 +239,7 @@ def parse_ssh_args(args: List[str]) -> Tuple[List[str], List[str], bool]:
return ssh_args, server_args, passthrough
def get_posix_cmd(terminfo: str, remote_args: List[str]) -> List[str]:
sh_script = SHELL_SCRIPT.replace('TERMINFO', terminfo, 1)
def get_posix_cmd(remote_args: List[str]) -> List[str]:
command_to_execute = ''
if remote_args:
# ssh simply concatenates multiple commands using a space see
@ -354,17 +247,12 @@ def get_posix_cmd(terminfo: str, remote_args: List[str]) -> List[str]:
# concatenated command as shell -c cmd
args = [c.replace("'", """'"'"'""") for c in remote_args]
command_to_execute = "exec $login_shell -c '{}'".format(' '.join(args))
sh_script = sh_script.replace('EXEC_CMD', command_to_execute)
sh_script = load_script(exec_cmd=command_to_execute)
return [f'sh -c {shlex.quote(sh_script)}']
def get_python_cmd(terminfo: str, command_to_execute: List[str]) -> List[str]:
import json
script = PYTHON_SCRIPT.format(
terminfo=terminfo.encode('utf-8').hex(),
command_to_execute=json.dumps(' '.join(command_to_execute)).encode('utf-8').hex()
)
return [f'python -c "{script}"']
def get_python_cmd(remote_args: List[str]) -> List[str]:
raise NotImplementedError('TODO: Implement me')
def main(args: List[str]) -> NoReturn:
@ -386,9 +274,8 @@ def main(args: List[str]) -> NoReturn:
cmd.append('-t')
cmd.append('--')
cmd.append(hostname)
terminfo = subprocess.check_output(['infocmp', '-a']).decode('utf-8')
f = get_posix_cmd if use_posix else get_python_cmd
cmd += f(terminfo, remote_args)
cmd += f(remote_args)
os.execvp('ssh', cmd)

View File

@ -10,6 +10,7 @@ from typing import Dict, List, Optional
from .constants import shell_integration_dir
from .options.types import Options
from .utils import log_error, which
from .fast_data_types import get_options
def setup_fish_env(env: Dict[str, str], argv: List[str]) -> None:
@ -120,11 +121,19 @@ def shell_integration_allows_rc_modification(opts: Options) -> bool:
return not (opts.shell_integration & {'disabled', 'no-rc'})
def get_effective_ksi_env_var(opts: Optional[Options] = None) -> str:
opts = opts or get_options()
if 'disabled' in opts.shell_integration:
return ''
return ' '.join(opts.shell_integration)
def modify_shell_environ(opts: Options, env: Dict[str, str], argv: List[str]) -> None:
shell = get_supported_shell_name(argv[0])
if shell is None or 'disabled' in opts.shell_integration:
ksi = get_effective_ksi_env_var(opts)
if shell is None or not ksi:
return
env['KITTY_SHELL_INTEGRATION'] = ' '.join(opts.shell_integration)
env['KITTY_SHELL_INTEGRATION'] = ksi
if not shell_integration_allows_rc_modification(opts):
return
f = ENV_MODIFIERS.get(shell)

View File

@ -10,6 +10,7 @@ import tempfile
from kittens.ssh.main import bootstrap_script, get_connection_data
from kitty.constants import is_macos
from kitty.fast_data_types import CURSOR_BEAM
from kitty.options.utils import shell_integration
from kitty.utils import SSHConnectionData
from . import BaseTest
@ -83,7 +84,7 @@ print(' '.join(map(str, buf)))'''), lines=13, cols=77)
self.check_bootstrap(sh, tdir, login_shell)
# check that turning off shell integration works
if ok_login_shell in ('bash', 'zsh'):
for val in ('', 'no-rc'):
for val in ('', 'no-rc', 'enabled no-rc'):
with tempfile.TemporaryDirectory() as tdir:
self.check_bootstrap('sh', tdir, ok_login_shell, val)
@ -91,14 +92,14 @@ print(' '.join(map(str, buf)))'''), lines=13, cols=77)
script = bootstrap_script(
EXEC_CMD=f'echo "UNTAR_DONE"; {extra_exec}',
OVERRIDE_LOGIN_SHELL=login_shell,
SHELL_INTEGRATION_VALUE=SHELL_INTEGRATION_VALUE,
)
env = basic_shell_env(home_dir)
# Avoid generating unneeded completion scripts
os.makedirs(os.path.join(home_dir, '.local', 'share', 'fish', 'generated_completions'), exist_ok=True)
# prevent newuser-install from running
open(os.path.join(home_dir, '.zshrc'), 'w').close()
pty = self.create_pty(f'{sh} -c {shlex.quote(script)}', cwd=home_dir, env=env)
options = {'shell_integration': shell_integration(SHELL_INTEGRATION_VALUE or 'disabled')}
pty = self.create_pty(f'{sh} -c {shlex.quote(script)}', cwd=home_dir, env=env, options=options)
if pre_data:
pty.write_buf = pre_data.encode('utf-8')
del script

View File

@ -77,7 +77,9 @@ get_data
command stty "$saved_tty_settings"
saved_tty_settings=""
if [ "$rc" != "0" ]; then die "Failed to extract data transmitted by ssh kitten over the TTY device"; fi
if [ ! -f "$HOME/.terminfo/kitty.terminfo" ]; then die "Extracted data transmitted by ssh kitten is incomplete"; fi
shell_integration_dir="$HOME/SHELL_INTEGRATION_DIR"
shell_integration_settings_file="$shell_integration_dir/settings/ksi_env_var"
if [ ! -f "$shell_integration_settings_file" ]; then die "Extracted data transmitted by ssh kitten is incomplete"; fi
if [ -n "$leading_data" ]; then
# clear current line as it might have things echoed on it from leading_data
# because we only turn off echo in this script whereas the leading bytes could
@ -100,7 +102,6 @@ if [ -x "$(command -v tic)" ]; then
if [ "$rc" != "0" ]; then die "$tic_out"; fi
fi
shell_integration_dir="$HOME/SHELL_INTEGRATION_DIR"
login_shell_is_ok() {
if [ -z "$login_shell" -o ! -x "$login_shell" ]; then return 1; fi
@ -187,7 +188,8 @@ else
fi
shell_name=$(basename $login_shell)
export KITTY_SHELL_INTEGRATION="SHELL_INTEGRATION_VALUE"
# read the variable and remove all leading and trailing spaces and collapse multiple spaces using xargs
export KITTY_SHELL_INTEGRATION="$(cat $shell_integration_settings_file | xargs echo)"
exec_bash_with_integration() {
export ENV="$shell_integration_dir/bash/kitty.bash"
@ -235,14 +237,18 @@ exec_with_shell_integration() {
}
case "$KITTY_SHELL_INTEGRATION" in
"")
("")
# only blanks or unset
unset KITTY_SHELL_INTEGRATION
;;
*"no-rc"*)
;;
*)
exec_with_shell_integration
unset KITTY_SHELL_INTEGRATION
(*)
# not blank
q=$(printf "%s" "$KITTY_SHELL_INTEGRATION" | grep '\bno-rc\b')
if [ -z "$q" ]; then
exec_with_shell_integration
# exec failed, unset
unset KITTY_SHELL_INTEGRATION
fi
;;
esac