''' LICENSING ------------------------------------------------- daemoniker: Cross-platform daemonization tools. Copyright (C) 2016 Muterra, Inc. Contributors ------------ Nick Badger badg@muterra.io | badg@nickbadger.com | nickbadger.com This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA ------------------------------------------------------ This was written with heavy consultation of the following resources: Chad J. Schroeder, Creating a daemon the Python way (Python recipe) http://code.activestate.com/recipes/ 278731-creating-a-daemon-the-python-way/ Ilya Otyutskiy, Daemonize https://github.com/thesharp/daemonize David Mytton, unknown, et al: A simple daemon in Python http://www.jejik.com/articles/2007/02/ a_simple_unix_linux_daemon_in_python/www.boxedice.com Andrew Gierth, Unix programming FAQ v1.37 http://www.faqs.org/faqs/unix-faq/programmer/faq/ ''' # Global dependencies import os import logging import atexit import traceback import sys # Intra-package dependencies from .utils import platform_specificker from .utils import default_to from ._daemonize_common import _make_range_tuples from ._daemonize_common import _redirect_stds from ._daemonize_common import _write_pid from ._daemonize_common import _acquire_pidfile _SUPPORTED_PLATFORM = platform_specificker( linux_choice=True, win_choice=False, cygwin_choice=False, osx_choice=True, # Dunno if this is a good idea but might as well try other_choice=True ) if _SUPPORTED_PLATFORM: import fcntl import pwd import grp import resource # ############################################### # Boilerplate # ############################################### logger = logging.getLogger(__name__) # Control * imports. __all__ = [ # 'Inquisitor', ] # ############################################### # Library # ############################################### # Daemonization and helpers class Daemonizer: ''' This is really very boring on the Unix side of things. with Daemonizer() as (is_setup, daemonize): if is_setup: setup_code_here() else: this_will_not_be_run_on_unix() *args = daemonize(*daemonizer_args, *args) ''' def __init__(self): self._is_parent = None self._daemonize_called = None def _daemonize(self, *args, **kwargs): ret_vec = daemonize(*args, _exit_caller=False, **kwargs) self._daemonize_called = True self._is_parent = ret_vec[0] return ret_vec def __enter__(self): self._daemonize_called = False self._is_parent = None # This will always only be entered by the parent. return True, self._daemonize def __exit__(self, exc_type, exc_value, exc_tb): ''' Exit doesn't really need to do any cleanup. But, it's needed for context managing. ''' # This should only happen if __exit__ was called directly, without # first calling __enter__ if self._daemonize_called is None: self._is_parent = None raise RuntimeError('Context manager was inappropriately exited.') # This will happen if we used the context manager, but never actually # called to daemonize. elif not self._daemonize_called: self._daemonize_called = None self._is_parent = None logger.warning('Daemonizer exited without calling daemonize.') # Note that any encountered error will be raise once the context is # departed, so there's no reason to handle or log errors here. return # We called to daemonize, and this is the parent. elif self._is_parent: # If there was an exception, give some information before the # summary self-execution that is os._exit if exc_type is not None: logger.error( 'Exception in parent:\n' + ''.join(traceback.format_tb(exc_tb)) + '\n' + repr(exc_value) ) print( 'Exception in parent:\n' + ''.join(traceback.format_tb(exc_tb)) + '\n' + repr(exc_value), file=sys.stderr ) os._exit(2) else: os._exit(0) # We called to daemonize, and this is the child. else: return def _fratricidal_fork(have_mercy=False): ''' Fork the current process, and immediately exit the parent. OKAY TECHNICALLY THIS WOULD BE PARRICIDE but it just doesn't have the same ring to it. have_mercy allows the parent to persist for a little while, but it must call os._exit(0) on its own later. ''' try: # This will create a clone of our process. The clone will get zero # for the PID, and the parent will get an actual PID. pid = os.fork() except OSError as exc: logger.critical( 'Fork failed with traceback: \n' + ''.join(traceback.format_exc()) ) raise SystemExit('Failed to fork.') from exc # If PID != 0, this is the parent process, and we should immediately # die. # Note that python handles forking failures for us. if pid != 0: # D-d-d-d-d-anger zoooone! But srsly, this has a lot of caveats emptor if have_mercy: # Return True for is_parent return True # Standard behavior is immediately leave. else: # Exit parent without cleanup. os._exit(0) # Return False for is_parent else: logger.info('Fork successful.') return False def _filial_usurpation(chdir, umask): ''' Decouple the child process from the parent environment. ''' # This prevents "directory busy" errors when attempting to remove # subdirectories. os.chdir(chdir) # Get new PID. # Stop listening to parent signals. # Put process in new parent group # Detatch controlling terminal. new_sid = os.setsid() if new_sid == -1: # A new pid of -1 is bad news bears logger.critical('Failed setsid call.') raise SystemExit('Failed setsid call.') # Set the permissions mask os.umask(umask) def _autoclose_files(shielded=None, fallback_limit=1024): ''' Automatically close any open file descriptors. shielded is iterable of file descriptors. ''' # Process shielded. shielded = default_to(shielded, []) # Figure out the maximum number of files to try to close. # This returns a tuple of softlimit, hardlimit; the hardlimit is always # greater. softlimit, hardlimit = resource.getrlimit(resource.RLIMIT_NOFILE) # If the hard limit is infinity, we can't iterate to it. if hardlimit == resource.RLIM_INFINITY: # Check the soft limit. If it's also infinity, fallback to guess. if softlimit == resource.RLIM_INFINITY: fdlimit = fallback_limit # The soft limit is finite, so fallback to that. else: fdlimit = softlimit # The hard limit is not infinity, so prefer it. else: fdlimit = hardlimit # Skip fd 0, 1, 2, which are used by stdin, stdout, and stderr # (respectively) ranges_to_close = _make_range_tuples( start=3, stop=fdlimit, exclude=shielded ) for start, stop in ranges_to_close: # How nice of os to include this for us! os.closerange(start, stop) def daemonize(pid_file, *args, chdir=None, stdin_goto=None, stdout_goto=None, stderr_goto=None, umask=0o027, shielded_fds=None, fd_fallback_limit=1024, success_timeout=30, strip_cmd_args=False, explicit_rescript=None, _exit_caller=True): ''' Performs a classic unix double-fork daemonization. Registers all appropriate cleanup functions. fd_check_limit is a fallback value for file descriptor searching while closing descriptors. umask is the eponymous unix umask. The default value: 1. will allow owner to have any permissions. 2. will prevent group from having write permission 3. will prevent other from having any permission See https://en.wikipedia.org/wiki/Umask _exit_caller=True makes the parent (grandparent) process immediately exit. If set to False, THE GRANDPARENT MUST CALL os._exit(0) UPON ITS FINISHING. This is a really sticky situation, and should be avoided outside of the shipped context manager. ''' if not _SUPPORTED_PLATFORM: raise OSError( 'The Unix daemonization function cannot be used on the current ' 'platform.' ) #################################################################### # Prep the arguments #################################################################### # Convert the pid_file to an abs path pid_file = os.path.abspath(pid_file) # Get the noop stream, in case Python is using something other than # /dev/null if hasattr(os, "devnull"): devnull = os.devnull else: devnull = "/dev/null" # Convert any unset std streams to go to dev null stdin_goto = default_to(stdin_goto, devnull) stdout_goto = default_to(stdout_goto, devnull) stderr_goto = default_to(stderr_goto, devnull) # Convert chdir to go to current dir, and also to an abs path. chdir = default_to(chdir, '.') chdir = os.path.abspath(chdir) # And convert shield_fds to a set shielded_fds = default_to(shielded_fds, set()) shielded_fds = set(shielded_fds) #################################################################### # Begin actual daemonization #################################################################### # Get a lock on the PIDfile before forking anything. locked_pidfile = _acquire_pidfile(pid_file) # Make sure we don't accidentally autoclose it though. shielded_fds.add(locked_pidfile.fileno()) # Define a memoized cleanup function. def cleanup(pid_path=pid_file, pid_lock=locked_pidfile): try: pid_lock.close() os.remove(pid_path) except: logger.error( 'Failed to clean up pidfile w/ traceback: \n' + ''.join(traceback.format_exc()) ) raise # Register this as soon as possible in case something goes wrong. atexit.register(cleanup) # Note that because fratricidal fork is calling os._exit(), our parents # will never call cleanup. # Now fork the toplevel parent, killing it (unless _exit_caller was False) keep_parent = not bool(_exit_caller) is_parent = _fratricidal_fork(have_mercy=keep_parent) # If is_parent, we know, for sure, that _exit_caller was False if is_parent: # Reset args to be an equivalent expansion of *[None]s to prevent # accidentally trying to modify them in the parent args = [None] * len(args) # is_parent, *args return [True] + list(args) # Okay, we're the child. else: # We need to detach ourself from the parent environment. _filial_usurpation(chdir, umask) # Okay, re-fork (no zombies!) and continue business as usual _fratricidal_fork() # Do some important housekeeping _write_pid(locked_pidfile) _autoclose_files(shielded_fds, fd_fallback_limit) _redirect_stds(stdin_goto, stdout_goto, stderr_goto) # We still need to adapt our return based on _exit_caller if not _exit_caller: # is_parent, *args return [False] + list(args) # Normal, bare daemonization call else: return args