# Wrap ssh to clean up the terminal and reconnect when a connection drops. # # A remote tmux, herdr, or editor arms terminal modes over the SSH pipe (mouse # tracking, focus reporting, the alternate screen) that only it can disarm. If # the connection dies instead of exiting cleanly, those modes stay armed on the # local terminal, and every mouse move floods the prompt with escape junk. ssh() { local rc started started=$SECONDS command ssh "$@" rc=$? [[ -t 1 ]] || return $rc _ssh_disarm # Reconnect only when an interactive session drops: ssh exits 255 for # transport failures, but a fast 255 with no established session is a # connect/auth failure, a remote command's own 255 passes through # indistinguishably and must not replay its side effects, and redirected # stdin would feed the remaining piped input to a fresh remote shell. if (( rc != 255 )) || [[ ! -t 0 ]] || ! _ssh_interactive "$@" || (( SECONDS - started < 30 )); then return $rc fi # Retry in a subshell: Ctrl-C reaches the whole foreground process group, # so it cancels both the in-flight attempt and the loop itself. Keep # retrying fast failures, since a rebooting server refuses connections too. ( while true; do echo "Connection lost. Reconnecting (Ctrl-C to stop)..." sleep 2 command ssh "$@" rc=$? _ssh_disarm (( rc != 255 )) && exit $rc done ) } # Disarm mouse tracking (1000/1002/1003, 1006 encoding), focus reporting # (1004), and the alternate screen (1049), and show the cursor again. _ssh_disarm() { printf '\e[?1000l\e[?1002l\e[?1003l\e[?1006l\e[?1004l\e[?1049l\e[?25h' } # True for an interactive session: a destination and no remote command. The # letters are the ssh(1) options that consume a value, so their arguments are # not mistaken for the destination. _ssh_interactive() { local value_opts="BbcDEeFIiJLlmOoPpQRSWw" local argv=("$@") arg letters i dest="" opts_done="" while (($#)); do arg="$1" shift if [[ -z $opts_done && $arg == "--" ]]; then opts_done=1 elif [[ -z $opts_done && $arg == -?* ]]; then letters="${arg#-}" for ((i = 0; i < ${#letters}; i++)); do if [[ $value_opts == *"${letters:i:1}"* ]]; then # The value is glued to the letter (-p2222) unless the letter ends # the argument, in which case it consumes the next one (-p 2222). (( i == ${#letters} - 1 )) && shift break fi done elif [[ -z $dest ]]; then dest="$arg" else return 1 fi done [[ -n $dest ]] || return 1 # A RemoteCommand from ssh_config or -o replays on reconnect just like a # positional command; ssh -G resolves the effective configuration for this # exact invocation without connecting. Fail closed when it cannot resolve, # since an undetected RemoteCommand must not replay. The explicit "none" # cancels a configured command, and some versions emit it when unset. local resolved resolved=$(command ssh -G "${argv[@]}" 2>/dev/null) || return 1 ! grep -i '^remotecommand ' <<<"$resolved" | grep -qvi '^remotecommand none$' }