|
@@ -6,6 +6,8 @@ import shlex
|
|
|
import signal
|
|
|
import subprocess
|
|
|
import sys
|
|
|
+import time
|
|
|
+import traceback
|
|
|
|
|
|
from .. import __version__
|
|
|
|
|
@@ -13,17 +15,23 @@ from ..platformflags import is_win32, is_linux, is_freebsd, is_darwin
|
|
|
from ..logger import create_logger
|
|
|
logger = create_logger()
|
|
|
|
|
|
+from ..helpers import EXIT_SUCCESS, EXIT_WARNING, EXIT_SIGNAL_BASE
|
|
|
|
|
|
-def daemonize():
|
|
|
- """Detach process from controlling terminal and run in background
|
|
|
|
|
|
- Returns: old and new get_process_id tuples
|
|
|
- """
|
|
|
+@contextlib.contextmanager
|
|
|
+def _daemonize():
|
|
|
from ..platform import get_process_id
|
|
|
old_id = get_process_id()
|
|
|
pid = os.fork()
|
|
|
if pid:
|
|
|
- os._exit(0)
|
|
|
+ exit_code = EXIT_SUCCESS
|
|
|
+ try:
|
|
|
+ yield old_id, None
|
|
|
+ except _ExitCodeException as e:
|
|
|
+ exit_code = e.exit_code
|
|
|
+ finally:
|
|
|
+ logger.debug('Daemonizing: Foreground process (%s, %s, %s) is now dying.' % old_id)
|
|
|
+ os._exit(exit_code)
|
|
|
os.setsid()
|
|
|
pid = os.fork()
|
|
|
if pid:
|
|
@@ -31,13 +39,101 @@ def daemonize():
|
|
|
os.chdir('/')
|
|
|
os.close(0)
|
|
|
os.close(1)
|
|
|
- os.close(2)
|
|
|
fd = os.open(os.devnull, os.O_RDWR)
|
|
|
os.dup2(fd, 0)
|
|
|
os.dup2(fd, 1)
|
|
|
- os.dup2(fd, 2)
|
|
|
new_id = get_process_id()
|
|
|
- return old_id, new_id
|
|
|
+ try:
|
|
|
+ yield old_id, new_id
|
|
|
+ finally:
|
|
|
+ # Close / redirect stderr to /dev/null only now
|
|
|
+ # for the case that we want to log something before yield returns.
|
|
|
+ os.close(2)
|
|
|
+ os.dup2(fd, 2)
|
|
|
+
|
|
|
+
|
|
|
+def daemonize():
|
|
|
+ """Detach process from controlling terminal and run in background
|
|
|
+
|
|
|
+ Returns: old and new get_process_id tuples
|
|
|
+ """
|
|
|
+ with _daemonize() as (old_id, new_id):
|
|
|
+ return old_id, new_id
|
|
|
+
|
|
|
+
|
|
|
+@contextlib.contextmanager
|
|
|
+def daemonizing(*, timeout=5):
|
|
|
+ """Like daemonize(), but as context manager.
|
|
|
+
|
|
|
+ The with-body is executed in the background process,
|
|
|
+ while the foreground process survives until the body is left
|
|
|
+ or the given timeout is exceeded. In the latter case a warning is
|
|
|
+ reported by the foreground.
|
|
|
+ Context variable is (old_id, new_id) get_process_id tuples.
|
|
|
+ An exception raised in the body is reported by the foreground
|
|
|
+ as a warning as well as propagated outside the body in the background.
|
|
|
+ In case of a warning, the foreground exits with exit code EXIT_WARNING
|
|
|
+ instead of EXIT_SUCCESS.
|
|
|
+ """
|
|
|
+ with _daemonize() as (old_id, new_id):
|
|
|
+ if new_id is None:
|
|
|
+ # The original / parent process, waiting for a signal to die.
|
|
|
+ logger.debug('Daemonizing: Foreground process (%s, %s, %s) is waiting for background process...' % old_id)
|
|
|
+ exit_code = EXIT_SUCCESS
|
|
|
+ # Indeed, SIGHUP and SIGTERM handlers should have been set on archiver.run(). Just in case...
|
|
|
+ with signal_handler('SIGINT', raising_signal_handler(KeyboardInterrupt)), \
|
|
|
+ signal_handler('SIGHUP', raising_signal_handler(SigHup)), \
|
|
|
+ signal_handler('SIGTERM', raising_signal_handler(SigTerm)):
|
|
|
+ try:
|
|
|
+ if timeout > 0:
|
|
|
+ time.sleep(timeout)
|
|
|
+ except SigTerm:
|
|
|
+ # Normal termination; expected from grandchild, see 'os.kill()' below
|
|
|
+ pass
|
|
|
+ except SigHup:
|
|
|
+ # Background wants to indicate a problem; see 'os.kill()' below,
|
|
|
+ # log message will come from grandchild.
|
|
|
+ exit_code = EXIT_WARNING
|
|
|
+ except KeyboardInterrupt:
|
|
|
+ # Manual termination.
|
|
|
+ logger.debug('Daemonizing: Foreground process (%s, %s, %s) received SIGINT.' % old_id)
|
|
|
+ exit_code = EXIT_SIGNAL_BASE + 2
|
|
|
+ except BaseException as e:
|
|
|
+ # Just in case...
|
|
|
+ logger.warning('Daemonizing: Foreground process received an exception while waiting:\n' +
|
|
|
+ ''.join(traceback.format_exception(e.__class__, e, e.__traceback__)))
|
|
|
+ exit_code = EXIT_WARNING
|
|
|
+ else:
|
|
|
+ logger.warning('Daemonizing: Background process did not respond (timeout). Is it alive?')
|
|
|
+ exit_code = EXIT_WARNING
|
|
|
+ finally:
|
|
|
+ # Don't call with-body, but die immediately!
|
|
|
+ # return would be sufficient, but we want to pass the exit code.
|
|
|
+ raise _ExitCodeException(exit_code)
|
|
|
+
|
|
|
+ # The background / grandchild process.
|
|
|
+ sig_to_foreground = signal.SIGTERM
|
|
|
+ logger.debug('Daemonizing: Background process (%s, %s, %s) is starting...' % new_id)
|
|
|
+ try:
|
|
|
+ yield old_id, new_id
|
|
|
+ except BaseException as e:
|
|
|
+ sig_to_foreground = signal.SIGHUP
|
|
|
+ logger.warning('Daemonizing: Background process raised an exception while starting:\n' +
|
|
|
+ ''.join(traceback.format_exception(e.__class__, e, e.__traceback__)))
|
|
|
+ raise e
|
|
|
+ else:
|
|
|
+ logger.debug('Daemonizing: Background process (%s, %s, %s) has started.' % new_id)
|
|
|
+ finally:
|
|
|
+ try:
|
|
|
+ os.kill(old_id[1], sig_to_foreground)
|
|
|
+ except BaseException as e:
|
|
|
+ logger.error('Daemonizing: Trying to kill the foreground process raised an exception:\n' +
|
|
|
+ ''.join(traceback.format_exception(e.__class__, e, e.__traceback__)))
|
|
|
+
|
|
|
+
|
|
|
+class _ExitCodeException(BaseException):
|
|
|
+ def __init__(self, exit_code):
|
|
|
+ self.exit_code = exit_code
|
|
|
|
|
|
|
|
|
class SignalException(BaseException):
|