summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cherrypy/__init__.py58
-rw-r--r--cherrypy/_cpchecker.py9
-rw-r--r--cherrypy/_cpconfig.py35
-rw-r--r--cherrypy/_cpengine.py380
-rw-r--r--cherrypy/_cpmodpy.py16
-rw-r--r--cherrypy/_cprequest.py3
-rw-r--r--cherrypy/_cpserver.py13
-rw-r--r--cherrypy/_cptools.py2
-rw-r--r--cherrypy/_cptree.py37
-rw-r--r--cherrypy/_cpwsgi.py14
-rw-r--r--cherrypy/lib/covercp.py5
-rw-r--r--cherrypy/lib/sessions.py27
-rw-r--r--cherrypy/pywebd/__init__.py49
-rw-r--r--cherrypy/pywebd/base.py163
-rw-r--r--cherrypy/pywebd/plugins.py417
-rw-r--r--cherrypy/pywebd/win32.py71
-rw-r--r--cherrypy/test/benchmark.py4
-rw-r--r--cherrypy/test/helper.py14
-rw-r--r--cherrypy/test/modpy.py2
-rw-r--r--cherrypy/test/test.py12
-rw-r--r--cherrypy/test/test_config.py5
-rw-r--r--cherrypy/test/test_states.py84
-rw-r--r--cherrypy/test/test_states_demo.py14
-rw-r--r--cherrypy/test/test_tools.py3
-rw-r--r--cherrypy/tutorial/tut02_expose_methods.py1
-rw-r--r--cherrypy/tutorial/tut03_get_and_post.py1
-rw-r--r--cherrypy/tutorial/tut04_complex_site.py1
-rw-r--r--cherrypy/tutorial/tut05_derived_objects.py1
-rw-r--r--cherrypy/tutorial/tut06_default_method.py1
-rw-r--r--cherrypy/tutorial/tut07_sessions.py1
-rw-r--r--cherrypy/tutorial/tut08_generators_and_yield.py1
-rw-r--r--cherrypy/tutorial/tut09_files.py1
-rw-r--r--cherrypy/tutorial/tut10_http_errors.py1
-rw-r--r--cherrypy/wsgiserver/__init__.py2
34 files changed, 936 insertions, 512 deletions
diff --git a/cherrypy/__init__.py b/cherrypy/__init__.py
index 00e68821..19edc9d3 100644
--- a/cherrypy/__init__.py
+++ b/cherrypy/__init__.py
@@ -57,7 +57,7 @@ These API's are described in the CherryPy specification:
http://www.cherrypy.org/wiki/CherryPySpec
"""
-__version__ = "3.0.1"
+__version__ = "3.1alpha"
from urlparse import urljoin as _urljoin
@@ -76,7 +76,8 @@ class _AttributeDocstrings(type):
a docstring for that attribute; the attribute docstring will be
popped from the class dict and folded into the class docstring.
- The naming convention for attribute docstrings is: <attrname> + "__doc".
+ The naming convention for attribute docstrings is:
+ <attrname> + "__doc".
For example:
class Thing(object):
@@ -157,15 +158,14 @@ from cherrypy._cperror import HTTPError, HTTPRedirect, InternalRedirect
from cherrypy._cperror import NotFound, CherryPyException, TimeoutError
from cherrypy import _cpdispatch as dispatch
-from cherrypy import _cprequest
-from cherrypy.lib import http as _http
-from cherrypy import _cpengine
-engine = _cpengine.Engine()
from cherrypy import _cptools
tools = _cptools.default_toolbox
Tool = _cptools.Tool
+from cherrypy import _cprequest
+from cherrypy.lib import http as _http
+
from cherrypy import _cptree
tree = _cptree.Tree()
from cherrypy._cptree import Application
@@ -173,13 +173,51 @@ from cherrypy import _cpwsgi as wsgi
from cherrypy import _cpserver
server = _cpserver.Server()
+from cherrypy import pywebd
+engine = pywebd.engine
+
+# Timeout monitor
+class _TimeoutMonitor(pywebd.plugins.Monitor):
+
+ def __init__(self, engine, channel=None):
+ self.servings = []
+ pywebd.plugins.Monitor.__init__(self, engine, self.run, channel)
+
+ def acquire(self):
+ self.servings.append((serving.request, serving.response))
+
+ def release(self):
+ try:
+ self.servings.remove((serving.request, serving.response))
+ except ValueError:
+ pass
+
+ def run(self):
+ """Check timeout on all responses. (Internal)"""
+ for req, resp in self.servings:
+ resp.check_timeout()
+_timeout_monitor = _TimeoutMonitor(engine, "CherryPy Timeout Monitor")
+
+# Add an autoreloader (the 'engine' config namespace may detach/attach it).
+engine.autoreload = pywebd.plugins.Autoreloader(engine)
+pywebd.plugins.Reexec(engine)
+_thread_manager = pywebd.plugins.ThreadManager(engine)
+
+
def quickstart(root, script_name="", config=None):
- """Mount the given root, start the engine and builtin server, then block."""
+ """Mount the given root, start the builtin server (and engine), then block."""
if config:
_global_conf_alias.update(config)
tree.mount(root, script_name, config)
- server.quickstart()
+
+ engine.subscribe('start', server.quickstart)
+
+ s = pywebd.plugins.SignalHandler(engine)
+ s.set_handler('SIGTERM', engine.stop)
+ s.set_handler('SIGHUP', engine.restart)
+
engine.start()
+ engine.block()
try:
@@ -323,7 +361,8 @@ log.screen = True
log.error_file = ''
# Using an access file makes CP about 10% slower. Leave off by default.
log.access_file = ''
-
+engine.log = lambda msg, traceback=False: log.error(msg, 'ENGINE',
+ traceback=traceback)
# Helper functions for CP apps #
@@ -453,3 +492,4 @@ config = _global_conf_alias = _cpconfig.Config()
from cherrypy import _cpchecker
checker = _cpchecker.Checker()
+engine.subscribe('start', checker)
diff --git a/cherrypy/_cpchecker.py b/cherrypy/_cpchecker.py
index e545a2c1..dfeb77e5 100644
--- a/cherrypy/_cpchecker.py
+++ b/cherrypy/_cpchecker.py
@@ -142,13 +142,14 @@ class Checker(object):
extra_config_namespaces = []
- def _known_ns(self, config):
+ def _known_ns(self, app):
ns = ["wsgi"]
- ns.extend(cherrypy.engine.request_class.namespaces.keys())
+ ns.extend(app.toolboxes.keys())
+ ns.extend(app.request_class.namespaces.keys())
ns.extend(cherrypy.config.namespaces.keys())
ns += self.extra_config_namespaces
- for section, conf in config.iteritems():
+ for section, conf in app.config.iteritems():
is_path_section = section.startswith("/")
if is_path_section and isinstance(conf, dict):
for k, v in conf.iteritems():
@@ -168,7 +169,7 @@ class Checker(object):
def check_config_namespaces(self):
"""Process config and warn on each unknown config namespace."""
for sn, app in cherrypy.tree.apps.iteritems():
- self._known_ns(app.config)
+ self._known_ns(app)
# -------------------------- Config Types -------------------------- #
diff --git a/cherrypy/_cpconfig.py b/cherrypy/_cpconfig.py
index d28a92c4..6540adc5 100644
--- a/cherrypy/_cpconfig.py
+++ b/cherrypy/_cpconfig.py
@@ -86,7 +86,7 @@ config, and only when you use cherrypy.config.update.
You can define your own namespaces to be called at the Global, Application,
or Request level, by adding a named handler to cherrypy.config.namespaces,
-app.namespaces, or cherrypy.engine.request_class.namespaces. The name can
+app.namespaces, or app.request_class.namespaces. The name can
be any string, and the handler must be either a callable or a (Python 2.5
style) context manager.
"""
@@ -143,11 +143,10 @@ def merge(base, other):
"""Merge one app config (from a dict, file, or filename) into another.
If the given config is a filename, it will be appended to
- cherrypy.engine.reload_files and monitored for changes.
+ the list of files to monitor for "autoreload" changes.
"""
if isinstance(other, basestring):
- if other not in cherrypy.engine.reload_files:
- cherrypy.engine.reload_files.append(other)
+ cherrypy.engine.publish('Autoreloader', 'add()', other)
# Load other into base
for section, value_map in as_dict(other).iteritems():
@@ -239,7 +238,6 @@ class Config(dict):
namespaces = NamespaceSet(
**{"server": lambda k, v: setattr(cherrypy.server, k, v),
- "engine": lambda k, v: setattr(cherrypy.engine, k, v),
"log": lambda k, v: setattr(cherrypy.log, k, v),
"checker": lambda k, v: setattr(cherrypy.checker, k, v),
})
@@ -256,8 +254,7 @@ class Config(dict):
"""Update self from a dict, file or filename."""
if isinstance(config, basestring):
# Filename
- if config not in cherrypy.engine.reload_files:
- cherrypy.engine.reload_files.append(config)
+ cherrypy.engine.publish('Autoreloader', 'add()', config)
config = _Parser().dict_from_file(config)
elif hasattr(config, 'read'):
# Open file object
@@ -287,6 +284,30 @@ class Config(dict):
self.namespaces({k: v})
+# Backward compatibility handler for the "engine" namespace.
+def _engine_namespace_handler(k, v):
+ engine = cherrypy.engine
+ if k == 'autoreload_on':
+ if v:
+ engine.autoreload.attach()
+ else:
+ engine.autoreload.detach()
+ elif k == 'autoreload_frequency':
+ engine.publish('Autoreloader', 'frequency', v)
+ elif k == 'autoreload_match':
+ engine.publish('Autoreloader', 'match', v)
+ elif k == 'reload_files':
+ engine.publish('Autoreloader', 'files', v)
+ elif k == 'deadlock_poll_freq':
+ engine.publish('CherryPy Timeout Monitor', 'frequency', v)
+ elif k == 'reexec_retry':
+ engine.publish('reexec', 'retry', v)
+ elif k == 'SIGHUP':
+ engine.listeners['SIGHUP'] = set([v])
+ elif k == 'SIGTERM':
+ engine.listeners['SIGTERM'] = set([v])
+Config.namespaces["engine"] = _engine_namespace_handler
+
class _Parser(ConfigParser.ConfigParser):
"""Sub-class of ConfigParser that keeps the case of options and that raises
diff --git a/cherrypy/_cpengine.py b/cherrypy/_cpengine.py
deleted file mode 100644
index 6c58b877..00000000
--- a/cherrypy/_cpengine.py
+++ /dev/null
@@ -1,380 +0,0 @@
-"""Create and manage the CherryPy application engine."""
-
-import cgi
-import os
-import re
-import signal
-import sys
-import threading
-import time
-
-import cherrypy
-from cherrypy import _cprequest
-
-# Use a flag to indicate the state of the application engine.
-STOPPED = 0
-STARTING = None
-STARTED = 1
-
-
-class PerpetualTimer(threading._Timer):
-
- def run(self):
- while True:
- self.finished.wait(self.interval)
- if self.finished.isSet():
- return
- self.function(*self.args, **self.kwargs)
-
-
-class Engine(object):
- """Interface for (HTTP) applications, plus process controls.
-
- Servers and gateways should not instantiate Request objects directly.
- Instead, they should ask an Engine object for a request via the
- Engine.request method.
-
- Blocking is completely optional! The Engine's blocking, signal and
- interrupt handling, privilege dropping, and autoreload features are
- not a good idea when driving CherryPy applications from another
- deployment tool (but an Engine is a great deployment tool itself).
- By calling start(blocking=False), you avoid blocking and interrupt-
- handling issues. By setting Engine.SIGHUP and Engine.SIGTERM to None,
- you can completely disable the signal handling (and therefore disable
- autoreloads triggered by SIGHUP). Set Engine.autoreload_on to False
- to disable autoreload entirely.
- """
-
- # Configurable attributes
- request_class = _cprequest.Request
- response_class = _cprequest.Response
- deadlock_poll_freq = 60
- autoreload_on = True
- autoreload_frequency = 1
- autoreload_match = ".*"
-
- def __init__(self):
- self.state = STOPPED
-
- # Startup/shutdown hooks
- self.on_start_engine_list = []
- self.on_stop_engine_list = []
- self.on_start_thread_list = []
- self.on_stop_thread_list = []
- self.seen_threads = {}
-
- self.servings = []
-
- self.mtimes = {}
- self.reload_files = []
-
- self.monitor_thread = None
-
- def start(self, blocking=True):
- """Start the application engine."""
- self.state = STARTING
-
- cherrypy.checker()
-
- for func in self.on_start_engine_list:
- func()
-
- self.state = STARTED
-
- self._set_signals()
-
- freq = self.deadlock_poll_freq
- if freq > 0:
- self.monitor_thread = PerpetualTimer(freq, self.monitor)
- self.monitor_thread.setName("CPEngine Monitor")
- self.monitor_thread.start()
-
- if blocking:
- self.block()
-
- def block(self):
- """Block forever (wait for stop(), KeyboardInterrupt or SystemExit)."""
- try:
- while self.state != STOPPED:
- # Note that autoreload_frequency controls
- # sleep timer even if autoreload is off.
- time.sleep(self.autoreload_frequency)
- if self.autoreload_on:
- self.autoreload()
- except KeyboardInterrupt:
- cherrypy.log("<Ctrl-C> hit: shutting down app engine", "ENGINE")
- cherrypy.server.stop()
- self.stop()
- except SystemExit:
- cherrypy.log("SystemExit raised: shutting down app engine", "ENGINE")
- cherrypy.server.stop()
- self.stop()
- raise
- except:
- # Don't bother logging, since we're going to re-raise.
- # Note that we don't stop the HTTP server here.
- self.stop()
- raise
-
- def reexec(self):
- """Re-execute the current process."""
- cherrypy.server.stop()
- self.stop()
-
- args = sys.argv[:]
- cherrypy.log("Re-spawning %s" % " ".join(args), "ENGINE")
- args.insert(0, sys.executable)
-
- if sys.platform == "win32":
- args = ['"%s"' % arg for arg in args]
-
- # Some platforms (OS X) will error if all threads are not
- # ABSOLUTELY terminated. See http://www.cherrypy.org/ticket/581.
- for trial in xrange(self.reexec_retry * 10):
- try:
- os.execv(sys.executable, args)
- return
- except OSError, x:
- if x.errno != 45:
- raise
- time.sleep(0.1)
- else:
- raise
-
- # Number of seconds to retry reexec if os.execv fails.
- reexec_retry = 2
-
- def autoreload(self):
- """Reload the process if registered files have been modified."""
- sysfiles = []
- for k, m in sys.modules.items():
- if re.match(self.autoreload_match, k):
- if hasattr(m, "__loader__"):
- if hasattr(m.__loader__, "archive"):
- k = m.__loader__.archive
- k = getattr(m, "__file__", None)
- sysfiles.append(k)
-
- for filename in sysfiles + self.reload_files:
- if filename:
- if filename.endswith(".pyc"):
- filename = filename[:-1]
-
- oldtime = self.mtimes.get(filename, 0)
- if oldtime is None:
- # Module with no .py file. Skip it.
- continue
-
- try:
- mtime = os.stat(filename).st_mtime
- except OSError:
- # Either a module with no .py file, or it's been deleted.
- mtime = None
-
- if filename not in self.mtimes:
- # If a module has no .py file, this will be None.
- self.mtimes[filename] = mtime
- else:
- if mtime is None or mtime > oldtime:
- # The file has been deleted or modified.
- self.reexec()
-
- def stop(self):
- """Stop the application engine."""
- if self.state != STOPPED:
- for thread_ident, i in self.seen_threads.iteritems():
- for func in self.on_stop_thread_list:
- func(i)
- self.seen_threads.clear()
-
- for func in self.on_stop_engine_list:
- func()
-
- if self.monitor_thread:
- self.monitor_thread.cancel()
- self.monitor_thread.join()
- self.monitor_thread = None
-
- self.state = STOPPED
- cherrypy.log("CherryPy shut down", "ENGINE")
-
- def restart(self):
- """Restart the application engine (does not block)."""
- self.stop()
- self.start(blocking=False)
-
- def wait(self):
- """Block the caller until ready to receive requests (or error)."""
- while not (self.state == STARTED):
- time.sleep(.1)
-
- def request(self, local_host, remote_host, scheme="http",
- server_protocol="HTTP/1.1"):
- """Obtain and return an HTTP Request object. (Core)
-
- local_host should be an http.Host object with the server info.
- remote_host should be an http.Host object with the client info.
- scheme: either "http" or "https"; defaults to "http"
- """
- if self.state == STOPPED:
- req = NotReadyRequest("The CherryPy engine has stopped.")
- elif self.state == STARTING:
- req = NotReadyRequest("The CherryPy engine could not start.")
- else:
- # Only run on_start_thread_list if the engine is running.
- threadID = threading._get_ident()
- if threadID not in self.seen_threads:
- i = len(self.seen_threads) + 1
- self.seen_threads[threadID] = i
-
- for func in self.on_start_thread_list:
- func(i)
- req = self.request_class(local_host, remote_host, scheme,
- server_protocol)
- resp = self.response_class()
- cherrypy.serving.load(req, resp)
- self.servings.append((req, resp))
- return req
-
- def release(self):
- """Close and de-reference the current request and response. (Core)"""
- req = cherrypy.serving.request
-
- try:
- req.close()
- except:
- cherrypy.log(traceback=True)
-
- try:
- self.servings.remove((req, cherrypy.serving.response))
- except ValueError:
- pass
-
- cherrypy.serving.clear()
-
- def monitor(self):
- """Check timeout on all responses. (Internal)"""
- if self.state == STARTED:
- for req, resp in self.servings:
- resp.check_timeout()
-
- def start_with_callback(self, func, args=None, kwargs=None):
- """Start the given func in a new thread, then start self and block."""
-
- if args is None:
- args = ()
- if kwargs is None:
- kwargs = {}
- args = (func,) + args
-
- def _callback(func, *a, **kw):
- self.wait()
- func(*a, **kw)
- t = threading.Thread(target=_callback, args=args, kwargs=kwargs)
- t.setName("CPEngine Callback " + t.getName())
- t.start()
-
- self.start()
-
-
- # Signal handling #
-
- SIGHUP = None
- SIGTERM = None
-
- if hasattr(signal, "SIGHUP"):
- def SIGHUP(self, signum=None, frame=None):
- self.reexec()
-
- if hasattr(signal, "SIGTERM"):
- def SIGTERM(signum=None, frame=None):
- cherrypy.server.stop()
- self.stop()
-
- def _set_signals(self):
- if self.SIGHUP:
- signal.signal(signal.SIGHUP, self.SIGHUP)
- if self.SIGTERM:
- signal.signal(signal.SIGTERM, self.SIGTERM)
-
-
- # Drop privileges #
-
- # Special thanks to Gavin Baker: http://antonym.org/node/100.
- try:
- import pwd, grp
- except ImportError:
- try:
- os.umask
- except AttributeError:
- def drop_privileges(self):
- """Drop privileges. Not implemented on this platform."""
- raise NotImplementedError
- else:
- umask = None
-
- def drop_privileges(self):
- """Drop privileges. Windows version (umask only)."""
- if self.umask is not None:
- old_umask = os.umask(self.umask)
- cherrypy.log('umask old: %03o, new: %03o' %
- (old_umask, self.umask), "PRIV")
- else:
- uid = None
- gid = None
- umask = None
-
- def drop_privileges(self):
- """Drop privileges. UNIX version (uid, gid, and umask)."""
- if not (self.uid is None and self.gid is None):
- if self.uid is None:
- uid = None
- elif isinstance(self.uid, basestring):
- uid = self.pwd.getpwnam(self.uid)[2]
- else:
- uid = self.uid
-
- if self.gid is None:
- gid = None
- elif isinstance(self.gid, basestring):
- gid = self.grp.getgrnam(self.gid)[2]
- else:
- gid = self.gid
-
- def names():
- name = self.pwd.getpwuid(os.getuid())[0]
- group = self.grp.getgrgid(os.getgid())[0]
- return name, group
-
- cherrypy.log('Started as %r/%r' % names(), "PRIV")
- if gid is not None:
- os.setgid(gid)
- if uid is not None:
- os.setuid(uid)
- cherrypy.log('Running as %r/%r' % names(), "PRIV")
-
- if self.umask is not None:
- old_umask = os.umask(self.umask)
- cherrypy.log('umask old: %03o, new: %03o' %
- (old_umask, self.umask), "PRIV")
-
-
-class NotReadyRequest:
-
- throw_errors = True
- show_tracebacks = True
- error_page = {}
-
- def __init__(self, msg):
- self.msg = msg
- self.protocol = (1,1)
-
- def close(self):
- pass
-
- def run(self, method, path, query_string, protocol, headers, rfile):
- self.method = "GET"
- cherrypy.HTTPError(503, self.msg).set_response()
- cherrypy.response.finalize()
- return cherrypy.response
-
diff --git a/cherrypy/_cpmodpy.py b/cherrypy/_cpmodpy.py
index 6a12ac95..4b58f5ae 100644
--- a/cherrypy/_cpmodpy.py
+++ b/cherrypy/_cpmodpy.py
@@ -15,7 +15,7 @@ class Root:
# We will use this method from the mod_python configuration
-# as the entyr point to our application
+# as the entry point to our application
def setup_server():
cherrypy.tree.mount(Root())
cherrypy.config.update({'environment': 'production',
@@ -23,7 +23,7 @@ def setup_server():
'show_tracebacks': False})
# You must start the engine in a non-blocking fashion
# so that mod_python can proceed
- cherrypy.engine.start(blocking=False)
+ cherrypy.engine.start()
##########################################
# mod_python settings for apache2
@@ -83,10 +83,8 @@ def setup(req):
"tools.ignore_headers.headers": ['Range'],
})
- if cherrypy.engine.state == cherrypy._cpengine.STOPPED:
- cherrypy.engine.start(blocking=False)
- elif cherrypy.engine.state == cherrypy._cpengine.STARTING:
- cherrypy.engine.wait()
+ cherrypy.engine.start()
+ cherrypy.engine.wait()
def cherrypy_cleanup(data):
cherrypy.engine.stop()
@@ -166,7 +164,7 @@ def handler(req):
redirections = []
while True:
- request = cherrypy.engine.request(local, remote, scheme)
+ request = app.get_serving(local, remote, scheme)
request.login = req.user
request.multithread = bool(threaded)
request.multiprocess = bool(forked)
@@ -178,7 +176,7 @@ def handler(req):
response = request.run(method, path, qs, sproto, headers, rfile)
break
except cherrypy.InternalRedirect, ir:
- cherrypy.engine.release()
+ app.release_serving()
prev = request
if not recursive:
@@ -198,7 +196,7 @@ def handler(req):
rfile = StringIO.StringIO()
send_response(req, response.status, response.header_list, response.body)
- cherrypy.engine.release()
+ app.release_serving()
except:
tb = format_exc()
cherrypy.log(tb)
diff --git a/cherrypy/_cprequest.py b/cherrypy/_cprequest.py
index b4b06595..9c0e0be3 100644
--- a/cherrypy/_cprequest.py
+++ b/cherrypy/_cprequest.py
@@ -393,7 +393,7 @@ class Request(object):
"request": request_namespace,
"response": response_namespace,
"error_page": error_page_namespace,
- # "tools": See _cptools.Toolbox
+ "tools": cherrypy.tools,
})
def __init__(self, local_host, remote_host, scheme="http",
@@ -549,6 +549,7 @@ class Request(object):
self.hooks.run('before_handler')
if self.handler:
cherrypy.response.body = self.handler()
+
self.hooks.run('before_finalize')
cherrypy.response.finalize()
except (cherrypy.HTTPRedirect, cherrypy.HTTPError), inst:
diff --git a/cherrypy/_cpserver.py b/cherrypy/_cpserver.py
index 8404b0e1..3c4be565 100644
--- a/cherrypy/_cpserver.py
+++ b/cherrypy/_cpserver.py
@@ -93,6 +93,7 @@ class Server(object):
"Try server.quickstart instead.")
for httpserver in self.httpservers:
self._start_http(httpserver)
+ cherrypy.engine.subscribe('stop', self.stop)
def _start_http(self, httpserver):
"""Start the given httpserver in a new thread."""
@@ -120,19 +121,18 @@ class Server(object):
"""HTTP servers MUST be started in new threads, so that the
main thread persists to receive KeyboardInterrupt's. If an
exception is raised in the httpserver's thread then it's
- trapped here, and the httpserver(s) and engine are shut down.
+ trapped here, and the engine (and therefore our httpservers)
+ are shut down.
"""
try:
httpserver.start()
except KeyboardInterrupt, exc:
cherrypy.log("<Ctrl-C> hit: shutting down HTTP servers", "SERVER")
self.interrupt = exc
- self.stop()
cherrypy.engine.stop()
except SystemExit, exc:
cherrypy.log("SystemExit raised: shutting down HTTP servers", "SERVER")
self.interrupt = exc
- self.stop()
cherrypy.engine.stop()
raise
@@ -154,7 +154,10 @@ class Server(object):
# Wait for port to be occupied
if isinstance(bind_addr, tuple):
- wait_for_occupied_port(*bind_addr)
+ host, port = bind_addr
+ if not host or host == '0.0.0.0':
+ host = socket.gethostname()
+ wait_for_occupied_port(host, port)
def stop(self):
"""Stop all HTTP servers."""
@@ -181,7 +184,7 @@ class Server(object):
return self.socket_file
host = self.socket_host
- if not host:
+ if not host or host == '0.0.0.0':
# The empty string signifies INADDR_ANY. Look up the host name,
# which should be the safest thing to spit out in a URL.
host = socket.gethostname()
diff --git a/cherrypy/_cptools.py b/cherrypy/_cptools.py
index ada109a2..8d7393be 100644
--- a/cherrypy/_cptools.py
+++ b/cherrypy/_cptools.py
@@ -329,11 +329,11 @@ class Toolbox(object):
"""A collection of Tools.
This object also functions as a config namespace handler for itself.
+ Custom toolboxes should be added to each Application's toolboxes dict.
"""
def __init__(self, namespace):
self.namespace = namespace
- cherrypy.engine.request_class.namespaces[namespace] = self
def __setattr__(self, name, value):
# If the Tool._name is None, supply it from the attribute name.
diff --git a/cherrypy/_cptree.py b/cherrypy/_cptree.py
index 5a9959c8..fe2aa36c 100644
--- a/cherrypy/_cptree.py
+++ b/cherrypy/_cptree.py
@@ -2,12 +2,15 @@
import os
import cherrypy
-from cherrypy import _cpconfig, _cplogging, _cpwsgi, tools
+from cherrypy import _cpconfig, _cplogging, _cprequest, _cpwsgi, tools
class Application(object):
"""A CherryPy Application.
+ Servers and gateways should not instantiate Request objects directly.
+ Instead, they should ask an Application object for a request object.
+
An instance of this class may also be used as a WSGI callable
(WSGI application object) for itself.
"""
@@ -28,6 +31,7 @@ class Application(object):
of {key: value} pairs."""
namespaces = _cpconfig.NamespaceSet()
+ toolboxes = {'tools': cherrypy.tools}
log = None
log__doc = """A LogManager instance. See _cplogging."""
@@ -35,6 +39,9 @@ class Application(object):
wsgiapp = None
wsgiapp__doc = """A CPWSGIApp instance. See _cpwsgi."""
+ request_class = _cprequest.Request
+ response_class = _cprequest.Response
+
def __init__(self, root, script_name=""):
self.log = _cplogging.LogManager(id(self), cherrypy.log.logger_root)
self.root = root
@@ -70,6 +77,34 @@ class Application(object):
# Handle namespaces specified in config.
self.namespaces(self.config.get("/", {}))
+ def get_serving(self, local, remote, scheme, sproto):
+ """Create and return a Request and Response object."""
+ req = self.request_class(local, remote, scheme, sproto)
+ req.app = self
+
+ for name, toolbox in self.toolboxes.iteritems():
+ req.namespaces[name] = toolbox
+
+ resp = self.response_class()
+ cherrypy.serving.load(req, resp)
+ cherrypy.engine.publish('CherryPy Timeout Monitor', 'acquire()')
+ cherrypy.engine.publish('acquire_thread')
+
+ return req, resp
+
+ def release_serving(self):
+ """Release the current serving (request and response)."""
+ req = cherrypy.serving.request
+
+ cherrypy.engine.publish('CherryPy Timeout Monitor', 'release()')
+
+ try:
+ req.close()
+ except:
+ cherrypy.log(traceback=True)
+
+ cherrypy.serving.clear()
+
def __call__(self, environ, start_response):
return self.wsgiapp(environ, start_response)
diff --git a/cherrypy/_cpwsgi.py b/cherrypy/_cpwsgi.py
index 31b6b3d2..83c665d4 100644
--- a/cherrypy/_cpwsgi.py
+++ b/cherrypy/_cpwsgi.py
@@ -113,8 +113,10 @@ class AppResponse(object):
request = None
def __init__(self, environ, start_response, cpapp):
+ self.cpapp = cpapp
+
try:
- self.request = self.get_engine_request(environ, cpapp)
+ self.request = self.get_request(environ)
meth = environ['REQUEST_METHOD']
path = environ.get('SCRIPT_NAME', '') + environ.get('PATH_INFO', '')
@@ -192,10 +194,11 @@ class AppResponse(object):
return "".join(b)
def close(self):
- _cherrypy.engine.release()
+ """Close and de-reference the current request and response. (Core)"""
+ self.cpapp.release_serving()
- def get_engine_request(self, environ, cpapp):
- """Return a Request object from the CherryPy Engine using environ."""
+ def get_request(self, environ):
+ """Create a Request object using environ."""
env = environ.get
local = _http.Host('', int(env('SERVER_PORT', 80)),
@@ -205,7 +208,7 @@ class AppResponse(object):
env('REMOTE_HOST', ''))
scheme = env('wsgi.url_scheme')
sproto = env('ACTUAL_SERVER_PROTOCOL', "HTTP/1.1")
- request = _cherrypy.engine.request(local, remote, scheme, sproto)
+ request, resp = self.cpapp.get_serving(local, remote, scheme, sproto)
# LOGON_USER is served by IIS, and is the name of the
# user after having been mapped to a local account.
@@ -214,7 +217,6 @@ class AppResponse(object):
request.multithread = environ['wsgi.multithread']
request.multiprocess = environ['wsgi.multiprocess']
request.wsgi_environ = environ
- request.app = cpapp
request.prev = env('cherrypy.request')
environ['cherrypy.request'] = request
return request
diff --git a/cherrypy/lib/covercp.py b/cherrypy/lib/covercp.py
index 7441aaaa..9ad83f6d 100644
--- a/cherrypy/lib/covercp.py
+++ b/cherrypy/lib/covercp.py
@@ -10,8 +10,8 @@ http://www.nedbatchelder.com/code/modules/coverage.html
To turn on coverage tracing, use the following code:
- cherrypy.engine.on_start_engine_list.insert(0, covercp.start)
- cherrypy.engine.on_start_thread_list.insert(0, covercp.start)
+ cherrypy.engine.subscribe('start', covercp.start)
+ cherrypy.engine.subscribe('start_thread', covercp.start)
Run your code, then use the covercp.serve() function to browse the
results in a web browser. If you run this module from the command line,
@@ -44,6 +44,7 @@ except ImportError:
def start(threadid=None):
pass
+start.priority = 20
# Guess initial depth to hide FIXME this doesn't work for non-cherrypy stuff
import cherrypy
diff --git a/cherrypy/lib/sessions.py b/cherrypy/lib/sessions.py
index e730ed42..3ef9bf80 100644
--- a/cherrypy/lib/sessions.py
+++ b/cherrypy/lib/sessions.py
@@ -23,16 +23,6 @@ import cherrypy
from cherrypy.lib import http
-class PerpetualTimer(threading._Timer):
-
- def run(self):
- while True:
- self.finished.wait(self.interval)
- if self.finished.isSet():
- return
- self.function(*self.args, **self.kwargs)
-
-
missing = object()
class Session(object):
@@ -57,7 +47,7 @@ class Session(object):
automatically on the first attempt to access session data."""
clean_thread = None
- clean_thread__doc = "Class-level PerpetualTimer which calls self.clean_up."
+ clean_thread__doc = "Class-level Monitor which calls self.clean_up."
clean_freq = 5
clean_freq__doc = "The poll rate for expired session cleanup in minutes."
@@ -75,14 +65,6 @@ class Session(object):
if self._load() is not None:
self.id = None
- def clean_interrupt(cls):
- """Stop the expired-session cleaning timer."""
- if cls.clean_thread:
- cls.clean_thread.cancel()
- cls.clean_thread.join()
- cls.clean_thread = None
- clean_interrupt = classmethod(clean_interrupt)
-
def clean_up(self):
"""Clean up expired sessions."""
pass
@@ -129,11 +111,12 @@ class Session(object):
# The instances are created and destroyed per-request.
cls = self.__class__
if not cls.clean_thread:
- cherrypy.engine.on_stop_engine_list.append(cls.clean_interrupt)
# clean_up is in instancemethod and not a classmethod,
# so tool config can be accessed inside the method.
- t = PerpetualTimer(self.clean_freq, self.clean_up)
- t.setName("CP Session Cleanup")
+ from cherrypy import pywebd
+ t = pywebd.plugins.Monitor(cherrypy.engine, self.clean_up,
+ "CP Session Cleanup")
+ t.frequency = self.clean_freq
cls.clean_thread = t
t.start()
diff --git a/cherrypy/pywebd/__init__.py b/cherrypy/pywebd/__init__.py
new file mode 100644
index 00000000..fbff0319
--- /dev/null
+++ b/cherrypy/pywebd/__init__.py
@@ -0,0 +1,49 @@
+"""Manage an HTTP server process via an extensible Engine object.
+
+An Engine object is used to contain and manage site-wide behavior:
+daemonization, HTTP server instantiation, autoreload, signal handling,
+drop privileges, initial logging, PID file management, etc.
+
+In addition, an Engine object provides a place for each web framework
+to hook in custom code that runs in response to site-wide events (like
+process start and stop), or which controls or otherwise interacts with
+the site-wide components mentioned above. For example, a framework which
+uses file-based templates would add known template filenames to the
+autoreload component.
+
+Ideally, an Engine object will be flexible enough to be useful in a variety
+of invocation scenarios:
+
+ 1. The deployer starts a site from the command line via a framework-
+ neutral deployment script; applications from multiple frameworks
+ are mixed in a single site. Command-line arguments and configuration
+ files are used to define site-wide components such as the HTTP server,
+ autoreload behavior, signal handling, etc.
+ 2. The deployer starts a site via some other process, such as Apache;
+ applications from multiple frameworks are mixed in a single site.
+ Autoreload and signal handling (from Python at least) are disabled.
+ 3. The deployer starts a site via a framework-specific mechanism;
+ for example, when running tests, exploring tutorials, or deploying
+ single applications from a single framework. The framework controls
+ which site-wide components are enabled as it sees fit.
+
+The Engine object in this package uses topic-based publish-subscribe
+messaging to accomplish all this. A few topic channels are built in
+('start', 'stop', 'restart' and 'graceful'). The 'plugins' module
+defines a few others which are specific to each tool. Frameworks are
+free to define their own. If a message is sent to a channel that has
+not been defined or has no listeners, there is no effect.
+
+In general, there should only ever be a single Engine object per process.
+Frameworks share a single Engine object by publishing messages and
+registering (subscribing) listeners.
+"""
+
+from cherrypy.pywebd import plugins
+
+try:
+ from cherrypy.pywebd import win32
+ engine = win32.Engine()
+except ImportError:
+ from cherrypy.pywebd import base
+ engine = base.Engine()
diff --git a/cherrypy/pywebd/base.py b/cherrypy/pywebd/base.py
new file mode 100644
index 00000000..a32a822c
--- /dev/null
+++ b/cherrypy/pywebd/base.py
@@ -0,0 +1,163 @@
+"""Base Engine class for pywebd."""
+
+try:
+ set
+except NameError:
+ from sets import Set as set
+import sys
+import threading
+import time
+import traceback
+
+
+# Use a flag to indicate the state of the engine.
+STOPPED = 0
+STARTING = None
+STARTED = 1
+
+
+class Engine(object):
+ """Process controls for HTTP site deployment."""
+
+ state = STOPPED
+
+ def __init__(self):
+ self.state = STOPPED
+ self.listeners = {}
+ 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)
+
+ if priority is None:
+ priority = getattr(callback, 'priority', 50)
+ self._priorities[(channel, callback)] = priority
+
+ def unsubscribe(self, channel, callback):
+ """Discard the given callback (if present)."""
+ listeners = self.listeners.get(channel)
+ if listeners and callback in listeners:
+ listeners.discard(callback)
+ del self._priorities[(channel, callback)]
+
+ def publish(self, channel, *args, **kwargs):
+ """Return output of all subscribers for the given channel."""
+ if channel not in self.listeners:
+ return []
+
+ exc = None
+ output = []
+
+ items = [(self._priorities[(channel, listener)], listener)
+ for listener in self.listeners[channel]]
+ items.sort()
+ for priority, listener in items:
+ # All listeners for a given channel are guaranteed to run even
+ # if others at the same channel fail. We will still log the
+ # failure, but proceed on to the next listener. The only way
+ # to stop all processing from one of these listeners is to
+ # raise SystemExit and stop the whole server.
+ try:
+ output.append(listener(*args, **kwargs))
+ except (KeyboardInterrupt, SystemExit):
+ raise
+ except:
+ self.log("Error in %r listener %r" % (channel, listener),
+ traceback=True)
+ exc = sys.exc_info()[1]
+ if exc:
+ raise
+ return output
+
+ def start(self):
+ """Start the engine."""
+ self.state = STARTING
+ self.log('Engine starting')
+ self.publish('start')
+ self.state = STARTED
+
+ def wait(self, interval=0.1):
+ """Block the caller until the Engine is in the STARTED state."""
+ while not (self.state == STARTED):
+ time.sleep(interval)
+
+ def exit(self, status=0):
+ """Stop the engine and exit the process."""
+ self.stop()
+ sys.exit(status)
+
+ def restart(self):
+ """Restart the process (may close connections)."""
+ self.stop()
+ self.log('Engine restart')
+ self.publish('reexec')
+
+ def graceful(self):
+ """Restart the engine without closing connections."""
+ self.log('Engine graceful restart')
+ self.publish('graceful')
+
+ def block(self, interval=1):
+ """Block forever (wait for stop(), KeyboardInterrupt or SystemExit)."""
+ try:
+ while self.state != STOPPED:
+ time.sleep(interval)
+ except (KeyboardInterrupt, IOError):
+ # The time.sleep call might raise
+ # "IOError: [Errno 4] Interrupted function call".
+ self.log('Keyboard Interrupt: shutting down engine')
+ self.stop()
+ except SystemExit:
+ self.log('SystemExit raised: shutting down engine')
+ self.stop()
+ raise
+
+ def stop(self):
+ """Stop the engine."""
+ if self.state != STOPPED:
+ self.log('Engine shutting down')
+ self.publish('stop')
+ self.state = STOPPED
+
+ def start_with_callback(self, func, args=None, kwargs=None):
+ """Start 'func' in a new thread T, then start self (and return T)."""
+ if args is None:
+ args = ()
+ if kwargs is None:
+ kwargs = {}
+ args = (func,) + args
+
+ def _callback(func, *a, **kw):
+ self.wait()
+ func(*a, **kw)
+ t = threading.Thread(target=_callback, args=args, kwargs=kwargs)
+ t.setName('Engine Callback ' + t.getName())
+ t.start()
+
+ self.start()
+
+ return t
+
+ def log(self, msg="", traceback=False):
+ if traceback:
+ msg = '\n'.join((msg, format_exc()))
+ print msg
+
+
+def format_exc(exc=None):
+ """Return exc (or sys.exc_info if None), formatted."""
+ if exc is None:
+ exc = sys.exc_info()
+ if exc == (None, None, None):
+ return ""
+ return "".join(traceback.format_exception(*exc))
+
+
+try:
+ from cherrypy.pywebd import win32
+ engine = win32.Engine()
+except ImportError:
+ engine = Engine()
diff --git a/cherrypy/pywebd/plugins.py b/cherrypy/pywebd/plugins.py
new file mode 100644
index 00000000..be76168b
--- /dev/null
+++ b/cherrypy/pywebd/plugins.py
@@ -0,0 +1,417 @@
+"""Plugins for a pywebd Engine."""
+
+import os
+import re
+try:
+ set
+except NameError:
+ from sets import Set as set
+import signal as _signal
+import sys
+import time
+import threading
+
+
+class SubscribedObject(object):
+ """An object whose attributes are manipulable via publishing.
+
+ An instance of this class will subscribe to a channel. Messages
+ published to that channel should be one of three types:
+
+ getattr:
+ >>> values = engine.publish('thing', 'attr')
+ Note that the 'publish' method will return a list of values
+ (from potentially multiple subscribed objects).
+
+ setattr:
+ >>> engine.publish('thing', 'attr', value)
+
+ call:
+ >>> engine.publish('thing', 'attr()', *a, **kw)
+ """
+
+ def __init__(self, engine, channel):
+ self.engine = engine
+ self.channel = channel
+ engine.subscribe(self.channel, self.handle)
+
+ def handle(self, attr, *args, **kwargs):
+ if attr.endswith("()"):
+ # Call
+ return getattr(self, attr[:-2])(*args, **kwargs)
+ else:
+ if args:
+ # Set
+ return setattr(self, attr, args[0])
+ else:
+ # Get
+ return getattr(self, attr)
+
+
+class SignalHandler(object):
+
+ def __init__(self, engine, signals=None):
+ if signals is None:
+ signals = [k for k in dir(_signal)
+ if k.startswith('SIG') and not k.startswith('SIG_')]
+ if not isinstance(signals, dict):
+ signals = dict([(getattr(_signal, k), k) for k in signals])
+ self.signals = signals
+
+ self.engine = engine
+ for num in self.signals:
+ self.set_handler(num)
+
+ def set_handler(self, signal, callback=None):
+ """Register a handler for the given signal (number or name).
+
+ If the optional callback is included, it will be registered
+ as a listener for the given signal.
+ """
+ if isinstance(signal, basestring):
+ signum = getattr(_signal, signal, None)
+ if signum is None:
+ return # ?
+ signame = signal
+ else:
+ signum = signal
+ signame = self.signals[signal]
+
+ # Should we do something with existing signal handlers?
+ # cur = _signal.getsignal(signum)
+ _signal.signal(signum, self.handle_signal)
+ if callback is not None:
+ self.engine.subscribe(signame, callback)
+
+ def handle_signal(self, signum=None, frame=None):
+ self.engine.publish(self.signals[signum])
+
+
+class Reexec(SubscribedObject):
+
+ def __init__(self, engine, retry=2):
+ self.engine = engine
+ self.retry = retry
+ engine.subscribe('reexec', self)
+
+ def __call__(self):
+ """Re-execute the current process."""
+ args = sys.argv[:]
+ self.engine.log('Re-spawning %s' % ' '.join(args))
+ args.insert(0, sys.executable)
+
+ if sys.platform == 'win32':
+ args = ['"%s"' % arg for arg in args]
+
+ # Some platforms (OS X) will error if all threads are not
+ # ABSOLUTELY terminated, and there doesn't seem to be a way
+ # around it other than waiting for the threads to stop.
+ # See http://www.cherrypy.org/ticket/581.
+ for trial in xrange(self.retry * 10):
+ try:
+ os.execv(sys.executable, args)
+ return
+ except OSError, x:
+ if x.errno != 45:
+ raise
+ time.sleep(0.1)
+ else:
+ raise
+
+
+class DropPrivileges(SubscribedObject):
+ """Drop privileges.
+
+ Special thanks to Gavin Baker: http://antonym.org/node/100.
+ """
+
+ def __init__(self, engine):
+ self.engine = engine
+
+ try:
+ import pwd, grp
+ except ImportError:
+ try:
+ os.umask
+ except AttributeError:
+ def __call__(self):
+ """Drop privileges. Not implemented on this platform."""
+ raise NotImplementedError
+ else:
+ umask = None
+
+ def __call__(self):
+ """Drop privileges. Windows version (umask only)."""
+ if umask is not None:
+ old_umask = os.umask(umask)
+ self.engine.log('umask old: %03o, new: %03o' %
+ (old_umask, umask))
+ else:
+ uid = None
+ gid = None
+ umask = None
+
+ def __call__(self):
+ """Drop privileges. UNIX version (uid, gid, and umask)."""
+ if not (uid is None and gid is None):
+ if uid is None:
+ uid = None
+ elif isinstance(uid, basestring):
+ uid = pwd.getpwnam(uid)[2]
+ else:
+ uid = uid
+
+ if gid is None:
+ gid = None
+ elif isinstance(gid, basestring):
+ gid = grp.getgrnam(gid)[2]
+ else:
+ gid = gid
+
+ def names():
+ name = pwd.getpwuid(os.getuid())[0]
+ group = grp.getgrgid(os.getgid())[0]
+ return name, group
+
+ self.engine.log('Started as %r/%r' % names())
+ if gid is not None:
+ os.setgid(gid)
+ if uid is not None:
+ os.setuid(uid)
+ self.engine.log('Running as %r/%r' % names())
+
+ if umask is not None:
+ old_umask = os.umask(umask)
+ self.engine.log('umask old: %03o, new: %03o' %
+ (old_umask, umask))
+ __call__.priority = 70
+
+
+def daemonize(stdin='/dev/null', stdout='/dev/null', stderr='/dev/null'):
+ """Daemonize the running script.
+
+ When this method returns, the process is completely decoupled from the
+ parent environment."""
+
+ # See http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC16
+ # and http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/66012
+
+ # Finish up with the current stdout/stderr
+ 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.
+ sys.stdout.close()
+ sys.exit(0)
+ except OSError, exc:
+ # Python raises OSError rather than returning negative numbers.
+ sys.exit("%s: fork #1 failed: (%d) %s\n" % (sys.argv[0], exc.errno, exc.strerror))
+
+ os.setsid()
+
+ # Do second fork
+ try:
+ pid = os.fork()
+ if pid > 0:
+ sys.stdout.close()
+ sys.exit(0) # Exit second parent
+ except OSError, exc:
+ sys.exit("%s: fork #2 failed: (%d) %s\n" % (sys.argv[0], exc.errno, exc.strerror))
+
+ os.chdir("/")
+ os.umask(0)
+
+ si = open(stdin, "r")
+ so = open(stdout, "a+")
+ se = open(stderr, "a+", 0)
+ os.dup2(si.fileno(), sys.stdin.fileno())
+ os.dup2(so.fileno(), sys.stdout.fileno())
+ os.dup2(se.fileno(), sys.stderr.fileno())
+daemonize.priority = 10
+
+
+class PIDFile(object):
+
+ def __init__(self, engine, pidfile):
+ self.engine = engine
+ self.pidfile = pidfile
+ engine.subscribe('start', self.start)
+ engine.subscribe('stop', self.stop)
+
+ def start(self):
+ open(self.pidfile, "wb").write(str(os.getpid()))
+ start.priority = 70
+
+ def stop(self):
+ try:
+ os.remove(self.pidfile)
+ except (KeyboardInterrupt, SystemExit):
+ raise
+ except:
+ pass
+
+
+class PerpetualTimer(threading._Timer):
+
+ def run(self):
+ while True:
+ self.finished.wait(self.interval)
+ if self.finished.isSet():
+ return
+ self.function(*self.args, **self.kwargs)
+
+
+class Monitor(SubscribedObject):
+ """Subscriber which periodically runs a callback in a separate thread."""
+
+ frequency = 60
+
+ def __init__(self, engine, callback, channel=None):
+ self.callback = callback
+ self.thread = None
+
+ if channel is None:
+ channel = self.__class__.__name__
+ SubscribedObject.__init__(self, engine, channel)
+
+ self.listeners = [('start', self.start),
+ ('stop', self.stop),
+ ('graceful', self.graceful),
+ ]
+ self.attach()
+
+ def attach(self):
+ for point, callback in self.listeners:
+ self.engine.subscribe(point, callback)
+
+ def detach(self):
+ for point, callback in self.listeners:
+ self.engine.unsubscribe(point, callback)
+
+ def start(self):
+ if self.frequency > 0:
+ self.thread = PerpetualTimer(self.frequency, self.callback)
+ self.thread.setName("pywebd %s" % self.channel)
+ self.thread.start()
+
+ def stop(self):
+ if self.thread:
+ if self.thread is not threading.currentThread():
+ self.thread.cancel()
+ self.thread.join()
+ self.thread = None
+
+ def graceful(self):
+ self.stop()
+ self.start()
+
+
+class Autoreloader(Monitor):
+ """Monitor which re-executes the process when files change."""
+
+ frequency = 1
+ match = '.*'
+
+ def __init__(self, engine):
+ self.mtimes = {}
+ self.files = set()
+ Monitor.__init__(self, engine, self.run)
+
+ def add(self, filename):
+ self.files.add(filename)
+
+ def discard(self, filename):
+ self.files.discard(filename)
+
+ def start(self):
+ self.mtimes = {}
+ self.files = set()
+ Monitor.start(self)
+
+ def run(self):
+ """Reload the process if registered files have been modified."""
+ sysfiles = set()
+ for k, m in sys.modules.items():
+ if re.match(self.match, k):
+ if hasattr(m, '__loader__'):
+ if hasattr(m.__loader__, 'archive'):
+ k = m.__loader__.archive
+ k = getattr(m, '__file__', None)
+ sysfiles.add(k)
+
+ for filename in sysfiles | self.files:
+ if filename:
+ if filename.endswith('.pyc'):
+ filename = filename[:-1]
+
+ oldtime = self.mtimes.get(filename, 0)
+ if oldtime is None:
+ # Module with no .py file. Skip it.
+ continue
+
+ try:
+ mtime = os.stat(filename).st_mtime
+ except OSError:
+ # Either a module with no .py file, or it's been deleted.
+ mtime = None
+
+ if filename not in self.mtimes:
+ # If a module has no .py file, this will be None.
+ self.mtimes[filename] = mtime
+ else:
+ if mtime is None or mtime > oldtime:
+ # The file has been deleted or modified.
+ self.engine.publish('reexec')
+
+
+class ThreadManager(object):
+ """Manager for HTTP request threads.
+
+ If you have control over thread creation and destruction, publish to
+ the 'acquire_thread' and 'release_thread' channels (for each thread).
+ This will register/unregister the current thread and publish to
+ 'start_thread' and 'stop_thread' listeners in the engine as needed.
+
+ If threads are created and destroyed by code you do not control (e.g.,
+ Apache), then, at the beginning of every HTTP request, publish to
+ 'acquire_thread' only. You should not publish to 'release_thread' in
+ this case, since you do not know whether the thread will be re-used or
+ not. The engine will call 'stop_thread' listeners for you when it stops.
+ """
+
+ def __init__(self, engine):
+ self.threads = {}
+ self.engine = engine
+ engine.subscribe('acquire_thread', self.acquire)
+ engine.subscribe('release_thread', self.release)
+ engine.subscribe('stop', self.release_all)
+ engine.subscribe('graceful', self.release_all)
+
+ def acquire(self):
+ """Run 'start_thread' listeners for the current thread. Idempotent."""
+ thread_ident = threading._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.
+ i = len(self.threads) + 1
+ self.threads[thread_ident] = i
+ self.engine.publish('start_thread', i)
+
+ def release(self):
+ """Release the current thread and run 'stop_thread' listeners."""
+ thread_ident = threading._get_ident()
+ i = self.threads.pop(thread_ident, None)
+ if i is not None:
+ self.engine.publish('stop_thread', i)
+
+ def release_all(self):
+ for thread_ident, i in self.threads.iteritems():
+ self.engine.publish('stop_thread', i)
+ self.threads.clear()
diff --git a/cherrypy/pywebd/win32.py b/cherrypy/pywebd/win32.py
new file mode 100644
index 00000000..236e7e7b
--- /dev/null
+++ b/cherrypy/pywebd/win32.py
@@ -0,0 +1,71 @@
+"""Windows service for pywebd. Requires pywin32."""
+
+import win32serviceutil
+import win32service
+import win32event
+import win32con
+import win32api
+
+from cherrypy.pywebd import base
+
+
+class Engine(base.Engine):
+
+ def __init__(self):
+ base.Engine.__init__(self)
+ self.stop_event = win32event.CreateEvent(None, 0, 0, None)
+ win32api.SetConsoleCtrlHandler(self.control_handler)
+
+ def control_handler(self, event):
+ if event in (win32con.CTRL_C_EVENT,
+ win32con.CTRL_BREAK_EVENT,
+ win32con.CTRL_CLOSE_EVENT):
+ self.log('Control event %s: shutting down engine' % event)
+ self.stop()
+ return 1
+ return 0
+
+ def block(self, interval=1):
+ """Block forever (wait for stop(), KeyboardInterrupt or SystemExit)."""
+ try:
+ win32event.WaitForSingleObject(self.stop_event,
+ win32event.INFINITE)
+ except SystemExit:
+ self.log('SystemExit raised: shutting down engine')
+ self.stop()
+ raise
+
+ def stop(self):
+ """Stop the engine."""
+ if self.state != base.STOPPED:
+ self.log('Engine shutting down')
+ self.publish('stop')
+ win32event.PulseEvent(self.stop_event)
+ self.state = base.STOPPED
+
+
+
+class PyWebService(win32serviceutil.ServiceFramework):
+ """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_ = "pywebd"
+ _exe_args_ = None # Default to no arguments
+
+ # Only exists on Windows 2000 or later, ignored on windows NT
+ _svc_description_ = "Python Web Service"
+
+ def SvcDoRun(self):
+ from cherrypy import pywebd
+ pywebd.engine.start()
+ pywebd.engine.block()
+
+ def SvcStop(self):
+ from cherrypy import pywebd
+ self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
+ pywebd.engine.stop()
+
+if __name__ == '__main__':
+ win32serviceutil.HandleCommandLine(PyWebService)
diff --git a/cherrypy/test/benchmark.py b/cherrypy/test/benchmark.py
index 2e18a692..97c7138e 100644
--- a/cherrypy/test/benchmark.py
+++ b/cherrypy/test/benchmark.py
@@ -149,7 +149,7 @@ Completed 800 requests
Completed 900 requests
-Server Software: CherryPy/3.0.1alpha
+Server Software: CherryPy/3.1alpha
Server Hostname: localhost
Server Port: 8080
@@ -308,7 +308,7 @@ def startup_modpython(req=None):
if ab_opt:
global AB_PATH
AB_PATH = ab_opt
- cherrypy.engine.start(blocking=False)
+ cherrypy.engine.start()
if cherrypy.engine.state == cherrypy._cpengine.STARTING:
cherrypy.engine.wait()
return 0 # apache.OK
diff --git a/cherrypy/test/helper.py b/cherrypy/test/helper.py
index 9524155d..2e40898e 100644
--- a/cherrypy/test/helper.py
+++ b/cherrypy/test/helper.py
@@ -121,6 +121,7 @@ def run_test_suite(moduleNames, server, conf):
cherrypy.engine.test_success = True
cherrypy.engine.start_with_callback(_run_test_suite_thread,
args=(moduleNames, conf))
+ cherrypy.engine.block()
if cherrypy.engine.test_success:
return 0
else:
@@ -180,24 +181,21 @@ def _run_test_suite_thread(moduleNames, conf):
teardown = getattr(m, "teardown_server", None)
if teardown:
teardown()
- thread.interrupt_main()
+ cherrypy.engine.stop()
def testmain(conf=None):
"""Run __main__ as a test module, with webtest debugging."""
if conf is None:
conf = {'server.socket_host': '127.0.0.1'}
setConfig(conf)
- try:
- cherrypy.server.quickstart()
- cherrypy.engine.start_with_callback(_test_main_thread)
- except KeyboardInterrupt:
- cherrypy.server.stop()
- cherrypy.engine.stop()
+ cherrypy.server.quickstart()
+ cherrypy.engine.start_with_callback(_test_main_thread)
+ cherrypy.engine.block()
def _test_main_thread():
try:
webtest.WebCase.PORT = cherrypy.server.socket_port
webtest.main()
finally:
- thread.interrupt_main()
+ cherrypy.engine.stop()
diff --git a/cherrypy/test/modpy.py b/cherrypy/test/modpy.py
index e035f616..59e5f9fb 100644
--- a/cherrypy/test/modpy.py
+++ b/cherrypy/test/modpy.py
@@ -118,7 +118,7 @@ def wsgisetup(req):
"log.error_file": os.path.join(curdir, "test.log"),
"environment": "production",
})
- cherrypy.engine.start(blocking=False)
+ cherrypy.engine.start()
from mod_python import apache
return apache.OK
diff --git a/cherrypy/test/test.py b/cherrypy/test/test.py
index 4dadce28..f23843ac 100644
--- a/cherrypy/test/test.py
+++ b/cherrypy/test/test.py
@@ -226,8 +226,8 @@ class CommandLineParser(object):
coverage.start()
import cherrypy
from cherrypy.lib import covercp
- cherrypy.engine.on_start_engine_list.insert(0, covercp.start)
- cherrypy.engine.on_start_thread_list.insert(0, covercp.start)
+ cherrypy.engine.hooks['start_process'].insert(0, covercp.start)
+ cherrypy.engine.hooks['start_thread'].insert(0, covercp.start)
except ImportError:
coverage = None
self.coverage = coverage
@@ -236,10 +236,10 @@ class CommandLineParser(object):
"""Stop the coverage tool, save results, and report."""
import cherrypy
from cherrypy.lib import covercp
- while covercp.start in cherrypy.engine.on_start_engine_list:
- cherrypy.engine.on_start_engine_list.remove(covercp.start)
- while covercp.start in cherrypy.engine.on_start_thread_list:
- cherrypy.engine.on_start_thread_list.remove(covercp.start)
+ while covercp.start in cherrypy.engine.hooks['start_process']:
+ cherrypy.engine.hooks['start_process'].remove(covercp.start)
+ while covercp.start in cherrypy.engine.hooks['start_thread']:
+ cherrypy.engine.hooks['start_thread'].remove(covercp.start)
if self.coverage:
self.coverage.save()
self.report_coverage()
diff --git a/cherrypy/test/test_config.py b/cherrypy/test/test_config.py
index 1bedb8a1..40d29950 100644
--- a/cherrypy/test/test_config.py
+++ b/cherrypy/test/test_config.py
@@ -60,7 +60,6 @@ def setup_server():
# 'value' is a type (like int or str).
return value(handler())
cherrypy.request.handler = wrapper
- cherrypy.engine.request_class.namespaces['raw'] = raw_namespace
class Raw:
@@ -80,7 +79,9 @@ filename: os.path.join(os.getcwd(), "hello.py")
root = Root()
root.foo = Foo()
root.raw = Raw()
- cherrypy.tree.mount(root, config=ioconf)
+ app = cherrypy.tree.mount(root, config=ioconf)
+ app.request_class.namespaces['raw'] = raw_namespace
+
cherrypy.tree.mount(Another(), "/another")
cherrypy.config.update({'environment': 'test_suite'})
diff --git a/cherrypy/test/test_states.py b/cherrypy/test/test_states.py
index 148315aa..1c945481 100644
--- a/cherrypy/test/test_states.py
+++ b/cherrypy/test/test_states.py
@@ -21,10 +21,10 @@ class Root:
raise KeyboardInterrupt()
ctrlc.exposed = True
- def restart(self):
- cherrypy.engine.restart()
- return "app was restarted succesfully"
- restart.exposed = True
+ def graceful(self):
+ cherrypy.engine.graceful()
+ return "app was (gracefully) restarted succesfully"
+ graceful.exposed = True
def block_explicit(self):
while True:
@@ -51,6 +51,7 @@ class Dependency:
def __init__(self):
self.running = False
self.startcount = 0
+ self.gracecount = 0
self.threads = {}
def start(self):
@@ -60,6 +61,9 @@ class Dependency:
def stop(self):
self.running = False
+ def graceful(self):
+ self.gracecount += 1
+
def startthread(self, thread_id):
self.threads[thread_id] = None
@@ -85,7 +89,7 @@ class ServerStateTests(helper.CPWebCase):
# Test server start
cherrypy.server.quickstart(self.server_class)
- cherrypy.engine.start(blocking=False)
+ cherrypy.engine.start()
self.assertEqual(cherrypy.engine.state, 1)
if self.server_class:
@@ -102,11 +106,11 @@ class ServerStateTests(helper.CPWebCase):
self.assertBody("Hello World")
self.assertEqual(len(db_connection.threads), 1)
- # Test engine stop
+ # Test engine stop. This will also stop the HTTP server.
cherrypy.engine.stop()
self.assertEqual(cherrypy.engine.state, 0)
- # Verify that the on_stop_engine function was called
+ # Verify that our custom stop function was called
self.assertEqual(db_connection.running, False)
self.assertEqual(len(db_connection.threads), 0)
@@ -122,53 +126,53 @@ class ServerStateTests(helper.CPWebCase):
self.getPage("/")
self.assertBody("Hello World")
cherrypy.engine.stop()
+ cherrypy.server.start()
cherrypy.engine.start_with_callback(stoptest)
+ cherrypy.engine.block()
self.assertEqual(cherrypy.engine.state, 0)
- cherrypy.server.stop()
def test_1_Restart(self):
cherrypy.server.start()
- cherrypy.engine.start(blocking=False)
+ cherrypy.engine.start()
# The db_connection should be running now
self.assertEqual(db_connection.running, True)
- sc = db_connection.startcount
+ grace = db_connection.gracecount
self.getPage("/")
self.assertBody("Hello World")
self.assertEqual(len(db_connection.threads), 1)
# Test server restart from this thread
- cherrypy.engine.restart()
+ cherrypy.engine.graceful()
self.assertEqual(cherrypy.engine.state, 1)
self.getPage("/")
self.assertBody("Hello World")
self.assertEqual(db_connection.running, True)
- self.assertEqual(db_connection.startcount, sc + 1)
+ self.assertEqual(db_connection.gracecount, grace + 1)
self.assertEqual(len(db_connection.threads), 1)
# Test server restart from inside a page handler
- self.getPage("/restart")
+ self.getPage("/graceful")
self.assertEqual(cherrypy.engine.state, 1)
- self.assertBody("app was restarted succesfully")
+ self.assertBody("app was (gracefully) restarted succesfully")
self.assertEqual(db_connection.running, True)
- self.assertEqual(db_connection.startcount, sc + 2)
+ self.assertEqual(db_connection.gracecount, grace + 2)
# Since we are requesting synchronously, is only one thread used?
- # Note that the "/restart" request has been flushed.
+ # Note that the "/graceful" request has been flushed.
self.assertEqual(len(db_connection.threads), 0)
cherrypy.engine.stop()
self.assertEqual(cherrypy.engine.state, 0)
self.assertEqual(db_connection.running, False)
self.assertEqual(len(db_connection.threads), 0)
- cherrypy.server.stop()
def test_2_KeyboardInterrupt(self):
if self.server_class:
# Raise a keyboard interrupt in the HTTP server's main thread.
# We must start the server in this, the main thread
- cherrypy.engine.start(blocking=False)
+ cherrypy.engine.start()
cherrypy.server.start()
self.persistent = True
@@ -193,7 +197,7 @@ class ServerStateTests(helper.CPWebCase):
# servers, this should occur in one of the worker threads.
# This should raise a BadStatusLine error, since the worker
# thread will just die without writing a response.
- cherrypy.engine.start(blocking=False)
+ cherrypy.engine.start()
cherrypy.server.start()
try:
@@ -210,17 +214,18 @@ class ServerStateTests(helper.CPWebCase):
self.assertEqual(len(db_connection.threads), 0)
def test_3_Deadlocks(self):
- cherrypy.engine.start(blocking=False)
+ cherrypy.engine.start()
cherrypy.server.start()
try:
- self.assertNotEqual(cherrypy.engine.monitor_thread, None)
+ self.assertNotEqual(cherrypy._timeout_monitor.thread, None)
# Request a "normal" page.
- self.assertEqual(cherrypy.engine.servings, [])
+ self.assertEqual(cherrypy._timeout_monitor.servings, [])
self.getPage("/")
self.assertBody("Hello World")
# request.close is called async.
- while cherrypy.engine.servings:
+ while cherrypy._timeout_monitor.servings:
+ print ".",
time.sleep(0.01)
# Request a page that explicitly checks itself for deadlock.
@@ -236,7 +241,6 @@ class ServerStateTests(helper.CPWebCase):
self.assertInBody("raise cherrypy.TimeoutError()")
finally:
cherrypy.engine.stop()
- cherrypy.server.stop()
def test_4_Autoreload(self):
if not self.server_class:
@@ -254,30 +258,27 @@ class ServerStateTests(helper.CPWebCase):
if self.scheme == "https":
args.append('-ssl')
pid = os.spawnl(os.P_NOWAIT, sys.executable, *args)
- pid = str(pid)
cherrypy._cpserver.wait_for_occupied_port(host, port)
try:
- self.getPage("/pid")
- assert self.body.isdigit(), self.body
- pid = self.body
+ self.getPage("/start")
+ start = float(self.body)
# Give the autoreloader time to cache the file time.
time.sleep(2)
# Touch the file
- f = open(demoscript, 'ab')
- f.write(" ")
- f.close()
+ os.utime(demoscript, None)
# Give the autoreloader time to re-exec the process
time.sleep(2)
cherrypy._cpserver.wait_for_occupied_port(host, port)
self.getPage("/pid")
- assert self.body.isdigit(), self.body
- self.assertNotEqual(self.body, pid)
- pid = self.body
+ pid = int(self.body)
+
+ self.getPage("/start")
+ self.assert_(float(self.body) > start)
finally:
# Shut down the spawned process
self.getPage("/stop")
@@ -288,7 +289,7 @@ class ServerStateTests(helper.CPWebCase):
print os.wait()
except AttributeError:
# Windows
- print os.waitpid(int(pid), 0)
+ print os.waitpid(pid, 0)
except OSError, x:
if x.args != (10, 'No child processes'):
raise
@@ -299,13 +300,15 @@ def run(server, conf):
helper.setConfig(conf)
ServerStateTests.server_class = server
suite = helper.CPTestLoader.loadTestsFromTestCase(ServerStateTests)
+ engine = cherrypy.engine
try:
global db_connection
db_connection = Dependency()
- cherrypy.engine.on_start_engine_list.append(db_connection.start)
- cherrypy.engine.on_stop_engine_list.append(db_connection.stop)
- cherrypy.engine.on_start_thread_list.append(db_connection.startthread)
- cherrypy.engine.on_stop_thread_list.append(db_connection.stopthread)
+ engine.subscribe('start', db_connection.start)
+ engine.subscribe('stop', db_connection.stop)
+ engine.subscribe('graceful', db_connection.graceful)
+ engine.subscribe('start_thread', db_connection.startthread)
+ engine.subscribe('stop_thread', db_connection.stopthread)
try:
import pyconquer
@@ -321,8 +324,7 @@ def run(server, conf):
tr.stop()
tr.out.close()
finally:
- cherrypy.server.stop()
- cherrypy.engine.stop()
+ engine.stop()
def run_all(host, port, ssl=False):
diff --git a/cherrypy/test/test_states_demo.py b/cherrypy/test/test_states_demo.py
index a8f367df..950816ce 100644
--- a/cherrypy/test/test_states_demo.py
+++ b/cherrypy/test/test_states_demo.py
@@ -1,5 +1,7 @@
import os
import sys
+import time
+starttime = time.time()
import cherrypy
@@ -10,13 +12,20 @@ class Root:
return "Hello World"
index.exposed = True
+ def mtimes(self):
+ return repr(cherrypy.engine.publish("Autoreloader", "mtimes"))
+ mtimes.exposed = True
+
def pid(self):
return str(os.getpid())
pid.exposed = True
+ def start(self):
+ return repr(starttime)
+ start.exposed = True
+
def stop(self):
cherrypy.engine.stop()
- cherrypy.server.stop()
stop.exposed = True
@@ -37,7 +46,6 @@ if __name__ == '__main__':
# and then immediately call getPage without getting 503.
cherrypy.config.update(conf)
cherrypy.tree.mount(Root(), config={'global': conf})
- cherrypy.engine.start(blocking=False)
+ cherrypy.engine.start()
cherrypy.server.quickstart()
cherrypy.engine.block()
- \ No newline at end of file
diff --git a/cherrypy/test/test_tools.py b/cherrypy/test/test_tools.py
index cde92cab..11f075f7 100644
--- a/cherrypy/test/test_tools.py
+++ b/cherrypy/test/test_tools.py
@@ -207,7 +207,8 @@ def setup_server():
'tools.gzip.priority': 10,
},
}
- cherrypy.tree.mount(root, config=conf)
+ app = cherrypy.tree.mount(root, config=conf)
+ app.request_class.namespaces['myauth'] = myauthtools
# Client-side code #
diff --git a/cherrypy/tutorial/tut02_expose_methods.py b/cherrypy/tutorial/tut02_expose_methods.py
index abde830f..274a6546 100644
--- a/cherrypy/tutorial/tut02_expose_methods.py
+++ b/cherrypy/tutorial/tut02_expose_methods.py
@@ -26,4 +26,5 @@ if __name__ == '__main__':
cherrypy.config.update(os.path.join(os.path.dirname(__file__), 'tutorial.conf'))
cherrypy.server.quickstart()
cherrypy.engine.start()
+ cherrypy.engine.block()
diff --git a/cherrypy/tutorial/tut03_get_and_post.py b/cherrypy/tutorial/tut03_get_and_post.py
index 43f4afb3..2b38877a 100644
--- a/cherrypy/tutorial/tut03_get_and_post.py
+++ b/cherrypy/tutorial/tut03_get_and_post.py
@@ -48,3 +48,4 @@ if __name__ == '__main__':
cherrypy.config.update(os.path.join(os.path.dirname(__file__), 'tutorial.conf'))
cherrypy.server.quickstart()
cherrypy.engine.start()
+ cherrypy.engine.block()
diff --git a/cherrypy/tutorial/tut04_complex_site.py b/cherrypy/tutorial/tut04_complex_site.py
index 42467ca5..3d5a66fa 100644
--- a/cherrypy/tutorial/tut04_complex_site.py
+++ b/cherrypy/tutorial/tut04_complex_site.py
@@ -90,4 +90,5 @@ if __name__ == '__main__':
cherrypy.config.update(os.path.join(os.path.dirname(__file__), 'tutorial.conf'))
cherrypy.server.quickstart()
cherrypy.engine.start()
+ cherrypy.engine.block()
diff --git a/cherrypy/tutorial/tut05_derived_objects.py b/cherrypy/tutorial/tut05_derived_objects.py
index f922e03c..c94935a6 100644
--- a/cherrypy/tutorial/tut05_derived_objects.py
+++ b/cherrypy/tutorial/tut05_derived_objects.py
@@ -77,4 +77,5 @@ if __name__ == '__main__':
cherrypy.config.update(os.path.join(os.path.dirname(__file__), 'tutorial.conf'))
cherrypy.server.quickstart()
cherrypy.engine.start()
+ cherrypy.engine.block()
diff --git a/cherrypy/tutorial/tut06_default_method.py b/cherrypy/tutorial/tut06_default_method.py
index b8b5afdb..66731cab 100644
--- a/cherrypy/tutorial/tut06_default_method.py
+++ b/cherrypy/tutorial/tut06_default_method.py
@@ -58,4 +58,5 @@ if __name__ == '__main__':
cherrypy.config.update(os.path.join(os.path.dirname(__file__), 'tutorial.conf'))
cherrypy.server.quickstart()
cherrypy.engine.start()
+ cherrypy.engine.block()
diff --git a/cherrypy/tutorial/tut07_sessions.py b/cherrypy/tutorial/tut07_sessions.py
index e7bb9772..8f4f2b1b 100644
--- a/cherrypy/tutorial/tut07_sessions.py
+++ b/cherrypy/tutorial/tut07_sessions.py
@@ -38,4 +38,5 @@ if __name__ == '__main__':
cherrypy.config.update(os.path.join(os.path.dirname(__file__), 'tutorial.conf'))
cherrypy.server.quickstart()
cherrypy.engine.start()
+ cherrypy.engine.block()
diff --git a/cherrypy/tutorial/tut08_generators_and_yield.py b/cherrypy/tutorial/tut08_generators_and_yield.py
index 396a1122..1a3e3c43 100644
--- a/cherrypy/tutorial/tut08_generators_and_yield.py
+++ b/cherrypy/tutorial/tut08_generators_and_yield.py
@@ -40,4 +40,5 @@ if __name__ == '__main__':
cherrypy.config.update(os.path.join(os.path.dirname(__file__), 'tutorial.conf'))
cherrypy.server.quickstart()
cherrypy.engine.start()
+ cherrypy.engine.block()
diff --git a/cherrypy/tutorial/tut09_files.py b/cherrypy/tutorial/tut09_files.py
index 7ac531b6..b3f040f6 100644
--- a/cherrypy/tutorial/tut09_files.py
+++ b/cherrypy/tutorial/tut09_files.py
@@ -99,3 +99,4 @@ if __name__ == '__main__':
cherrypy.config.update(os.path.join(os.path.dirname(__file__), 'tutorial.conf'))
cherrypy.server.quickstart()
cherrypy.engine.start()
+ cherrypy.engine.block()
diff --git a/cherrypy/tutorial/tut10_http_errors.py b/cherrypy/tutorial/tut10_http_errors.py
index 0ae31b89..a64fbbed 100644
--- a/cherrypy/tutorial/tut10_http_errors.py
+++ b/cherrypy/tutorial/tut10_http_errors.py
@@ -76,3 +76,4 @@ if __name__ == '__main__':
cherrypy.config.update(os.path.join(os.path.dirname(__file__), 'tutorial.conf'))
cherrypy.server.quickstart()
cherrypy.engine.start()
+ cherrypy.engine.block()
diff --git a/cherrypy/wsgiserver/__init__.py b/cherrypy/wsgiserver/__init__.py
index d7c1d1ab..4eecbe7e 100644
--- a/cherrypy/wsgiserver/__init__.py
+++ b/cherrypy/wsgiserver/__init__.py
@@ -756,7 +756,7 @@ class CherryPyWSGIServer(object):
"""
protocol = "HTTP/1.1"
- version = "CherryPy/3.0.1"
+ version = "CherryPy/3.1alpha"
ready = False
_interrupt = None
ConnectionClass = HTTPConnection