普通文本  |  254行  |  7.65 KB

"""WSGI Paste wrapper for mod_python. Requires Python 2.2 or greater.


Example httpd.conf section for a Paste app with an ini file::

    <Location />
        SetHandler python-program
        PythonHandler paste.modpython
        PythonOption paste.ini /some/location/your/pasteconfig.ini
    </Location>

Or if you want to load a WSGI application under /your/homedir in the module
``startup`` and the WSGI app is ``app``::

    <Location />
        SetHandler python-program
        PythonHandler paste.modpython
        PythonPath "['/virtual/project/directory'] + sys.path"
        PythonOption wsgi.application startup::app
    </Location>


If you'd like to use a virtual installation, make sure to add it in the path
like so::

    <Location />
        SetHandler python-program
        PythonHandler paste.modpython
        PythonPath "['/virtual/project/directory', '/virtual/lib/python2.4/'] + sys.path"
        PythonOption paste.ini /virtual/project/directory/pasteconfig.ini
    </Location>

Some WSGI implementations assume that the SCRIPT_NAME environ variable will
always be equal to "the root URL of the app"; Apache probably won't act as
you expect in that case. You can add another PythonOption directive to tell
modpython_gateway to force that behavior:

    PythonOption SCRIPT_NAME /mcontrol

Some WSGI applications need to be cleaned up when Apache exits. You can
register a cleanup handler with yet another PythonOption directive:

    PythonOption wsgi.cleanup module::function

The module.function will be called with no arguments on server shutdown,
once for each child process or thread.

This module highly based on Robert Brewer's, here:
http://projects.amor.org/misc/svn/modpython_gateway.py
"""

import six
import traceback

try:
    from mod_python import apache
except:
    pass
from paste.deploy import loadapp

class InputWrapper(object):

    def __init__(self, req):
        self.req = req

    def close(self):
        pass

    def read(self, size=-1):
        return self.req.read(size)

    def readline(self, size=-1):
        return self.req.readline(size)

    def readlines(self, hint=-1):
        return self.req.readlines(hint)

    def __iter__(self):
        line = self.readline()
        while line:
            yield line
            # Notice this won't prefetch the next line; it only
            # gets called if the generator is resumed.
            line = self.readline()


class ErrorWrapper(object):

    def __init__(self, req):
        self.req = req

    def flush(self):
        pass

    def write(self, msg):
        self.req.log_error(msg)

    def writelines(self, seq):
        self.write(''.join(seq))


bad_value = ("You must provide a PythonOption '%s', either 'on' or 'off', "
             "when running a version of mod_python < 3.1")


class Handler(object):

    def __init__(self, req):
        self.started = False

        options = req.get_options()

        # Threading and forking
        try:
            q = apache.mpm_query
            threaded = q(apache.AP_MPMQ_IS_THREADED)
            forked = q(apache.AP_MPMQ_IS_FORKED)
        except AttributeError:
            threaded = options.get('multithread', '').lower()
            if threaded == 'on':
                threaded = True
            elif threaded == 'off':
                threaded = False
            else:
                raise ValueError(bad_value % "multithread")

            forked = options.get('multiprocess', '').lower()
            if forked == 'on':
                forked = True
            elif forked == 'off':
                forked = False
            else:
                raise ValueError(bad_value % "multiprocess")

        env = self.environ = dict(apache.build_cgi_env(req))

        if 'SCRIPT_NAME' in options:
            # Override SCRIPT_NAME and PATH_INFO if requested.
            env['SCRIPT_NAME'] = options['SCRIPT_NAME']
            env['PATH_INFO'] = req.uri[len(options['SCRIPT_NAME']):]
        else:
            env['SCRIPT_NAME'] = ''
            env['PATH_INFO'] = req.uri

        env['wsgi.input'] = InputWrapper(req)
        env['wsgi.errors'] = ErrorWrapper(req)
        env['wsgi.version'] = (1, 0)
        env['wsgi.run_once'] = False
        if env.get("HTTPS") in ('yes', 'on', '1'):
            env['wsgi.url_scheme'] = 'https'
        else:
            env['wsgi.url_scheme'] = 'http'
        env['wsgi.multithread']  = threaded
        env['wsgi.multiprocess'] = forked

        self.request = req

    def run(self, application):
        try:
            result = application(self.environ, self.start_response)
            for data in result:
                self.write(data)
            if not self.started:
                self.request.set_content_length(0)
            if hasattr(result, 'close'):
                result.close()
        except:
            traceback.print_exc(None, self.environ['wsgi.errors'])
            if not self.started:
                self.request.status = 500
                self.request.content_type = 'text/plain'
                data = "A server error occurred. Please contact the administrator."
                self.request.set_content_length(len(data))
                self.request.write(data)

    def start_response(self, status, headers, exc_info=None):
        if exc_info:
            try:
                if self.started:
                    six.reraise(exc_info[0], exc_info[1], exc_info[2])
            finally:
                exc_info = None

        self.request.status = int(status[:3])

        for key, val in headers:
            if key.lower() == 'content-length':
                self.request.set_content_length(int(val))
            elif key.lower() == 'content-type':
                self.request.content_type = val
            else:
                self.request.headers_out.add(key, val)

        return self.write

    def write(self, data):
        if not self.started:
            self.started = True
        self.request.write(data)


startup = None
cleanup = None
wsgiapps = {}

def handler(req):
    options = req.get_options()
    # Run a startup function if requested.
    global startup
    if 'wsgi.startup' in options and not startup:
        func = options['wsgi.startup']
        if func:
            module_name, object_str = func.split('::', 1)
            module = __import__(module_name, globals(), locals(), [''])
            startup = apache.resolve_object(module, object_str)
            startup(req)

    # Register a cleanup function if requested.
    global cleanup
    if 'wsgi.cleanup' in options and not cleanup:
        func = options['wsgi.cleanup']
        if func:
            module_name, object_str = func.split('::', 1)
            module = __import__(module_name, globals(), locals(), [''])
            cleanup = apache.resolve_object(module, object_str)
            def cleaner(data):
                cleanup()
            try:
                # apache.register_cleanup wasn't available until 3.1.4.
                apache.register_cleanup(cleaner)
            except AttributeError:
                req.server.register_cleanup(req, cleaner)

    # Import the wsgi 'application' callable and pass it to Handler.run
    global wsgiapps
    appini = options.get('paste.ini')
    app = None
    if appini:
        if appini not in wsgiapps:
            wsgiapps[appini] = loadapp("config:%s" % appini)
        app = wsgiapps[appini]

    # Import the wsgi 'application' callable and pass it to Handler.run
    appwsgi = options.get('wsgi.application')
    if appwsgi and not appini:
        modname, objname = appwsgi.split('::', 1)
        module = __import__(modname, globals(), locals(), [''])
        app = getattr(module, objname)

    Handler(req).run(app)

    # status was set in Handler; always return apache.OK
    return apache.OK