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.

Asked By: user1011113

||

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.

Answered By: warownia1

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()
Answered By: Misha Brt
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.