Prevent SIGINT from closing child process in bash script
Asked Answered
G

3

6

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)

Gernhard answered 6/8, 2016 at 2:52 Comment(2)
As an aside, the tedious if [ -e ${cachedir} ] sequence can be replaced simply with mkdir -p "$cachedir/out". If all the directories exist, it does nothing; if one or more path components are missing, it creates them all. Notice also how the braces are completely superfluous, but double quotes are necessary to correctly cope with directory names which contain whitespace or a number of other problematic characters.Engeddi
Here is another similar question: #50552518Telamon
I
1

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.

Improvident answered 6/8, 2016 at 3:4 Comment(2)
Please take a look at the script.Gernhard
@JohnLeuenhagen If you're not ignoring SIGINT and you're trapping it with a custom handler, note that the current command will catch the signal and bash will enter the handler and only return when the current command exits. If you want to handle SIGINT and also grant that it is ignored on all child processes, you need to do it explicitly, such as (trap '' SIGINT && rm ${cachedir}/*.mkv), for each command.Improvident
C
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
Circumvolution answered 6/8, 2016 at 4:45 Comment(0)
D
0

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.

Darnley answered 6/8, 2016 at 5:7 Comment(1)
This allowed the handler to be run and the process frozen, but it seems as though the SIGINT still gets through to ffmpeg after the handler completes.Gernhard

© 2022 - 2024 — McMap. All rights reserved.