End-goal: BASH script that is waiting for background jobs to finish does not abort on first Ctrl-c
; instead, it requires a second Ctrl-c
to quit.
I’m well aware of how the BASH-builtin trap
works. You can either:
Use it to ignore a signal altogether (e.g.,
trap '' 2
) … orUse it to have arbitrary commands executed before a signals original function is allowed to happen (e.g.,
trap cmd 2
, wherecmd
is run before the parent script will be interrupted due toSIGINT
)
So the question boils down to this:
How can I effectively combine 1 & 2 together, i.e., prevent the end-result a signal would lead to (1 — e.g., stop script cancelling due to
SIGINT
) while also making that signal cause something else (2 — e.g., increment a counter, check the counter and conditionally either print a warning or exit).
Put more simply:
How can I make a signal do something else entirely; not just insert a job before it does its thing.
Here’s some example code to demonstrate what I’m aiming at; however, it of course doesn’t work — because trap
can only do 1 or 2 from above.
#!/bin/bash
declare -i number_of_times_trap_triggered
cleanup_bg_jobs() {
number_of_times_trap_triggered+=1
if [[ ${number_of_times_trap_triggered} -eq 1 ]]; then
echo "There are background jobs still running"
echo "Hit Ctrl-c again to cancel all bg jobs & quit"
else
echo "Aborting background jobs"
for pid in ${bg_jobs}; do echo " Killing ${pid}"; kill -9 ${pid}; done
fi
}
f() { sleep 5m; }
trap cleanup_bg_jobs 2
bg_jobs=
for job in 1 2 3; do
f &
bg_jobs+=" $!"
done
wait
So this is the output you end up getting when you press Ctrl-c
once.
[rsaw:~]$ ./zax
^CThere are background jobs still running
Hit Ctrl-c again to cancel all bg jobs & quit
[rsaw:~]$ ps axf|tail -6
24569 pts/3 S 0:00 /bin/bash ./zax
24572 pts/3 S 0:00 _ sleep 5m
24570 pts/3 S 0:00 /bin/bash ./zax
24573 pts/3 S 0:00 _ sleep 5m
24571 pts/3 S 0:00 /bin/bash ./zax
24574 pts/3 S 0:00 _ sleep 5m
Of course I could modify that to clean up the jobs on the first Ctrl-c
, but that’s not what I want. I want to stop BASH from quiting after the first trap is triggered … until it’s triggered a second time.
PS: Target platform is Linux (I couldn’t care less about POSIX compliance) with BASH v4+
Advertisement
Answer
A colleague (Grega) just gave me a solution which … well I can’t believe I didn’t think of it first.
“My approach would … be to lay it off for long enough, possibly forever, using a function that just never returns or something (another wait?), so that the second handler can do its job properly.”
For the record, wait
would not work here. (Recursive.) However, adding a sleep
command to my original code’s cleanup_bg_jobs()
function would take care of it .. but would lead to orphaned processes. So I leveraged process groups to ensure that all children of the script really do get killed. Simplified example for posterity:
#!/bin/bash
declare -i count=
handle_interrupt() {
count+=1
if [[ ${count} -eq 1 ]]; then
echo "Background jobs still running"
echo "Hit Ctrl-c again to cancel all bg jobs & quit"
sleep 1h
else
echo "Aborting background jobs"
pkill --pgroup 0
fi
}
f() { tload &>/dev/null; }
trap handle_interrupt 2
for job in 1 2 3; do
f &
done
wait