modpython.py 7.6 KB

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