2

In a shell script, I do the following:

#!/bin/sh

while true; do ssh -o ExitOnForwardFailure=yes -L 8080:localhost:80 -N server; sleep 1; done &

... rest of the script, which uses the tunnel as made above ...

This guarantees that the tunnel is always kept open and thus is re-opened in cases when the connection gets lost. This tunnel is used in other parts of the main script, omitted here. Those parts can handle a nonfunctional tunnel, they simply retry later on.

When the main script dies, for example due to a SIGTERM or SIGINT, I want that while loop to stop as well. There is no need to keep that tunnel open after the main script dies.

What is the common approach to do this in shell scripting? Note that I want two things:

  1. prevent re-execution of the ssh command
  2. stop and disconnect the current ongoing ssh session as soon as possible

I am not sure how to do all this in shell scripting.

Note that I am currently working in plain sh, but I can move on to bash if needed.

Pritzl
  • 305

1 Answers1

4

A somewhat general approach.

while true; do foo; sleep 1; done &
# the rest of the script here
kill -- -"$$"

The trick is the script runs child processes (here foo among others) with Process Group ID (PGID) equal to the PID of the shell. This propagates to grandchildren and so on. The shell itself is in this process group as well. There are exceptions (jobs in interactive shells, timeout) so this is not as general as you may want, still with foo being ssh or similar simple command in a non-interactive script the approach should work.

kill with a negative argument sends signals to the entire process group.

One caveat though: a possible race condition. In general foo may get killed before the subshell receives and handles the signal. If the delay is long enough (for whatever reason), a new foo may be spawned (especially if without sleep 1) after kill does its job. Consider this improvement:

while true; do foo; sleep 1; done &
subpid=$!
# the rest of the script here
kill "$subpid"
wait "$subpid" 2>/dev/null
# at this moment we're certain the subshell is no more, new foo will not be spawned
trap '' TERM
# foo will maintain the old PGID, so…
kill -- -"$$" 2>/dev/null

The trap is here only to make the main shell exit gracefully without printing Terminated to the console.


Not a general approach for any background process, yet usually a useful method for ssh in similar scenario.

Use autossh. From its manual:

autossh is a program to start a copy of ssh and monitor it, restarting it as necessary should it die or stop passing traffic.

[…]

autossh tries to distinguish the manner of death of the ssh process it is monitoring and act appropriately. The rules are:

  1. If the ssh process exited normally (for example, someone typed exit in an interactive session), autossh exits rather than restarting;
  2. If autossh itself receives a SIGTERM, SIGINT, or a SIGKILL signal, it assumes that it was deliberately signalled, and exits after killing the child ssh process;
  3. […]
  4. […]
  5. If the child ssh process dies for any other reason, autossh will attempt to start a new one.

Therefore:

autossh … &
apid=$!
# the rest of the script here
kill "$apid"

Note you won't be notified if the tunnel cannot be established in the first place. Since this is a possible flaw in your original approach as well, I'm not addressing this problem here.