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