How to propagate SIGTERM to children created via subprocess
Question:
Given the following Python scripts:
a.py:
#!/usr/bin/env python3
# a.py
import signal
import subprocess
import os
def main():
print('Starting process {}'.format(os.getpid()))
subprocess.check_call('./b.py')
if __name__ == '__main__':
main()
b.py:
#!/usr/bin/env python3
# b.py
import signal
import time
import os
def cleanup(signum, frame):
print('Cleaning up...')
raise RuntimeError("Error")
def main():
print('Starting process {}'.format(os.getpid()))
signal.signal(signal.SIGINT, cleanup)
signal.signal(signal.SIGTERM, cleanup)
while True:
print('Hello')
time.sleep(1)
if __name__ == '__main__':
main()
If I execute a.py
, and then later I kill it via kill -15 <pid_of_a_py>
, it kills a.py
, but b.py
keeps running:
$ ./a.py
Starting process 119429
Starting process 119430
Hello
Hello
Hello
Hello
Hello
Terminated // On a separate terminal, I ran "kill -15 119429"
$ Hello
Hello
Hello
Hello
Why is that? How can I make sure that SIGTERM
is propagated from a.py
to b.py
? Consider also a deeper chain a.py -> b.py -> c.py -> d.py
… Where I only want to explicitly setup error handling and cleanup for the innermost script.
Answers:
One solution is to explicitly throw SystemExit
from a.py
#!/usr/bin/env python3
# a.py
import signal
import subprocess
import os
def cleanup(signum, frame):
raise SystemExit(signum)
def main():
signal.signal(signal.SIGINT, cleanup)
signal.signal(signal.SIGTERM, cleanup)
print('Starting process {}'.format(os.getpid()))
subprocess.check_call('./b.py')
if __name__ == '__main__':
main()
Alternatively you can start the process with Popen
and call Popen.send_signal
to the child process when parent exits.
EDIT:
I’ve done some reading on the topic and it’s an expected behaviour. kill -15 <pid>
sends the signal to the specified process and only this one, the signal is not supposed to be propagated. However, you can send a signal to the process group which will kill all children as well. The syntax is kill -15 -<pgid>
(note extra dash). The process group id is typically the same as the leader process id.
There is a way to achieve the same using psutil
import os
import signal
import psutil
def kill_proc_tree(pid, sig=signal.SIGTERM, include_parent=True,
timeout=None, on_terminate=None):
"""Kill a process tree (including grandchildren) with signal
"sig" and return a (gone, still_alive) tuple.
"on_terminate", if specified, is a callback function which is
called as soon as a child terminates.
"""
assert pid != os.getpid(), "won't kill myself"
parent = psutil.Process(pid)
children = parent.children(recursive=True)
if include_parent:
children.append(parent)
for p in children:
try:
p.send_signal(sig)
except psutil.NoSuchProcess:
pass
gone, alive = psutil.wait_procs(children, timeout=timeout,
callback=on_terminate)
return (gone, alive)
You might also want to implement such a logic:
- send
SIGTERM
to a list of processes
- give them some time to terminate
- send
SIGKILL
to those ones which are still alive
import psutil
def on_terminate(proc):
print("process {} terminated with exit code {}".format(proc, proc.returncode))
procs = psutil.Process().children()
for p in procs:
p.terminate()
gone, alive = psutil.wait_procs(procs, timeout=3, callback=on_terminate)
for p in alive:
p.kill()
Given the following Python scripts:
a.py:
#!/usr/bin/env python3
# a.py
import signal
import subprocess
import os
def main():
print('Starting process {}'.format(os.getpid()))
subprocess.check_call('./b.py')
if __name__ == '__main__':
main()
b.py:
#!/usr/bin/env python3
# b.py
import signal
import time
import os
def cleanup(signum, frame):
print('Cleaning up...')
raise RuntimeError("Error")
def main():
print('Starting process {}'.format(os.getpid()))
signal.signal(signal.SIGINT, cleanup)
signal.signal(signal.SIGTERM, cleanup)
while True:
print('Hello')
time.sleep(1)
if __name__ == '__main__':
main()
If I execute a.py
, and then later I kill it via kill -15 <pid_of_a_py>
, it kills a.py
, but b.py
keeps running:
$ ./a.py
Starting process 119429
Starting process 119430
Hello
Hello
Hello
Hello
Hello
Terminated // On a separate terminal, I ran "kill -15 119429"
$ Hello
Hello
Hello
Hello
Why is that? How can I make sure that SIGTERM
is propagated from a.py
to b.py
? Consider also a deeper chain a.py -> b.py -> c.py -> d.py
… Where I only want to explicitly setup error handling and cleanup for the innermost script.
One solution is to explicitly throw SystemExit
from a.py
#!/usr/bin/env python3
# a.py
import signal
import subprocess
import os
def cleanup(signum, frame):
raise SystemExit(signum)
def main():
signal.signal(signal.SIGINT, cleanup)
signal.signal(signal.SIGTERM, cleanup)
print('Starting process {}'.format(os.getpid()))
subprocess.check_call('./b.py')
if __name__ == '__main__':
main()
Alternatively you can start the process with Popen
and call Popen.send_signal
to the child process when parent exits.
EDIT:
I’ve done some reading on the topic and it’s an expected behaviour. kill -15 <pid>
sends the signal to the specified process and only this one, the signal is not supposed to be propagated. However, you can send a signal to the process group which will kill all children as well. The syntax is kill -15 -<pgid>
(note extra dash). The process group id is typically the same as the leader process id.
There is a way to achieve the same using psutil
import os
import signal
import psutil
def kill_proc_tree(pid, sig=signal.SIGTERM, include_parent=True,
timeout=None, on_terminate=None):
"""Kill a process tree (including grandchildren) with signal
"sig" and return a (gone, still_alive) tuple.
"on_terminate", if specified, is a callback function which is
called as soon as a child terminates.
"""
assert pid != os.getpid(), "won't kill myself"
parent = psutil.Process(pid)
children = parent.children(recursive=True)
if include_parent:
children.append(parent)
for p in children:
try:
p.send_signal(sig)
except psutil.NoSuchProcess:
pass
gone, alive = psutil.wait_procs(children, timeout=timeout,
callback=on_terminate)
return (gone, alive)
You might also want to implement such a logic:
- send
SIGTERM
to a list of processes - give them some time to terminate
- send
SIGKILL
to those ones which are still alive
import psutil
def on_terminate(proc):
print("process {} terminated with exit code {}".format(proc, proc.returncode))
procs = psutil.Process().children()
for p in procs:
p.terminate()
gone, alive = psutil.wait_procs(procs, timeout=3, callback=on_terminate)
for p in alive:
p.kill()