|
- """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)
|