Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Catch SIGINT in bash, handle AND ignore

Tags:

bash

Is it possible in bash to intercept a SIGINT, do something, and then ignore it (keep bash running).

I know that I can ignore the SIGINT with

trap '' SIGINT 

And I can also do something on the sigint with

trap handler SIGINT 

But that will still stop the script after the handler executes. E.g.

#!/bin/bash  handler() {     kill -s SIGINT $PID }  program & PID=$!  trap handler SIGINT  wait $PID  #do some other cleanup with results from program 

When I press ctrl+c, the SIGINT to program will be sent, but bash will skip the wait BEFORE program was properly shut down and created its output in its signal handler.

Using @suspectus answer I can change the wait $PID to:

while kill -0 $PID > /dev/null 2>&1 do     wait $PID done 

This actually works for me I am just not 100% sure if this is 'clean' or a 'dirty workaround'.

like image 864
Zulan Avatar asked Apr 03 '13 11:04

Zulan


2 Answers

trap will return from the handler, but after the command called when the handler was invoked.

So the solution is a little clumsy but I think it does what is required. trap handler INT also will work.

trap 'echo "Be patient"' INT  for ((n=20; n; n--)) do     sleep 1 done 
like image 73
suspectus Avatar answered Sep 26 '22 02:09

suspectus


The short answer: SIGINT in bash can be caught, handled and then ignored, assumed that "ignored" here means that bash continues to run the script. The wanted actions of the handler can even be postponed to build a kind of "transaction" so that SIGINT will be fired (or "ignored") AFTER a group of statements have done their work.

But since the above example touches many aspects of bash (foreground vs. background behavior, trap and wait) AND 8 years went away since then, the solution discussed here may not immediately work on all systems without further finetuning.

The solution discussed here was successfully tested on a "Linux mint-mate 5.4.0-73-generic x86_64" system with "GNU bash, Version 4.4.20(1)-release":

  1. The wait shell builtin command IS DESIGNED to be interruptable. But one can examine the exit status of wait, which is 128 + signal number = 130 (in the case of SIGINT). So if you want to trick around and wait til the background is process really finished, one can also do something like this:
wait ${programPID} while [ $? -ge 128 ]; do    # 1st opportunity to place your **handler actions** is here    wait ${programPID} done 

But let it also said that we ran into a bug/feature while testing all of this. The problem was that wait kept on returning 130 even after the process in the background was no longer there. The documentation says that wait will return 127 in the case of a false process id, but this did not happen in our tests. Keep in mind to check the existence of the background process before running the wait command in the while loop, if you also run into this problem.

  1. Assumed that the following script is your program, which simply counts down from 5 to 0 and also tee's its output to a file named program.out. The while loop here is considered as a "transaction", which shall not be disturbed by SIGINT. And one last comment: This code does NOT ignore SIGINT after doing postponed actions, but instead restores the old SIGINT handler and raises a SIGINT:
#!/bin/bash rm -f program.out  # Will be set to 1 by the SIGINT ignoring/postponing handler declare -ig SIGINT_RECEIVED=0 # On <CTRL>+C or "kill -s SIGINT $$" set flag for [later|postponed] examination function _set_SIGINT_RECEIVED {     SIGINT_RECEIVED=1 }  # Remember current SIGINT handler old_SIGINT_handler=$(trap -p SIGINT) # Prepare for later restoration via ${old_SIGINT_handler} old_SIGINT_handler=${old_SIGINT_handler:-trap - SIGINT}  # Start your "transaction", which should NOT be disturbed by SIGINT trap -- '_set_SIGINT_RECEIVED' SIGINT  count=5 echo $count | tee -a program.out while (( count-- )); do     sleep 1     echo $count | tee -a program.out done  # End of your "transaction" # Look whether SIGINT was received if [ ${SIGINT_RECEIVED} -eq 1 ]; then     # Your **handler actions** are here     echo "SIGINT was received during transaction..." | tee -a program.out     echo "... doing postponed work now..." | tee -a program.out     echo "... restoring old SIGINT handler and sending SIGINT" | tee -a program.out     echo "program finished after SIGINT postponed." | tee -a program.out     ${old_SIGINT_handler}     kill -s SIGINT $$ fi echo "program finished without having received SIGINT." | tee -a program.out 

But let it also be said here that we ran into problems after sending program in the background. The problem was that program inherited a trap '' SIGINT which means that SIGINT was generally ignored and program was NOT able to set another handler via trap -- '_set_SIGINT_RECEIVED' SIGINT.

  1. We solved this problem by putting program in a subshell and sending this subshell in the background, as you will see now in the MAIN script example, which runs in the foreground. And one last comment also: In this script you can decide via variable ignore_SIGINT_after_handling whether to finally ignore SIGINT and continue to run the script OR to execute the default SIGINT behavior after your handler action has finished its work:
#!/bin/bash  # Will be set to 1 by the SIGINT ignoring/postponing handler declare -ig SIGINT_RECEIVED=0 # On <CTRL>+C or "kill -s SIGINT $$" set flag for later examination function _set_SIGINT_RECEIVED {     SIGINT_RECEIVED=1 }  # Set to 1 if you want to keep bash running after handling SIGINT in a particular way #  or to 0 (or any other value) to run original SIGINT action after postponing SIGINT ignore_SIGINT_after_handling=1  # Remember current SIGINT handler old_SIGINT_handler=$(trap -p SIGINT) # Prepare for later restoration via ${old_SIGINT_handler} old_SIGINT_handler=${old_SIGINT_handler:-trap - SIGINT}  # Start your "transaction", which should NOT be disturbed by SIGINT trap -- '_set_SIGINT_RECEIVED' SIGINT      # Do your work, for eample      (./program) &     programPID=$!     wait ${programPID}     while [ $? -ge 128 ]; do        # 1st opportunity to place a part of your **handler actions** is here        # i.e. send SIGINT to ${programPID} and make sure that it is only sent once        # even if MAIN receives more SIGINT's during this loop        wait ${programPID}     done  # End of your "transaction" # Look whether SIGINT was received if [ ${SIGINT_RECEIVED} -eq 1 ]; then     # Your postponed **handler actions** are here     echo -e "\nMAIN is doing postponed work now..."     if [ ${ignore_SIGINT_after_handling} -eq 1 ]; then         echo "... and continuing with normal program execution..."     else         echo "... and restoring old SIGINT handler and sending SIGINT via 'kill -s SIGINT \$\$'"         ${old_SIGINT_handler}         kill -s SIGINT $$     fi fi  # Restore "old" SIGINT behaviour ${old_SIGINT_handler} # Prepare for next "transaction" SIGINT_RECEIVED=0 echo "" echo "This message has to be shown in the case of normal program execution" echo "as well as after a caught and handled and then ignored SIGINT" echo "End of MAIN script received" 

Hope this helps a bit. Shall everybody have a good time.

like image 39
Richard Gantz Avatar answered Sep 26 '22 02:09

Richard Gantz