|
- """Miscellaneous decorators and other helpers for running a CherryPy
- server"""
-
- import cherrypy
- import sys
- import os
- import decorator
- import simplejson as json
-
- # Decorators
- def chunked_response(func):
- """Decorator to enable chunked responses."""
- # Set this to False to get better tracebacks from some requests
- # (/stream/extract, /stream/intervals).
- func._cp_config = { 'response.stream': True }
- return func
-
- def response_type(content_type):
- """Return a decorator-generating function that sets the
- response type to the specified string."""
- def wrapper(func, *args, **kwargs):
- cherrypy.response.headers['Content-Type'] = content_type
- return func(*args, **kwargs)
- return decorator.decorator(wrapper)
-
- @decorator.decorator
- def workaround_cp_bug_1200(func, *args, **kwargs): # pragma: no cover
- """Decorator to work around CherryPy bug #1200 in a response
- generator.
-
- Even if chunked responses are disabled, LookupError or
- UnicodeError exceptions may still be swallowed by CherryPy due to
- bug #1200. This throws them as generic Exceptions instead so that
- they make it through.
- """
- exc_info = None
- try:
- for val in func(*args, **kwargs):
- yield val
- except (LookupError, UnicodeError):
- # Re-raise it, but maintain the original traceback
- exc_info = sys.exc_info()
- new_exc = Exception(exc_info[0].__name__ + ": " + str(exc_info[1]))
- raise new_exc, None, exc_info[2]
- finally:
- del exc_info
-
- def exception_to_httperror(*expected):
- """Return a decorator-generating function that catches expected
- errors and throws a HTTPError describing it instead.
-
- @exception_to_httperror(NilmDBError, ValueError)
- def foo():
- pass
- """
- def wrapper(func, *args, **kwargs):
- exc_info = None
- try:
- return func(*args, **kwargs)
- except expected:
- # Re-raise it, but maintain the original traceback
- exc_info = sys.exc_info()
- new_exc = cherrypy.HTTPError("400 Bad Request", str(exc_info[1]))
- raise new_exc, None, exc_info[2]
- finally:
- del exc_info
- # We need to preserve the function's argspecs for CherryPy to
- # handle argument errors correctly. Decorator.decorator takes
- # care of that.
- return decorator.decorator(wrapper)
-
- # Custom CherryPy tools
-
- def CORS_allow(methods):
- """This does several things:
-
- Handles CORS preflight requests.
- Adds Allow: header to all requests.
- Raise 405 if request.method not in method.
-
- It is similar to cherrypy.tools.allow, with the CORS stuff added.
-
- Add this to CherryPy with:
- cherrypy.tools.CORS_allow = cherrypy.Tool('on_start_resource', CORS_allow)
- """
- request = cherrypy.request.headers
- response = cherrypy.response.headers
-
- if not isinstance(methods, (tuple, list)): # pragma: no cover
- methods = [ methods ]
- methods = [ m.upper() for m in methods if m ]
- if not methods: # pragma: no cover
- methods = [ 'GET', 'HEAD' ]
- elif 'GET' in methods and 'HEAD' not in methods: # pragma: no cover
- methods.append('HEAD')
- response['Allow'] = ', '.join(methods)
-
- # Allow all origins
- if 'Origin' in request:
- response['Access-Control-Allow-Origin'] = request['Origin']
-
- # If it's a CORS request, send response.
- request_method = request.get("Access-Control-Request-Method", None)
- request_headers = request.get("Access-Control-Request-Headers", None)
- if (cherrypy.request.method == "OPTIONS" and
- request_method and request_headers):
- response['Access-Control-Allow-Headers'] = request_headers
- response['Access-Control-Allow-Methods'] = ', '.join(methods)
- # Try to stop further processing and return a 200 OK
- cherrypy.response.status = "200 OK"
- cherrypy.response.body = ""
- cherrypy.request.handler = lambda: ""
- return
-
- # Reject methods that were not explicitly allowed
- if cherrypy.request.method not in methods:
- raise cherrypy.HTTPError(405)
-
-
- # Helper for json_in tool to process JSON data into normal request
- # parameters.
- def json_to_request_params(body):
- cherrypy.lib.jsontools.json_processor(body)
- if not isinstance(cherrypy.request.json, dict):
- raise cherrypy.HTTPError(415)
- cherrypy.request.params.update(cherrypy.request.json)
-
- # Used as an "error_page.default" handler
- def json_error_page(status, message, traceback, version,
- force_traceback = False):
- """Return a custom error page in JSON so the client can parse it"""
- errordata = { "status" : status,
- "message" : message,
- "traceback" : traceback }
- # Don't send a traceback if the error was 400-499 (client's fault)
- try:
- code = int(status.split()[0])
- if not force_traceback:
- if code >= 400 and code <= 499:
- errordata["traceback"] = ""
- except Exception: # pragma: no cover
- pass
- # Override the response type, which was previously set to text/html
- cherrypy.serving.response.headers['Content-Type'] = (
- "application/json;charset=utf-8" )
- # Undo the HTML escaping that cherrypy's get_error_page function applies
- # (cherrypy issue 1135)
- for k, v in errordata.iteritems():
- v = v.replace("<","<")
- v = v.replace(">",">")
- v = v.replace("&","&")
- errordata[k] = v
- return json.dumps(errordata, separators=(',',':'))
-
- # Start/stop CherryPy standalone server
- def cherrypy_start(blocking = False, event = False, embedded = False):
- """Start the CherryPy server, handling errors and signals
- somewhat gracefully."""
-
- if not embedded: # pragma: no cover
- # Handle signals nicely
- if hasattr(cherrypy.engine, "signal_handler"):
- cherrypy.engine.signal_handler.subscribe()
- if hasattr(cherrypy.engine, "console_control_handler"):
- cherrypy.engine.console_control_handler.subscribe()
-
- # Cherrypy stupidly calls os._exit(70) when it can't bind the
- # port. At least try to print a reasonable error and continue
- # in this case, rather than just dying silently (as we would
- # otherwise do in embedded mode)
- real_exit = os._exit
- def fake_exit(code): # pragma: no cover
- if code == os.EX_SOFTWARE:
- fprintf(sys.stderr, "error: CherryPy called os._exit!\n")
- else:
- real_exit(code)
- os._exit = fake_exit
- cherrypy.engine.start()
- os._exit = real_exit
-
- # Signal that the engine has started successfully
- if event is not None:
- event.set()
-
- if blocking:
- try:
- cherrypy.engine.wait(cherrypy.engine.states.EXITING,
- interval = 0.1, channel = 'main')
- except (KeyboardInterrupt, IOError): # pragma: no cover
- cherrypy.engine.log('Keyboard Interrupt: shutting down bus')
- cherrypy.engine.exit()
- except SystemExit: # pragma: no cover
- cherrypy.engine.log('SystemExit raised: shutting down bus')
- cherrypy.engine.exit()
- raise
-
- # Stop CherryPy server
- def cherrypy_stop():
- cherrypy.engine.exit()
|