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

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()