diff --git a/README.md b/README.md index 21b9561..75433c7 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,24 @@ -# šŸ–„ļø panssh – Pantheon Interactive SSH Session Emulator +# šŸ–„ļø PanSSH +## An emulated SSH login for Pantheon sites -`panssh` emulates an interactive SSH connection to a Pantheon site's application environment using only their available (limited) SSH service. It provides command history, local editing of remote files and an emulated current working directory. +PanSSH emulates an interactive SSH connection to a Pantheon site's application environment using only their available (limited) SSH service. It provides command history, local editing of remote files and an emulated current working directory. -You can do almost everything that you could if a standard SSH login were available, and it looks and feels near identical. +You can do almost everything that you could if a standard SSH login were available, and it looks and feels so familiar that you may not notice the difference. + +### Quick start + +##### Download and run the main script + +``` +curl -so panssh https://raw.githubusercontent.com/LastCallMedia/panssh/refs/heads/lando/panssh +chmod +x panssh +./panssh +``` +Further instructions will then be displayed. ### Recent changes -* Tab-completion is now included, on supporting systems: +* 1.2.1: Added [.lando.panssh.yml](https://github.com/LastCallMedia/panssh/blob/lando/lando/.lando.panssh.yml), which provides [easy setup](https://github.com/LastCallMedia/panssh/blob/lando/lando/README.md) of PanSSH in a [Lando](https://lando.dev/) project. +* 1.2.0: Tab-completion is now included, on supporting systems: * Local site and environment names. * Remote directory and file names. @@ -19,11 +32,13 @@ panssh site.env ``` ### Non-Interactive + +##### From command-line: ``` -# From command-line: panssh site.env "command1; command2; ..." - -# From stdin: +``` +##### From stdin: +``` panssh site.env < script.sh echo "commands" | panssh site.env ``` @@ -64,11 +79,20 @@ echo "commands" | panssh site.env ### No installation -* Mark the main `panssh` script as executable: `chmod +x panssh` -* Run it as just `./panssh` to see further information. +The only required file is the `panssh` script. + +##### Download just the main script + +``` +curl -so panssh https://raw.githubusercontent.com/LastCallMedia/panssh/refs/heads/lando/panssh +``` +* Mark the script as executable: `chmod +x panssh` +* Run it as just `./panssh` to see further instructions. ### Minimal installation +Clone the [PanSSH repository](https://github.com/LastCallMedia/panssh) or download and unzip the [zip archive](https://github.com/LastCallMedia/panssh/archive/refs/heads/lando.zip). + Mark the main `panssh` script as executable, then copy or move it to any suitable directory that's included in your PATH. ``` @@ -76,7 +100,7 @@ chmod +x panssh sudo mv panssh /usr/local/bin/ ``` -Run it as just `panssh` to see further information. +Run it as just `panssh` to see further instructions. ### Optional: tab-completion of local site and environment names @@ -84,7 +108,7 @@ Copy the `panssh` completion script from `bash-completion/` to the `bash-complet * For recent Ubuntu distributions, you can probably use `/usr/local/share/bash-completion/completions/` * For MacOS, maybe `/opt/homebrew/etc/bash_completion.d/` or `usr/local/etc/bash_completion.d`, depending on your system. -Test tab-completion by entering `panssh ` then pressing the tab key. +Test tab-completion by entering `panssh ` then pressing the tab key (create a sites configuration file first). ### Optional: tab-completion of remote directory and file names diff --git a/lando/.lando.panssh.yml b/lando/.lando.panssh.yml new file mode 100644 index 0000000..bb5198e --- /dev/null +++ b/lando/.lando.panssh.yml @@ -0,0 +1,43 @@ +# This file is an optional part of the PanSSH utility. +# It provides service and tooling setup to facilitate use of PanSSH under Lando. + +# See: https://github.com/LastCallMedia/panssh +# and: https://github.com/LastCallMedia/panssh/lando/README.md + +# Commands provided: +# +# lando panssh : starts PanSSH for the specified site and environment. +# lando pssh : starts PanSSH for the related site and environment matching your current git branch. + +services: + appserver: + build_as_root: + - echo "PanSSH installation..." + # PanSSH main script. + - curl -so /usr/local/bin/panssh https://raw.githubusercontent.com/LastCallMedia/panssh/refs/heads/lando/panssh + - chmod +x /usr/local/bin/panssh + # PanSSH readx utility script. + - mkdir -p /usr/local/lib/panssh/ + - curl -so /usr/local/lib/panssh/readx.source.sh https://raw.githubusercontent.com/LastCallMedia/panssh/refs/heads/lando/readx.source.sh + # Bash tab-completion support and basic editor. + - apt-get -qqq update && apt-get -qqq install bash-completion nano + # Bash-completion support for the panssh command (and other commands available via `lando ssh`). + # For this to work, we would have to either uncomment /etc/bash.bashrc "enable bash completion" section, + # or include the equivalent in ~/.bashrc or some other suitable location. + # - mkdir -p /usr/local/share/bash-completion/completions + # - curl -so /usr/share/bash-completion/completions/panssh https://raw.githubusercontent.com/LastCallMedia/panssh/refs/heads/lando/bash-completion/panssh + - echo "PanSSH installation complete." + build: + # Run terminus (if available) to create the .panssh.sites file. + - hash terminus && terminus auth:login && terminus site:list --format=csv --fields=name,id > $HOME/.panssh.sites + +tooling: + panssh: + service: appserver + description: "Opens an emulated interactive SSH connection to a Pantheon site/environment." + usage: $0 panssh . + cmd: panssh + pssh: + service: appserver + description: "Opens an emulated interactive SSH connection to the related site and environment matching your git branch." + cmd: br="$(git branch --show-current)"; test "$br" = "master" && br="dev"; panssh "$PANTHEON_SITE_NAME.$br" diff --git a/lando/README.md b/lando/README.md new file mode 100644 index 0000000..3ab7d1d --- /dev/null +++ b/lando/README.md @@ -0,0 +1,34 @@ +# šŸ–„ļø PanSSH under Lando + +This file is an optional part of the [PanSSH](https://github.com/LastCallMedia/panssh) utility. It provides service and tooling setup to facilitate use of PanSSH under [Lando](https://lando.dev/). + +## āœ… Requirements + +- A Lando application built using the [Lando Pantheon Plugin](https://docs.lando.dev/plugins/pantheon/index.html) or other configuration which meets [PanSSH requirements](https://github.com/LastCallMedia/panssh/blob/main/README.md#-requirements). + +## šŸ“¦ Installation + +1. Either clone the [PanSSH repository](https://github.com/LastCallMedia/panssh) or download `.lando.panssh.yml` directly: + +``` +curl -so .lando.panssh.yml https://raw.githubusercontent.com/LastCallMedia/panssh/refs/heads/lando/lando/.lando.panssh.yml +``` +2. Place `.lando.panssh.yml` in the same location as your application's `.lando.yml` file. + +2. Do one of: + 1. Rename it to `.lando.local.yml`, or merge it into your existing `.lando.local.yml` file, if present. + 2. Add `.lando.panssh.yml` into the `postLandoFiles` section of your `~/.lando/config.yml` (recommended). + + āš™ļø In either case, see: https://docs.lando.dev/landofile/#configuration for further information. You may need to create `~/.lando/config.yml` if it is currently missing or empty. + +3. Run `lando rebuild` to apply the changes. This will download and install the relevant components into your Lando application: + * PanSSH scripts. + * Supporting OS packages. + * PanSSH sites configuration file (runs Terminus). + +## Commands provided: + +* `lando panssh ` — starts PanSSH for the specified site and environment. +* `lando pssh` — starts PanSSH for the related site and environment name matching your current git branch. + +--- diff --git a/panssh b/panssh index b3d849c..a013b48 100755 --- a/panssh +++ b/panssh @@ -11,7 +11,7 @@ # License: MIT ############################################################################### -readonly PANSSH_VERSION="PanSSH 1.2.0" +readonly PANSSH_VERSION="PanSSH 1.2.1" # --- Configuration --- readonly SSH_PORT="2222" @@ -24,42 +24,48 @@ mkdir -p -m 700 "$STORAGE_DIR" "$TEMP_DIR" readonly LOCAL_HOST=$(hostname -s) readonly LOCAL_USER=$(whoami) readonly REMOTE_HOME="/tmp/panssh-home.$LOCAL_USER.$LOCAL_HOST" -readonly REMOTE_ENV="HOME=\"$REMOTE_HOME\" XDG_CACHE_HOME=\"$REMOTE_HOME/.cache\"" +readonly REMOTE_ENV="HOME=\"$REMOTE_HOME\" XDG_CACHE_HOME=\"$REMOTE_HOME/.cache\" WP_CLI_CACHE_DIR=\"$REMOTE_HOME/.cache/wp-cli\"" -readonly SSH_SOCKET="$TEMP_DIR/ssh.socket" -readonly SSH_GENERAL_OPTIONS=" \ +readonly SSH_OPTIONS=" \ + -o ConnectTimeout=30 \ -o ControlMaster=auto \ - -o ControlPath=$SSH_SOCKET \ + -o ControlPath=$TEMP_DIR/ssh.socket \ -o ControlPersist=5m \ - -o StrictHostKeyChecking=no \ - -o ConnectTimeout=30" -readonly SSH_OPTIONS="-p $SSH_PORT $SSH_GENERAL_OPTIONS" -readonly SCP_OPTIONS="-q -P $SSH_PORT $SSH_GENERAL_OPTIONS" + -o LogLevel=info \ + -o Port=$SSH_PORT \ + -o StrictHostKeyChecking=accept-new \ + -o UserKnownHostsFile=$STORAGE_DIR/known_hosts" # Location of this script. readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -# --- Load SITE_IDs from config --- +# Load SITE_IDs from config --- if [[ ! -f "$SITES_FILE" ]]; then - echo "🟔 Sites file not found at: $SITES_FILE" - echo - echo "To create it, run:" - echo " terminus site:list --format=csv --fields=name,id > $SITES_FILE" + echo -e "\n$PANSSH_VERSION" + echo -e "\n🟔 Sites file not found at: $SITES_FILE" + echo -e "\nTo create it, run:" + [[ "$LANDO" == "ON" ]] && echo -e " lando ssh" + echo -e " terminus site:list --format=csv --fields=name,id > $SITES_FILE\n" exit 1 fi -# --- Parse command-line, get site.env argument --- +# Parse command-line, get site.env argument. if [[ ! "$1" =~ ^([a-zA-Z0-9\-]+)\.([a-zA-Z0-9\-]+)$ ]]; then + [[ "$0" == "./panssh" ]] && panssh="$0" || panssh="panssh" echo -e "\n$PANSSH_VERSION" echo -e "\nUsage examples:\n" - echo -e " panssh site.env" - echo -e " panssh site.env \"commands\"" - echo -e " panssh site.env < script.sh" - echo -e " echo \"commands\" | panssh site.env" + echo -e " $panssh site.env" + echo -e " $panssh site.env \"commands\"" + echo -e " $panssh site.env < script.sh" + echo -e " echo \"commands\" | $panssh site.env" echo -e "\nSpecial commands:\n" echo -e " View a file: .vw " echo -e " Edit a file: .ed " echo -e " Toggle automatic directory listing: .ls\n" + echo -e "To create or update your available sites configuration, run:" + [[ "$LANDO" == "ON" ]] && echo -e " lando ssh" + echo -e " terminus site:list --format=csv --fields=name,id > $SITES_FILE\n" + exit 1 fi @@ -79,30 +85,40 @@ readonly HOST="appserver.$ENV_ID.$SITE_ID.drush.in" # --- SSH wrapper --- ssh_exec() { local cmd=$(printf '%q' "$1") - ssh $SSH_OPTIONS "$USER@$HOST" "echo $cmd | bash; exit \${PIPESTATUS[1]}" - return $? + ssh $SSH_OPTIONS "$USER@$HOST" "echo $cmd | bash" + # Don't add anything here without saving and returning correct status. } -# --- SSH wrapper with current directory tracking --- +# --- SSH wrapper with syntax check, status and current directory tracking --- ssh_exec_track() { - local cmd="$1" - local marker="___PANSSH_CWD___" - local status=0 - - while IFS= read -r line; do - if [[ "$line" == "$marker "* ]]; then - local meta="${line#"$marker "}" - status="${meta%%,*}" - current_dir="${meta#*,}" - else - echo "$line" - fi + local cwd_marker="PANSSH_status_cwd:" + + # Build command parts. + local pre_cmd="$1" + local cmd="$2" + local clean_cmd="${cmd%;}" + local post_cmd="echo \"$cwd_marker\$?|\$(pwd)\"" + + # Run the command and print its output until we hit our marker. + local line + while IFS= read -r line && [[ "$line" != "$cwd_marker"* ]]; do + echo "$line" done < <( - ssh $SSH_OPTIONS "$USER@$HOST" \ - "echo $(printf '%q' "$cmd; echo $marker \$?,\$(pwd)") | bash" + # Run syntax check first, then full command if syntax is valid. + ssh_exec "bash -n -c '$cmd' && bash -c '$pre_cmd; $clean_cmd; $post_cmd'" ) - return "$status" + # Extract status code and working directory from the final line. + local status_cwd="${line#"$cwd_marker"}" + + # Check for failure - can happen due to syntax error (status 2), etc. + if [[ -z "$status_cwd" ]]; then + return 2 + fi + + # Set remote directory and return status code. + REMOTE_CWD="${status_cwd#*|}" + return "${status_cwd%%|*}" } # --- Clean up and close SSH connection --- @@ -110,7 +126,7 @@ cleanup() { local status=$? history -a ssh -O exit $SSH_OPTIONS "$USER@$HOST" 2>/dev/null - rm -r "$TEMP_DIR" #> /dev/null 2>&1 + rm -r "$TEMP_DIR" echo -e "\nConnection to $SITE_NAME.$ENV_ID closed." >&2 exit $status } @@ -124,13 +140,19 @@ if [[ $# -gt 1 ]] || ! [ -t 0 ]; then else cmd=$(cat) fi - ssh_exec "export $REMOTE_ENV; $cmd" + ssh_exec "$REMOTE_ENV; $cmd" exit $? fi # --- File transfer between local and host --- transfer_file() { - scp $SCP_OPTIONS "$1" "$2" + # Standard scp fails under Lando, reason unknown. + # Until resolved, we use rsync as a workaround. + if [[ $LANDO != "ON" ]]; then + scp -q $SSH_OPTIONS "$1" "$2" + else + rsync -qe "ssh $SSH_OPTIONS" "$1" "$2" + fi return $? } @@ -176,7 +198,7 @@ edit_file() { elif [[ "$action" == "ed" ]] && ! ssh_exec "[ -w \"$remote_path\" ]"; then echo "🚫 File is not writable." else - can_edit=1 + can_edit=1 download_needed=1 fi elif [[ "$action" == "ed" ]]; then @@ -187,7 +209,7 @@ edit_file() { elif ! ssh_exec "[ -w \"$dirname\" ]"; then echo "🚫 Directory is not writable." else - can_edit=1 + can_edit=1 fi else echo "🚫 File not found." @@ -284,9 +306,10 @@ export HISTCONTROL=ignorespace:ignoredups:erasedups history -r # Set up initial state. -auto_ls=0 +REMOTE_CWD="/code" +current_dir="$REMOTE_CWD" current_status=0 -current_dir="/code" +auto_ls=0 # Basic environment checks. if ssh_exec "cd $current_dir"; then @@ -329,7 +352,10 @@ while true; do [[ -z "$cmd" ]] && continue # Handle `exit`. - [[ "$cmd" == "exit" ]] && break + if [[ "$cmd" =~ ^exit([[:space:]]+(.*))?$ ]]; then + current_status="${BASH_REMATCH[2]:-0}" + break; + fi # Handle `.ed` (edit) and `.vw` (view) if [[ "$cmd" =~ ^\.(ed|vw)[[:space:]]+([^[:space:]]+)(.*)$ ]]; then @@ -352,15 +378,18 @@ while true; do continue fi - # Run the command with our environment and correct directory. - current_dir_before="$current_dir" - ssh_exec_track "export $REMOTE_ENV; cd \"$current_dir\" && $cmd" + # Run the command with our environment, current directory and correct initial value for $? + ssh_exec_track "export $REMOTE_ENV; cd \"$current_dir\" && bash -c \"exit $current_status\"" "$cmd" current_status=$? - # Automatic directory listing if enabled and directory was changed. - if [[ "$auto_ls" -eq 1 ]] \ - && [[ "$current_dir" != "$current_dir_before" ]]; then - ssh_exec "cd \"$current_dir\" && ls -pC --group-directories-first" + # Check whether the command changed the current directory. + if [[ -n "$REMOTE_CWD" && "$REMOTE_CWD" != "$current_dir" ]]; then + current_dir="$REMOTE_CWD" + + # Automatic directory listing if enabled. + if [[ "$auto_ls" -eq 1 ]]; then + ssh_exec "cd \"$current_dir\" && ls -pC --group-directories-first" + fi fi done