Catch SIGINT in bash, handle AND ignore
Asked Answered
C

3

44

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'.

Castrato answered 3/4, 2013 at 11:0 Comment(0)
M
22

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
Melone answered 3/4, 2013 at 11:51 Comment(7)
If the trap is triggered (to do so, you need to replace sleep 1 with sleep 1 & wait), the loop exits after pressing Control-C; you do not continue running where the signal occurred after the trap returns.Arbour
@anishsane 2. what does the user want? I thought the user wanted to trap SIGINT and maintain control of his script.Melone
works on GNU bash, version 3.2.48(1)-release (x86_64-apple-darwin10.0)Melone
Please note that I changed my original example to reflect my real use-case. Actually your answer helps me, but I'm not sure that is very clean.Castrato
I'll just leave this here... $ echo {20..1} # gives 20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 Or alternatively let i=0; while [ $i -lt 20 ]; do sleep 1; let i=$i+1; doneKendo
Or to complete Luc comment with a more unix-like way: you can use seq: $(seq 20 -1 1)Subcommittee
@Subcommittee ha, at first I thought you were saying 12431234123412341234123 was the output, and I was trying to figure out why it would produce thatBehr
H
4

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.

Hillock answered 12/5, 2021 at 6:41 Comment(0)
P
0

i had the same problem: my script was exiting after my sigint handler

i solved this by recursion

#! /bin/sh

# devloop.sh
# run command in infinite loop
# wait before restarting, to allow stopping the loop
# license: MIT, author: milahu
# https://mcmap.net/q/377722/-catch-sigint-in-bash-handle-and-ignore

restart_delay=2

command="$1" # TODO use all args: $@

# example: drop cache, run vite
#command="rm -rf node_modules/.vite/ ; npx vite --clearScreen false"
if [ -z "$command" ]
then
  command="( set -x; sleep 5 ); false # example command: sleep 5 seconds, set rc=1"
fi

loop_next() {

  echo
  echo "starting command. hit Ctrl+C to restart"
  echo "  $command"

  (eval "$command") &
  command_pid=$!

  #echo "main pid: $$"; echo "cmd  pid: $command_pid" # debug

  restart_command() {
    echo
    echo "restarting command in $restart_delay seconds. hit Ctrl+C to stop"
    sleep $restart_delay
    loop_next # recursion
  }

  stop_command() {
    echo
    echo "got Ctrl+C -> stopping command"
    kill $command_pid
    trap exit SIGINT # handle second Ctrl+C
    restart_command
  }

  trap stop_command SIGINT # handle first Ctrl+C

  wait $command_pid # this is blocking

  echo "command stopped. return code: $?"
  restart_command
}

echo starting loop
loop_next
Podolsk answered 24/10, 2022 at 8:24 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.