|  | @@ -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):
 |