160

I have a Bash script, which looks similar to this:

#!/bin/bash
echo "Doing some initial work....";
/bin/start/main/server --nodaemon

Now if the bash shell running the script receives a SIGTERM signal, it should also send a SIGTERM to the running server (which blocks, so no trap possible). Is that possible?

5 Answers 5

185

Try:

#!/bin/bash 

_term() { 
  echo "Caught SIGTERM signal!" 
  kill -TERM "$child" 2>/dev/null
}

trap _term SIGTERM

echo "Doing some initial work...";
/bin/start/main/server --nodaemon &

child=$! 
wait "$child"

Normally, bash will ignore any signals while a child process is executing. Starting the server with & will background it into the shell's job control system, with $! holding the server's PID (to be used with wait and kill). Calling wait will then wait for the job with the specified PID (the server) to finish, or for any signals to be fired.

When the shell receives SIGTERM (or the server exits independently), the wait call will return (exiting with the server's exit code, or with the signal number + 128 in case a signal was received). Afterward, if the shell received SIGTERM, it will call the _term function specified as the SIGTERM trap handler before exiting (in which we do any cleanup and manually propagate the signal to the server process using kill).

15
  • 11
    But exec replaces the shell with the given program, I am not clear on why the subsequent wait call is then needed?
    – iruvar
    Commented Jul 26, 2014 at 22:08
  • 7
    I think that 1_CR's point is valid. Either you simply use exec /bin/start/main/server --nodaemon (in which case the shell process is replaced with the server process and you don't need to propagate any signals) or you use /bin/start/main/server --nodaemon &, but then exec is not really meaningful. Commented Nov 13, 2014 at 19:39
  • 7
    If you want your shell script to terminate only after child is terminated, then in the _term() function you should wait "$child" again. This might be necessary if you have some other supervising process waiting for the shell script to die before restarting it again, or if you also trapped EXIT to do some cleanup and neeed it to run only after the child process has finished.
    – LeoRochael
    Commented May 8, 2017 at 22:54
  • 2
    I think bash ignores SIGTERM only in interactive mode according to man and my local testing. Otherwise good answer. Commented Jun 26, 2017 at 10:48
  • 2
    @AlexanderMills Read the other answers. Either you're looking for exec, or you want to set up traps. Commented Sep 29, 2018 at 16:06
154

Bash does not forward signals like SIGTERM to processes it is currently waiting on. If you want to end your script by segueing into your server (allowing it to handle signals and anything else, as if you had started the server directly), you should use exec, which will replace the shell with the process being opened:

#!/bin/bash
echo "Doing some initial work....";
exec /bin/start/main/server --nodaemon

If you need to keep the shell around for some reason (ie. you need to do some cleanup after the server terminates), you should use a combination of trap, wait, and kill. See SensorSmith's answer.

3
  • 4
    This is the correct answer! So much more concise and addresses OP's original ask exactly
    – BrDaHa
    Commented Aug 15, 2019 at 17:05
  • 1
    I was pretty happy about the accepted answer because exec has one limitation: You replace the process image, so you are not able to execute code after the process exits (e.g. cleanup, which was important for me).
    – Ercksen
    Commented May 24, 2023 at 16:47
  • God bless you!!
    – Alexey Sh.
    Commented Jun 8 at 22:55
54

Andreas Veithen points out that if you do not need to return from the call (like in the OP's example) simply calling through the exec command is sufficient (@Stuart P. Bentley's answer). Otherwise the "traditional" trap 'kill $CHILDPID' TERM (@cuonglm's answer) is a start, but the wait call actually returns after the trap handler runs which can still be before the child process actually exits. So an "extra" call to wait is advisable (@user1463361's answer).

While this is an improvement it still has a race condition which means that the process may never exit (unless the signaler retries sending the TERM signal). The window of vulnerability is between registering the trap handler and recording the child's PID.

The following eliminates that vulnerability (packaged in functions for reuse).

prep_term()
{
    unset term_child_pid
    unset term_kill_needed
    trap 'handle_term' TERM INT
}

handle_term()
{
    if [ "${term_child_pid}" ]; then
        kill -TERM "${term_child_pid}" 2>/dev/null
    else
        term_kill_needed="yes"
    fi
}

wait_term()
{
    term_child_pid=$!
    if [ "${term_kill_needed}" ]; then
        kill -TERM "${term_child_pid}" 2>/dev/null 
    fi
    wait ${term_child_pid} 2>/dev/null
    trap - TERM INT
    wait ${term_child_pid} 2>/dev/null
}

