1"""WSGI Paste wrapper for mod_python. Requires Python 2.2 or greater. 2 3 4Example httpd.conf section for a Paste app with an ini file:: 5 6 <Location /> 7 SetHandler python-program 8 PythonHandler paste.modpython 9 PythonOption paste.ini /some/location/your/pasteconfig.ini 10 </Location> 11 12Or if you want to load a WSGI application under /your/homedir in the module 13``startup`` and the WSGI app is ``app``:: 14 15 <Location /> 16 SetHandler python-program 17 PythonHandler paste.modpython 18 PythonPath "['/virtual/project/directory'] + sys.path" 19 PythonOption wsgi.application startup::app 20 </Location> 21 22 23If you'd like to use a virtual installation, make sure to add it in the path 24like so:: 25 26 <Location /> 27 SetHandler python-program 28 PythonHandler paste.modpython 29 PythonPath "['/virtual/project/directory', '/virtual/lib/python2.4/'] + sys.path" 30 PythonOption paste.ini /virtual/project/directory/pasteconfig.ini 31 </Location> 32 33Some WSGI implementations assume that the SCRIPT_NAME environ variable will 34always be equal to "the root URL of the app"; Apache probably won't act as 35you expect in that case. You can add another PythonOption directive to tell 36modpython_gateway to force that behavior: 37 38 PythonOption SCRIPT_NAME /mcontrol 39 40Some WSGI applications need to be cleaned up when Apache exits. You can 41register a cleanup handler with yet another PythonOption directive: 42 43 PythonOption wsgi.cleanup module::function 44 45The module.function will be called with no arguments on server shutdown, 46once for each child process or thread. 47 48This module highly based on Robert Brewer's, here: 49http://projects.amor.org/misc/svn/modpython_gateway.py 50""" 51 52import six 53import traceback 54 55try: 56 from mod_python import apache 57except: 58 pass 59from paste.deploy import loadapp 60 61class InputWrapper(object): 62 63 def __init__(self, req): 64 self.req = req 65 66 def close(self): 67 pass 68 69 def read(self, size=-1): 70 return self.req.read(size) 71 72 def readline(self, size=-1): 73 return self.req.readline(size) 74 75 def readlines(self, hint=-1): 76 return self.req.readlines(hint) 77 78 def __iter__(self): 79 line = self.readline() 80 while line: 81 yield line 82 # Notice this won't prefetch the next line; it only 83 # gets called if the generator is resumed. 84 line = self.readline() 85 86 87class ErrorWrapper(object): 88 89 def __init__(self, req): 90 self.req = req 91 92 def flush(self): 93 pass 94 95 def write(self, msg): 96 self.req.log_error(msg) 97 98 def writelines(self, seq): 99 self.write(''.join(seq)) 100 101 102bad_value = ("You must provide a PythonOption '%s', either 'on' or 'off', " 103 "when running a version of mod_python < 3.1") 104 105 106class Handler(object): 107 108 def __init__(self, req): 109 self.started = False 110 111 options = req.get_options() 112 113 # Threading and forking 114 try: 115 q = apache.mpm_query 116 threaded = q(apache.AP_MPMQ_IS_THREADED) 117 forked = q(apache.AP_MPMQ_IS_FORKED) 118 except AttributeError: 119 threaded = options.get('multithread', '').lower() 120 if threaded == 'on': 121 threaded = True 122 elif threaded == 'off': 123 threaded = False 124 else: 125 raise ValueError(bad_value % "multithread") 126 127 forked = options.get('multiprocess', '').lower() 128 if forked == 'on': 129 forked = True 130 elif forked == 'off': 131 forked = False 132 else: 133 raise ValueError(bad_value % "multiprocess") 134 135 env = self.environ = dict(apache.build_cgi_env(req)) 136 137 if 'SCRIPT_NAME' in options: 138 # Override SCRIPT_NAME and PATH_INFO if requested. 139 env['SCRIPT_NAME'] = options['SCRIPT_NAME'] 140 env['PATH_INFO'] = req.uri[len(options['SCRIPT_NAME']):] 141 else: 142 env['SCRIPT_NAME'] = '' 143 env['PATH_INFO'] = req.uri 144 145 env['wsgi.input'] = InputWrapper(req) 146 env['wsgi.errors'] = ErrorWrapper(req) 147 env['wsgi.version'] = (1, 0) 148 env['wsgi.run_once'] = False 149 if env.get("HTTPS") in ('yes', 'on', '1'): 150 env['wsgi.url_scheme'] = 'https' 151 else: 152 env['wsgi.url_scheme'] = 'http' 153 env['wsgi.multithread'] = threaded 154 env['wsgi.multiprocess'] = forked 155 156 self.request = req 157 158 def run(self, application): 159 try: 160 result = application(self.environ, self.start_response) 161 for data in result: 162 self.write(data) 163 if not self.started: 164 self.request.set_content_length(0) 165 if hasattr(result, 'close'): 166 result.close() 167 except: 168 traceback.print_exc(None, self.environ['wsgi.errors']) 169 if not self.started: 170 self.request.status = 500 171 self.request.content_type = 'text/plain' 172 data = "A server error occurred. Please contact the administrator." 173 self.request.set_content_length(len(data)) 174 self.request.write(data) 175 176 def start_response(self, status, headers, exc_info=None): 177 if exc_info: 178 try: 179 if self.started: 180 six.reraise(exc_info[0], exc_info[1], exc_info[2]) 181 finally: 182 exc_info = None 183 184 self.request.status = int(status[:3]) 185 186 for key, val in headers: 187 if key.lower() == 'content-length': 188 self.request.set_content_length(int(val)) 189 elif key.lower() == 'content-type': 190 self.request.content_type = val 191 else: 192 self.request.headers_out.add(key, val) 193 194 return self.write 195 196 def write(self, data): 197 if not self.started: 198 self.started = True 199 self.request.write(data) 200 201 202startup = None 203cleanup = None 204wsgiapps = {} 205 206def handler(req): 207 options = req.get_options() 208 # Run a startup function if requested. 209 global startup 210 if 'wsgi.startup' in options and not startup: 211 func = options['wsgi.startup'] 212 if func: 213 module_name, object_str = func.split('::', 1) 214 module = __import__(module_name, globals(), locals(), ['']) 215 startup = apache.resolve_object(module, object_str) 216 startup(req) 217 218 # Register a cleanup function if requested. 219 global cleanup 220 if 'wsgi.cleanup' in options and not cleanup: 221 func = options['wsgi.cleanup'] 222 if func: 223 module_name, object_str = func.split('::', 1) 224 module = __import__(module_name, globals(), locals(), ['']) 225 cleanup = apache.resolve_object(module, object_str) 226 def cleaner(data): 227 cleanup() 228 try: 229 # apache.register_cleanup wasn't available until 3.1.4. 230 apache.register_cleanup(cleaner) 231 except AttributeError: 232 req.server.register_cleanup(req, cleaner) 233 234 # Import the wsgi 'application' callable and pass it to Handler.run 235 global wsgiapps 236 appini = options.get('paste.ini') 237 app = None 238 if appini: 239 if appini not in wsgiapps: 240 wsgiapps[appini] = loadapp("config:%s" % appini) 241 app = wsgiapps[appini] 242 243 # Import the wsgi 'application' callable and pass it to Handler.run 244 appwsgi = options.get('wsgi.application') 245 if appwsgi and not appini: 246 modname, objname = appwsgi.split('::', 1) 247 module = __import__(modname, globals(), locals(), ['']) 248 app = getattr(module, objname) 249 250 Handler(req).run(app) 251 252 # status was set in Handler; always return apache.OK 253 return apache.OK 254