Start a new process group for the Python code (using the setsid
utility) and whatever other processes (including the application proper) it starts, so you can kill the entire process group if needed.
You can use the following construct to do so:
exec 3>&1
pgid=$(exec setsid bash -c 'echo $$; exec >&3-; exec COMMAND...')
where COMMAND...
is the command and its parameters you'd normally start. Note that it is within single quotes, and that the command to be run must be evaluatable as a string (as opposed to a normal shell expression).
The first redirection, 3>&1
, copies the standard output descriptor to descriptor 3. The redirection >&3-
moves descriptor 3 to standard output.
The $(...)
executes the ...
, and evaluates to the data it wrote to standard output. Above, we read the standard output into shell variable pgid
.
The subshell (...
) is replaced with the setsid
utility, which executes its argument in a new session (process group). Here, it executes bash
, which prints out the current process PID ($$
), moves the original standard output from descriptor 3 back, and replaces itself/executes the desired COMMAND...
.
The shell will execute that line for as long as the COMMAND...
runs, and will only progress to the next line after COMMAND...
itself exits.
If the COMMAND...
leaves spurious processes running, and you want to kill them, all you need to do is run
kill -SIGNAL -$pgid
The issue is which signal to send. KILL
will immediately kill the leftover processes in the process group, so it is the simplest option. However, if the processes behave nicely, you should be able to ask them to exit by sending them a TERM
signal instead. Of course, sometimes the processes may be left in a wonky state, so that they don't react to TERM
and do need to be KILL
ed. To solve this, you can use a small loop, and ps
to see if there are any processes left:
retries=50
while ((1)); do
# Processes left?
left=$(ps -o pid= -s $pgrp)
[ -n "$left" ] || break
# Ask them to terminate.
kill -TERM $left
# Wait 0.1 seconds.
sleep .1
# Decrement the retry counter.
((--retries > 0)) || break
done
# If there are any processes left in the group,
# send them a KILL signal.
left=$(ps -o pid= -s $pgrp)
[ -n "$left" ] && kill -KILL $left
Note that the 50 retries (0.1 seconds each) mean that this waits only up to 5 seconds before sending a kill signal. That may not be a suitable value, depending on the application and the kind of hardware it is running. For example, if the machine is a laptop, and the application saves some history or logs into a number of files, and the drive happens to be sleeping at the exit point, I'd up the delay to perhaps 15 to 30 seconds.