Catch signal in bash but don't finish currentl

2019-05-30 14:28发布

问题:

I would like to catch a signal ( let's focus on INT) and not finish the currently running command so that it finishes after running signal handler.

Let's say I have the following script:

#!/bin/bash

READY=0

ctrl_c(){
        READY=1
}

trap ctrl_c INT

while true; do
        echo first sleep
        sleep 1
        echo second sleep
        sleep 1
        if [ $READY -ne 0 ] ; then
                echo -e "\n$READY"
                echo Exit
                exit
        fi
done

When I hit CtrlC after first sleep then I get immediately dropped down to second sleep, second sleep 1 and script finished.

When I hit CtrlC after second sleep (while second sleep 1 is being run) then I get immediately script termination.

How can I ensure that this script runs in whole seconds that is: sleeps are not terminated?

回答1:

You can do it by exploiting the fact that only the foreground process will receive the signal. So, you can run your command in background, trap in foreground and wait until the command exits.

But in addition, since wait will also exit on reception of a trapped signal:

When Bash is waiting for an asynchronous command via the wait builtin, the reception of a signal for which a trap has been set will cause the wait builtin to return immediately with an exit status greater than 128, immediately after which the trap is executed.

we'll have to wait in a loop, aborting only on exit status below (or equal to) 128 -- assuming the command will never exit with status above 128. If this assumption is not valid in your case, then this solution won't work.

We can wrap all this in a function, let's call it trapwrap:

trapwrap() {
    declare -i pid status=255
    # set the trap for the foreground process
    trap ctrl_c INT
    # run the command in background
    "$@" & pid=$!
    # wait until bg command finishes, handling interruptions by trapped signals
    while (( status > 128 )); do
        wait $pid
        status=$?
    done
    # restore the trap
    trap - INT
    # return the command exit status
    return $status
}

(Explanation: first we declare pid and status as integers, so we don't have to escape them later on. After setting the trap for SIGINT to a previously defined user function ctrl_c, we run the user-supplied command in background and store its PID in pid. We wait for pid's completion in an infinite loop because wait will also break on trapped SIGINT, in which case it exits with status >128. We loop until wait exits with <=128, as that signifies the exit status is actually coming from the background process which just finished. Finally, we restore the trap and return the command exit status.)

Then, we can use the trapwrap like this:

#!/bin/bash

READY=0

ctrl_c() {
    READY=1
}

while true; do
    echo first sleep
    trapwrap sleep 1

    echo second sleep
    trapwrap sleep 1

    if [ $READY -ne 0 ] ; then
        echo -e "\n$READY"
        echo Exit
        exit
    fi
done

When run, you get the expected result:

first sleep
^C^C^C^Csecond sleep
^C^C
1
Exit