Ensuring subprocesses are dead on exiting Python program

后端 未结 14 1661
有刺的猬
有刺的猬 2020-12-02 09:17

Is there a way to ensure all created subprocess are dead at exit time of a Python program? By subprocess I mean those created with subprocess.Popen().

If not, should

相关标签:
14条回答
  • 2020-12-02 09:29

    Find out a solution for linux (without installing prctl):

    def _set_pdeathsig(sig=signal.SIGTERM):
        """help function to ensure once parent process exits, its childrent processes will automatically die
        """
        def callable():
            libc = ctypes.CDLL("libc.so.6")
            return libc.prctl(1, sig)
        return callable
    
    
    subprocess.Popen(your_command, preexec_fn=_set_pdeathsig(signal.SIGTERM)) 
    
    0 讨论(0)
  • 2020-12-02 09:29

    orip's answer is helpful but has the downside that it kills your process and returns an error code your parent. I avoided that like this:

    class CleanChildProcesses:
      def __enter__(self):
        os.setpgrp() # create new process group, become its leader
      def __exit__(self, type, value, traceback):
        try:
          os.killpg(0, signal.SIGINT) # kill all processes in my group
        except KeyboardInterrupt:
          # SIGINT is delievered to this process as well as the child processes.
          # Ignore it so that the existing exception, if any, is returned. This
          # leaves us with a clean exit code if there was no exception.
          pass
    

    And then:

      with CleanChildProcesses():
        # Do your work here
    

    Of course you can do this with try/except/finally but you have to handle the exceptional and non-exceptional cases separately.

    0 讨论(0)
  • 2020-12-02 09:33

    You can try subalive, a package I wrote for similar problem. It uses periodic alive ping via RPC, and the slave process automatically terminates when the master stops alive pings for some reason.

    https://github.com/waszil/subalive

    Example for master:

    from subalive import SubAliveMaster
    
    # start subprocess with alive keeping
    SubAliveMaster(<path to your slave script>)
    
    # do your stuff
    # ...
    

    Example for slave subprocess:

    from subalive import SubAliveSlave
    
    # start alive checking
    SubAliveSlave()
    
    # do your stuff
    # ...
    
    0 讨论(0)
  • 2020-12-02 09:37

    You can use atexit for this, and register any clean up tasks to be run when your program exits.

    atexit.register(func[, *args[, **kargs]])

    In your cleanup process, you can also implement your own wait, and kill it when a your desired timeout occurs.

    >>> import atexit
    >>> import sys
    >>> import time
    >>> 
    >>> 
    >>>
    >>> def cleanup():
    ...     timeout_sec = 5
    ...     for p in all_processes: # list of your processes
    ...         p_sec = 0
    ...         for second in range(timeout_sec):
    ...             if p.poll() == None:
    ...                 time.sleep(1)
    ...                 p_sec += 1
    ...         if p_sec >= timeout_sec:
    ...             p.kill() # supported from python 2.6
    ...     print 'cleaned up!'
    ...
    >>>
    >>> atexit.register(cleanup)
    >>>
    >>> sys.exit()
    cleaned up!
    

    Note -- Registered functions won't be run if this process (parent process) is killed.

    The following windows method is no longer needed for python >= 2.6

    Here's a way to kill a process in windows. Your Popen object has a pid attribute, so you can just call it by success = win_kill(p.pid) (Needs pywin32 installed):

        def win_kill(pid):
            '''kill a process by specified PID in windows'''
            import win32api
            import win32con
    
            hProc = None
            try:
                hProc = win32api.OpenProcess(win32con.PROCESS_TERMINATE, 0, pid)
                win32api.TerminateProcess(hProc, 0)
            except Exception:
                return False
            finally:
                if hProc != None:
                    hProc.Close()
    
            return True
    
    0 讨论(0)
  • 2020-12-02 09:37

    Warning: Linux-only! You can make your child receive a signal when its parent dies.

    First install python-prctl==1.5.0 then change your parent code to launch your child processes as follows

    subprocess.Popen(["sleep", "100"], preexec_fn=lambda: prctl.set_pdeathsig(signal.SIGKILL))
    

    What this says is:

    • launch subprocess: sleep 100
    • after forking and before exec of the subprocess, the child registers for "send me a SIGKILL when my parent terminates".
    0 讨论(0)
  • 2020-12-02 09:37

    I actually needed to do this, but it involved running remote commands. We wanted to be able to stop the processes by closing the connection to the server. Also, if, for example, you are running in the python repl, you can select to run as foreground if you want to be able to use Ctrl-C to exit.

    import os, signal, time
    
    class CleanChildProcesses:
        """
        with CleanChildProcesses():
            Do work here
        """
        def __init__(self, time_to_die=5, foreground=False):
            self.time_to_die = time_to_die  # how long to give children to die before SIGKILL
            self.foreground = foreground  # If user wants to receive Ctrl-C
            self.is_foreground = False
            self.SIGNALS = (signal.SIGHUP, signal.SIGTERM, signal.SIGABRT, signal.SIGALRM, signal.SIGPIPE)
            self.is_stopped = True  # only call stop once (catch signal xor exiting 'with')
    
        def _run_as_foreground(self):
            if not self.foreground:
                return False
            try:
                fd = os.open(os.ctermid(), os.O_RDWR)
            except OSError:
                # Happens if process not run from terminal (tty, pty)
                return False
    
            os.close(fd)
            return True
    
        def _signal_hdlr(self, sig, framte):
            self.__exit__(None, None, None)
    
        def start(self):
            self.is_stopped = False
            """
            When running out of remote shell, SIGHUP is only sent to the session
            leader normally, the remote shell, so we need to make sure we are sent 
            SIGHUP. This also allows us not to kill ourselves with SIGKILL.
            - A process group is called orphaned when the parent of every member is 
                either in the process group or outside the session. In particular, 
                the process group of the session leader is always orphaned.
            - If termination of a process causes a process group to become orphaned, 
                and some member is stopped, then all are sent first SIGHUP and then 
                SIGCONT.
            consider: prctl.set_pdeathsig(signal.SIGTERM)
            """
            self.childpid = os.fork()  # return 0 in the child branch, and the childpid in the parent branch
            if self.childpid == 0:
                try:
                    os.setpgrp()  # create new process group, become its leader
                    os.kill(os.getpid(), signal.SIGSTOP)  # child fork stops itself
                finally:
                    os._exit(0)  # shut down without going to __exit__
    
            os.waitpid(self.childpid, os.WUNTRACED)  # wait until child stopped after it created the process group
            os.setpgid(0, self.childpid)  # join child's group
    
            if self._run_as_foreground():
                hdlr = signal.signal(signal.SIGTTOU, signal.SIG_IGN)  # ignore since would cause this process to stop
                self.controlling_terminal = os.open(os.ctermid(), os.O_RDWR)
                self.orig_fore_pg = os.tcgetpgrp(self.controlling_terminal)  # sends SIGTTOU to this process
                os.tcsetpgrp(self.controlling_terminal, self.childpid)
                signal.signal(signal.SIGTTOU, hdlr)
                self.is_foreground = True
    
            self.exit_signals = dict((s, signal.signal(s, self._signal_hdlr))
                                     for s in self.SIGNALS)                                     
    
        def stop(self):
            try:
                for s in self.SIGNALS:
                    #don't get interrupted while cleaning everything up
                    signal.signal(s, signal.SIG_IGN)
    
                self.is_stopped = True
    
                if self.is_foreground:
                    os.tcsetpgrp(self.controlling_terminal, self.orig_fore_pg)
                    os.close(self.controlling_terminal)
                    self.is_foreground = False
    
                try:
                    os.kill(self.childpid, signal.SIGCONT)
                except OSError:
                    """
                    can occur if process finished and one of:
                    - was reaped by another process
                    - if parent explicitly ignored SIGCHLD
                        signal.signal(signal.SIGCHLD, signal.SIG_IGN)
                    - parent has the SA_NOCLDWAIT flag set 
                    """
                    pass
    
                os.setpgrp()  # leave the child's process group so I won't get signals
                try:
                    os.killpg(self.childpid, signal.SIGINT)
                    time.sleep(self.time_to_die)  # let processes end gracefully
                    os.killpg(self.childpid, signal.SIGKILL)  # In case process gets stuck while dying
                    os.waitpid(self.childpid, 0)  # reap Zombie child process
                except OSError as e:
                    pass
            finally:
                for s, hdlr in self.exit_signals.iteritems():
                    signal.signal(s, hdlr)  # reset default handlers
    
        def __enter__(self):
            if self.is_stopped:
                self.start()
    
        def __exit__(self, exit_type, value, traceback):
            if not self.is_stopped:
                self.stop()
    

    Thanks to Malcolm Handley for the initial design. Done with python2.7 on linux.

    0 讨论(0)
提交回复
热议问题