"""Fork and detach the current process.""" import errno import os import resource import subprocess import sys import traceback from multiprocessing import Value maxfd = 2048 class Error(Exception): """Raised on error.""" class Detach(object): def __init__(self, stdout=None, stderr=None, stdin=None, close_fds=False, exclude_fds=None, daemonize=False): """ Fork and detach a process. The stdio streams of the child default to /dev/null but may be overridden with the `stdout`, `stderr`, and `stdin` parameters. If `close_fds` is True then all open file descriptors (except those passed as overrides for stdio) are closed by the child process. File descriptors in `exclude_fds` will not be closed. If `daemonize` is True then the parent process exits. """ self.stdout = stdout self.stderr = stderr self.stdin = stdin self.close_fds = close_fds self.exclude_fds = set() self.daemonize = daemonize self.pid = None self.shared_pid = Value('i', 0) for item in list(exclude_fds or []) + [stdout, stderr, stdin]: if hasattr(item, 'fileno'): item = item.fileno() self.exclude_fds.add(item) def _get_max_fd(self): """Return the maximum file descriptor value.""" limits = resource.getrlimit(resource.RLIMIT_NOFILE) result = limits[1] if result == resource.RLIM_INFINITY: result = maxfd return result def _close_fd(self, fd): """Close a file descriptor if it is open.""" try: os.close(fd) except OSError, exc: if exc.errno != errno.EBADF: msg = "Failed to close file descriptor {}: {}".format(fd, exc) raise Error(msg) def _close_open_fds(self): """Close open file descriptors.""" maxfd = self._get_max_fd() for fd in reversed(range(maxfd)): if fd not in self.exclude_fds: self._close_fd(fd) def _redirect(self, stream, target): """Redirect a system stream to the provided target.""" if target is None: target_fd = os.open(os.devnull, os.O_RDWR) else: target_fd = target.fileno() os.dup2(target_fd, stream.fileno()) def __enter__(self): """Fork and detach the process.""" pid = os.fork() if pid > 0: # parent os.waitpid(pid, 0) self.pid = self.shared_pid.value else: # first child os.setsid() pid = os.fork() if pid > 0: # first child self.shared_pid.value = pid os._exit(0) else: # second child if self.close_fds: self._close_open_fds() self._redirect(sys.stdout, self.stdout) self._redirect(sys.stderr, self.stderr) self._redirect(sys.stdin, self.stdin) return self def __exit__(self, exc_cls, exc_val, exc_tb): """Exit processes.""" if self.daemonize or not self.pid: if exc_val: traceback.print_exception(exc_cls, exc_val, exc_tb) os._exit(0) def call(args, stdout=None, stderr=None, stdin=None, daemonize=False, preexec_fn=None, shell=False, cwd=None, env=None): """ Run an external command in a separate process and detach it from the current process. Excepting `stdout`, `stderr`, and `stdin` all file descriptors are closed after forking. If `daemonize` is True then the parent process exits. All stdio is redirected to `os.devnull` unless specified. The `preexec_fn`, `shell`, `cwd`, and `env` parameters are the same as their `Popen` counterparts. Return the PID of the child process if not daemonized. """ stream = lambda s, m: s is None and os.open(os.devnull, m) or s stdout = stream(stdout, os.O_WRONLY) stderr = stream(stderr, os.O_WRONLY) stdin = stream(stdin, os.O_RDONLY) shared_pid = Value('i', 0) pid = os.fork() if pid > 0: os.waitpid(pid, 0) child_pid = shared_pid.value del shared_pid if daemonize: sys.exit(0) return child_pid else: os.setsid() proc = subprocess.Popen(args, stdout=stdout, stderr=stderr, stdin=stdin, close_fds=True, preexec_fn=preexec_fn, shell=shell, cwd=cwd, env=env) shared_pid.value = proc.pid os._exit(0)