modpython.py 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  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 traceback
  38. try:
  39. from mod_python import apache
  40. except:
  41. pass
  42. from paste.deploy import loadapp
  43. class InputWrapper(object):
  44. def __init__(self, req):
  45. self.req = req
  46. def close(self):
  47. pass
  48. def read(self, size=-1):
  49. return self.req.read(size)
  50. def readline(self, size=-1):
  51. return self.req.readline(size)
  52. def readlines(self, hint=-1):
  53. return self.req.readlines(hint)
  54. def __iter__(self):
  55. line = self.readline()
  56. while line:
  57. yield line
  58. # Notice this won't prefetch the next line; it only
  59. # gets called if the generator is resumed.
  60. line = self.readline()
  61. class ErrorWrapper(object):
  62. def __init__(self, req):
  63. self.req = req
  64. def flush(self):
  65. pass
  66. def write(self, msg):
  67. self.req.log_error(msg)
  68. def writelines(self, seq):
  69. self.write(''.join(seq))
  70. bad_value = ("You must provide a PythonOption '%s', either 'on' or 'off', "
  71. "when running a version of mod_python < 3.1")
  72. class Handler(object):
  73. def __init__(self, req):
  74. self.started = False
  75. options = req.get_options()
  76. # Threading and forking
  77. try:
  78. q = apache.mpm_query
  79. threaded = q(apache.AP_MPMQ_IS_THREADED)
  80. forked = q(apache.AP_MPMQ_IS_FORKED)
  81. except AttributeError:
  82. threaded = options.get('multithread', '').lower()
  83. if threaded == 'on':
  84. threaded = True
  85. elif threaded == 'off':
  86. threaded = False
  87. else:
  88. raise ValueError(bad_value % "multithread")
  89. forked = options.get('multiprocess', '').lower()
  90. if forked == 'on':
  91. forked = True
  92. elif forked == 'off':
  93. forked = False
  94. else:
  95. raise ValueError(bad_value % "multiprocess")
  96. env = self.environ = dict(apache.build_cgi_env(req))
  97. if 'SCRIPT_NAME' in options:
  98. # Override SCRIPT_NAME and PATH_INFO if requested.
  99. env['SCRIPT_NAME'] = options['SCRIPT_NAME']
  100. env['PATH_INFO'] = req.uri[len(options['SCRIPT_NAME']):]
  101. else:
  102. env['SCRIPT_NAME'] = ''
  103. env['PATH_INFO'] = req.uri
  104. env['wsgi.input'] = InputWrapper(req)
  105. env['wsgi.errors'] = ErrorWrapper(req)
  106. env['wsgi.version'] = (1, 0)
  107. env['wsgi.run_once'] = False
  108. if env.get("HTTPS") in ('yes', 'on', '1'):
  109. env['wsgi.url_scheme'] = 'https'
  110. else:
  111. env['wsgi.url_scheme'] = 'http'
  112. env['wsgi.multithread'] = threaded
  113. env['wsgi.multiprocess'] = forked
  114. self.request = req
  115. def run(self, application):
  116. try:
  117. result = application(self.environ, self.start_response)
  118. for data in result:
  119. self.write(data)
  120. if not self.started:
  121. self.request.set_content_length(0)
  122. if hasattr(result, 'close'):
  123. result.close()
  124. except:
  125. traceback.print_exc(None, self.environ['wsgi.errors'])
  126. if not self.started:
  127. self.request.status = 500
  128. self.request.content_type = 'text/plain'
  129. data = "A server error occurred. Please contact the administrator."
  130. self.request.set_content_length(len(data))
  131. self.request.write(data)
  132. def start_response(self, status, headers, exc_info=None):
  133. if exc_info:
  134. try:
  135. if self.started:
  136. raise exc_info[0], exc_info[1], exc_info[2]
  137. finally:
  138. exc_info = None
  139. self.request.status = int(status[:3])
  140. for key, val in headers:
  141. if key.lower() == 'content-length':
  142. self.request.set_content_length(int(val))
  143. elif key.lower() == 'content-type':
  144. self.request.content_type = val
  145. else:
  146. self.request.headers_out.add(key, val)
  147. return self.write
  148. def write(self, data):
  149. if not self.started:
  150. self.started = True
  151. self.request.write(data)
  152. startup = None
  153. cleanup = None
  154. wsgiapps = {}
  155. def handler(req):
  156. options = req.get_options()
  157. # Run a startup function if requested.
  158. global startup
  159. if 'wsgi.startup' in options and not startup:
  160. func = options['wsgi.startup']
  161. if func:
  162. module_name, object_str = func.split('::', 1)
  163. module = __import__(module_name, globals(), locals(), [''])
  164. startup = apache.resolve_object(module, object_str)
  165. startup(req)
  166. # Register a cleanup function if requested.
  167. global cleanup
  168. if 'wsgi.cleanup' in options and not cleanup:
  169. func = options['wsgi.cleanup']
  170. if func:
  171. module_name, object_str = func.split('::', 1)
  172. module = __import__(module_name, globals(), locals(), [''])
  173. cleanup = apache.resolve_object(module, object_str)
  174. def cleaner(data):
  175. cleanup()
  176. try:
  177. # apache.register_cleanup wasn't available until 3.1.4.
  178. apache.register_cleanup(cleaner)
  179. except AttributeError:
  180. req.server.register_cleanup(req, cleaner)
  181. # Import the wsgi 'application' callable and pass it to Handler.run
  182. global wsgiapps
  183. appini = options.get('paste.ini')
  184. app = None
  185. if appini:
  186. if appini not in wsgiapps:
  187. wsgiapps[appini] = loadapp("config:%s" % appini)
  188. app = wsgiapps[appini]
  189. # Import the wsgi 'application' callable and pass it to Handler.run
  190. appwsgi = options.get('wsgi.application')
  191. if appwsgi and not appini:
  192. modname, objname = appwsgi.split('::', 1)
  193. module = __import__(modname, globals(), locals(), [''])
  194. app = getattr(module, objname)
  195. Handler(req).run(app)
  196. # status was set in Handler; always return apache.OK
  197. return apache.OK