mirror of
https://github.com/Tautulli/Tautulli.git
synced 2025-08-19 21:03:21 -07:00
Update cherrpy to 17.4.2
This commit is contained in:
parent
f28e741ad7
commit
4d6279a626
131 changed files with 15864 additions and 10389 deletions
|
@ -1,4 +1,4 @@
|
|||
"""An implementation of the Web Site Process Bus.
|
||||
r"""An implementation of the Web Site Process Bus.
|
||||
|
||||
This module is completely standalone, depending only on the stdlib.
|
||||
|
||||
|
@ -61,12 +61,28 @@ the new state.::
|
|||
"""
|
||||
|
||||
import atexit
|
||||
|
||||
try:
|
||||
import ctypes
|
||||
except ImportError:
|
||||
"""Google AppEngine is shipped without ctypes
|
||||
|
||||
:seealso: http://stackoverflow.com/a/6523777/70170
|
||||
"""
|
||||
ctypes = None
|
||||
|
||||
import operator
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
import traceback as _traceback
|
||||
import warnings
|
||||
import subprocess
|
||||
import functools
|
||||
|
||||
import six
|
||||
|
||||
|
||||
# Here I save the value of os.getcwd(), which, if I am imported early enough,
|
||||
# will be the directory from which the startup script was run. This is needed
|
||||
|
@ -78,15 +94,13 @@ _startup_cwd = os.getcwd()
|
|||
|
||||
|
||||
class ChannelFailures(Exception):
|
||||
"""Exception raised during errors on Bus.publish()."""
|
||||
|
||||
"""Exception raised when errors occur in a listener during Bus.publish().
|
||||
"""
|
||||
delimiter = '\n'
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
# Don't use 'super' here; Exceptions are old-style in Py2.4
|
||||
# See https://bitbucket.org/cherrypy/cherrypy/issue/959
|
||||
Exception.__init__(self, *args, **kwargs)
|
||||
"""Initialize ChannelFailures errors wrapper."""
|
||||
super(ChannelFailures, self).__init__(*args, **kwargs)
|
||||
self._exceptions = list()
|
||||
|
||||
def handle_exception(self):
|
||||
|
@ -98,12 +112,14 @@ class ChannelFailures(Exception):
|
|||
return self._exceptions[:]
|
||||
|
||||
def __str__(self):
|
||||
"""Render the list of errors, which happened in channel."""
|
||||
exception_strings = map(repr, self.get_instances())
|
||||
return self.delimiter.join(exception_strings)
|
||||
|
||||
__repr__ = __str__
|
||||
|
||||
def __bool__(self):
|
||||
"""Determine whether any error happened in channel."""
|
||||
return bool(self._exceptions)
|
||||
__nonzero__ = __bool__
|
||||
|
||||
|
@ -116,12 +132,14 @@ class _StateEnum(object):
|
|||
name = None
|
||||
|
||||
def __repr__(self):
|
||||
return "states.%s" % self.name
|
||||
return 'states.%s' % self.name
|
||||
|
||||
def __setattr__(self, key, value):
|
||||
if isinstance(value, self.State):
|
||||
value.name = key
|
||||
object.__setattr__(self, key, value)
|
||||
|
||||
|
||||
states = _StateEnum()
|
||||
states.STOPPED = states.State()
|
||||
states.STARTING = states.State()
|
||||
|
@ -142,7 +160,6 @@ else:
|
|||
|
||||
|
||||
class Bus(object):
|
||||
|
||||
"""Process state-machine and messenger for HTTP site deployment.
|
||||
|
||||
All listeners for a given channel are guaranteed to be called even
|
||||
|
@ -158,18 +175,31 @@ class Bus(object):
|
|||
max_cloexec_files = max_files
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize pub/sub bus."""
|
||||
self.execv = False
|
||||
self.state = states.STOPPED
|
||||
channels = 'start', 'stop', 'exit', 'graceful', 'log', 'main'
|
||||
self.listeners = dict(
|
||||
[(channel, set()) for channel
|
||||
in ('start', 'stop', 'exit', 'graceful', 'log', 'main')])
|
||||
(channel, set())
|
||||
for channel in channels
|
||||
)
|
||||
self._priorities = {}
|
||||
|
||||
def subscribe(self, channel, callback, priority=None):
|
||||
"""Add the given callback at the given channel (if not present)."""
|
||||
if channel not in self.listeners:
|
||||
self.listeners[channel] = set()
|
||||
self.listeners[channel].add(callback)
|
||||
def subscribe(self, channel, callback=None, priority=None):
|
||||
"""Add the given callback at the given channel (if not present).
|
||||
|
||||
If callback is None, return a partial suitable for decorating
|
||||
the callback.
|
||||
"""
|
||||
if callback is None:
|
||||
return functools.partial(
|
||||
self.subscribe,
|
||||
channel,
|
||||
priority=priority,
|
||||
)
|
||||
|
||||
ch_listeners = self.listeners.setdefault(channel, set())
|
||||
ch_listeners.add(callback)
|
||||
|
||||
if priority is None:
|
||||
priority = getattr(callback, 'priority', 50)
|
||||
|
@ -190,14 +220,11 @@ class Bus(object):
|
|||
exc = ChannelFailures()
|
||||
output = []
|
||||
|
||||
items = [(self._priorities[(channel, listener)], listener)
|
||||
for listener in self.listeners[channel]]
|
||||
try:
|
||||
items.sort(key=lambda item: item[0])
|
||||
except TypeError:
|
||||
# Python 2.3 had no 'key' arg, but that doesn't matter
|
||||
# since it could sort dissimilar types just fine.
|
||||
items.sort()
|
||||
raw_items = (
|
||||
(self._priorities[(channel, listener)], listener)
|
||||
for listener in self.listeners[channel]
|
||||
)
|
||||
items = sorted(raw_items, key=operator.itemgetter(0))
|
||||
for priority, listener in items:
|
||||
try:
|
||||
output.append(listener(*args, **kwargs))
|
||||
|
@ -209,26 +236,26 @@ class Bus(object):
|
|||
if exc and e.code == 0:
|
||||
e.code = 1
|
||||
raise
|
||||
except:
|
||||
except Exception:
|
||||
exc.handle_exception()
|
||||
if channel == 'log':
|
||||
# Assume any further messages to 'log' will fail.
|
||||
pass
|
||||
else:
|
||||
self.log("Error in %r listener %r" % (channel, listener),
|
||||
self.log('Error in %r listener %r' % (channel, listener),
|
||||
level=40, traceback=True)
|
||||
if exc:
|
||||
raise exc
|
||||
return output
|
||||
|
||||
def _clean_exit(self):
|
||||
"""An atexit handler which asserts the Bus is not running."""
|
||||
"""Assert that the Bus is not running in atexit handler callback."""
|
||||
if self.state != states.EXITING:
|
||||
warnings.warn(
|
||||
"The main thread is exiting, but the Bus is in the %r state; "
|
||||
"shutting it down automatically now. You must either call "
|
||||
"bus.block() after start(), or call bus.exit() before the "
|
||||
"main thread exits." % self.state, RuntimeWarning)
|
||||
'The main thread is exiting, but the Bus is in the %r state; '
|
||||
'shutting it down automatically now. You must either call '
|
||||
'bus.block() after start(), or call bus.exit() before the '
|
||||
'main thread exits.' % self.state, RuntimeWarning)
|
||||
self.exit()
|
||||
|
||||
def start(self):
|
||||
|
@ -243,13 +270,13 @@ class Bus(object):
|
|||
self.log('Bus STARTED')
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
raise
|
||||
except:
|
||||
self.log("Shutting down due to error in start listener:",
|
||||
except Exception:
|
||||
self.log('Shutting down due to error in start listener:',
|
||||
level=40, traceback=True)
|
||||
e_info = sys.exc_info()[1]
|
||||
try:
|
||||
self.exit()
|
||||
except:
|
||||
except Exception:
|
||||
# Any stop/exit errors will be logged inside publish().
|
||||
pass
|
||||
# Re-raise the original error
|
||||
|
@ -258,6 +285,7 @@ class Bus(object):
|
|||
def exit(self):
|
||||
"""Stop all services and prepare to exit the process."""
|
||||
exitstate = self.state
|
||||
EX_SOFTWARE = 70
|
||||
try:
|
||||
self.stop()
|
||||
|
||||
|
@ -267,19 +295,19 @@ class Bus(object):
|
|||
# This isn't strictly necessary, but it's better than seeing
|
||||
# "Waiting for child threads to terminate..." and then nothing.
|
||||
self.log('Bus EXITED')
|
||||
except:
|
||||
except Exception:
|
||||
# This method is often called asynchronously (whether thread,
|
||||
# signal handler, console handler, or atexit handler), so we
|
||||
# can't just let exceptions propagate out unhandled.
|
||||
# Assume it's been logged and just die.
|
||||
os._exit(70) # EX_SOFTWARE
|
||||
os._exit(EX_SOFTWARE)
|
||||
|
||||
if exitstate == states.STARTING:
|
||||
# exit() was called before start() finished, possibly due to
|
||||
# Ctrl-C because a start listener got stuck. In this case,
|
||||
# we could get stuck in a loop where Ctrl-C never exits the
|
||||
# process, so we just call os.exit here.
|
||||
os._exit(70) # EX_SOFTWARE
|
||||
os._exit(EX_SOFTWARE)
|
||||
|
||||
def restart(self):
|
||||
"""Restart the process (may close connections).
|
||||
|
@ -317,11 +345,11 @@ class Bus(object):
|
|||
raise
|
||||
|
||||
# Waiting for ALL child threads to finish is necessary on OS X.
|
||||
# See https://bitbucket.org/cherrypy/cherrypy/issue/581.
|
||||
# See https://github.com/cherrypy/cherrypy/issues/581.
|
||||
# It's also good to let them all shut down before allowing
|
||||
# the main thread to call atexit handlers.
|
||||
# See https://bitbucket.org/cherrypy/cherrypy/issue/751.
|
||||
self.log("Waiting for child threads to terminate...")
|
||||
# See https://github.com/cherrypy/cherrypy/issues/751.
|
||||
self.log('Waiting for child threads to terminate...')
|
||||
for t in threading.enumerate():
|
||||
# Validate the we're not trying to join the MainThread
|
||||
# that will cause a deadlock and the case exist when
|
||||
|
@ -329,18 +357,13 @@ class Bus(object):
|
|||
# that another thread executes cherrypy.engine.exit()
|
||||
if (
|
||||
t != threading.currentThread() and
|
||||
t.isAlive() and
|
||||
not isinstance(t, threading._MainThread)
|
||||
not isinstance(t, threading._MainThread) and
|
||||
# Note that any dummy (external) threads are
|
||||
# always daemonic.
|
||||
not t.daemon
|
||||
):
|
||||
# Note that any dummy (external) threads are always daemonic.
|
||||
if hasattr(threading.Thread, "daemon"):
|
||||
# Python 2.6+
|
||||
d = t.daemon
|
||||
else:
|
||||
d = t.isDaemon()
|
||||
if not d:
|
||||
self.log("Waiting for thread %s." % t.getName())
|
||||
t.join()
|
||||
self.log('Waiting for thread %s.' % t.getName())
|
||||
t.join()
|
||||
|
||||
if self.execv:
|
||||
self._do_execv()
|
||||
|
@ -352,23 +375,9 @@ class Bus(object):
|
|||
else:
|
||||
states = [state]
|
||||
|
||||
def _wait():
|
||||
while self.state not in states:
|
||||
time.sleep(interval)
|
||||
self.publish(channel)
|
||||
|
||||
# From http://psyco.sourceforge.net/psycoguide/bugs.html:
|
||||
# "The compiled machine code does not include the regular polling
|
||||
# done by Python, meaning that a KeyboardInterrupt will not be
|
||||
# detected before execution comes back to the regular Python
|
||||
# interpreter. Your program cannot be interrupted if caught
|
||||
# into an infinite Psyco-compiled loop."
|
||||
try:
|
||||
sys.modules['psyco'].cannotcompile(_wait)
|
||||
except (KeyError, AttributeError):
|
||||
pass
|
||||
|
||||
_wait()
|
||||
while self.state not in states:
|
||||
time.sleep(interval)
|
||||
self.publish(channel)
|
||||
|
||||
def _do_execv(self):
|
||||
"""Re-execute the current process.
|
||||
|
@ -376,14 +385,20 @@ class Bus(object):
|
|||
This must be called from the main thread, because certain platforms
|
||||
(OS X) don't allow execv to be called in a child thread very well.
|
||||
"""
|
||||
args = sys.argv[:]
|
||||
try:
|
||||
args = self._get_true_argv()
|
||||
except NotImplementedError:
|
||||
"""It's probably win32 or GAE"""
|
||||
args = [sys.executable] + self._get_interpreter_argv() + sys.argv
|
||||
|
||||
self.log('Re-spawning %s' % ' '.join(args))
|
||||
|
||||
self._extend_pythonpath(os.environ)
|
||||
|
||||
if sys.platform[:4] == 'java':
|
||||
from _systemrestart import SystemRestart
|
||||
raise SystemRestart
|
||||
else:
|
||||
args.insert(0, sys.executable)
|
||||
if sys.platform == 'win32':
|
||||
args = ['"%s"' % arg for arg in args]
|
||||
|
||||
|
@ -392,6 +407,134 @@ class Bus(object):
|
|||
self._set_cloexec()
|
||||
os.execv(sys.executable, args)
|
||||
|
||||
@staticmethod
|
||||
def _get_interpreter_argv():
|
||||
"""Retrieve current Python interpreter's arguments.
|
||||
|
||||
Returns empty tuple in case of frozen mode, uses built-in arguments
|
||||
reproduction function otherwise.
|
||||
|
||||
Frozen mode is possible for the app has been packaged into a binary
|
||||
executable using py2exe. In this case the interpreter's arguments are
|
||||
already built-in into that executable.
|
||||
|
||||
:seealso: https://github.com/cherrypy/cherrypy/issues/1526
|
||||
Ref: https://pythonhosted.org/PyInstaller/runtime-information.html
|
||||
"""
|
||||
return ([]
|
||||
if getattr(sys, 'frozen', False)
|
||||
else subprocess._args_from_interpreter_flags())
|
||||
|
||||
@staticmethod
|
||||
def _get_true_argv():
|
||||
"""Retrieve all real arguments of the python interpreter.
|
||||
|
||||
...even those not listed in ``sys.argv``
|
||||
|
||||
:seealso: http://stackoverflow.com/a/28338254/595220
|
||||
:seealso: http://stackoverflow.com/a/6683222/595220
|
||||
:seealso: http://stackoverflow.com/a/28414807/595220
|
||||
"""
|
||||
try:
|
||||
char_p = ctypes.c_char_p if six.PY2 else ctypes.c_wchar_p
|
||||
|
||||
argv = ctypes.POINTER(char_p)()
|
||||
argc = ctypes.c_int()
|
||||
|
||||
ctypes.pythonapi.Py_GetArgcArgv(
|
||||
ctypes.byref(argc),
|
||||
ctypes.byref(argv),
|
||||
)
|
||||
|
||||
_argv = argv[:argc.value]
|
||||
|
||||
# The code below is trying to correctly handle special cases.
|
||||
# `-c`'s argument interpreted by Python itself becomes `-c` as
|
||||
# well. Same applies to `-m`. This snippet is trying to survive
|
||||
# at least the case with `-m`
|
||||
# Ref: https://github.com/cherrypy/cherrypy/issues/1545
|
||||
# Ref: python/cpython@418baf9
|
||||
argv_len, is_command, is_module = len(_argv), False, False
|
||||
|
||||
try:
|
||||
m_ind = _argv.index('-m')
|
||||
if m_ind < argv_len - 1 and _argv[m_ind + 1] in ('-c', '-m'):
|
||||
"""
|
||||
In some older Python versions `-m`'s argument may be
|
||||
substituted with `-c`, not `-m`
|
||||
"""
|
||||
is_module = True
|
||||
except (IndexError, ValueError):
|
||||
m_ind = None
|
||||
|
||||
try:
|
||||
c_ind = _argv.index('-c')
|
||||
if c_ind < argv_len - 1 and _argv[c_ind + 1] == '-c':
|
||||
is_command = True
|
||||
except (IndexError, ValueError):
|
||||
c_ind = None
|
||||
|
||||
if is_module:
|
||||
"""It's containing `-m -m` sequence of arguments"""
|
||||
if is_command and c_ind < m_ind:
|
||||
"""There's `-c -c` before `-m`"""
|
||||
raise RuntimeError(
|
||||
"Cannot reconstruct command from '-c'. Ref: "
|
||||
'https://github.com/cherrypy/cherrypy/issues/1545')
|
||||
# Survive module argument here
|
||||
original_module = sys.argv[0]
|
||||
if not os.access(original_module, os.R_OK):
|
||||
"""There's no such module exist"""
|
||||
raise AttributeError(
|
||||
"{} doesn't seem to be a module "
|
||||
'accessible by current user'.format(original_module))
|
||||
del _argv[m_ind:m_ind + 2] # remove `-m -m`
|
||||
# ... and substitute it with the original module path:
|
||||
_argv.insert(m_ind, original_module)
|
||||
elif is_command:
|
||||
"""It's containing just `-c -c` sequence of arguments"""
|
||||
raise RuntimeError(
|
||||
"Cannot reconstruct command from '-c'. "
|
||||
'Ref: https://github.com/cherrypy/cherrypy/issues/1545')
|
||||
except AttributeError:
|
||||
"""It looks Py_GetArgcArgv is completely absent in some environments
|
||||
|
||||
It is known, that there's no Py_GetArgcArgv in MS Windows and
|
||||
``ctypes`` module is completely absent in Google AppEngine
|
||||
|
||||
:seealso: https://github.com/cherrypy/cherrypy/issues/1506
|
||||
:seealso: https://github.com/cherrypy/cherrypy/issues/1512
|
||||
:ref: http://bit.ly/2gK6bXK
|
||||
"""
|
||||
raise NotImplementedError
|
||||
else:
|
||||
return _argv
|
||||
|
||||
@staticmethod
|
||||
def _extend_pythonpath(env):
|
||||
"""Prepend current working dir to PATH environment variable if needed.
|
||||
|
||||
If sys.path[0] is an empty string, the interpreter was likely
|
||||
invoked with -m and the effective path is about to change on
|
||||
re-exec. Add the current directory to $PYTHONPATH to ensure
|
||||
that the new process sees the same path.
|
||||
|
||||
This issue cannot be addressed in the general case because
|
||||
Python cannot reliably reconstruct the
|
||||
original command line (http://bugs.python.org/issue14208).
|
||||
|
||||
(This idea filched from tornado.autoreload)
|
||||
"""
|
||||
path_prefix = '.' + os.pathsep
|
||||
existing_path = env.get('PYTHONPATH', '')
|
||||
needs_patch = (
|
||||
sys.path[0] == '' and
|
||||
not existing_path.startswith(path_prefix)
|
||||
)
|
||||
|
||||
if needs_patch:
|
||||
env['PYTHONPATH'] = path_prefix + existing_path
|
||||
|
||||
def _set_cloexec(self):
|
||||
"""Set the CLOEXEC flag on all open files (except stdin/out/err).
|
||||
|
||||
|
@ -437,10 +580,11 @@ class Bus(object):
|
|||
|
||||
return t
|
||||
|
||||
def log(self, msg="", level=20, traceback=False):
|
||||
def log(self, msg='', level=20, traceback=False):
|
||||
"""Log the given message. Append the last traceback if requested."""
|
||||
if traceback:
|
||||
msg += "\n" + "".join(_traceback.format_exception(*sys.exc_info()))
|
||||
msg += '\n' + ''.join(_traceback.format_exception(*sys.exc_info()))
|
||||
self.publish('log', msg, level)
|
||||
|
||||
|
||||
bus = Bus()
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue