Prevent SIGINT from closing child process in bash

2019-07-12 09:10发布

问题:

I am writing a bash script in which I wrote a handler to take care of when the user pressed Control+C, (by using trap interruptHandler SIGINT) but the SIGINT gets sent to both the bash script and the child process that is currently running, closing the child process. How can I prevent this from happening?

edit: here's the script, don't critique my skills too much..

#!/bin/bash
trap "interruptHandler" SIGINT

inInterrupt=false;
quit=false;

if [ -z ${cachedir+x} ]; then cachedir=~/.cache/zlima12.encoding; fi
cachedir=$(realpath ${cachedir});


if [ ! -e ${cachedir} ]; then mkdir ${cachedir}; fi
if [ ! -e ${cachedir}/out ]; then mkdir ${cachedir}/out; fi

cleanCache ()
{
    rm ${cachedir}/*.mkv;
    rm ${cachedir}/out/*.mkv;
}

interruptHandler ()
{
    if [ ${inInterrupt} != true ]; then
        printf "BASHPID: ${BASHPID}";
        inInterrupt=true;
        ffmpegPID=$(pgrep -P ${BASHPID});
        kill -s SIGTSTP ${ffmpegPID};
        printf "\nWould you like to quit now(1) or allow the current file to be encoded(2)? ";
        read response;
        if [ ${response} = "1" ]; then kill ${ffmpegPID}; cleanCache;
        elif [ ${response} = "2" ]; then quit=true; kill -s SIGCONT ${ffmpegPID};
        else printf "I'm not sure what you said... continuing execution.\n"; kill -s SIGCONT ${ffmpegPID};
        fi

        inInterrupt=false;
    fi
}



for param in "$@"; do

    dir=$(realpath ${param});

    if [ ! -e ${dir} ]; then
        printf "Directory ${dir} doesn't seem to exist... Exiting...\n"
        exit 1;
    elif [ -e ${dir}/new ]; then
        printf "${dir}/new already exists! Proceed? (y/n) ";
        read response;
        if [ ${response} != y ]; then exit 1; fi
    else
        mkdir ${dir}/new;
    fi

    for file in ${dir}/*.mkv; do
        filename="$(basename ${file})";
        cp $file ${cachedir}/${filename};
        ffmpeg -vsync passthrough -i ${cachedir}/${filename} -c:v libx265 -c:a copy -f matroska ${cachedir}/out/${filename};
        rm ${cachedir}/${filename};
        mv ${cachedir}/out/${filename} ${dir}/new/${filename};

        if [ ${quit} = true ]; then exit 0; fi
    done
done

(This is a script to encode matroska (mkv) files to H.265 in case you're curious)

回答1:

The signal is sent to all jobs in the current foreground process. So the easiest way to prevent the signal from going to the child is to get it out of the foreground. Just background the ffmpeg call by doing:

...
ffmpeg -vsync passthrough -i ${cachedir}/${filename} -c:v libx265 -c:a copy -f matroska ${cachedir}/out/${filename} &
wait
...

Note that this also gives you the pid of the child more robustly that trying to parse the output of ps, so you might want to do:

ffmpeg ... &
ffmpegPID=$!
wait


回答2:

Performed a simple test here and it delivers the expected result:

int.sh contents:

#!/bin/bash

trap '' SIGINT
tail -f /var/log/syslog >& /dev/null

Testing:

$ ./int.sh
^C^C
# ... SIGINT ignored (CTRL+C) ...
# ... Will send SIGTSTP with CTRL+Z ...
^Z
[1]+  Stopped                 ./int.sh
$ kill %1
$
[1]+  Terminated              ./int.sh
$

EDIT (answering the question edit):

You probably want to trap and ignore SIGINT for every other command, such as (trap '' SIGINT && command) in your script, so you can prevent the signal being caught from the current command before interruptHandler is invoked.

A simple example of what's happening:

#!/bin/bash

function intHandler() {
        echo "If SIGINT was caught, this will be printed AFTER sleep exits."
}

trap intHandler SIGINT

sleep 5 # Sleep will exit as soon as SIGINT is caught

Output:

$ time ./int.sh 
^C
# ... Right here, only 0.6 seconds have elapsed before the below message being printed ...
If SIGINT was caught, this will be printed AFTER sleep exits.

real    0m0.634s
user    0m0.004s
sys     0m0.000s

Note that it only lasted for 0.6 seconds due to SIGINT being caught.

But when you ignore SIGINT for sleep:

function intHandler() {
        echo "If SIGINT was caught, this will be printed AFTER sleep exits."
}

trap intHandler SIGINT

(trap '' SIGINT && sleep 5)

The output is:

$ time ./int.sh
^C
# ... Right here, 5 seconds have elapsed without any message ...
If SIGINT was caught, this will be printed AFTER sleep exits.

real    0m5.007s
user    0m0.000s
sys     0m0.000s

Note that despite the SIGINT was delivered and caught by the script, the intHandler will only return when the current sleep exits, and also note that the current sleep didn't caught the SIGINT from the parent (it lasted for the full 5 seconds) as the subshell where it's running on (the ( ... )) is ignoring SIGINT.



回答3:

Take a look at this:

#!/bin/bash
echo $$
trap 'echo "got C-c"' SIGINT
#bash -c 'trap - SIGINT; echo $$; exec sleep 60' &
sleep 60 &
pid=$!
echo "$$: waiting on $pid"
while kill -0 $pid 2>/dev/null; do
      wait $pid
done
echo done

Explanations:

  • The child ffmpeg (sleep here) must ignore SIGINT itself. To do that, start it with bash -c, reset the handler, then exec. It is enough to get the child out of foreground to prevent it from receiving SIGINT.

  • In the parent, a simple wait will not do, for reasons explained here. (Try it.) In that case, the parent would continue after executing its SIGINT handler but before the child was done. Instead, we use a loop and wait using the child pid.

  • Upon legitimate exit of the child, one more kill will be executed on a nonexisting pid, whose stderr we ignore.