-1

I have this:

ssh -i "alex-kp.pem" 'ec2-user@ec2-xxx.us-west-2.compute.amazonaws.com' '

cd codes/vbe eval $(ssh-agent) ssh-add -D ssh-add ~/.ssh/id_vbe '

it executes the commands but then just exits the session too, so I return back to my local machine. Anyone know why?

2 Answers2

2

You have provided a command (well, a series of commands) to execute on the remote system. Once that command (those commands) have completed, the session exits. This is defined and should be expected behaviour - see man ssh:

If a command is specified, it is executed on the remote host instead of a login shell.

A simpler example, which executes hostname on the remote system

ssh -i alex-kp.pem ec2-user@ec2-xxx.us-west-2.compute.amazonaws.com hostname
Kusalananda
  • 333,661
Chris Davies
  • 116,213
  • 16
  • 160
  • 287
0

What happens on the remote side

Local ssh user@server 'shell_code' makes the SSH server run something like:

"$SHELL" -c 'shell_code'

I'm not claiming it's exactly like this, but it's certainly close enough to understand what happens. I wrote it as if it was invoked in a shell, so it looks familiar; but in fact there is no shell yet.

A shell invoked this way exits when it reaches the end of the shell_code. In your case the code is multi-line, this changes nothing.

Without shell code, e.g. upon plain ssh user@server, the SSH server starts something different:

"$SHELL" -i

Again, I'm not claiming it's exactly like this.

A shell invoked this way is an interactive shell that prints a prompt and waits for input. It seems this is the behavior you wanted to achieve.

It's the SSH server that deliberately uses one form or the other, depending on the form of local ssh invocation. The latter form works well for interactive use and the former form equally well runs some non-interactive shell code remotely, as if it was local. E.g. locally you can do:

<data md5sum

but if you are missing md5sum locally, you can use a remote one:

<data ssh user@server md5sum

Like local md5sum exits after doing its job, ssh … md5sum also exits after the remote md5sum does its job. Quite useful.

Or you can pipe to a remote file:

<data ssh user@server 'cat >copy'

and cat, the remote shell and the local ssh will exit automatically when appropriate.

Or you can ask server the time:

ssh user@server date

and it will behave like local date, it won't put you in an interactive remote shell.


When interactive shells exit automatically

I expected it [ssh … date] to not exit by default. When you type commands in your terminal, does it exit by default?

It does, really. An interactive shell does exit when it reaches EOF when reading commands. The thing is it doesn't "experience" EOF from the terminal until you hit Ctrl+d (once or twice). This is how it works with a terminal; but if you make an interactive shell read from a regular file or from a pipe then it will see EOF exactly when you expect this:

{ echo date; echo sleep 5; echo date; } | bash -i

and it will exit.

So every shell exits by default when there is truly nothing more to do. Interactive shells allegedly "don't exit" because reading from a terminal is deliberately designed not to trigger EOF when there is no input at the moment, with an assumption that input may come any minute. It's about the terminal, not about the shell nor any program in general; I mean e.g. </etc/fstab cat exits but plain cat reading from a tty can work indefinitely, it's the same story.


Importance of remote tty

The two ways an SSH server can run a shell differ not only in their forms. Remote interactive commands work well when they use a tty they find local (so it's a tty on the server side, it sits between local ssh and the commands); but remote non-interactive commands (like our example md5sum) work well when there is no tty allocated. The SSH server allocates a tty or not and it does it automatically (but there are options for ssh to explicitly choose if needed: -t, -tt, -T). I think you can get some insight from the following question (and answer) of mine, where I tried to get the best from both at once: ssh with separate stdin, stdout, stderr AND tty.


What you can do

It looks you want to get a remote interactive shell that executes some shell code before giving you the first prompt. The easiest way is to put the code into the right startup script (~/.bashrc if the shell is Bash) and just ssh (without passing any shell code) to an interactive remote shell. It's a permanent solution, not useful if you want to be able to run one-time code on demand and still get a prompt.

To execute shell code on demand you need a contraption like this:

ssh -t … '
  exec bash --rcfile <(cat <<"EOF"
    # your custom shell code here
    . ~/.bashrc
    # and/or here
EOF
) -i'

This assumes the shell to be started by the SSH server understands <(…) (Bash does understand) and the shell you want as the ultimate interactive shell is Bash.

The SSH server will run a non-interactive shell, still with a tty (because of -t). This shell will exec to an interactive Bash that will source a custom file instead of ~/.bashrc. The file will be a pipe from the process substitution and its content will be your custom shell code plus the instruction to source ~/.bashrc anyway. This way you will make the ultimate shell execute your custom shell code and then give you the prompt.

Note an interactive shell spawned directly by the SSH server would be a login shell, it would source /etc/profile. Here neither shell is a login shell. We could run the ultimate shell with -l, but it would ignore --rcfile then.

However, your specific custom shell code sets up things that survive exec (they can be inherited in general). This means we can simplify our snippet to:

ssh -t … '
  cd codes/vbe
  eval $(ssh-agent)
  ssh-add -D
  ssh-add ~/.ssh/id_vbe
  exec bash -i
'

And I think in this case you can add -l and make the ultimate Bash a login shell, if this is what you need. Remember that login and non-login shells source different files; the files may source one another (e.g. ~/.bash_profile may source ~/.bashrc); even non-interactive Bash may source ~/.bashrc; and there are two remote shells in sequence, each may source something. It's likely the remote ~/.bashrc will be sourced two times. Depending on its content side effects may appear.

A very different alternative is to invoke ssh … (without shell code) from a local expect script. The script should inject the desired shell code and finally let you interact. The below quick and dirty example assumes that your remote prompt can be detected by matching @*:*$ .

expect -c '
 spawn ssh -i alex-kp.pem ec2-user@ec2-xxx.us-west-2.compute.amazonaws.com
 expect "@*:*$ "
 send "cd codes/vbe\r"
 expect "@*:*$ "
 send "eval \$(ssh-agent)\r"
 expect "@*:*$ "
 send "ssh-add -D\r"
 expect "@*:*$ "
 send "ssh-add ~/.ssh/id_vbe\r"
 interact
'

I don't know AWS, but in general ssh-agent started by eval $(ssh-agent) may or may not survive after you disconnect. If it's going to survive then manual cleaning (ssh-agent -k) before exiting the remote shell is a good idea.