0

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.

user1011113
  • 1,114
  • 8
  • 27

2 Answers2

1

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.

warownia1
  • 2,771
  • 1
  • 22
  • 30
  • Thanks for the reply! I was hoping I wouldn't have to go that way, it's too easy to simply call subprocess and forget to setup the signals. Plus I'll need to copy-paste all that boilerplate for each step in the chain a.py -> b.py -> c.py -> d.py. Feels like signals should be passed on to children by default... – user1011113 Jun 04 '21 at 05:47
  • I updated the answer with additional explanation and more suitable solution. – warownia1 Jun 04 '21 at 09:37
  • Interesting, thanks a lot! That's exactly what I was looking for, works as expected! – user1011113 Jun 04 '21 at 13:32
0

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()
GopherM
  • 352
  • 2
  • 8