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