Shell Script – Prevent SIGINT from Interrupting Function Calls and Child Processes

shell-scriptsignalstrap:

Consider the following script:

#!/bin/bash

set -o pipefail
set -o history

trapper() {
   func="$1" ; shift
   for sig ; do
      trap "$func $sig" "$sig"
   done
}

err_handler () {
  case $2 in
    INT)
       stop_received=1
    ;;
    TSTP)
    ;;
    ERR)
       if [[ $2 != "INT" ]]; then # for some reason, ERR gets triggered on SIGINT
          code=$?
          if [ $code -ne 0 ]; then
             echo "Failed on line $1"
             echo "$BASH_COMMAND returned $?"
             echo "Content of variables at the time of failure:"
             echo "$(set -o posix; set)"
             exit 1
          fi
       fi
    ;;
  esac
}

main() {
   ping -c 5 www.google.com # this is a test to see if INT interrupts main()
   # do a bunch of stuff, I mean a real bunch of stuff, like check some
   # files, do some processing, connect to a database,
   # do more processing, move some files around, you get the drift
}

exec > >(tee -a my.log)
exec 2>&1

trapper 'err_handler $LINENO' INT TSTP ERR
while main
do
  if [[ "$stop_received" == "1" ]]; then
     break
  fi
  setsid sleep 2 & wait
done
trap ERR

What I am trying to accomplish is to run the script in an infinite loop until either the function main() returns some non zero value, i.e. some error occurred, or SIGINT is received.

However, I don't want SIGINT to stop main() from executing, in other words, if the script receives a SIGINT, it should wait for main() to finish, then exit nicely. But when I hit CTRL+C, I can see that ping is interrupted. At the moment I commented out everything under main() just to see if this works. Since ping gets interrupted, I am assuming other commands under main() would also get interrupted. When ping is interrupted, the processing jumps to the line where I check if $stop_received=1 and then the loop breaks and the script quits. If I replace the break with just an echo, then the script just continues on to the next iteration of the while main loop.

How can I stop SIGINT from interrupting the currently running command(s)? Since my script does a bunch of stuff, including DML statements in a database, interrupting main() would cause lot of grief.

Secondly, the script does not trap ctrl+z either. Or rather, the script just gets stuck on ctrl+z requiring a kill pid to terminate. I assumed that sleep being a child of bash as opposed to the script itself, ctrl+z would cause the script to pause, leaving sleep in limbo land. Hence the setsid and wait on sleep, but it still hangs.

thanks.

Best Answer

There are several ways you can cut off the effect of Ctrl+C:

  • Change the terminal setting so that it doesn't generate a signal.
  • Block the signal so that it is saved for later delivery, when the signal becomes unblocked.
  • Ignore the signal, or set a handler for it.
  • Run subprocesses in a background process group.

Since you want to detect that Ctrl+C has been pressed, ignoring the signal is out. You could change the terminal settings, but then you would need to write custom key processing code. Shells don't provide access to signal blocking.

You can however isolate subprocesses from receiving the signal automatically by running them in a separate process group. Interactive shells run background commands in a separate process group by default, but non-interactive shells run them in the same process group, and all processes in the foreground process group receive a signal from terminal events. To tell the shell to run background jobs in a separate process group, run set -m. Running setsid ping … is another way of forcing ping to run in a separate process group.

set -m
interrupted=
trap 'echo Interrupted, but ping may still be running' INT
set -m
ping … &
while wait; [ $? -ge 128 ]; do echo "Waiting for background jobs"; done
echo ping has finished

If you want Ctrl+Z to suspend a background process group, you'll need to propagate the signal from the shell.

Controlling signals finely is a bit of a stretch for a shell script, and shells other than ATT ksh tend to be a little buggy when you reach the corner cases, so consider a language that gives you more control such as Perl, Python or Ruby.

Related Question