I am looking for a way to clean up the mess when my top-level script exits.
Especially if I want to use set -e
, I wish the background process would die when the script exits.
This question is related to
shell
Just for diversity I will post variation of https://stackoverflow.com/a/2173421/102484 , because that solution leads to message "Terminated" in my environment:
trap 'test -z "$intrap" && export intrap=1 && kill -- -$$' SIGINT SIGTERM EXIT
Another option is it to have the script set itself as the process group leader, and trap a killpg on your process group on exit.
trap 'kill $(jobs -p)' EXIT
I would make only minor changes to Johannes' answer and use jobs -pr to limit the kill to running processes and add a few more signals to the list:
trap 'kill $(jobs -pr)' SIGINT SIGTERM EXIT
To be on the safe side I find it better to define a cleanup function and call it from trap:
cleanup() {
local pids=$(jobs -pr)
[ -n "$pids" ] && kill $pids
}
trap "cleanup" INT QUIT TERM EXIT [...]
or avoiding the function altogether:
trap '[ -n "$(jobs -pr)" ] && kill $(jobs -pr)' INT QUIT TERM EXIT [...]
Why? Because by simply using trap 'kill $(jobs -pr)' [...]
one assumes that there will be background jobs running when the trap condition is signalled. When there are no jobs one will see the following (or similar) message:
kill: usage: kill [-s sigspec | -n signum | -sigspec] pid | jobspec ... or kill -l [sigspec]
because jobs -pr
is empty - I ended in that 'trap' (pun intended).
The trap 'kill 0' SIGINT SIGTERM EXIT
solution described in @tokland's answer is really nice, but latest Bash crashes with a segmentation fault when using it. That's because Bash, starting from v. 4.3, allows trap recursion, which becomes infinite in this case:
SIGINT
or SIGTERM
or EXIT
;kill 0
, which sends SIGTERM
to all processes in the group, including the shell itself;This can be worked around by manually de-registering the trap:
trap 'trap - SIGTERM && kill 0' SIGINT SIGTERM EXIT
The more fancy way that allows printing the received signal and avoids "Terminated:" messages:
#!/usr/bin/env bash
trap_with_arg() { # from https://stackoverflow.com/a/2183063/804678
local func="$1"; shift
for sig in "$@"; do
trap "$func $sig" "$sig"
done
}
stop() {
trap - SIGINT EXIT
printf '\n%s\n' "received $1, killing child processes"
kill -s SIGINT 0
}
trap_with_arg 'stop' EXIT SIGINT SIGTERM SIGHUP
{ i=0; while (( ++i )); do sleep 0.5 && echo "a: $i"; done } &
{ i=0; while (( ++i )); do sleep 0.6 && echo "b: $i"; done } &
while true; do read; done
UPD: added a minimal example; improved stop
function to avoid de-trapping unnecessary signals and to hide "Terminated:" messages from the output. Thanks Trevor Boyd Smith for the suggestions!
jobs -p does not work in all shells if called in a sub-shell, possibly unless its output is redirected into a file but not a pipe. (I assume it was originally intended for interactive use only.)
What about the following:
trap 'while kill %% 2>/dev/null; do jobs > /dev/null; done' INT TERM EXIT [...]
The call to "jobs" is needed with Debian's dash shell, which fails to update the current job ("%%") if it is missing.
function cleanup_func {
sleep 0.5
echo cleanup
}
trap "exit \$exit_code" INT TERM
trap "exit_code=\$?; cleanup_func; kill 0" EXIT
# exit 1
# exit 0
Like https://stackoverflow.com/a/22644006/10082476, but with added exit-code
So script the loading of the script. Run a killall
(or whatever is available on your OS) command that executes as soon as the script is finished.
Another option is it to have the script set itself as the process group leader, and trap a killpg on your process group on exit.
A nice version that works under Linux, BSD and MacOS X. First tries to send SIGTERM, and if it doesn't succeed, kills the process after 10 seconds.
KillJobs() {
for job in $(jobs -p); do
kill -s SIGTERM $job > /dev/null 2>&1 || (sleep 10 && kill -9 $job > /dev/null 2>&1 &)
done
}
TrapQuit() {
# Whatever you need to clean here
KillJobs
}
trap TrapQuit EXIT
Please note that jobs does not include grand children processes.
Update: https://stackoverflow.com/a/53714583/302079 improves this by adding exit status and a cleanup function.
trap "exit" INT TERM
trap "kill 0" EXIT
Why convert INT
and TERM
to exit? Because both should trigger the kill 0
without entering an infinite loop.
Why trigger kill 0
on EXIT
? Because normal script exits should trigger kill 0
, too.
Why kill 0
? Because nested subshells need to be killed as well. This will take down the whole process tree.
So script the loading of the script. Run a killall
(or whatever is available on your OS) command that executes as soon as the script is finished.
Another option is it to have the script set itself as the process group leader, and trap a killpg on your process group on exit.
jobs -p does not work in all shells if called in a sub-shell, possibly unless its output is redirected into a file but not a pipe. (I assume it was originally intended for interactive use only.)
What about the following:
trap 'while kill %% 2>/dev/null; do jobs > /dev/null; done' INT TERM EXIT [...]
The call to "jobs" is needed with Debian's dash shell, which fails to update the current job ("%%") if it is missing.
To be on the safe side I find it better to define a cleanup function and call it from trap:
cleanup() {
local pids=$(jobs -pr)
[ -n "$pids" ] && kill $pids
}
trap "cleanup" INT QUIT TERM EXIT [...]
or avoiding the function altogether:
trap '[ -n "$(jobs -pr)" ] && kill $(jobs -pr)' INT QUIT TERM EXIT [...]
Why? Because by simply using trap 'kill $(jobs -pr)' [...]
one assumes that there will be background jobs running when the trap condition is signalled. When there are no jobs one will see the following (or similar) message:
kill: usage: kill [-s sigspec | -n signum | -sigspec] pid | jobspec ... or kill -l [sigspec]
because jobs -pr
is empty - I ended in that 'trap' (pun intended).
So script the loading of the script. Run a killall
(or whatever is available on your OS) command that executes as soon as the script is finished.
The trap 'kill 0' SIGINT SIGTERM EXIT
solution described in @tokland's answer is really nice, but latest Bash crashes with a segmentation fault when using it. That's because Bash, starting from v. 4.3, allows trap recursion, which becomes infinite in this case:
SIGINT
or SIGTERM
or EXIT
;kill 0
, which sends SIGTERM
to all processes in the group, including the shell itself;This can be worked around by manually de-registering the trap:
trap 'trap - SIGTERM && kill 0' SIGINT SIGTERM EXIT
The more fancy way that allows printing the received signal and avoids "Terminated:" messages:
#!/usr/bin/env bash
trap_with_arg() { # from https://stackoverflow.com/a/2183063/804678
local func="$1"; shift
for sig in "$@"; do
trap "$func $sig" "$sig"
done
}
stop() {
trap - SIGINT EXIT
printf '\n%s\n' "received $1, killing child processes"
kill -s SIGINT 0
}
trap_with_arg 'stop' EXIT SIGINT SIGTERM SIGHUP
{ i=0; while (( ++i )); do sleep 0.5 && echo "a: $i"; done } &
{ i=0; while (( ++i )); do sleep 0.6 && echo "b: $i"; done } &
while true; do read; done
UPD: added a minimal example; improved stop
function to avoid de-trapping unnecessary signals and to hide "Terminated:" messages from the output. Thanks Trevor Boyd Smith for the suggestions!
A nice version that works under Linux, BSD and MacOS X. First tries to send SIGTERM, and if it doesn't succeed, kills the process after 10 seconds.
KillJobs() {
for job in $(jobs -p); do
kill -s SIGTERM $job > /dev/null 2>&1 || (sleep 10 && kill -9 $job > /dev/null 2>&1 &)
done
}
TrapQuit() {
# Whatever you need to clean here
KillJobs
}
trap TrapQuit EXIT
Please note that jobs does not include grand children processes.
Just for diversity I will post variation of https://stackoverflow.com/a/2173421/102484 , because that solution leads to message "Terminated" in my environment:
trap 'test -z "$intrap" && export intrap=1 && kill -- -$$' SIGINT SIGTERM EXIT
I made an adaption of @tokland's answer combined with the knowledge from http://veithen.github.io/2014/11/16/sigterm-propagation.html when I noticed that trap
doesn't trigger if I'm running a foreground process (not backgrounded with &
):
#!/bin/bash
# killable-shell.sh: Kills itself and all children (the whole process group) when killed.
# Adapted from http://stackoverflow.com/a/2173421 and http://veithen.github.io/2014/11/16/sigterm-propagation.html
# Note: Does not work (and cannot work) when the shell itself is killed with SIGKILL, for then the trap is not triggered.
trap "trap - SIGTERM && echo 'Caught SIGTERM, sending SIGTERM to process group' && kill -- -$$" SIGINT SIGTERM EXIT
echo $@
"$@" &
PID=$!
wait $PID
trap - SIGINT SIGTERM EXIT
wait $PID
Example of it working:
$ bash killable-shell.sh sleep 100
sleep 100
^Z
[1] + 31568 suspended bash killable-shell.sh sleep 100
$ ps aux | grep "sleep"
niklas 31568 0.0 0.0 19640 1440 pts/18 T 01:30 0:00 bash killable-shell.sh sleep 100
niklas 31569 0.0 0.0 14404 616 pts/18 T 01:30 0:00 sleep 100
niklas 31605 0.0 0.0 18956 936 pts/18 S+ 01:30 0:00 grep --color=auto sleep
$ bg
[1] + 31568 continued bash killable-shell.sh sleep 100
$ kill 31568
Caught SIGTERM, sending SIGTERM to process group
[1] + 31568 terminated bash killable-shell.sh sleep 100
$ ps aux | grep "sleep"
niklas 31717 0.0 0.0 18956 936 pts/18 S+ 01:31 0:00 grep --color=auto sleep
trap 'kill $(jobs -p)' EXIT
I would make only minor changes to Johannes' answer and use jobs -pr to limit the kill to running processes and add a few more signals to the list:
trap 'kill $(jobs -pr)' SIGINT SIGTERM EXIT
I made an adaption of @tokland's answer combined with the knowledge from http://veithen.github.io/2014/11/16/sigterm-propagation.html when I noticed that trap
doesn't trigger if I'm running a foreground process (not backgrounded with &
):
#!/bin/bash
# killable-shell.sh: Kills itself and all children (the whole process group) when killed.
# Adapted from http://stackoverflow.com/a/2173421 and http://veithen.github.io/2014/11/16/sigterm-propagation.html
# Note: Does not work (and cannot work) when the shell itself is killed with SIGKILL, for then the trap is not triggered.
trap "trap - SIGTERM && echo 'Caught SIGTERM, sending SIGTERM to process group' && kill -- -$$" SIGINT SIGTERM EXIT
echo $@
"$@" &
PID=$!
wait $PID
trap - SIGINT SIGTERM EXIT
wait $PID
Example of it working:
$ bash killable-shell.sh sleep 100
sleep 100
^Z
[1] + 31568 suspended bash killable-shell.sh sleep 100
$ ps aux | grep "sleep"
niklas 31568 0.0 0.0 19640 1440 pts/18 T 01:30 0:00 bash killable-shell.sh sleep 100
niklas 31569 0.0 0.0 14404 616 pts/18 T 01:30 0:00 sleep 100
niklas 31605 0.0 0.0 18956 936 pts/18 S+ 01:30 0:00 grep --color=auto sleep
$ bg
[1] + 31568 continued bash killable-shell.sh sleep 100
$ kill 31568
Caught SIGTERM, sending SIGTERM to process group
[1] + 31568 terminated bash killable-shell.sh sleep 100
$ ps aux | grep "sleep"
niklas 31717 0.0 0.0 18956 936 pts/18 S+ 01:31 0:00 grep --color=auto sleep
Update: https://stackoverflow.com/a/53714583/302079 improves this by adding exit status and a cleanup function.
trap "exit" INT TERM
trap "kill 0" EXIT
Why convert INT
and TERM
to exit? Because both should trigger the kill 0
without entering an infinite loop.
Why trigger kill 0
on EXIT
? Because normal script exits should trigger kill 0
, too.
Why kill 0
? Because nested subshells need to be killed as well. This will take down the whole process tree.
Another option is it to have the script set itself as the process group leader, and trap a killpg on your process group on exit.
function cleanup_func {
sleep 0.5
echo cleanup
}
trap "exit \$exit_code" INT TERM
trap "exit_code=\$?; cleanup_func; kill 0" EXIT
# exit 1
# exit 0
Like https://stackoverflow.com/a/22644006/10082476, but with added exit-code
Source: Stackoverflow.com