Update cherrpy to 17.4.2

This commit is contained in:
JonnyWong16 2019-11-23 18:55:19 -08:00
commit 4d6279a626
131 changed files with 15864 additions and 10389 deletions

View file

@ -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')

View file

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

View file

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

View file

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

View file

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