You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

136 lines
4.5KB

  1. """Fork and detach the current process."""
  2. import errno
  3. import os
  4. import resource
  5. import subprocess
  6. import sys
  7. import traceback
  8. from multiprocessing import Value
  9. maxfd = 2048
  10. class Error(Exception):
  11. """Raised on error."""
  12. class Detach(object):
  13. def __init__(self, stdout=None, stderr=None, stdin=None, close_fds=False, exclude_fds=None,
  14. daemonize=False):
  15. """
  16. Fork and detach a process. The stdio streams of the child default to /dev/null but may be
  17. overridden with the `stdout`, `stderr`, and `stdin` parameters. If `close_fds` is True then
  18. all open file descriptors (except those passed as overrides for stdio) are closed by the
  19. child process. File descriptors in `exclude_fds` will not be closed. If `daemonize` is True
  20. then the parent process exits.
  21. """
  22. self.stdout = stdout
  23. self.stderr = stderr
  24. self.stdin = stdin
  25. self.close_fds = close_fds
  26. self.exclude_fds = set()
  27. self.daemonize = daemonize
  28. self.pid = None
  29. self.shared_pid = Value('i', 0)
  30. for item in list(exclude_fds or []) + [stdout, stderr, stdin]:
  31. if hasattr(item, 'fileno'):
  32. item = item.fileno()
  33. self.exclude_fds.add(item)
  34. def _get_max_fd(self):
  35. """Return the maximum file descriptor value."""
  36. limits = resource.getrlimit(resource.RLIMIT_NOFILE)
  37. result = limits[1]
  38. if result == resource.RLIM_INFINITY:
  39. result = maxfd
  40. return result
  41. def _close_fd(self, fd):
  42. """Close a file descriptor if it is open."""
  43. try:
  44. os.close(fd)
  45. except OSError, exc:
  46. if exc.errno != errno.EBADF:
  47. msg = "Failed to close file descriptor {}: {}".format(fd, exc)
  48. raise Error(msg)
  49. def _close_open_fds(self):
  50. """Close open file descriptors."""
  51. maxfd = self._get_max_fd()
  52. for fd in reversed(range(maxfd)):
  53. if fd not in self.exclude_fds:
  54. self._close_fd(fd)
  55. def _redirect(self, stream, target):
  56. """Redirect a system stream to the provided target."""
  57. if target is None:
  58. target_fd = os.open(os.devnull, os.O_RDWR)
  59. else:
  60. target_fd = target.fileno()
  61. os.dup2(target_fd, stream.fileno())
  62. def __enter__(self):
  63. """Fork and detach the process."""
  64. pid = os.fork()
  65. if pid > 0:
  66. # parent
  67. os.waitpid(pid, 0)
  68. self.pid = self.shared_pid.value
  69. else:
  70. # first child
  71. os.setsid()
  72. pid = os.fork()
  73. if pid > 0:
  74. # first child
  75. self.shared_pid.value = pid
  76. os._exit(0)
  77. else:
  78. # second child
  79. if self.close_fds:
  80. self._close_open_fds()
  81. self._redirect(sys.stdout, self.stdout)
  82. self._redirect(sys.stderr, self.stderr)
  83. self._redirect(sys.stdin, self.stdin)
  84. return self
  85. def __exit__(self, exc_cls, exc_val, exc_tb):
  86. """Exit processes."""
  87. if self.daemonize or not self.pid:
  88. if exc_val:
  89. traceback.print_exception(exc_cls, exc_val, exc_tb)
  90. os._exit(0)
  91. def call(args, stdout=None, stderr=None, stdin=None, daemonize=False,
  92. preexec_fn=None, shell=False, cwd=None, env=None):
  93. """
  94. Run an external command in a separate process and detach it from the current process. Excepting
  95. `stdout`, `stderr`, and `stdin` all file descriptors are closed after forking. If `daemonize`
  96. is True then the parent process exits. All stdio is redirected to `os.devnull` unless
  97. specified. The `preexec_fn`, `shell`, `cwd`, and `env` parameters are the same as their `Popen`
  98. counterparts. Return the PID of the child process if not daemonized.
  99. """
  100. stream = lambda s, m: s is None and os.open(os.devnull, m) or s
  101. stdout = stream(stdout, os.O_WRONLY)
  102. stderr = stream(stderr, os.O_WRONLY)
  103. stdin = stream(stdin, os.O_RDONLY)
  104. shared_pid = Value('i', 0)
  105. pid = os.fork()
  106. if pid > 0:
  107. os.waitpid(pid, 0)
  108. child_pid = shared_pid.value
  109. del shared_pid
  110. if daemonize:
  111. sys.exit(0)
  112. return child_pid
  113. else:
  114. os.setsid()
  115. proc = subprocess.Popen(args, stdout=stdout, stderr=stderr, stdin=stdin, close_fds=True,
  116. preexec_fn=preexec_fn, shell=shell, cwd=cwd, env=env)
  117. shared_pid.value = proc.pid
  118. os._exit(0)