mirror of
https://github.com/Tautulli/Tautulli.git
synced 2025-08-20 05:13: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
|
@ -10,5 +10,8 @@ use with the bus. Some use tool-specific channels; see the documentation
|
|||
for each class.
|
||||
"""
|
||||
|
||||
from cherrypy.process.wspbus import bus
|
||||
from cherrypy.process import plugins, servers
|
||||
from .wspbus import bus
|
||||
from . import plugins, servers
|
||||
|
||||
|
||||
__all__ = ('bus', 'plugins', 'servers')
|
||||
|
|
|
@ -7,8 +7,10 @@ import sys
|
|||
import time
|
||||
import threading
|
||||
|
||||
from cherrypy._cpcompat import basestring, get_daemon, get_thread_ident
|
||||
from cherrypy._cpcompat import ntob, Timer, SetDaemonProperty
|
||||
from six.moves import _thread
|
||||
|
||||
from cherrypy._cpcompat import text_or_bytes
|
||||
from cherrypy._cpcompat import ntob, Timer
|
||||
|
||||
# _module__file__base is used by Autoreload to make
|
||||
# absolute any filenames retrieved from sys.modules which are not
|
||||
|
@ -104,15 +106,14 @@ class SignalHandler(object):
|
|||
if sys.platform[:4] == 'java':
|
||||
del self.handlers['SIGUSR1']
|
||||
self.handlers['SIGUSR2'] = self.bus.graceful
|
||||
self.bus.log("SIGUSR1 cannot be set on the JVM platform. "
|
||||
"Using SIGUSR2 instead.")
|
||||
self.bus.log('SIGUSR1 cannot be set on the JVM platform. '
|
||||
'Using SIGUSR2 instead.')
|
||||
self.handlers['SIGINT'] = self._jython_SIGINT_handler
|
||||
|
||||
self._previous_handlers = {}
|
||||
# used to determine is the process is a daemon in `self._is_daemonized`
|
||||
self._original_pid = os.getpid()
|
||||
|
||||
|
||||
def _jython_SIGINT_handler(self, signum=None, frame=None):
|
||||
# See http://bugs.jython.org/issue1313
|
||||
self.bus.log('Keyboard Interrupt: shutting down bus')
|
||||
|
@ -131,12 +132,10 @@ class SignalHandler(object):
|
|||
is executing inside other process like in a CI tool
|
||||
(Buildbot, Jenkins).
|
||||
"""
|
||||
if (self._original_pid != os.getpid() and
|
||||
not os.isatty(sys.stdin.fileno())):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
return (
|
||||
self._original_pid != os.getpid() and
|
||||
not os.isatty(sys.stdin.fileno())
|
||||
)
|
||||
|
||||
def subscribe(self):
|
||||
"""Subscribe self.handlers to signals."""
|
||||
|
@ -152,19 +151,19 @@ class SignalHandler(object):
|
|||
signame = self.signals[signum]
|
||||
|
||||
if handler is None:
|
||||
self.bus.log("Restoring %s handler to SIG_DFL." % signame)
|
||||
self.bus.log('Restoring %s handler to SIG_DFL.' % signame)
|
||||
handler = _signal.SIG_DFL
|
||||
else:
|
||||
self.bus.log("Restoring %s handler %r." % (signame, handler))
|
||||
self.bus.log('Restoring %s handler %r.' % (signame, handler))
|
||||
|
||||
try:
|
||||
our_handler = _signal.signal(signum, handler)
|
||||
if our_handler is None:
|
||||
self.bus.log("Restored old %s handler %r, but our "
|
||||
"handler was not registered." %
|
||||
self.bus.log('Restored old %s handler %r, but our '
|
||||
'handler was not registered.' %
|
||||
(signame, handler), level=30)
|
||||
except ValueError:
|
||||
self.bus.log("Unable to restore %s handler %r." %
|
||||
self.bus.log('Unable to restore %s handler %r.' %
|
||||
(signame, handler), level=40, traceback=True)
|
||||
|
||||
def set_handler(self, signal, listener=None):
|
||||
|
@ -176,39 +175,39 @@ class SignalHandler(object):
|
|||
If the given signal name or number is not available on the current
|
||||
platform, ValueError is raised.
|
||||
"""
|
||||
if isinstance(signal, basestring):
|
||||
if isinstance(signal, text_or_bytes):
|
||||
signum = getattr(_signal, signal, None)
|
||||
if signum is None:
|
||||
raise ValueError("No such signal: %r" % signal)
|
||||
raise ValueError('No such signal: %r' % signal)
|
||||
signame = signal
|
||||
else:
|
||||
try:
|
||||
signame = self.signals[signal]
|
||||
except KeyError:
|
||||
raise ValueError("No such signal: %r" % signal)
|
||||
raise ValueError('No such signal: %r' % signal)
|
||||
signum = signal
|
||||
|
||||
prev = _signal.signal(signum, self._handle_signal)
|
||||
self._previous_handlers[signum] = prev
|
||||
|
||||
if listener is not None:
|
||||
self.bus.log("Listening for %s." % signame)
|
||||
self.bus.log('Listening for %s.' % signame)
|
||||
self.bus.subscribe(signame, listener)
|
||||
|
||||
def _handle_signal(self, signum=None, frame=None):
|
||||
"""Python signal handler (self.set_handler subscribes it for you)."""
|
||||
signame = self.signals[signum]
|
||||
self.bus.log("Caught signal %s." % signame)
|
||||
self.bus.log('Caught signal %s.' % signame)
|
||||
self.bus.publish(signame)
|
||||
|
||||
def handle_SIGHUP(self):
|
||||
"""Restart if daemonized, else exit."""
|
||||
if self._is_daemonized():
|
||||
self.bus.log("SIGHUP caught while daemonized. Restarting.")
|
||||
self.bus.log('SIGHUP caught while daemonized. Restarting.')
|
||||
self.bus.restart()
|
||||
else:
|
||||
# not daemonized (may be foreground or background)
|
||||
self.bus.log("SIGHUP caught but not daemonized. Exiting.")
|
||||
self.bus.log('SIGHUP caught but not daemonized. Exiting.')
|
||||
self.bus.exit()
|
||||
|
||||
|
||||
|
@ -223,7 +222,8 @@ class DropPrivileges(SimplePlugin):
|
|||
|
||||
"""Drop privileges. uid/gid arguments not available on Windows.
|
||||
|
||||
Special thanks to `Gavin Baker <http://antonym.org/2005/12/dropping-privileges-in-python.html>`_
|
||||
Special thanks to `Gavin Baker
|
||||
<http://antonym.org/2005/12/dropping-privileges-in-python.html>`_
|
||||
"""
|
||||
|
||||
def __init__(self, bus, umask=None, uid=None, gid=None):
|
||||
|
@ -233,57 +233,57 @@ class DropPrivileges(SimplePlugin):
|
|||
self.gid = gid
|
||||
self.umask = umask
|
||||
|
||||
def _get_uid(self):
|
||||
@property
|
||||
def uid(self):
|
||||
"""The uid under which to run. Availability: Unix."""
|
||||
return self._uid
|
||||
|
||||
def _set_uid(self, val):
|
||||
@uid.setter
|
||||
def uid(self, val):
|
||||
if val is not None:
|
||||
if pwd is None:
|
||||
self.bus.log("pwd module not available; ignoring uid.",
|
||||
self.bus.log('pwd module not available; ignoring uid.',
|
||||
level=30)
|
||||
val = None
|
||||
elif isinstance(val, basestring):
|
||||
elif isinstance(val, text_or_bytes):
|
||||
val = pwd.getpwnam(val)[2]
|
||||
self._uid = val
|
||||
uid = property(_get_uid, _set_uid,
|
||||
doc="The uid under which to run. Availability: Unix.")
|
||||
|
||||
def _get_gid(self):
|
||||
@property
|
||||
def gid(self):
|
||||
"""The gid under which to run. Availability: Unix."""
|
||||
return self._gid
|
||||
|
||||
def _set_gid(self, val):
|
||||
@gid.setter
|
||||
def gid(self, val):
|
||||
if val is not None:
|
||||
if grp is None:
|
||||
self.bus.log("grp module not available; ignoring gid.",
|
||||
self.bus.log('grp module not available; ignoring gid.',
|
||||
level=30)
|
||||
val = None
|
||||
elif isinstance(val, basestring):
|
||||
elif isinstance(val, text_or_bytes):
|
||||
val = grp.getgrnam(val)[2]
|
||||
self._gid = val
|
||||
gid = property(_get_gid, _set_gid,
|
||||
doc="The gid under which to run. Availability: Unix.")
|
||||
|
||||
def _get_umask(self):
|
||||
@property
|
||||
def umask(self):
|
||||
"""The default permission mode for newly created files and directories.
|
||||
|
||||
Usually expressed in octal format, for example, ``0644``.
|
||||
Availability: Unix, Windows.
|
||||
"""
|
||||
return self._umask
|
||||
|
||||
def _set_umask(self, val):
|
||||
@umask.setter
|
||||
def umask(self, val):
|
||||
if val is not None:
|
||||
try:
|
||||
os.umask
|
||||
except AttributeError:
|
||||
self.bus.log("umask function not available; ignoring umask.",
|
||||
self.bus.log('umask function not available; ignoring umask.',
|
||||
level=30)
|
||||
val = None
|
||||
self._umask = val
|
||||
umask = property(
|
||||
_get_umask,
|
||||
_set_umask,
|
||||
doc="""The default permission mode for newly created files and
|
||||
directories.
|
||||
|
||||
Usually expressed in octal format, for example, ``0644``.
|
||||
Availability: Unix, Windows.
|
||||
""")
|
||||
|
||||
def start(self):
|
||||
# uid/gid
|
||||
|
@ -347,7 +347,7 @@ class Daemonizer(SimplePlugin):
|
|||
process still return proper exit codes. Therefore, if you use this
|
||||
plugin to daemonize, don't use the return code as an accurate indicator
|
||||
of whether the process fully started. In fact, that return code only
|
||||
indicates if the process succesfully finished the first fork.
|
||||
indicates if the process successfully finished the first fork.
|
||||
"""
|
||||
|
||||
def __init__(self, bus, stdin='/dev/null', stdout='/dev/null',
|
||||
|
@ -372,6 +372,15 @@ class Daemonizer(SimplePlugin):
|
|||
'Daemonizing now may cause strange failures.' %
|
||||
threading.enumerate(), level=30)
|
||||
|
||||
self.daemonize(self.stdin, self.stdout, self.stderr, self.bus.log)
|
||||
|
||||
self.finalized = True
|
||||
start.priority = 65
|
||||
|
||||
@staticmethod
|
||||
def daemonize(
|
||||
stdin='/dev/null', stdout='/dev/null', stderr='/dev/null',
|
||||
logger=lambda msg: None):
|
||||
# See http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC16
|
||||
# (or http://www.faqs.org/faqs/unix-faq/programmer/faq/ section 1.7)
|
||||
# and http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/66012
|
||||
|
@ -380,41 +389,29 @@ class Daemonizer(SimplePlugin):
|
|||
sys.stdout.flush()
|
||||
sys.stderr.flush()
|
||||
|
||||
# Do first fork.
|
||||
try:
|
||||
pid = os.fork()
|
||||
if pid == 0:
|
||||
# This is the child process. Continue.
|
||||
pass
|
||||
else:
|
||||
# This is the first parent. Exit, now that we've forked.
|
||||
self.bus.log('Forking once.')
|
||||
os._exit(0)
|
||||
except OSError:
|
||||
# Python raises OSError rather than returning negative numbers.
|
||||
exc = sys.exc_info()[1]
|
||||
sys.exit("%s: fork #1 failed: (%d) %s\n"
|
||||
% (sys.argv[0], exc.errno, exc.strerror))
|
||||
error_tmpl = (
|
||||
'{sys.argv[0]}: fork #{n} failed: ({exc.errno}) {exc.strerror}\n'
|
||||
)
|
||||
|
||||
os.setsid()
|
||||
for fork in range(2):
|
||||
msg = ['Forking once.', 'Forking twice.'][fork]
|
||||
try:
|
||||
pid = os.fork()
|
||||
if pid > 0:
|
||||
# This is the parent; exit.
|
||||
logger(msg)
|
||||
os._exit(0)
|
||||
except OSError as exc:
|
||||
# Python raises OSError rather than returning negative numbers.
|
||||
sys.exit(error_tmpl.format(sys=sys, exc=exc, n=fork + 1))
|
||||
if fork == 0:
|
||||
os.setsid()
|
||||
|
||||
# Do second fork
|
||||
try:
|
||||
pid = os.fork()
|
||||
if pid > 0:
|
||||
self.bus.log('Forking twice.')
|
||||
os._exit(0) # Exit second parent
|
||||
except OSError:
|
||||
exc = sys.exc_info()[1]
|
||||
sys.exit("%s: fork #2 failed: (%d) %s\n"
|
||||
% (sys.argv[0], exc.errno, exc.strerror))
|
||||
|
||||
os.chdir("/")
|
||||
os.umask(0)
|
||||
|
||||
si = open(self.stdin, "r")
|
||||
so = open(self.stdout, "a+")
|
||||
se = open(self.stderr, "a+")
|
||||
si = open(stdin, 'r')
|
||||
so = open(stdout, 'a+')
|
||||
se = open(stderr, 'a+')
|
||||
|
||||
# os.dup2(fd, fd2) will close fd2 if necessary,
|
||||
# so we don't explicitly close stdin/out/err.
|
||||
|
@ -423,9 +420,7 @@ class Daemonizer(SimplePlugin):
|
|||
os.dup2(so.fileno(), sys.stdout.fileno())
|
||||
os.dup2(se.fileno(), sys.stderr.fileno())
|
||||
|
||||
self.bus.log('Daemonized to PID: %s' % os.getpid())
|
||||
self.finalized = True
|
||||
start.priority = 65
|
||||
logger('Daemonized to PID: %s' % os.getpid())
|
||||
|
||||
|
||||
class PIDFile(SimplePlugin):
|
||||
|
@ -442,7 +437,7 @@ class PIDFile(SimplePlugin):
|
|||
if self.finalized:
|
||||
self.bus.log('PID %r already written to %r.' % (pid, self.pidfile))
|
||||
else:
|
||||
open(self.pidfile, "wb").write(ntob("%s\n" % pid, 'utf8'))
|
||||
open(self.pidfile, 'wb').write(ntob('%s\n' % pid, 'utf8'))
|
||||
self.bus.log('PID %r written to %r.' % (pid, self.pidfile))
|
||||
self.finalized = True
|
||||
start.priority = 70
|
||||
|
@ -453,7 +448,7 @@ class PIDFile(SimplePlugin):
|
|||
self.bus.log('PID file removed: %r.' % self.pidfile)
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
raise
|
||||
except:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
|
@ -481,13 +476,13 @@ class PerpetualTimer(Timer):
|
|||
except Exception:
|
||||
if self.bus:
|
||||
self.bus.log(
|
||||
"Error in perpetual timer thread function %r." %
|
||||
'Error in perpetual timer thread function %r.' %
|
||||
self.function, level=40, traceback=True)
|
||||
# Quit on first error to avoid massive logs.
|
||||
raise
|
||||
|
||||
|
||||
class BackgroundTask(SetDaemonProperty, threading.Thread):
|
||||
class BackgroundTask(threading.Thread):
|
||||
|
||||
"""A subclass of threading.Thread whose run() method repeats.
|
||||
|
||||
|
@ -499,7 +494,7 @@ class BackgroundTask(SetDaemonProperty, threading.Thread):
|
|||
"""
|
||||
|
||||
def __init__(self, interval, function, args=[], kwargs={}, bus=None):
|
||||
threading.Thread.__init__(self)
|
||||
super(BackgroundTask, self).__init__()
|
||||
self.interval = interval
|
||||
self.function = function
|
||||
self.args = args
|
||||
|
@ -523,7 +518,7 @@ class BackgroundTask(SetDaemonProperty, threading.Thread):
|
|||
self.function(*self.args, **self.kwargs)
|
||||
except Exception:
|
||||
if self.bus:
|
||||
self.bus.log("Error in background task thread function %r."
|
||||
self.bus.log('Error in background task thread function %r.'
|
||||
% self.function, level=40, traceback=True)
|
||||
# Quit on first error to avoid massive logs.
|
||||
raise
|
||||
|
@ -560,24 +555,24 @@ class Monitor(SimplePlugin):
|
|||
bus=self.bus)
|
||||
self.thread.setName(threadname)
|
||||
self.thread.start()
|
||||
self.bus.log("Started monitor thread %r." % threadname)
|
||||
self.bus.log('Started monitor thread %r.' % threadname)
|
||||
else:
|
||||
self.bus.log("Monitor thread %r already started." % threadname)
|
||||
self.bus.log('Monitor thread %r already started.' % threadname)
|
||||
start.priority = 70
|
||||
|
||||
def stop(self):
|
||||
"""Stop our callback's background task thread."""
|
||||
if self.thread is None:
|
||||
self.bus.log("No thread running for %s." %
|
||||
self.bus.log('No thread running for %s.' %
|
||||
self.name or self.__class__.__name__)
|
||||
else:
|
||||
if self.thread is not threading.currentThread():
|
||||
name = self.thread.getName()
|
||||
self.thread.cancel()
|
||||
if not get_daemon(self.thread):
|
||||
self.bus.log("Joining %r" % name)
|
||||
if not self.thread.daemon:
|
||||
self.bus.log('Joining %r' % name)
|
||||
self.thread.join()
|
||||
self.bus.log("Stopped thread %r." % name)
|
||||
self.bus.log('Stopped thread %r.' % name)
|
||||
self.thread = None
|
||||
|
||||
def graceful(self):
|
||||
|
@ -632,23 +627,40 @@ class Autoreloader(Monitor):
|
|||
|
||||
def sysfiles(self):
|
||||
"""Return a Set of sys.modules filenames to monitor."""
|
||||
files = set()
|
||||
for k, m in list(sys.modules.items()):
|
||||
if re.match(self.match, k):
|
||||
if (
|
||||
hasattr(m, '__loader__') and
|
||||
hasattr(m.__loader__, 'archive')
|
||||
):
|
||||
f = m.__loader__.archive
|
||||
else:
|
||||
f = getattr(m, '__file__', None)
|
||||
if f is not None and not os.path.isabs(f):
|
||||
# ensure absolute paths so a os.chdir() in the app
|
||||
# doesn't break me
|
||||
f = os.path.normpath(
|
||||
os.path.join(_module__file__base, f))
|
||||
files.add(f)
|
||||
return files
|
||||
search_mod_names = filter(re.compile(self.match).match, sys.modules)
|
||||
mods = map(sys.modules.get, search_mod_names)
|
||||
return set(filter(None, map(self._file_for_module, mods)))
|
||||
|
||||
@classmethod
|
||||
def _file_for_module(cls, module):
|
||||
"""Return the relevant file for the module."""
|
||||
return (
|
||||
cls._archive_for_zip_module(module)
|
||||
or cls._file_for_file_module(module)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _archive_for_zip_module(module):
|
||||
"""Return the archive filename for the module if relevant."""
|
||||
try:
|
||||
return module.__loader__.archive
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def _file_for_file_module(cls, module):
|
||||
"""Return the file for the module."""
|
||||
try:
|
||||
return module.__file__ and cls._make_absolute(module.__file__)
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def _make_absolute(filename):
|
||||
"""Ensure filename is absolute to avoid effect of os.chdir."""
|
||||
return filename if os.path.isabs(filename) else (
|
||||
os.path.normpath(os.path.join(_module__file__base, filename))
|
||||
)
|
||||
|
||||
def run(self):
|
||||
"""Reload the process if registered files have been modified."""
|
||||
|
@ -674,10 +686,10 @@ class Autoreloader(Monitor):
|
|||
else:
|
||||
if mtime is None or mtime > oldtime:
|
||||
# The file has been deleted or modified.
|
||||
self.bus.log("Restarting because %s changed." %
|
||||
self.bus.log('Restarting because %s changed.' %
|
||||
filename)
|
||||
self.thread.cancel()
|
||||
self.bus.log("Stopped thread %r." %
|
||||
self.bus.log('Stopped thread %r.' %
|
||||
self.thread.getName())
|
||||
self.bus.restart()
|
||||
return
|
||||
|
@ -717,7 +729,7 @@ class ThreadManager(SimplePlugin):
|
|||
If the current thread has already been seen, any 'start_thread'
|
||||
listeners will not be run again.
|
||||
"""
|
||||
thread_ident = get_thread_ident()
|
||||
thread_ident = _thread.get_ident()
|
||||
if thread_ident not in self.threads:
|
||||
# We can't just use get_ident as the thread ID
|
||||
# because some platforms reuse thread ID's.
|
||||
|
@ -727,7 +739,7 @@ class ThreadManager(SimplePlugin):
|
|||
|
||||
def release_thread(self):
|
||||
"""Release the current thread and run 'stop_thread' listeners."""
|
||||
thread_ident = get_thread_ident()
|
||||
thread_ident = _thread.get_ident()
|
||||
i = self.threads.pop(thread_ident, None)
|
||||
if i is not None:
|
||||
self.bus.publish('stop_thread', i)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
"""
|
||||
r"""
|
||||
Starting in CherryPy 3.1, cherrypy.server is implemented as an
|
||||
:ref:`Engine Plugin<plugins>`. It's an instance of
|
||||
:class:`cherrypy._cpserver.Server`, which is a subclass of
|
||||
|
@ -12,10 +12,14 @@ If you need to start more than one HTTP server (to serve on multiple ports, or
|
|||
protocols, etc.), you can manually register each one and then start them all
|
||||
with engine.start::
|
||||
|
||||
s1 = ServerAdapter(cherrypy.engine, MyWSGIServer(host='0.0.0.0', port=80))
|
||||
s2 = ServerAdapter(cherrypy.engine,
|
||||
another.HTTPServer(host='127.0.0.1',
|
||||
SSL=True))
|
||||
s1 = ServerAdapter(
|
||||
cherrypy.engine,
|
||||
MyWSGIServer(host='0.0.0.0', port=80)
|
||||
)
|
||||
s2 = ServerAdapter(
|
||||
cherrypy.engine,
|
||||
another.HTTPServer(host='127.0.0.1', SSL=True)
|
||||
)
|
||||
s1.subscribe()
|
||||
s2.subscribe()
|
||||
cherrypy.engine.start()
|
||||
|
@ -58,10 +62,10 @@ hello.py::
|
|||
import cherrypy
|
||||
|
||||
class HelloWorld:
|
||||
\"""Sample request handler class.\"""
|
||||
'''Sample request handler class.'''
|
||||
@cherrypy.expose
|
||||
def index(self):
|
||||
return "Hello world!"
|
||||
index.exposed = True
|
||||
|
||||
cherrypy.tree.mount(HelloWorld())
|
||||
# CherryPy autoreload must be disabled for the flup server to work
|
||||
|
@ -113,9 +117,18 @@ Please see `Lighttpd FastCGI Docs
|
|||
an explanation of the possible configuration options.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import warnings
|
||||
import contextlib
|
||||
|
||||
import portend
|
||||
|
||||
|
||||
class Timeouts:
|
||||
occupied = 5
|
||||
free = 1
|
||||
|
||||
|
||||
class ServerAdapter(object):
|
||||
|
@ -150,49 +163,56 @@ class ServerAdapter(object):
|
|||
|
||||
def start(self):
|
||||
"""Start the HTTP server."""
|
||||
if self.bind_addr is None:
|
||||
on_what = "unknown interface (dynamic?)"
|
||||
elif isinstance(self.bind_addr, tuple):
|
||||
on_what = self._get_base()
|
||||
else:
|
||||
on_what = "socket file: %s" % self.bind_addr
|
||||
|
||||
if self.running:
|
||||
self.bus.log("Already serving on %s" % on_what)
|
||||
self.bus.log('Already serving on %s' % self.description)
|
||||
return
|
||||
|
||||
self.interrupt = None
|
||||
if not self.httpserver:
|
||||
raise ValueError("No HTTP server has been created.")
|
||||
raise ValueError('No HTTP server has been created.')
|
||||
|
||||
# Start the httpserver in a new thread.
|
||||
if isinstance(self.bind_addr, tuple):
|
||||
wait_for_free_port(*self.bind_addr)
|
||||
if not os.environ.get('LISTEN_PID', None):
|
||||
# Start the httpserver in a new thread.
|
||||
if isinstance(self.bind_addr, tuple):
|
||||
portend.free(*self.bind_addr, timeout=Timeouts.free)
|
||||
|
||||
import threading
|
||||
t = threading.Thread(target=self._start_http_thread)
|
||||
t.setName("HTTPServer " + t.getName())
|
||||
t.setName('HTTPServer ' + t.getName())
|
||||
t.start()
|
||||
|
||||
self.wait()
|
||||
self.running = True
|
||||
self.bus.log("Serving on %s" % on_what)
|
||||
self.bus.log('Serving on %s' % self.description)
|
||||
start.priority = 75
|
||||
|
||||
@property
|
||||
def description(self):
|
||||
"""
|
||||
A description about where this server is bound.
|
||||
"""
|
||||
if self.bind_addr is None:
|
||||
on_what = 'unknown interface (dynamic?)'
|
||||
elif isinstance(self.bind_addr, tuple):
|
||||
on_what = self._get_base()
|
||||
else:
|
||||
on_what = 'socket file: %s' % self.bind_addr
|
||||
return on_what
|
||||
|
||||
def _get_base(self):
|
||||
if not self.httpserver:
|
||||
return ''
|
||||
host, port = self.bind_addr
|
||||
host, port = self.bound_addr
|
||||
if getattr(self.httpserver, 'ssl_adapter', None):
|
||||
scheme = "https"
|
||||
scheme = 'https'
|
||||
if port != 443:
|
||||
host += ":%s" % port
|
||||
host += ':%s' % port
|
||||
else:
|
||||
scheme = "http"
|
||||
scheme = 'http'
|
||||
if port != 80:
|
||||
host += ":%s" % port
|
||||
host += ':%s' % port
|
||||
|
||||
return "%s://%s" % (scheme, host)
|
||||
return '%s://%s' % (scheme, host)
|
||||
|
||||
def _start_http_thread(self):
|
||||
"""HTTP servers MUST be running in new threads, so that the
|
||||
|
@ -204,32 +224,52 @@ class ServerAdapter(object):
|
|||
try:
|
||||
self.httpserver.start()
|
||||
except KeyboardInterrupt:
|
||||
self.bus.log("<Ctrl-C> hit: shutting down HTTP server")
|
||||
self.bus.log('<Ctrl-C> hit: shutting down HTTP server')
|
||||
self.interrupt = sys.exc_info()[1]
|
||||
self.bus.exit()
|
||||
except SystemExit:
|
||||
self.bus.log("SystemExit raised: shutting down HTTP server")
|
||||
self.bus.log('SystemExit raised: shutting down HTTP server')
|
||||
self.interrupt = sys.exc_info()[1]
|
||||
self.bus.exit()
|
||||
raise
|
||||
except:
|
||||
except Exception:
|
||||
self.interrupt = sys.exc_info()[1]
|
||||
self.bus.log("Error in HTTP server: shutting down",
|
||||
self.bus.log('Error in HTTP server: shutting down',
|
||||
traceback=True, level=40)
|
||||
self.bus.exit()
|
||||
raise
|
||||
|
||||
def wait(self):
|
||||
"""Wait until the HTTP server is ready to receive requests."""
|
||||
while not getattr(self.httpserver, "ready", False):
|
||||
while not getattr(self.httpserver, 'ready', False):
|
||||
if self.interrupt:
|
||||
raise self.interrupt
|
||||
time.sleep(.1)
|
||||
|
||||
# Wait for port to be occupied
|
||||
if isinstance(self.bind_addr, tuple):
|
||||
host, port = self.bind_addr
|
||||
wait_for_occupied_port(host, port)
|
||||
# bypass check when LISTEN_PID is set
|
||||
if os.environ.get('LISTEN_PID', None):
|
||||
return
|
||||
|
||||
# bypass check when running via socket-activation
|
||||
# (for socket-activation the port will be managed by systemd)
|
||||
if not isinstance(self.bind_addr, tuple):
|
||||
return
|
||||
|
||||
# wait for port to be occupied
|
||||
with _safe_wait(*self.bound_addr):
|
||||
portend.occupied(*self.bound_addr, timeout=Timeouts.occupied)
|
||||
|
||||
@property
|
||||
def bound_addr(self):
|
||||
"""
|
||||
The bind address, or if it's an ephemeral port and the
|
||||
socket has been bound, return the actual port bound.
|
||||
"""
|
||||
host, port = self.bind_addr
|
||||
if port == 0 and self.httpserver.socket:
|
||||
# Bound to ephemeral port. Get the actual port allocated.
|
||||
port = self.httpserver.socket.getsockname()[1]
|
||||
return host, port
|
||||
|
||||
def stop(self):
|
||||
"""Stop the HTTP server."""
|
||||
|
@ -238,11 +278,11 @@ class ServerAdapter(object):
|
|||
self.httpserver.stop()
|
||||
# Wait for the socket to be truly freed.
|
||||
if isinstance(self.bind_addr, tuple):
|
||||
wait_for_free_port(*self.bind_addr)
|
||||
portend.free(*self.bound_addr, timeout=Timeouts.free)
|
||||
self.running = False
|
||||
self.bus.log("HTTP Server %s shut down" % self.httpserver)
|
||||
self.bus.log('HTTP Server %s shut down' % self.httpserver)
|
||||
else:
|
||||
self.bus.log("HTTP Server %s already shut down" % self.httpserver)
|
||||
self.bus.log('HTTP Server %s already shut down' % self.httpserver)
|
||||
stop.priority = 25
|
||||
|
||||
def restart(self):
|
||||
|
@ -359,107 +399,18 @@ class FlupSCGIServer(object):
|
|||
self.scgiserver._threadPool.maxSpare = 0
|
||||
|
||||
|
||||
def client_host(server_host):
|
||||
"""Return the host on which a client can connect to the given listener."""
|
||||
if server_host == '0.0.0.0':
|
||||
# 0.0.0.0 is INADDR_ANY, which should answer on localhost.
|
||||
return '127.0.0.1'
|
||||
if server_host in ('::', '::0', '::0.0.0.0'):
|
||||
# :: is IN6ADDR_ANY, which should answer on localhost.
|
||||
# ::0 and ::0.0.0.0 are non-canonical but common
|
||||
# ways to write IN6ADDR_ANY.
|
||||
return '::1'
|
||||
return server_host
|
||||
|
||||
|
||||
def check_port(host, port, timeout=1.0):
|
||||
"""Raise an error if the given port is not free on the given host."""
|
||||
if not host:
|
||||
raise ValueError("Host values of '' or None are not allowed.")
|
||||
host = client_host(host)
|
||||
port = int(port)
|
||||
|
||||
import socket
|
||||
|
||||
# AF_INET or AF_INET6 socket
|
||||
# Get the correct address family for our host (allows IPv6 addresses)
|
||||
@contextlib.contextmanager
|
||||
def _safe_wait(host, port):
|
||||
"""
|
||||
On systems where a loopback interface is not available and the
|
||||
server is bound to all interfaces, it's difficult to determine
|
||||
whether the server is in fact occupying the port. In this case,
|
||||
just issue a warning and move on. See issue #1100.
|
||||
"""
|
||||
try:
|
||||
info = socket.getaddrinfo(host, port, socket.AF_UNSPEC,
|
||||
socket.SOCK_STREAM)
|
||||
except socket.gaierror:
|
||||
if ':' in host:
|
||||
info = [(
|
||||
socket.AF_INET6, socket.SOCK_STREAM, 0, "", (host, port, 0, 0)
|
||||
)]
|
||||
else:
|
||||
info = [(socket.AF_INET, socket.SOCK_STREAM, 0, "", (host, port))]
|
||||
|
||||
for res in info:
|
||||
af, socktype, proto, canonname, sa = res
|
||||
s = None
|
||||
try:
|
||||
s = socket.socket(af, socktype, proto)
|
||||
# See http://groups.google.com/group/cherrypy-users/
|
||||
# browse_frm/thread/bbfe5eb39c904fe0
|
||||
s.settimeout(timeout)
|
||||
s.connect((host, port))
|
||||
s.close()
|
||||
except socket.error:
|
||||
if s:
|
||||
s.close()
|
||||
else:
|
||||
raise IOError("Port %s is in use on %s; perhaps the previous "
|
||||
"httpserver did not shut down properly." %
|
||||
(repr(port), repr(host)))
|
||||
|
||||
|
||||
# Feel free to increase these defaults on slow systems:
|
||||
free_port_timeout = 0.1
|
||||
occupied_port_timeout = 1.0
|
||||
|
||||
|
||||
def wait_for_free_port(host, port, timeout=None):
|
||||
"""Wait for the specified port to become free (drop requests)."""
|
||||
if not host:
|
||||
raise ValueError("Host values of '' or None are not allowed.")
|
||||
if timeout is None:
|
||||
timeout = free_port_timeout
|
||||
|
||||
for trial in range(50):
|
||||
try:
|
||||
# we are expecting a free port, so reduce the timeout
|
||||
check_port(host, port, timeout=timeout)
|
||||
except IOError:
|
||||
# Give the old server thread time to free the port.
|
||||
time.sleep(timeout)
|
||||
else:
|
||||
return
|
||||
|
||||
raise IOError("Port %r not free on %r" % (port, host))
|
||||
|
||||
|
||||
def wait_for_occupied_port(host, port, timeout=None):
|
||||
"""Wait for the specified port to become active (receive requests)."""
|
||||
if not host:
|
||||
raise ValueError("Host values of '' or None are not allowed.")
|
||||
if timeout is None:
|
||||
timeout = occupied_port_timeout
|
||||
|
||||
for trial in range(50):
|
||||
try:
|
||||
check_port(host, port, timeout=timeout)
|
||||
except IOError:
|
||||
# port is occupied
|
||||
return
|
||||
else:
|
||||
time.sleep(timeout)
|
||||
|
||||
if host == client_host(host):
|
||||
raise IOError("Port %r not bound on %r" % (port, host))
|
||||
|
||||
# On systems where a loopback interface is not available and the
|
||||
# server is bound to all interfaces, it's difficult to determine
|
||||
# whether the server is in fact occupying the port. In this case,
|
||||
# just issue a warning and move on. See issue #1100.
|
||||
msg = "Unable to verify that the server is bound on %r" % port
|
||||
warnings.warn(msg)
|
||||
yield
|
||||
except portend.Timeout:
|
||||
if host == portend.client_host(host):
|
||||
raise
|
||||
msg = 'Unable to verify that the server is bound on %r' % port
|
||||
warnings.warn(msg)
|
||||
|
|
|
@ -85,19 +85,20 @@ class Win32Bus(wspbus.Bus):
|
|||
return self.events[state]
|
||||
except KeyError:
|
||||
event = win32event.CreateEvent(None, 0, 0,
|
||||
"WSPBus %s Event (pid=%r)" %
|
||||
'WSPBus %s Event (pid=%r)' %
|
||||
(state.name, os.getpid()))
|
||||
self.events[state] = event
|
||||
return event
|
||||
|
||||
def _get_state(self):
|
||||
@property
|
||||
def state(self):
|
||||
return self._state
|
||||
|
||||
def _set_state(self, value):
|
||||
@state.setter
|
||||
def state(self, value):
|
||||
self._state = value
|
||||
event = self._get_state_event(value)
|
||||
win32event.PulseEvent(event)
|
||||
state = property(_get_state, _set_state)
|
||||
|
||||
def wait(self, state, interval=0.1, channel=None):
|
||||
"""Wait for the given state(s), KeyboardInterrupt or SystemExit.
|
||||
|
@ -135,7 +136,8 @@ class _ControlCodes(dict):
|
|||
for key, val in self.items():
|
||||
if val is obj:
|
||||
return key
|
||||
raise ValueError("The given object could not be found: %r" % obj)
|
||||
raise ValueError('The given object could not be found: %r' % obj)
|
||||
|
||||
|
||||
control_codes = _ControlCodes({'graceful': 138})
|
||||
|
||||
|
@ -153,14 +155,14 @@ class PyWebService(win32serviceutil.ServiceFramework):
|
|||
|
||||
"""Python Web Service."""
|
||||
|
||||
_svc_name_ = "Python Web Service"
|
||||
_svc_display_name_ = "Python Web Service"
|
||||
_svc_name_ = 'Python Web Service'
|
||||
_svc_display_name_ = 'Python Web Service'
|
||||
_svc_deps_ = None # sequence of service names on which this depends
|
||||
_exe_name_ = "pywebsvc"
|
||||
_exe_name_ = 'pywebsvc'
|
||||
_exe_args_ = None # Default to no arguments
|
||||
|
||||
# Only exists on Windows 2000 or later, ignored on windows NT
|
||||
_svc_description_ = "Python Web Service"
|
||||
_svc_description_ = 'Python Web Service'
|
||||
|
||||
def SvcDoRun(self):
|
||||
from cherrypy import process
|
||||
|
@ -173,6 +175,7 @@ class PyWebService(win32serviceutil.ServiceFramework):
|
|||
process.bus.exit()
|
||||
|
||||
def SvcOther(self, control):
|
||||
from cherrypy import process
|
||||
process.bus.publish(control_codes.key_for(control))
|
||||
|
||||
|
||||
|
|
|
@ -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