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