diff --git a/src/cherrypy/__init__.py b/src/cherrypy/__init__.py index 317e465062..274f00694b 100644 --- a/src/cherrypy/__init__.py +++ b/src/cherrypy/__init__.py @@ -57,7 +57,7 @@ These API's are described in the CherryPy specification: http://www.cherrypy.org/wiki/CherryPySpec """ -__version__ = "3.1.0" +__version__ = "3.1.1" from urlparse import urljoin as _urljoin @@ -337,6 +337,10 @@ class _ThreadLocalProxy(object): def __len__(self): child = getattr(serving, self.__attrname__) return len(child) + + def __nonzero__(self): + child = getattr(serving, self.__attrname__) + return bool(child) # Create request and response object (the same objects will be used diff --git a/src/cherrypy/_cpconfig.py b/src/cherrypy/_cpconfig.py index 815bb1936e..adc9911b17 100644 --- a/src/cherrypy/_cpconfig.py +++ b/src/cherrypy/_cpconfig.py @@ -303,13 +303,26 @@ def _engine_namespace_handler(k, v): elif k == 'autoreload_match': engine.autoreload.match = v elif k == 'reload_files': - engine.autoreload.files = v + engine.autoreload.files = set(v) elif k == 'deadlock_poll_freq': engine.timeout_monitor.frequency = v elif k == 'SIGHUP': engine.listeners['SIGHUP'] = set([v]) elif k == 'SIGTERM': engine.listeners['SIGTERM'] = set([v]) + elif "." in k: + plugin, attrname = k.split(".", 1) + plugin = getattr(engine, plugin) + if attrname == 'on': + if v and callable(getattr(plugin, 'subscribe', None)): + plugin.subscribe() + return + elif (not v) and callable(getattr(plugin, 'unsubscribe', None)): + plugin.unsubscribe() + return + setattr(plugin, attrname, v) + else: + setattr(engine, k, v) Config.namespaces["engine"] = _engine_namespace_handler diff --git a/src/cherrypy/_cpdispatch.py b/src/cherrypy/_cpdispatch.py index 4e7104bf72..c29fb05bbe 100644 --- a/src/cherrypy/_cpdispatch.py +++ b/src/cherrypy/_cpdispatch.py @@ -21,7 +21,127 @@ class PageHandler(object): self.kwargs = kwargs def __call__(self): - return self.callable(*self.args, **self.kwargs) + try: + return self.callable(*self.args, **self.kwargs) + except TypeError, x: + test_callable_spec(self.callable, self.args, self.kwargs) + raise + +def test_callable_spec(callable, callable_args, callable_kwargs): + """ + Inspect callable and test to see if the given args are suitable for it. + + When an error occurs during the handler's invoking stage there are 2 + erroneous cases: + 1. Too many parameters passed to a function which doesn't define + one of *args or **kwargs. + 2. Too little parameters are passed to the function. + + There are 3 sources of parameters to a cherrypy handler. + 1. query string parameters are passed as keyword parameters to the handler. + 2. body parameters are also passed as keyword parameters. + 3. when partial matching occurs, the final path atoms are passed as + positional args. + Both the query string and path atoms are part of the URI. If they are + incorrect, then a 404 Not Found should be raised. Conversely the body + parameters are part of the request; if they are invalid a 400 Bad Request. + """ + (args, varargs, varkw, defaults) = inspect.getargspec(callable) + + if args and args[0] == 'self': + args = args[1:] + + arg_usage = dict([(arg, 0,) for arg in args]) + vararg_usage = 0 + varkw_usage = 0 + extra_kwargs = set() + + for i, value in enumerate(callable_args): + try: + arg_usage[args[i]] += 1 + except IndexError: + vararg_usage += 1 + + for key in callable_kwargs.keys(): + try: + arg_usage[key] += 1 + except KeyError: + varkw_usage += 1 + extra_kwargs.add(key) + + for i, val in enumerate(defaults or []): + # Defaults take effect only when the arg hasn't been used yet. + if arg_usage[args[i]] == 0: + arg_usage[args[i]] += 1 + + missing_args = [] + multiple_args = [] + for key, usage in arg_usage.iteritems(): + if usage == 0: + missing_args.append(key) + elif usage > 1: + multiple_args.append(key) + + if missing_args: + # In the case where the method allows body arguments + # there are 3 potential errors: + # 1. not enough query string parameters -> 404 + # 2. not enough body parameters -> 400 + # 3. not enough path parts (partial matches) -> 404 + # + # We can't actually tell which case it is, + # so I'm raising a 404 because that covers 2/3 of the + # possibilities + # + # In the case where the method does not allow body + # arguments it's definitely a 404. + raise cherrypy.HTTPError(404, + message="Missing parameters: %s" % ",".join(missing_args)) + + # the extra positional arguments come from the path - 404 Not Found + if not varargs and vararg_usage > 0: + raise cherrypy.HTTPError(404) + + body_params = cherrypy.request.body_params or {} + body_params = set(body_params.keys()) + qs_params = set(callable_kwargs.keys()) - body_params + + if multiple_args: + + if qs_params.intersection(set(multiple_args)): + # If any of the multiple parameters came from the query string then + # it's a 404 Not Found + error = 404 + else: + # Otherwise it's a 400 Bad Request + error = 400 + + raise cherrypy.HTTPError(error, + message="Multiple values for parameters: "\ + "%s" % ",".join(multiple_args)) + + if not varkw and varkw_usage > 0: + + # If there were extra query string parameters, it's a 404 Not Found + extra_qs_params = set(qs_params).intersection(extra_kwargs) + if extra_qs_params: + raise cherrypy.HTTPError(404, + message="Unexpected query string "\ + "parameters: %s" % ", ".join(extra_qs_params)) + + # If there were any extra body parameters, it's a 400 Not Found + extra_body_params = set(body_params).intersection(extra_kwargs) + if extra_body_params: + raise cherrypy.HTTPError(400, + message="Unexpected body parameters: "\ + "%s" % ", ".join(extra_body_params)) + + +try: + import inspect +except ImportError: + test_callable_spec = lambda callable, args, kwargs: None + class LateParamPageHandler(PageHandler): diff --git a/src/cherrypy/_cplogging.py b/src/cherrypy/_cplogging.py index 12c93d3cd8..8556e108f9 100644 --- a/src/cherrypy/_cplogging.py +++ b/src/cherrypy/_cplogging.py @@ -126,7 +126,6 @@ class LogManager(object): if stream is None: stream=sys.stderr h = logging.StreamHandler(stream) - h.setLevel(logging.DEBUG) h.setFormatter(logfmt) h._cpbuiltin = "screen" log.addHandler(h) @@ -149,7 +148,6 @@ class LogManager(object): def _add_builtin_file_handler(self, log, fname): h = logging.FileHandler(fname) - h.setLevel(logging.DEBUG) h.setFormatter(logfmt) h._cpbuiltin = "file" log.addHandler(h) @@ -197,7 +195,6 @@ class LogManager(object): if enable: if not h: h = WSGIErrorHandler() - h.setLevel(logging.DEBUG) h.setFormatter(logfmt) h._cpbuiltin = "wsgi" log.addHandler(h) diff --git a/src/cherrypy/_cprequest.py b/src/cherrypy/_cprequest.py index 9ec310c972..3b245519cb 100644 --- a/src/cherrypy/_cprequest.py +++ b/src/cherrypy/_cprequest.py @@ -8,7 +8,7 @@ import types import cherrypy from cherrypy import _cpcgifs, _cpconfig from cherrypy._cperror import format_exc, bare_error -from cherrypy.lib import http +from cherrypy.lib import http, file_generator class Hook(object): @@ -747,15 +747,6 @@ class Request(object): cherrypy.response.finalize() -def file_generator(input, chunkSize=65536): - """Yield the given input (a file object) in chunks (default 64k). (Core)""" - chunk = input.read(chunkSize) - while chunk: - yield chunk - chunk = input.read(chunkSize) - input.close() - - class Body(object): """The body of the HTTP response (the response entity).""" diff --git a/src/cherrypy/_cpserver.py b/src/cherrypy/_cpserver.py index 53259cb086..0888295bec 100644 --- a/src/cherrypy/_cpserver.py +++ b/src/cherrypy/_cpserver.py @@ -49,6 +49,7 @@ class Server(ServerAdapter): protocol_version = 'HTTP/1.1' reverse_dns = False thread_pool = 10 + thread_pool_max = -1 max_request_header_size = 500 * 1024 max_request_body_size = 100 * 1024 * 1024 instance = None diff --git a/src/cherrypy/_cptools.py b/src/cherrypy/_cptools.py index 930ddab277..80ff583d22 100644 --- a/src/cherrypy/_cptools.py +++ b/src/cherrypy/_cptools.py @@ -466,7 +466,7 @@ _d.log_tracebacks = Tool('before_error_response', cptools.log_traceback) _d.log_headers = Tool('before_error_response', cptools.log_request_headers) _d.log_hooks = Tool('on_end_request', cptools.log_hooks, priority=100) _d.err_redirect = ErrorTool(cptools.redirect) -_d.etags = Tool('before_finalize', cptools.validate_etags) +_d.etags = Tool('before_finalize', cptools.validate_etags, priority=75) _d.decode = Tool('before_handler', encoding.decode) # the order of encoding, gzip, caching is important _d.encode = Tool('before_finalize', encoding.encode, priority=70) diff --git a/src/cherrypy/_cptree.py b/src/cherrypy/_cptree.py index 3dcd4a47e5..36d00865a2 100644 --- a/src/cherrypy/_cptree.py +++ b/src/cherrypy/_cptree.py @@ -153,6 +153,8 @@ class Tree(object): root: an instance of a "controller class" (a collection of page handler methods) which represents the root of the application. + This may also be an Application instance, or None if using + a dispatcher other than the default. script_name: a string containing the "mount point" of the application. This should start with a slash, and be the path portion of the URL at which to mount the given root. For example, if root.index() @@ -168,11 +170,15 @@ class Tree(object): if isinstance(root, Application): app = root + if script_name != "" and script_name != app.script_name: + raise ValueError, "Cannot specify a different script name and pass an Application instance to cherrypy.mount" + script_name = app.script_name else: app = Application(root, script_name) # If mounted at "", add favicon.ico - if script_name == "" and root and not hasattr(root, "favicon_ico"): + if (script_name == "" and root is not None + and not hasattr(root, "favicon_ico")): favicon = os.path.join(os.getcwd(), os.path.dirname(__file__), "favicon.ico") root.favicon_ico = tools.staticfile.handler(favicon) diff --git a/src/cherrypy/_cpwsgi_server.py b/src/cherrypy/_cpwsgi_server.py index 5953cf2ab0..ac8bfaa2ec 100644 --- a/src/cherrypy/_cpwsgi_server.py +++ b/src/cherrypy/_cpwsgi_server.py @@ -43,6 +43,7 @@ class CPWSGIServer(wsgiserver.CherryPyWSGIServer): s.__init__(self, bind_addr, cherrypy.tree, server.thread_pool, server.socket_host, + max = server.thread_pool_max, request_queue_size = server.socket_queue_size, timeout = server.socket_timeout, shutdown_timeout = server.shutdown_timeout, diff --git a/src/cherrypy/cherryd b/src/cherrypy/cherryd index 3d5cbdefce..ef1bd7a3d4 100644 --- a/src/cherrypy/cherryd +++ b/src/cherrypy/cherryd @@ -8,9 +8,10 @@ from cherrypy.process import plugins, servers def start(configfiles=None, daemonize=False, environment=None, - fastcgi=False, pidfile=None, imports=None): + fastcgi=False, scgi=False, pidfile=None, imports=None): """Subscribe all engine plugins and start the engine.""" - for i in imports: + sys.path = [''] + sys.path + for i in imports or []: exec "import %s" % i for c in configfiles or []: @@ -35,16 +36,27 @@ def start(configfiles=None, daemonize=False, environment=None, if hasattr(engine, "console_control_handler"): engine.console_control_handler.subscribe() - if fastcgi: - # turn off autoreload when using fastcgi - cherrypy.config.update({'autoreload.on': False}) - + if fastcgi and scgi: + # fastcgi and scgi aren't allowed together. + cherrypy.log.error("fastcgi and scgi aren't allowed together.", 'ENGINE') + sys.exit(1) + elif fastcgi or scgi: + # Turn off autoreload when using fastcgi or scgi. + cherrypy.config.update({'engine.autoreload_on': False}) + # Turn off the default HTTP server (which is subscribed by default). cherrypy.server.unsubscribe() - fastcgi_port = cherrypy.config.get('server.socket_port', 4000) - fastcgi_bindaddr = cherrypy.config.get('server.socket_host', '0.0.0.0') - bindAddress = (fastcgi_bindaddr, fastcgi_port) - f = servers.FlupFCGIServer(application=cherrypy.tree, bindAddress=bindAddress) + sock_file = cherrypy.config.get('server.socket_file', None) + if sock_file: + bindAddress = sock_file + else: + flup_port = cherrypy.config.get('server.socket_port', 4000) + flup_bindaddr = cherrypy.config.get('server.socket_host', '0.0.0.0') + bindAddress = (flup_bindaddr, flup_port) + if fastcgi: + f = servers.FlupFCGIServer(application=cherrypy.tree, bindAddress=bindAddress) + else: + f = servers.FlupSCGIServer(application=cherrypy.tree, bindAddress=bindAddress) s = servers.ServerAdapter(engine, httpserver=f, bind_addr=bindAddress) s.subscribe() @@ -70,6 +82,8 @@ if __name__ == '__main__': help="apply the given config environment") p.add_option('-f', action="store_true", dest='fastcgi', help="start a fastcgi server instead of the default HTTP server") + p.add_option('-s', action="store_true", dest='scgi', + help="start a scgi server instead of the default HTTP server") p.add_option('-i', '--import', action="append", dest='imports', help="specify modules to import") p.add_option('-p', '--pidfile', dest='pidfile', default=None, @@ -77,6 +91,6 @@ if __name__ == '__main__': options, args = p.parse_args() start(options.config, options.daemonize, - options.environment, options.fastcgi, options.pidfile, + options.environment, options.fastcgi, options.scgi, options.pidfile, options.imports) diff --git a/src/cherrypy/lib/__init__.py b/src/cherrypy/lib/__init__.py index 4e225cb12e..47be2eddd0 100644 --- a/src/cherrypy/lib/__init__.py +++ b/src/cherrypy/lib/__init__.py @@ -133,3 +133,26 @@ def unrepr(s): return _Builder().build(obj) + +def file_generator(input, chunkSize=65536): + """Yield the given input (a file object) in chunks (default 64k). (Core)""" + chunk = input.read(chunkSize) + while chunk: + yield chunk + chunk = input.read(chunkSize) + input.close() + + +def file_generator_limited(fileobj, count, chunk_size=65536): + """Yield the given file object in chunks, stopping after `count` + bytes has been emitted. Default chunk size is 64kB. (Core) + """ + remaining = count + while remaining > 0: + chunk = fileobj.read(min(chunk_size, remaining)) + chunklen = len(chunk) + if chunklen == 0: + return + remaining -= chunklen + yield chunk + diff --git a/src/cherrypy/lib/cptools.py b/src/cherrypy/lib/cptools.py index 966c328945..eefd1ae73a 100644 --- a/src/cherrypy/lib/cptools.py +++ b/src/cherrypy/lib/cptools.py @@ -212,7 +212,7 @@ class SessionAuth(object): def on_check(self, username): pass - def login_screen(self, from_page='..', username='', error_msg=''): + def login_screen(self, from_page='..', username='', error_msg='', **kwargs): return """
Message: %(error_msg)s