Search code examples
pythonsubprocesspopensigint

Python unable to terminate spawned child processes by terminating parent process


I have a master script (Python) from where I want to spawn several child scripts. The child scripts should run independently without bothering each other (just writing to a file in a loop). What I want - when the master script is terminated, so should all the spawned child processes.

But however, even through CTRL-C kills the master process and control is returned to command prompt, the child processes keep running as I can see the individual output files getting updated continually. Any idea on how to make this work?

Child script

import sys
from time import sleep

arg = sys.argv[1]
sleepTime = int(sys.argv[2])
while True:
    with open(f"{arg}.txt", "w") as f:
        f.write(arg)
    sleep(sleepTime)

Master script

import sys, subprocess, os, signal
from time import sleep

d = ['fool', 'gool', 'lool']
for i in range(3):
    try:
        subprocess.Popen(['python3', 'child.py', d[i], str(i + 3)], stdin=subprocess.PIPE)
    except KeyboardInterrupt:
        sys.exit(1)

What I run in command line to kick off everything:

python3 master.py

Solution

  • How to terminate kids when parent terminates

    Rather to continue in comments, I put this in an answer. Yet, it is still not clear to me what you are trying to do, since it is a bit contradictory, even tho, this give an answer.

    The reason why it seems contradictory to me, is because you say you want the kid to terminate when the parent does. But then, your kid will have no chance to do anything at all, since the parent just spawn 3 kids (which is very fast) and then does nothing. So terminates. The whole point of spawning subprocess is so that the parent can either do something else after the spawn, or terminate.

    If you want to program the fact that the kid terminate when the parent does, you can do it that way

    import sys, subprocess, os, signal
    from time import sleep
    import atexit
    
    d = ['fool', 'gool', 'lool']
    pp=[]
    for i in range(3):
        try:
            p=subprocess.Popen(['python3', 'child.py', d[i], str(i + 3)], stdin=subprocess.PIPE)
            pp.append(p)
        except KeyboardInterrupt:
            sys.exit(1)
    
    
    def masterFinish():
        for p in pp:
            p.terminate()
        sys.exit(1)
    atexit.register(masterFinish)
    

    Or, more consistently that way

    import sys, subprocess, os, signal
    from time import sleep
    import atexit
    
    d = ['fool', 'gool', 'lool']
    pp=[]
    for i in range(3):
        p=subprocess.Popen(['python3', 'child.py', d[i], str(i + 3)], stdin=subprocess.PIPE)
        pp.append(p)
    
    def masterFinish():
        for p in pp:
            p.terminate()
        sys.exit(1)
    atexit.register(masterFinish)
    

    Since you don't really need that void KeyboardInterrupt handler, that would do something only during the very very short amount of time it takes for the parent to spawn each kid.

    But, as I've said, that means that the kid are terminated as soon as the parent is. That is after the 3rd kid is spawn. In the meantime the even the 1st kids is probably still in the process of starting python interpreter, and probably had not the time to even run the first line of python code. And therefore is terminate before it had a chance to do something.

    On my machine, it creates no file at all. Kid 1 is spawn, then kid 2, then kid 3, then they are all terminated and the "master" ends. And that's all. That is what you asked for, but I doubt that is what you wanted.

    How to handle Ctrl-C in kids

    My feeling is that your "interruptHandler" was meant to run in background, which each of the kid, and ensure that the kid terminates when ctrl-c is received. But it doesn't work like that. The part of the code that runs in background is the one of the kids. Your handler is part of "master" code,

    So where you should put your "Interrupt handler" is in child.py code.

    import sys
    from time import sleep
    
    arg = sys.argv[1]
    sleepTime = int(sys.argv[2])
    try:
        while True:
            with open(f"{arg}.txt", "w") as f:
                f.write(arg)
            sleep(sleepTime)
    except KeyboardInterrupt:
        sys.exit(1)
    

    That is probably closer to what you were trying to do. Maybe you already tried it, and then tried something else because it was "not working". But it was. Just, the kid needs to receive the Ctrl-C. Which it wont, since they are detached, because the master is done.

    Just add anything to master code, and they would receive that ctrl-c

    import sys, subprocess, os, signal
    from time import sleep
    
    d = ['fool', 'gool', 'lool']
    for i in range(3):
        try:
            subprocess.Popen(['python3', 'child.py', d[i], str(i + 3)], stdin=subprocess.PIPE)
        except KeyboardInterrupt:
            sys.exit(1)
    
    sleep(30)
    

    And then if you hit ctrl-c, the kids are exiting. And so does the master.

    Note that neither the interrupt handler of the master nor the one of the kids are really usefull here.

    • As I said earlier, the one of the master was in use only during the very short amount of times it takes to call subprocess. So it is over for a while before you have a chance to type ctrl-c, that would occur most probably during sleep(30).
    • And the one I added to the kid (to show how you were supposed to add an interrupt handler to kids: in their code, not in master's code), would work (it is the system.exit of that handler that will be run and exit the kid), but still wasn't necessary, since it was the default behavior anyway.

    Also note, that this covers only the ctrl-c pat (it explains why your kid didn't receive the ctrl-c). With this sleep(30), and therefore during the 30 seconds while master is still running, if you type ctrl-c, the kids will also receive it, and end.

    It doesn't cover what happens after those 30 seconds. So, need my first answer anyway (atexit — or other ways to implement the same thing. For example, just terminate the kid at the end of master code)

    import sys
    from time import sleep
    
    arg = sys.argv[1]
    sleepTime = int(sys.argv[2])
    while True:
        with open(f"{arg}.txt", "w") as f:
            f.write(arg)
        sleep(sleepTime)
    
    import sys, subprocess, os, signal
    from time import sleep
    
    d = ['fool', 'gool', 'lool']
    pp=[]
    for i in range(3):
        p=subprocess.Popen(['python3', 'child.py', d[i], str(i + 3)], stdin=subprocess.PIPE)
        pp.append(p)
    
    sleep(30) # or do something else
    
    for p in pp: p.terminate()
    

    In this last example, with no void interrupt handler, neither in the master nor in child, both master and child would receive a ctrl-c during the 30 seconds periods while master is running with child in background. And would react to it with default behavior, that is by terminating.

    Then, after the end of the 30 seconds (or whatever else master is doing while child work), master ends, and before ending, ends its child.