327 lines
10 KiB
Python
327 lines
10 KiB
Python
'''
|
|
LICENSING
|
|
-------------------------------------------------
|
|
|
|
daemoniker: Cross-platform daemonization tools.
|
|
Copyright (C) 2016 Muterra, Inc.
|
|
|
|
Contributors
|
|
------------
|
|
Nick Badger
|
|
badg@muterra.io | badg@nickbadger.com | nickbadger.com
|
|
|
|
This library is free software; you can redistribute it and/or
|
|
modify it under the terms of the GNU Lesser General Public
|
|
License as published by the Free Software Foundation; either
|
|
version 2.1 of the License, or (at your option) any later version.
|
|
|
|
This library is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
Lesser General Public License for more details.
|
|
|
|
You should have received a copy of the GNU Lesser General Public
|
|
License along with this library; if not, write to the
|
|
Free Software Foundation, Inc.,
|
|
51 Franklin Street,
|
|
Fifth Floor,
|
|
Boston, MA 02110-1301 USA
|
|
|
|
------------------------------------------------------
|
|
'''
|
|
|
|
# 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()
|