3

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.

2 Answers 2

5

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.

1
  • Thanks for the detailed explanation. Reckon it's time to consider other languages that are a little more capable.
    – Jim Walker
    Sep 27, 2014 at 15:43
1

For disallowing interrupting the program:

trap "" ERR HUP INT QUIT TERM TSTP TTIN TTOU

But if a sub-command handles traps by itself, and that command must really complete, you need to prevent passing signals to it.

For people on Linux that don't mind installing extra commands, you can just use:

waitFor [command]

Alternatively you can adapt the latest source code of waitFor into your program as needed, or use the code from Gilles' answer. Although that has the disadvantage of not benefiting from updates upstream.

Just mind that other terminals and the service manager can still terminate "command". If you want the service manager to be unable to close "command", it shall be run as a service with the appropriate kill mode and kill signal set.

You must log in to answer this question.

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