I really needed to do this, but that included running remote commands. We wanted to be able to stop processes by closing the connection to the server. Also, if, for example, you are running in a python replica, you can choose to run as the foreground if you want 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 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 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. Made with python2.7 on linux.