diff options
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 |