Bash – How to prevent CTRL+C to break ssh connection

bashssh

When I want to stop tail -f I press CTRL+C and return to prompt.
But when I run ssh connection the CTRL+C breaks the connection. (meanings of flags described here)

ssh -t svf "cd ~/w/logs; tail -f some_file.log; exec $SHELL -l"

this post does not describes how to prevent remote shell to exit for ssh -t remotehost command args ...

How to stop command (in this example tail -f) with CTRL+C and do not break ssh connection?

Best Answer

In

ssh -t svf "cd ~/w/logs; tail -f some_file.log; exec $SHELL -l"

-t means the local tty line discipline is set to raw (pass-through) mode and a pseudo-tty is created on the remote side.

That pseudo-tty will get default settings which on most systems means for instance that a ^C character will cause a SIGINT to be sent to the foreground process group of that pseudo-terminal.

Here, when you ssh with a command, sshd runs login-shell-of-the-remote-user -c that-command. That's a non-interactive shell, so it doesn't perform job control.

In particular, that shell like every command it runs will run in the same process group which is the foreground process group of the terminal.

The $SHELL -l shell though (note that $SHELL is expanded on the local machine which is probably not what you want) will be an interactive shell (as it's called without -c and its stdin is a terminal device), so will run commands (pipelines) in different process groups and makes them the foreground process group of the terminal or not as necessary.

Here, to make that only tail -f receives the SIGINT, or at least that only it and not the shell is killed, you've got several options depending on the login shell of the remote user.

If the login shell of the remote user is bash or yash, you can do:

ssh -t svf 'set -m; cd ~/w/logs && tail -f some_file.log; exec "$SHELL" -l'

The -m option enables job-control. That tells the shell to run pipelines in separate process groups (like for interactive shells) and (for bash and yash, generally not for all other shells) that the process group of pipelines run in foreground is made the foreground process group of the terminal device (so that they get the SIGINT on ^C, or SIGTSTP on ^Z for instance), and the ones started in background are not (so that they be forbidden (receive SIGTTIN) to read from the terminal for instance).

That way tail -f will run in its own process group, which will be the foreground process group. If you type CTRL+C while tail is running, only tail will receive the SIGINT.

Another alternative if you know the shell is Bourne like, is to set a handler for SIGINT:

ssh -t svf 'trap : INT; cd ~/w/logs && tail -f some_file.log; exec "$SHELL" -l'

Here, we're configuring the shell so it executes the : (no-op) command upon receiving SIGINT. Child processes will inherit that handler, but upon executing a command, the handler will be reset to the default (of terminating the process).

So now, upon CTRL-C while tail is running, both the shell and tail will receive the SIGINT, but only tail will die of it, and the shell will carry on executing "$SHELL" -l.

If you know bash or yash are available on the remote host (and the remote shell is Bourne or csh like), you could also do:

ssh -t svf 'cd ~/w/logs && bash -c "set -m; tail -f some_file.log"; exec "$SHELL" -l'

We invoke the bash shell explicitly and turn the -m option so that tail will be started in its own foreground process group and only it will receive the SIGINT like for the firs solution.

With bash-4.3 or above, you can replace the bash -c "set -m; with bash -mc ". That won't work with older versions which ignore the -m (and -i) option passed the the bash interpreter if combined with -c.

Related Question