Files
neko-daemonizer-dante/neko_daemonizer_dante/daemonizer/_daemonize_unix.py
2024-02-11 17:12:19 +02:00

384 lines
12 KiB
Python

'''
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