Init
This commit is contained in:
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
/.idea/
|
||||
/tests/
|
||||
|
||||
poetry.lock
|
||||
|
||||
**/__pycache__/
|
||||
3
README.md
Normal file
3
README.md
Normal 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)
|
||||
5
neko_daemonizer_dante/__init__.py
Normal file
5
neko_daemonizer_dante/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from . import utils
|
||||
from . import daemonizer
|
||||
|
||||
|
||||
__all__ = ['daemonizer']
|
||||
99
neko_daemonizer_dante/daemonizer/__init__.py
Normal file
99
neko_daemonizer_dante/daemonizer/__init__.py
Normal 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.'
|
||||
)
|
||||
252
neko_daemonizer_dante/daemonizer/_daemonize_common.py
Normal file
252
neko_daemonizer_dante/daemonizer/_daemonize_common.py
Normal 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
|
||||
383
neko_daemonizer_dante/daemonizer/_daemonize_unix.py
Normal file
383
neko_daemonizer_dante/daemonizer/_daemonize_unix.py
Normal 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
|
||||
586
neko_daemonizer_dante/daemonizer/_daemonize_windows.py
Normal file
586
neko_daemonizer_dante/daemonizer/_daemonize_windows.py
Normal 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)
|
||||
81
neko_daemonizer_dante/daemonizer/_privdrop_common.py
Normal file
81
neko_daemonizer_dante/daemonizer/_privdrop_common.py
Normal 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
|
||||
# ###############################################
|
||||
150
neko_daemonizer_dante/daemonizer/_privdrop_unix.py
Normal file
150
neko_daemonizer_dante/daemonizer/_privdrop_unix.py
Normal 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)
|
||||
90
neko_daemonizer_dante/daemonizer/_privdrop_windows.py
Normal file
90
neko_daemonizer_dante/daemonizer/_privdrop_windows.py
Normal 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
|
||||
# ###############################################
|
||||
177
neko_daemonizer_dante/daemonizer/_signals_common.py
Normal file
177
neko_daemonizer_dante/daemonizer/_signals_common.py
Normal 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
|
||||
235
neko_daemonizer_dante/daemonizer/_signals_unix.py
Normal file
235
neko_daemonizer_dante/daemonizer/_signals_unix.py
Normal 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()
|
||||
326
neko_daemonizer_dante/daemonizer/_signals_windows.py
Normal file
326
neko_daemonizer_dante/daemonizer/_signals_windows.py
Normal 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()
|
||||
111
neko_daemonizer_dante/daemonizer/exceptions.py
Normal file
111
neko_daemonizer_dante/daemonizer/exceptions.py
Normal 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)
|
||||
91
neko_daemonizer_dante/daemonizer/utils.py
Normal file
91
neko_daemonizer_dante/daemonizer/utils.py
Normal 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
|
||||
4
neko_daemonizer_dante/neko/__init__.py
Normal file
4
neko_daemonizer_dante/neko/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from .integrations import ConfigParserInterface
|
||||
|
||||
|
||||
__all__ = [ConfigParserInterface]
|
||||
12
neko_daemonizer_dante/neko/integrations.py
Normal file
12
neko_daemonizer_dante/neko/integrations.py
Normal 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'
|
||||
}
|
||||
)
|
||||
71
neko_daemonizer_dante/neko/interfaces.py
Normal file
71
neko_daemonizer_dante/neko/interfaces.py
Normal 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]
|
||||
8
neko_daemonizer_dante/utils/__init__.py
Normal file
8
neko_daemonizer_dante/utils/__init__.py
Normal file
@@ -0,0 +1,8 @@
|
||||
import warnings
|
||||
|
||||
warnings.filterwarnings(
|
||||
action="ignore",
|
||||
category=RuntimeWarning,
|
||||
module='runpy',
|
||||
lineno=128
|
||||
)
|
||||
19
pyproject.toml
Normal file
19
pyproject.toml
Normal 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"
|
||||
Reference in New Issue
Block a user