# EXAMPLE USAGE
prep_term
/bin/something &
wait_term
8
  • 5
    Excellent job - I've updated the link in my answer to point here (on top of this being a more comprehensive solution, I'm still a little irked that the StackExchange UI doesn't credit me in cuonglm's answer for fixing the script to actually do what it's supposed to and writing pretty much all the explanatory text after the OP who didn't even understand made a few minor re-edits). Commented Sep 29, 2018 at 16:01
  • 5
    @StuartP.Bentley, thanks. I was surprised assembling this required two (not accepted) answers and an external reference, and then I had to run down the race condition. I will upgrade my references to links as what little additional kudos I can give. Commented Sep 30, 2018 at 20:21
  • 2
    @TorstenBronger it should be portable, but I haven't tested it under anything but Bash. I did not use any deliberate Bashisms (no 'function' keyword, no double brace conditionals, no fancy tricks in the output redirection, and the trap syntax is Posix). Commented Nov 12, 2019 at 17:24
  • 2
    @TorstenBronger re-tested under Ubuntu 18.04 Bash 4.4.20 (not my original target) and get Bash debug-ish output with line # and "Terminated", but when the child had NOT exited prior to the trap (odd). It might be legal for the PID to be forgotten after the first wait, but the 2nd wait IS necessary on some systems, so no good answer. (Exit code was still available in this test.) I edited to redirect the "error" output to null for when/systems on which it happens. Commented Sep 20, 2020 at 23:58
  • 2
    At gist.github.com/bronger/… you see what was necessary in my case (zsh). It still does not cover all edge cases, but they might be considered programming errors anyway. Commented Sep 21, 2020 at 5:09
11

Provided solution doesn't work for me because process was killed before the wait command actually finished. I found that article http://veithen.github.io/2014/11/16/sigterm-propagation.html, the last snippet work good in my case of application started in the OpenShift with custom sh runner. The sh script is required because I need to have an ability to get thread dumps which is impossible in case PID of Java process is 1.

trap 'kill -TERM $PID' TERM INT
$JAVA_EXECUTABLE $JAVA_ARGS &
PID=$!
wait $PID
trap - TERM INT
wait $PID
EXIT_STATUS=$?
2

To add a few points to the above answers:

  1. Starting the process in the background with '&' and wait for its pid as advised by @cuonglm will make it possible for the handler to execute while the child is running, but the child will loose the ability to catch any input as stdin will be closed as soon as the child is detached. To force stdin to stay open, you might add an infinite loop which you pipe to the child process. See this post.

Then read input in the current shell and write it to the proc file of the child's process so it goes to its stdin:

(while true; do sleep 10000; done) | /bin/start/main/server --nodaemon &
child_pid=$!
while :
do
    result=$(kill -0 $mypid > /dev/null 2>&1)
    if [ $? -ne 0 ] ; then
        # process is gone
        break
    else
        # read input in the current shell and store it in a variable. The timeout only works with Bash, not with Bourne-Shell. You will need to find a way to read stdin instead and sleep 1 sec between each loop
        read -t 1 input 
        
        # echo the input to the proc file of the runuser process so it goes to its stdin
        echo $input > /proc/$child_pid/fd/0 2>/dev/null 
    fi
done
wait $child_pid

NOTE: this works pretty well on Linux but might need some adjustments for other Unix platforms.

EDIT: a simpler method is to duplicate stdin to a file descriptor which can then be used as the stdin for the background process:

exec 3<&0
/bin/start/main/server --nodaemon <&3 &
  1. exec is the second solution as suggested by @Stuart P. Bentley, but sometimes you need to create the process with a new PID or the command used might not let you the choice and create the process with a new PID or even a new PGID (that's the case for example for runuser with the -l option).

  2. An alternative to 1) and 2) is to send the signal to the process group instead of targeting a specific PID.

This can be done by using kill with a minus (-) before the child's PID:

kill -TERM -$child_pid

Bash will indeed let you trap signals that are targeting a process group without needing to start the process in the background. This method will not loose the ability for the child process to read stdin. This is also a good solution if your child is running in a different process group as the handler will let you forward the signal to the child. Limitation is that other members of the group will also receive the signal, which might be a problem or not depending on the scenario.

0

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .