This commit is contained in:
hhh
2024-02-11 17:12:19 +02:00
commit 08bc8bec96
20 changed files with 2709 additions and 0 deletions

6
.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
/.idea/
/tests/
poetry.lock
**/__pycache__/

3
README.md Normal file
View File

@@ -0,0 +1,3 @@
[nekomata is now in early-dev state](https://nekomata.kotikot.com/)
Based on [py_daemoniker](https://github.com/Muterra/py_daemoniker)

View File

@@ -0,0 +1,5 @@
from . import utils
from . import daemonizer
__all__ = ['daemonizer']

View File

@@ -0,0 +1,99 @@
'''
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
------------------------------------------------------
'''
# ###############################################
# Boilerplate
# ###############################################
# Logging shenanigans
import logging
# Py2.7+, but this is Py3.5.1+
from logging import NullHandler
logging.getLogger(__name__).addHandler(NullHandler())
# Control * imports.
__all__ = [
'Daemonizer',
'daemonize',
'SignalHandler1',
'IGNORE_SIGNAL',
'send',
'SIGINT',
'SIGTERM',
'SIGABRT',
]
# ###############################################
# Library
# ###############################################
# Submodules
from . import exceptions
from . import utils
from ._signals_common import IGNORE_SIGNAL
from ._signals_common import send
from .exceptions import SIGINT
from .exceptions import SIGTERM
from .exceptions import SIGABRT
# Add in toplevel stuff
from .utils import platform_specificker
platform_switch = platform_specificker(
linux_choice = 'unix',
win_choice = 'windows',
# Dunno if this is a good idea but might as well try
cygwin_choice = None,
osx_choice = 'unix',
other_choice = 'unix'
)
if platform_switch == 'unix':
from ._daemonize_unix import Daemonizer
from ._daemonize_unix import daemonize
from ._signals_unix import SignalHandler1
elif platform_switch == 'windows':
from ._daemonize_windows import Daemonizer
from ._daemonize_windows import daemonize
from ._signals_windows import SignalHandler1
else:
raise RuntimeError(
'Your runtime environment is unsupported by daemoniker.'
)

View File

@@ -0,0 +1,252 @@
"""
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
------------------------------------------------------
"""
# Global dependencies
import os
import sys
import signal
import logging
import atexit
import traceback
import shutil
# Intra-package dependencies
from .utils import default_to
# ###############################################
# Boilerplate
# ###############################################
import logging
logger = logging.getLogger(__name__)
# Control * imports.
__all__ = [
# 'Inquisitor',
]
# ###############################################
# Library
# ###############################################
def _make_range_tuples(start, stop, exclude):
""" Creates a list of tuples for all ranges needed to close all
files between start and stop, except exclude. Ex:
start=3, stop=7, exclude={4,}:
(3, 4),
(5, 7)
"""
# Make a list copy of exclude, discarding anything less than stop
exclude = [ii for ii in exclude if ii >= start]
# Sort ascending
exclude.sort()
ranges = []
seeker = start
for ii in exclude:
# Only add actual slices (it wouldn't matter if we added empty ones,
# but there's also no reason to).
if seeker != ii:
this_range = (seeker, ii)
ranges.append(this_range)
# But always do this.
seeker = ii + 1
# Don't forget to add the final range!
if seeker < stop:
final_range = (seeker, stop)
ranges.append(final_range)
return ranges
def _flush_stds():
""" Flush stdout and stderr.
Note special casing needed for pythonw.exe, which has no stdout or
stderr.
"""
try:
sys.stdout.flush()
except BlockingIOError:
logger.error(
'Failed to flush stdout w/ traceback: \n' +
''.join(traceback.format_exc())
)
# Honestly not sure if we should exit here.
try:
sys.stderr.flush()
except BlockingIOError:
logger.error(
'Failed to flush stderr w/ traceback: \n' +
''.join(traceback.format_exc())
)
# Honestly not sure if we should exit here.
def _redirect_stds(stdin_goto, stdout_goto, stderr_goto):
""" Set stdin, stdout, sterr. If any of the paths don't exist,
create them first.
"""
# The general strategy here is to:
# 1. figure out which unique paths we need to open for the redirects
# 2. figure out the minimum access we need to open them with
# 3. open the files to get them a file descriptor
# 4. copy those file descriptors into the FD's used for stdio, etc
# 5. close the original file descriptors
# Remove repeated values through a set.
streams = {stdin_goto, stdout_goto, stderr_goto}
# Transform that into a dictionary of {location: 0, location: 0...}
# Basically, start from zero permissions
streams = {stream: 0 for stream in streams}
# And now create a bitmask for each of reading and writing
read_mask = 0b01
write_mask = 0b10
rw_mask = 0b11
# Update the streams dict depending on what access each stream requires
streams[stdin_goto] |= read_mask
streams[stdout_goto] |= write_mask
streams[stderr_goto] |= write_mask
# Now create a lookup to transform our masks into file access levels
access_lookup = {
read_mask: os.O_RDONLY,
write_mask: os.O_WRONLY,
rw_mask: os.O_RDWR
}
access_lookup_2 = {
read_mask: 'r',
write_mask: 'w',
rw_mask: 'w+'
}
access_mode = {}
# Now, use our mask lookup to translate into actual file descriptors
for stream in streams:
# First create the file if its missing.
if not os.path.exists(stream):
with open(stream, 'w'):
pass
# Transform the mask into the actual access level.
access = access_lookup[streams[stream]]
# Open the file with that level of access.
stream_fd = os.open(stream, access)
# Also alias the mode in case of pythonw.exe
access_mode[stream] = access_lookup_2[streams[stream]]
# And update streams to be that, instead of the access mask.
streams[stream] = stream_fd
# We cannot immediately close the stream, because we'll get an
# error about a bad file descriptor.
# Okay, duplicate our streams into the FDs for stdin, stdout, stderr.
stdin_fd = streams[stdin_goto]
stdout_fd = streams[stdout_goto]
stderr_fd = streams[stderr_goto]
# Note that we need special casing for pythonw.exe, which has no stds
if sys.stdout is None:
open_streams = {}
for stream in streams:
open_streams[stream] = os.fdopen(
fd = streams[stream],
mode = access_mode[stream]
)
sys.stdin = open_streams[stdin_goto]
sys.stdout = open_streams[stdout_goto]
sys.stderr = open_streams[stderr_goto]
else:
# Flush before transitioning
_flush_stds()
# Do iiiitttttt
os.dup2(stdin_fd, 0)
os.dup2(stdout_fd, 1)
os.dup2(stderr_fd, 2)
# Finally, close the extra fds.
for duped_fd in streams.values():
os.close(duped_fd)
def _write_pid(locked_pidfile):
""" Write our PID to the (already "locked" (by us)) PIDfile.
"""
locked_pidfile.seek(0)
locked_pidfile.truncate(0)
pid = str(os.getpid())
locked_pidfile.write(pid + '\n')
locked_pidfile.flush()
def _acquire_pidfile(pid_file, ignore_lock=False, silence_logger=False):
""" Opens the pid_file, but unfortunately, as this is Windows, we
cannot really lock it. Assume existence is equivalent to locking,
unless autoclean=True.
"""
try:
if os.path.isfile(pid_file):
if ignore_lock:
if not silence_logger:
logger.warning(
'PID file already exists. It will be overwritten with '
'the new PID upon successful daemonization.'
)
open_pid = open(pid_file, 'r+')
else:
if not silence_logger:
logger.critical(
'PID file already exists. Acquire with autoclean=True '
'to force cleanup of existing PID file. Traceback:\n' +
''.join(traceback.format_exc())
)
raise SystemExit('Unable to acquire PID file.')
else:
open_pid = open(pid_file, 'w+')
except (IOError, OSError) as exc:
logger.critical(
'Unable to create/open the PID file w/ traceback: \n' +
''.join(traceback.format_exc())
)
raise SystemExit('Unable to create/open PID file.') from exc
return open_pid

View File

@@ -0,0 +1,383 @@
'''
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

View File

@@ -0,0 +1,586 @@
"""
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
------------------------------------------------------
"""
# Global dependencies
import logging
import traceback
import os
import sys
import signal
import pickle
import base64
import subprocess
import shlex
import tempfile
import atexit
# Intra-package dependencies
from .utils import platform_specificker
from .utils import default_to
from ._daemonize_common import _redirect_stds
from ._daemonize_common import _write_pid
from ._daemonize_common import _acquire_pidfile
_SUPPORTED_PLATFORM = platform_specificker(
linux_choice=False,
win_choice=True,
# Dunno if this is a good idea but might as well try
cygwin_choice=True,
osx_choice=False,
other_choice=False
)
# ###############################################
# Boilerplate
# ###############################################
logger = logging.getLogger(__name__)
# Control * imports.
__all__ = [
# 'Inquisitor',
]
# ###############################################
# Library
# ###############################################
class Daemonizer:
""" Emulates Unix daemonization and registers all appropriate
cleanup functions.
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):
""" Inspect the environment and determine if we're the parent
or the child.
"""
self._is_parent = None
self._daemonize_called = None
def _daemonize(self, *args, **kwargs):
""" Very simple pass-through that does not exit the caller.
"""
self._daemonize_called = True
if self._is_parent:
return _daemonize1(*args, _exit_caller=False, **kwargs)
else:
return _daemonize2(*args, **kwargs)
def __enter__(self):
self._daemonize_called = False
if '__INVOKE_DAEMON__' in os.environ:
self._is_parent = False
else:
self._is_parent = True
# In both cases, just return _is_parent and _daemonize
return self._is_parent, 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 _capability_check(pythonw_path, script_path):
""" Does a compatibility and capability check.
"""
if not _SUPPORTED_PLATFORM:
raise OSError(
'The Windows Daemonizer cannot be used on the current '
'platform.'
)
if not os.path.exists(pythonw_path):
raise SystemExit(
'pythonw.exe must be available in the same directory as the '
'current Python interpreter to support Windows daemonization.'
)
if not os.path.exists(script_path):
raise SystemExit(
'Daemonizer cannot locate the script to daemonize (it seems '
'to have lost itself).'
)
def _filial_usurpation(chdir):
""" Changes our working directory, helping decouple the child
process from the parent. Not necessary on windows, but could help
standardize stuff for cross-platform apps.
"""
# Well this is certainly a stub.
os.chdir(chdir)
def _clean_file(path):
""" Remove the file at path, if it exists, suppressing any errors.
"""
# Clean up the PID file.
try:
# This will raise if the child process had a chance to register
# and complete its exit handler.
os.remove(path)
# So catch that error if it happens.
except OSError:
pass
class _NamespacePasser:
""" Creates a path in a secure temporary directory, such that the
path can be used to write in a reentrant manner. Upon context exit,
the file will be overwritten with zeros, removed, and then the temp
directory cleaned up.
We can't use the normal tempfile stuff because:
1. it doesn't zero the file
2. it prevents reentrant opening
Using this in a context manager will return the path to the file as
the "as" target, ie, "with _ReentrantSecureTempfile() as path:".
"""
def __init__(self):
""" Store args and kwargs to pass into enter and exit.
"""
seed = os.urandom(16)
self._stem = base64.urlsafe_b64encode(seed).decode()
self._tempdir = None
self.name = None
def __enter__(self):
try:
# Create a resident tempdir
self._tempdir = tempfile.TemporaryDirectory()
# Calculate the final path
self.name = self._tempdir.name + '/' + self._stem
# Ensure the file exists, so future cleaning calls won't error
with open(self.name, 'wb'):
pass
except Exception as e:
if self._tempdir is not None:
self._tempdir.cleanup()
raise e
else:
return self.name
def __exit__(self, exc_type, exc_value, exc_tb):
''' Zeroes the file, removes it, and cleans up the temporary
directory.
'''
try:
# Open the existing file and overwrite it with zeros.
with open(self.name, 'r+b') as f:
to_erase = f.read()
eraser = bytes(len(to_erase))
f.seek(0)
f.write(eraser)
# Remove the file. We just accessed it, so it's guaranteed to exist
os.remove(self.name)
# Warn of any errors in the above, and then re-raise.
except:
logger.error(
'Error while shredding secure temp file.\n' +
''.join(traceback.format_exc())
)
raise
finally:
self._tempdir.cleanup()
def _fork_worker(namespace_path, child_env, pid_file, invocation, chdir,
stdin_goto, stdout_goto, stderr_goto, _exit_caller, args):
''' Opens a fork worker, shielding the parent from cancellation via
signal sending. Basically, thanks Windows for being a dick about
signals.
'''
# Find out our PID so the daughter can tell us to exit
my_pid = os.getpid()
# Pack up all of the args that the child will need to use.
# Prepend it to *args
payload = (my_pid, pid_file, chdir, stdin_goto, stdout_goto,
stderr_goto, _exit_caller) + args
# Pack it up. We're shielded from pickling errors already because pickle is
# needed to start the worker.
# Write the payload to the namespace passer using the highest available
# protocol
with open(namespace_path, 'wb') as f:
pickle.dump(payload, f, protocol=-1)
# Invoke the invocation!
daemon = subprocess.Popen(
invocation,
# This is important, because the parent _forkish is telling the child
# to run as a daemon via env. Also note that we need to calculate this
# in the root _daemonize1, or else we'll have a polluted environment
# due to the '__CREATE_DAEMON__' key.
env=child_env,
# This is vital; without it, our process will be reaped at parent
# exit.
creationflags=subprocess.CREATE_NEW_CONSOLE,
)
# Busy wait until either the daemon exits, or it sends a signal to kill us.
daemon.wait()
def _daemonize1(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):
''' Create an independent process for invocation, telling it to
store its "pid" in the pid_file (actually, the pid of its signal
listener). Payload is an iterable of variables to pass the invoked
command for returning from _respawnish.
Note that a bare call to this function will result in all code
before the daemonize() call to be run twice.
The daemon's pid will be recorded in pid_file, but creating a
SignalHandler will overwrite it with the signaling subprocess
PID, which will change after every received signal.
*args will be passed to child. Waiting for success signal will
timeout after success_timeout seconds.
strip_cmd_args will ignore all additional command-line args in the
second run.
all other args identical to unix version of daemonize.
umask, shielded_fds, fd_fallback_limit are unused for this
Windows version.
success_timeout is the wait for a signal. If nothing happens
after timeout, we will raise a ChildProcessError.
_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.
'''
####################################################################
# Error trap and calculate invocation
####################################################################
# Convert any unset std streams to go to dev null
stdin_goto = default_to(stdin_goto, os.devnull)
stdin_goto = os.path.abspath(stdin_goto)
stdout_goto = default_to(stdout_goto, os.devnull)
stdout_goto = os.path.abspath(stdout_goto)
stderr_goto = default_to(stderr_goto, os.devnull)
stderr_goto = os.path.abspath(stderr_goto)
# Convert chdir to go to current dir, and also to an abs path.
chdir = default_to(chdir, '.')
chdir = os.path.abspath(chdir)
# First make sure we can actually do this.
# We need to check the path to pythonw.exe
python_path = sys.executable
python_path = os.path.abspath(python_path)
python_dir = os.path.dirname(python_path)
pythonw_path = python_dir + '/pythonw.exe'
# We also need to check our script is known and available
script_path = sys.argv[0]
script_path = os.path.abspath(script_path)
_capability_check(pythonw_path, script_path)
before_script = '"'
if script_path.endswith('\\__main__.py'):
script_path = script_path[:-12].split('\\')[-1]
before_script = '-m "'
if explicit_rescript is None:
invocation = '"' + pythonw_path + '" ' + before_script + script_path + '"'
# Note that we don't need to worry about being too short like this;
# python doesn't care with slicing. But, don't forget to escape the
# invocation.
if not strip_cmd_args:
for cmd_arg in sys.argv[1:]:
invocation += ' ' + shlex.quote(cmd_arg)
else:
invocation = '"' + pythonw_path + '" ' + explicit_rescript
####################################################################
# Begin actual forking
####################################################################
# Convert the pid_file to an abs path
pid_file = os.path.abspath(pid_file)
# Get a "lock" on the PIDfile before forking anything by opening it
# without silencing anything. Unless we error out while birthing, it
# will be our daughter's job to clean up this file.
open_pidfile = _acquire_pidfile(pid_file)
open_pidfile.close()
try:
# Now open up a secure way to pass a namespace to the daughter process.
with _NamespacePasser() as fpath:
# Determine the child env
child_env = {'__INVOKE_DAEMON__': fpath}
child_env.update(_get_clean_env())
# We need to shield ourselves from signals, or we'll be terminated
# by python before running cleanup. So use a spawned worker to
# handle the actual daemon creation.
with _NamespacePasser() as worker_argpath:
# Write an argvector for the worker to the namespace passer
worker_argv = (
fpath, # namespace_path
child_env,
pid_file,
invocation,
chdir,
stdin_goto,
stdout_goto,
stderr_goto,
_exit_caller,
args
)
with open(worker_argpath, 'wb') as f:
# Use the highest available protocol
pickle.dump(worker_argv, f, protocol=-1)
# Create an env for the worker to let it know what to do
worker_env = {'__CREATE_DAEMON__': 'True'}
worker_env.update(_get_clean_env())
# Figure out the path to the current file
# worker_target = os.path.abspath(__file__)
worker_cmd = ('"' + python_path + '" -m ' +
'neko_daemonizer_dante.daemonizer._daemonize_windows ' +
'"' + worker_argpath + '"')
try:
# This will wait for the worker to finish, or cancel it at
# the timeout.
worker = subprocess.run(
worker_cmd,
env=worker_env,
timeout=success_timeout
)
# Make sure it actually terminated via the success signal
if worker.returncode != signal.SIGINT:
raise RuntimeError(
'Daemon creation worker exited prematurely.'
)
except subprocess.TimeoutExpired as exc:
raise ChildProcessError(
'Timeout while waiting for daemon init.'
) from exc
# If anything goes wrong in there, we need to clean up the pidfile.
except:
_clean_file(pid_file)
raise
# Success.
# _exit_caller = True. Exit the interpreter.
if _exit_caller:
os._exit(0)
# Don't _exit_caller. Return is_parent=True, and change all of the args to
# None to prevent accidental modification attempts in the parent.
else:
# Reset args to be an equivalent expansion of *[None]s
args = [None] * len(args)
# is_parent, *args
return [True] + list(args)
def _daemonize2(*_daemonize1_args, **_daemonize1_kwargs):
""" Unpacks the daemonization. Modifies the new environment as per
the parent's forkish() call. Registers appropriate cleanup methods
for the pid_file. Signals successful daemonization. Returns the
*args passed to parent forkish() call.
"""
####################################################################
# Unpack and prep the arguments
####################################################################
# Unpack the namespace.
ns_passer_path = os.environ['__INVOKE_DAEMON__']
with open(ns_passer_path, 'rb') as f:
pkg = pickle.load(f)
(
parent,
pid_file,
chdir,
stdin_goto,
stdout_goto,
stderr_goto,
_exit_caller,
*args
) = pkg
####################################################################
# Resume actual daemonization
####################################################################
# Do some important housekeeping
_redirect_stds(stdin_goto, stdout_goto, stderr_goto)
_filial_usurpation(chdir)
# Get the "locked" PIDfile, bypassing _acquire entirely.
with open(pid_file, 'w+') as open_pidfile:
_write_pid(open_pidfile)
# Define a memoized cleanup function.
def cleanup(pid_path=pid_file):
try:
os.remove(pid_path)
except Exception:
if os.path.exists(pid_path):
logger.error(
'Failed to clean up pidfile w/ traceback: \n' +
''.join(traceback.format_exc())
)
else:
logger.info('Pidfile was removed prior to atexit cleanup.')
# Register this as soon as possible in case something goes wrong.
atexit.register(cleanup)
# "Notify" parent of success
os.kill(parent, signal.SIGINT)
# If our parent exited, we are being called directly and don't need to
# worry about any of this silliness.
if _exit_caller:
return args
# Our parent did not exit, so we're within a context manager, and our
# application expects a return value for is_parent
else:
# is_parent, *args
return [False] + list(args)
if '__INVOKE_DAEMON__' in os.environ:
daemonize = _daemonize2
else:
daemonize = _daemonize1
def _get_clean_env():
""" Gets a clean copy of our environment, with any flags stripped.
"""
env2 = dict(os.environ)
flags = {
'__INVOKE_DAEMON__',
'__CREATE_DAEMON__',
'__CREATE_SIGHANDLER__'
}
for key in flags:
if key in env2:
del env2[key]
return env2
if __name__ == '__main__':
''' Do this so we can support process-based workers using Popen
instead of multiprocessing, which would impose extra requirements
on whatever code used Windows daemonization to avoid infinite
looping.
'''
if '__CREATE_DAEMON__' in os.environ:
# Use this to create a daemon worker, similar to a signal handler.
argpath = sys.argv[1]
with open(argpath, 'rb') as f:
args = pickle.load(f)
_fork_worker(*args)

View File

@@ -0,0 +1,81 @@
'''
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
------------------------------------------------------
'''
# Global dependencies
# import logging
# import traceback
# import os
# import sys
# import time
# import signal
# import pickle
# import base64
# import subprocess
# import multiprocessing
# import shlex
# import tempfile
# import atexit
# import ctypes
# import threading
# Intra-package dependencies
# from .utils import default_to
# from ._daemonize import _redirect_stds
# from ._daemonize import _write_pid
# from ._daemonize import send
# from ._daemonize import ping
# from .exceptions import SignalError
# from .exceptions import ReceivedSignal
# from .exceptions import SIGABRT
# from .exceptions import SIGINT
# from .exceptions import SIGTERM
# ###############################################
# Boilerplate
# ###############################################
import logging
logger = logging.getLogger(__name__)
# Control * imports.
__all__ = [
# 'Inquisitor',
]
# ###############################################
# Library
# ###############################################

View File

@@ -0,0 +1,150 @@
'''
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
'''
# Global dependencies
import os
import sys
import signal
import logging
import atexit
import traceback
import shutil
# Intra-package dependencies
from .utils import platform_specificker
from .utils import default_to
_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
# ###############################################
import logging
logger = logging.getLogger(__name__)
# Control * imports.
__all__ = [
# 'Inquisitor',
]
# ###############################################
# Library
# ###############################################
def _setuser(user):
''' Normalizes user to a uid and sets the current uid, or does
nothing if user is None.
'''
if user is None:
return
# Normalize group to gid
elif isinstance(user, str):
uid = pwd.getpwnam(user).pw_uid
# The group is already a gid.
else:
uid = user
try:
os.setuid(uid)
except OSError:
self.logger.error('Unable to change user.')
sys.exit(1)
def _setgroup(group):
''' Normalizes group to a gid and sets the current gid, or does
nothing if group is None.
'''
if group is None:
return
# Normalize group to gid
elif isinstance(group, str):
gid = grp.getgrnam(group).gr_gid
# The group is already a gid.
else:
gid = group
try:
os.setgid(gid)
except OSError:
self.logger.error('Unable to change group.')
sys.exit(1)
def daemote(pid_file, user, group):
''' Change gid and uid, dropping privileges.
Either user or group may explicitly pass None to keep it the same.
The pid_file will be chown'ed so it can still be cleaned up.
'''
if not _SUPPORTED_PLATFORM:
raise OSError('Daemotion is unsupported on your platform.')
# No need to do anything special, just chown the pidfile
# This will also catch any bad group, user names
shutil.chown(pid_file, user, group)
# Now update group and then user
_setgroup(group)
_setuser(user)

View File

@@ -0,0 +1,90 @@
'''
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
------------------------------------------------------
'''
# Global dependencies
# import logging
# import traceback
# import os
# import sys
# import time
# import signal
# import pickle
# import base64
# import subprocess
# import multiprocessing
# import shlex
# import tempfile
# import atexit
# import ctypes
# import threading
# Intra-package dependencies
from .utils import platform_specificker
# from .utils import default_to
# from ._daemonize import _redirect_stds
# from ._daemonize import _write_pid
# from ._daemonize import send
# from ._daemonize import ping
# from .exceptions import SignalError
# from .exceptions import ReceivedSignal
# from .exceptions import SIGABRT
# from .exceptions import SIGINT
# from .exceptions import SIGTERM
_SUPPORTED_PLATFORM = platform_specificker(
linux_choice = False,
win_choice = True,
# Dunno if this is a good idea but might as well try
cygwin_choice = True,
osx_choice = False,
other_choice = False
)
# ###############################################
# Boilerplate
# ###############################################
import logging
logger = logging.getLogger(__name__)
# Control * imports.
__all__ = [
# 'Inquisitor',
]
# ###############################################
# Library
# ###############################################

View File

@@ -0,0 +1,177 @@
'''
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
------------------------------------------------------
'''
# Global dependencies
import os
import sys
import signal
import logging
import atexit
import traceback
import shutil
# Intra-package dependencies
from .utils import default_to
from .exceptions import DaemonikerSignal
# ###############################################
# Boilerplate
# ###############################################
import logging
logger = logging.getLogger(__name__)
# Control * imports.
__all__ = [
# 'Inquisitor',
]
# ###############################################
# Library
# ###############################################
IGNORE_SIGNAL = 1793
def _noop(*args, **kwargs):
''' Used for ignoring signals.
'''
pass
def send(pid_file, signal):
''' Sends the signal in signum to the pid_file. Num can be either
int or one of the exceptions.
'''
if isinstance(signal, DaemonikerSignal):
signum = signal.SIGNUM
elif isinstance(signal, type) and issubclass(signal, DaemonikerSignal):
signum = signal.SIGNUM
else:
signum = int(signal)
with open(pid_file, 'r') as f:
pid = int(f.read())
os.kill(pid, signum)
def ping(pid_file):
''' Returns True if the process in pid_file is available, and False
otherwise. Note that availability does not imply the process is
running, just that it recently has been. For example, recently-
exited processes will still return True.
Uhhh shit, this isn't going to work well, windows converts signal 0
into an interrupt. Okay, punt for now.
'''
try:
send(pid_file, 0)
except OSError:
return False
else:
return True
def _normalize_handler(handler, default_handler):
''' Normalizes a signal handler. Converts None to the default, and
IGNORE_SIGNAL to noop.
'''
# None -> _default_handler
handler = default_to(handler, default_handler)
# IGNORE_SIGNAL -> _noop
handler = default_to(handler, _noop, comparator=IGNORE_SIGNAL)
return handler
class _SighandlerCore:
''' Core, platform-independent functionality for signal handlers.
'''
@property
def sigint(self):
''' Gets sigint.
'''
return self._sigint
@sigint.setter
def sigint(self, handler):
''' Normalizes and sets sigint.
'''
self._sigint = _normalize_handler(handler, self._default_handler)
@sigint.deleter
def sigint(self):
''' Returns the sigint handler to the default.
'''
self.sigint = None
@property
def sigterm(self):
''' Gets sigterm.
'''
return self._sigterm
@sigterm.setter
def sigterm(self, handler):
''' Normalizes and sets sigterm.
'''
self._sigterm = _normalize_handler(handler, self._default_handler)
@sigterm.deleter
def sigterm(self):
''' Returns the sigterm handler to the default.
'''
self.sigterm = None
@property
def sigabrt(self):
''' Gets sigabrt.
'''
return self._sigabrt
@sigabrt.setter
def sigabrt(self, handler):
''' Normalizes and sets sigabrt.
'''
self._sigabrt = _normalize_handler(handler, self._default_handler)
@sigabrt.deleter
def sigabrt(self):
''' Returns the sigabrt handler to the default.
'''
self.sigabrt = None

View File

@@ -0,0 +1,235 @@
'''
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
------------------------------------------------------
'''
# Global dependencies
import os
import sys
import signal
import logging
import atexit
import traceback
import shutil
# Intra-package dependencies
from .utils import platform_specificker
from .utils import default_to
from ._signals_common import _SighandlerCore
from .exceptions import DaemonikerSignal
from .exceptions import SignalError
from .exceptions import SIGINT
from .exceptions import SIGTERM
from .exceptions import SIGABRT
_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
# ###############################################
import logging
logger = logging.getLogger(__name__)
# Control * imports.
__all__ = [
# 'Inquisitor',
]
# ###############################################
# Library
# ###############################################
def _restore_any_previous_handler(signum, maybe_handler, force_clear=False):
''' Makes sure that a previous handler was actually set, and then
restores it.
maybe_handler is the cached potential handler. If it's the constant
(ZeroDivisionError) we've been using to denote nothingness, then
maybe_handler will either:
1. do nothing, if force_clear == False
2. restore signal.SIG_DEFL, if force_clear == True
'''
# Nope, wasn't previously set
if maybe_handler == ZeroDivisionError:
# Restore the default if we're forcing it
if force_clear:
signal.signal(signum, signal.SIG_DFL)
# (Do nothing otherwise)
# It was previously set, so re-set it.
else:
signal.signal(signum, maybe_handler)
class SignalHandler1(_SighandlerCore):
''' Signal handling system using lightweight wrapper around built-in
signal.signal handling.
'''
def __init__(self, pid_file, sigint=None, sigterm=None, sigabrt=None):
''' Creates a signal handler, using the passed callables. None
will assign the default handler (raise in main). passing
IGNORE_SIGNAL constant will result in the signal being noop'd.
'''
self.sigint = sigint
self.sigterm = sigterm
self.sigabrt = sigabrt
# Yeah, except this isn't used at all (just here for cross-platform
# consistency)
self._pidfile = pid_file
# Assign these to impossible values so we can compare against them
# later, when deciding whether or not to restore a previous handler.
# Don't use None, in case it gets used to denote a default somewhere.
# This is deliberately unconventional.
self._old_sigint = ZeroDivisionError
self._old_sigterm = ZeroDivisionError
self._old_sigabrt = ZeroDivisionError
self._running = False
def start(self):
''' Starts signal handling.
'''
if self._running:
raise RuntimeError('SignalHandler is already running.')
# Initialize stuff to values that are impossible for signal.signal.
# Don't use None, in case it gets used to denote a default somewhere.
# This is deliberately unconventional.
old_sigint = ZeroDivisionError
old_sigterm = ZeroDivisionError
old_sigabrt = ZeroDivisionError
try:
# First we need to make closures around all of our attributes, so
# they can be updated after we start listening to signals
def sigint_closure(signum, frame):
return self.sigint(signum)
def sigterm_closure(signum, frame):
return self.sigterm(signum)
def sigabrt_closure(signum, frame):
return self.sigabrt(signum)
# Now simply register those with signal.signal
old_sigint = signal.signal(signal.SIGINT, sigint_closure)
old_sigterm = signal.signal(signal.SIGTERM, sigterm_closure)
old_sigabrt = signal.signal(signal.SIGABRT, sigabrt_closure)
# If that fails, restore previous state and reraise
except:
_restore_any_previous_handler(signal.SIGINT, old_sigint)
_restore_any_previous_handler(signal.SIGTERM, old_sigterm)
_restore_any_previous_handler(signal.SIGABRT, old_sigabrt)
raise
# If that succeeds, set self._running and cache old handlers
else:
self._old_sigint = old_sigint
self._old_sigterm = old_sigterm
self._old_sigabrt = old_sigabrt
self._running = True
def stop(self):
''' Stops signal handling, returning all signal handlers to
their previous handlers, or restoring their defaults if
something went fishy.
'''
try:
_restore_any_previous_handler(
signal.SIGINT,
self._old_sigint,
force_clear = True
)
_restore_any_previous_handler(
signal.SIGTERM,
self._old_sigterm,
force_clear = True
)
_restore_any_previous_handler(
signal.SIGABRT,
self._old_sigabrt,
force_clear = True
)
# If we get an exception there, just force restoring all defaults and
# reraise
except:
signal.signal(signal.SIGINT, signal.SIG_DFL)
signal.signal(signal.SIGTERM, signal.SIG_DFL)
signal.signal(signal.SIGABRT, signal.SIG_DFL)
raise
finally:
# See notes above re: using ZeroDivisionError instead of None
self._old_sigint = ZeroDivisionError
self._old_sigterm = ZeroDivisionError
self._old_sigabrt = ZeroDivisionError
self._running = False
@staticmethod
def _default_handler(signum, *args):
''' The default signal handler for Unix.
'''
# Just parallel the sighandlers that are available in Windows, because
# it is definitely the limiting factor here
sigs = {
signal.SIGABRT: SIGABRT,
signal.SIGINT: SIGINT,
signal.SIGTERM: SIGTERM,
}
try:
exc = sigs[signum]
except KeyError:
exc = DaemonikerSignal
raise exc()

View File

@@ -0,0 +1,326 @@
'''
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
------------------------------------------------------
'''
# Global dependencies
import logging
import os
import sys
import time
import signal
import subprocess
import atexit
import ctypes
import threading
# Intra-package dependencies
from .utils import platform_specificker
from ._daemonize_windows import _get_clean_env
from ._signals_common import _SighandlerCore
from .exceptions import DaemonikerSignal
from .exceptions import SIGABRT
from .exceptions import SIGINT
from .exceptions import SIGTERM
_SUPPORTED_PLATFORM = platform_specificker(
linux_choice=False,
win_choice=True,
# Dunno if this is a good idea but might as well try
cygwin_choice=True,
osx_choice=False,
other_choice=False
)
# ###############################################
# Boilerplate
# ###############################################
logger = logging.getLogger(__name__)
# Control * imports.
__all__ = [
# 'Inquisitor',
]
# ###############################################
# Library
# ###############################################
def _sketch_raise_in_main(exc):
''' Sketchy way to raise an exception in the main thread.
'''
if isinstance(exc, BaseException):
exc = type(exc)
elif issubclass(exc, BaseException):
pass
else:
raise TypeError('Must raise an exception.')
# Figure out the id of the main thread
main_id = threading.main_thread().ident
thread_ref = ctypes.c_long(main_id)
exc = ctypes.py_object(exc)
result = ctypes.pythonapi.PyThreadState_SetAsyncExc(
thread_ref,
exc
)
# 0 Is failed.
if result == 0:
raise SystemError('Main thread had invalid ID?')
# 1 succeeded
# > 1 failed
elif result > 1:
ctypes.pythonapi.PyThreadState_SetAsyncExc(main_id, 0)
raise SystemError('Failed to raise in main thread.')
def _infinite_noop():
''' Give a process something to do while it waits for a signal.
'''
while True:
time.sleep(9999)
def _await_signal(process):
''' Waits for the process to die, and then returns the exit code for
the process, converting CTRL_C_EVENT and CTRL_BREAK_EVENT into
SIGINT.
'''
# Note that this is implemented with a busy wait
process.wait()
code = process.returncode
if code == signal.CTRL_C_EVENT:
code = signal.SIGINT
elif code == signal.CTRL_BREAK_EVENT:
code = signal.SIGINT
return code
class SignalHandler1(_SighandlerCore):
''' Signal handling system using a daughter thread and a disposable
daughter process.
'''
def __init__(self, pid_file, sigint=None, sigterm=None, sigabrt=None):
''' Creates a signal handler, using the passed callables. None
will assign the default handler (raise in main). passing
IGNORE_SIGNAL constant will result in the signal being noop'd.
'''
self.sigint = sigint
self.sigterm = sigterm
self.sigabrt = sigabrt
self._pidfile = pid_file
self._running = None
self._worker = None
self._thread = None
self._watcher = None
self._opslock = threading.Lock()
self._stopped = threading.Event()
self._started = threading.Event()
def start(self):
try:
with self._opslock:
if self._running:
raise RuntimeError('SignalHandler is already running.')
self._stopped.clear()
self._running = True
self._thread = threading.Thread(
target=self._listen_loop,
# We need to always reset the PID file.
daemon=False
)
self._thread.start()
atexit.register(self.stop)
# Only set up a watcher once, and then let it run forever.
if self._watcher is None:
self._watcher = threading.Thread(
target=self._watch_for_exit,
# Who watches the watchman?
# Daemon threading this is important to protect us against
# issues during closure.
daemon=True
)
self._watcher.start()
self._started.wait()
except:
self._stop_nowait()
raise
def stop(self):
''' Hold the phone! Idempotent.
'''
self._stop_nowait()
self._stopped.wait()
def _stop_nowait(self):
''' Stops the listener without waiting for the _stopped flag.
Only called directly if there's an error while starting.
'''
with self._opslock:
self._running = False
# If we were running, kill the process so that the loop breaks free
if self._worker is not None and self._worker.returncode is None:
self._worker.terminate()
atexit.unregister(self.stop)
def _listen_loop(self):
""" Manages all signals.
"""
python_path = sys.executable
python_path = os.path.abspath(python_path)
worker_cmd = ('"' + python_path + '" -m ' +
'neko_daemonizer_dante.daemonizer._signals_windows')
worker_env = {'__CREATE_SIGHANDLER__': 'True'}
worker_env.update(_get_clean_env())
# Iterate until we're reaped by the main thread exiting.
try:
while self._running:
try:
# Create a process. Depend upon it being reaped if the
# parent quits
self._worker = subprocess.Popen(
worker_cmd,
env=worker_env,
)
worker_pid = self._worker.pid
# Record the PID of the worker in the pidfile, overwriting
# its contents.
with open(self._pidfile, 'w+') as f:
f.write(str(worker_pid) + '\n')
finally:
# Now let the start() call know we are CTR, even if we
# raised, so it can proceed. I might consider adding an
# error signal so that the start call will raise if it
# didn't work.
self._started.set()
# Wait for the worker to generate a signal. Calling stop()
# will break out of this.
signum = _await_signal(self._worker)
# If the worker was terminated by stopping the
# SignalHandler, then discard errything.
if self._running:
# Handle the signal, catching unknown ones with the
# default handler. Do this each time so that the
# SigHandler can be updated while running.
signums = {
signal.SIGABRT: self.sigabrt,
signal.SIGINT: self.sigint,
signal.SIGTERM: self.sigterm
}
try:
handler = signums[signum]
except KeyError:
handler = self._default_handler
handler(signum)
# If we exit, be sure to reset self._running and stop the running
# worker, if there is one (note terinate is idempotent)
finally:
try:
self._running = False
self._worker.terminate()
self._worker = None
# Restore our actual PID to the pidfile, overwriting its
# contents.
with open(self._pidfile, 'w+') as f:
f.write(str(os.getpid()) + '\n')
finally:
self._stopped.set()
self._started.clear()
def _watch_for_exit(self):
''' Automatically watches for termination of the main thread and
then closes self gracefully.
'''
main = threading.main_thread()
main.join()
self._stop_nowait()
@staticmethod
def _default_handler(signum, *args):
''' The default signal handler. Don't register with built-in
signal.signal! This needs to be used on the subprocess await
death workaround.
'''
# All valid cpython windows signals
sigs = {
signal.SIGABRT: SIGABRT,
# signal.SIGFPE: 'fpe', # Don't catch this
# signal.SIGSEGV: 'segv', # Don't catch this
# signal.SIGILL: 'illegal', # Don't catch this
signal.SIGINT: SIGINT,
signal.SIGTERM: SIGTERM,
# Note that signal.CTRL_C_EVENT and signal.CTRL_BREAK_EVENT are
# converted to SIGINT in _await_signal
}
try:
exc = sigs[signum]
except KeyError:
exc = DaemonikerSignal
_sketch_raise_in_main(exc)
if __name__ == '__main__':
''' Do this so we can support process-based workers using Popen
instead of multiprocessing, which would impose extra requirements
on whatever code used Windows daemonization to avoid infinite
looping.
'''
if '__CREATE_SIGHANDLER__' in os.environ:
# Use this to create a signal handler.
_infinite_noop()

View File

@@ -0,0 +1,111 @@
'''
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
------------------------------------------------------
'''
import signal
# Control * imports.
__all__ = [
# Base class for all of the above
'DaemonikerException',
# These are daemonization/sighandling errors and exceptions
'SignalError',
# These are actual signals
'DaemonikerSignal',
'SIGABRT',
'SIGINT',
'SIGTERM',
]
class DaemonikerException(Exception):
''' This is suclassed for all exceptions and warnings, so that code
using daemoniker as an import can successfully catch all daemoniker
exceptions with a single except.
'''
pass
# ###############################################
# Signal handling errors and exceptions
# ###############################################
class SignalError(DaemonikerException, RuntimeError):
''' This exception (or a subclass thereof) is raised for all issues
related to signal handling.
'''
pass
# ###############################################
# Signals themselves
# ###############################################
class _SignalMeta(type):
def __int__(self):
return self.SIGNUM
class DaemonikerSignal(BaseException, metaclass=_SignalMeta):
''' Subclasses of this exception are raised by all of the default
signal handlers defined using SignalHandlers.
This subclasses BaseException because, when unhandled, it should
always be a system-exiting exception. That being said, it should not
subclass SystemExit, because that's a whole different can of worms.
'''
SIGNUM = -1
def __int__(self):
return self.SIGNUM
ReceivedSignal = DaemonikerSignal
class SIGABRT(DaemonikerSignal):
''' Raised upon receipt of SIGABRT.
'''
SIGNUM = int(signal.SIGABRT)
class SIGINT(DaemonikerSignal):
''' Raised upon receipt of SIGINT, CTRL_C_EVENT, CTRL_BREAK_EVENT.
'''
SIGNUM = int(signal.SIGINT)
class SIGTERM(DaemonikerSignal):
''' Raised upon receipt of SIGTERM.
'''
SIGNUM = int(signal.SIGTERM)

View File

@@ -0,0 +1,91 @@
'''
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
------------------------------------------------------
'''
import sys
# Control * imports.
__all__ = [
]
# ###############################################
# Logging boilerplate
# ###############################################
import logging
logger = logging.getLogger(__name__)
# ###############################################
# Lib
# ###############################################
def platform_specificker(linux_choice, win_choice, cygwin_choice, osx_choice,
other_choice):
''' For the three choices, returns whichever is appropriate for this
platform.
"Other" means a non-linux Unix system, see python.sys docs:
For Unix systems, except on Linux, this is the lowercased OS
name as returned by uname -s with the first part of the version
as returned by uname -r appended, e.g. 'sunos5' or 'freebsd8',
at the time when Python was built.
'''
platform = sys.platform
if platform.startswith('linux'):
return linux_choice
elif platform.startswith('win32'):
return win_choice
elif platform.startswith('cygwin'):
return cygwin_choice
elif platform.startswith('darwin'):
return osx_choice
else:
return other_choice
def default_to(check, default, comparator=None):
''' If check is None, apply default; else, return check.
'''
if comparator is None:
if check is None:
return default
else:
return check
else:
if check == comparator:
return default
else:
return check

View File

@@ -0,0 +1,4 @@
from .integrations import ConfigParserInterface
__all__ = [ConfigParserInterface]

View File

@@ -0,0 +1,12 @@
from neko_configparser import ConfigParserInterface
neko_config = ConfigParserInterface.parse_config()
def integrate():
ConfigParserInterface.ensure_config(
partition='daemonizer_dante',
module_config={
'pids_folder': f'{ConfigParserInterface.get_nekomata_folder()}/pids'
}
)

View File

@@ -0,0 +1,71 @@
import os.path
from typing import Callable, Coroutine
from neko_daemonizer_std import DaemonizerInterface
import uuid
from .integrations import neko_config
from ..daemonizer import Daemonizer
def _get_pidfile():
if not os.path.isdir(neko_config.daemonizer_dante.pids_folder):
os.makedirs(neko_config.daemonizer_dante.pids_folder)
return (
f'{neko_config.daemonizer_dante.pids_folder}/{uuid.uuid4().hex}.pid'
)
class RDaemonizerInterface(DaemonizerInterface):
@staticmethod
def release_execution(
main: Callable,
setup: Callable = None,
post_daemonized: Callable = None,
pre_call: Callable = None,
) -> None:
with Daemonizer() as (is_setup, daemonizer):
if is_setup and setup:
setup()
is_parent, _ = daemonizer(
_get_pidfile(),
None
)
if is_parent and post_daemonized:
post_daemonized()
elif pre_call:
pre_call()
main()
@staticmethod
async def async_release_execution(
main: Coroutine,
setup: Coroutine = None,
post_daemonized: Coroutine = None,
pre_call: Coroutine = None,
) -> None:
with Daemonizer() as (is_setup, daemonizer):
if is_setup:
await setup
is_parent, _ = daemonizer(
_get_pidfile(),
None
)
if is_parent:
await post_daemonized
else:
await pre_call
await main
DaemonizerInterface = RDaemonizerInterface
__all__ = [DaemonizerInterface]

View File

@@ -0,0 +1,8 @@
import warnings
warnings.filterwarnings(
action="ignore",
category=RuntimeWarning,
module='runpy',
lineno=128
)

19
pyproject.toml Normal file
View File

@@ -0,0 +1,19 @@
[tool.poetry]
name = "neko-daemonizer-dante"
version = "0.1.0"
description = ""
authors = ["hhh"]
readme = "README.md"
[tool.poetry.dependencies]
python = "^3.11"
[tool.poetry.group.neko.dependencies]
neko-daemonizer-std = {path = "../neko-daemonizer-std", develop = true}
neko-configparser = {path = "../neko-configparser", develop = true}
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"