1 """WSGI handler for serving chained WSGI modules from physical files
3 The WSGI handler in this module ensures that the SCRIPT_FILENAME
4 variable is properly set in every request and points out a file that
5 exists and is readable. It then dispatches the request in one of two
6 ways: If the header X-Ash-Python-Handler is set in the request, its
7 value is used as the name of a handler object to dispatch the request
8 to; otherwise, the file extension of the SCRIPT_FILENAME is used to
9 determine the handler object.
11 The name of a handler object is specified as a string, which is split
12 along its last constituent dot. The part left of the dot is the name
13 of a module, which is imported; and the part right of the dot is the
14 name of an object in that module, which should be a callable adhering
15 to the WSGI specification. Alternatively, the module part may be
16 omitted (such that the name is a string with no dots), in which case
17 the handler object is looked up from this module.
19 By default, this module will handle files with the extensions `.wsgi'
20 or `.wsgi3' using the `chain' handler, which chainloads such files and
21 runs them as independent WSGI applications. See its documentation for
24 This module itself contains both an `application' and a `wmain'
25 object. If this module is used by ashd-wsgi(1) or scgi-wsgi(1) so that
26 its wmain function is called, arguments can be specified to it to
27 install handlers for other file extensions. Such arguments take the
28 form `.EXT=HANDLER', where EXT is the file extension to be handled,
29 and HANDLER is a handler name, as described above. For example, the
30 argument `.fpy=my.module.foohandler' can be given to pass requests for
31 `.fpy' files to the function `foohandler' in the module `my.module'
32 (which must, of course, be importable). When writing such handler
33 functions, you may want to use the getmod() function in this module.
36 import sys, os, threading, types, logging, importlib, getopt
37 from . import wsgiutil
39 __all__ = ["application", "wmain", "getmod", "cachedmod", "chain"]
41 log = logging.getLogger("wsgidir")
43 class cachedmod(object):
44 """Cache entry for modules loaded by getmod()
46 Instances of this class are returned by the getmod()
47 function. They contain three data attributes:
48 * mod - The loaded module
49 * lock - A threading.Lock object, which can be used for
50 manipulating this instance in a thread-safe manner
51 * mtime - The time the file was last modified
53 Additional data attributes can be arbitrarily added for recording
54 any meta-data about the module.
56 def __init__(self, mod = None, mtime = -1):
57 self.lock = threading.Lock()
61 class current(object):
63 self.cond = threading.Condition()
65 def wait(self, timeout=None):
67 self.cond.wait(timeout)
71 self.cond.notify_all()
76 cachelock = threading.Lock()
88 """Load the given file as a module, caching it appropriately
90 The given file is loaded and compiled into a Python module. The
91 compiled module is cached and returned upon subsequent requests
92 for the same file, unless the file has changed (as determined by
93 its mtime), in which case the cached module is discarded and the
94 new file contents are reloaded in its place.
96 The return value is an instance of the cachedmod class, which can
97 be used for locking purposes and for storing arbitrary meta-data
98 about the module. See its documentation for details.
103 entry = modcache[path]
105 entry = [threading.Lock(), None]
106 modcache[path] = entry
108 if entry[1] is None or sb.st_mtime > entry[1].mtime:
109 with open(path, "rb") as f:
111 code = compile(text, path, "exec")
112 mod = types.ModuleType(mangle(path))
114 mod.__current__ = current()
116 exec(code, mod.__dict__)
118 mod.__current__.uncurrent()
121 if entry[1] is not None:
122 entry[1].mod.__current__.uncurrent()
123 entry[1] = cachedmod(mod, sb.st_mtime)
126 def importlocal(filename):
128 cf = inspect.currentframe()
129 if cf is None: raise ImportError("could not get current frame")
130 if cf.f_back is None: raise ImportError("could not get caller frame")
131 cfile = cf.f_back.f_code.co_filename
132 if not os.path.exists(cfile):
133 raise ImportError("caller is not in a proper file")
134 path = os.path.realpath(os.path.join(os.path.dirname(cfile), filename))
135 if '.' not in os.path.basename(path):
136 for ext in [".pyl", ".py"]:
137 if os.path.exists(path + ext):
141 raise ImportError("could not resolve file: " + filename)
143 if not os.path.exists(cfile):
144 raise ImportError("no such file: " + filename)
145 return getmod(path).mod
147 class handler(object):
149 self.lock = threading.Lock()
152 self.addext("wsgi", "chain")
153 self.addext("wsgi3", "chain")
155 def resolve(self, name):
157 if name in self.handlers:
158 return self.handlers[name]
161 return globals()[name]
164 mod = importlib.import_module(mname)
165 ret = getattr(mod, hname)
166 self.handlers[name] = ret
169 def addext(self, ext, handler):
170 self.exts[ext] = self.resolve(handler)
172 def handle(self, env, startreq):
173 if not "SCRIPT_FILENAME" in env:
174 log.error("wsgidir called without SCRIPT_FILENAME set")
175 return wsgiutil.simpleerror(env, startreq, 500, "Internal Error", "The server is erroneously configured.")
176 path = env["SCRIPT_FILENAME"]
177 if not os.access(path, os.R_OK):
178 log.error("%s: not readable" % path)
179 return wsgiutil.simpleerror(env, startreq, 500, "Internal Error", "The server is erroneously configured.")
180 if "HTTP_X_ASH_PYTHON_HANDLER" in env:
182 handler = self.resolve(env["HTTP_X_ASH_PYTHON_HANDLER"])
184 log.error("could not load handler %s" % env["HTTP_X_ASH_PYTHON_HANDLER"], exc_info=sys.exc_info())
185 return wsgiutil.simpleerror(env, startreq, 500, "Internal Error", "The server is erroneously configured.")
187 base = os.path.basename(path)
190 log.error("wsgidir called with neither X-Ash-Python-Handler nor a file extension: %s" % path)
191 return wsgiutil.simpleerror(env, startreq, 500, "Internal Error", "The server is erroneously configured.")
193 if not ext in self.exts:
194 log.error("unregistered file extension: %s" % ext)
195 return wsgiutil.simpleerror(env, startreq, 500, "Internal Error", "The server is erroneously configured.")
196 handler = self.exts[ext]
197 return handler(env, startreq)
200 """Main function for ashd(7)-compatible WSGI handlers
202 Returns the `application' function. If any arguments are given,
203 they are parsed according to the module documentation.
208 opts, args = getopt.getopt(argv, "-V")
211 import wsgiref.validate
212 ret = wsgiref.validate.validator(ret)
217 hnd.addext(arg[1:p], arg[p + 1:])
220 def chain(env, startreq):
221 """Chain-loading WSGI handler
223 This handler loads requested files, compiles them and loads them
224 into their own modules. The compiled modules are cached and reused
225 until the file is modified, in which case the previous module is
226 discarded and the new file contents are loaded into a new module
227 in its place. When chaining such modules, an object named `wmain'
228 is first looked for and called with no arguments if found. The
229 object it returns is then used as the WSGI application object for
230 that module, which is reused until the module is reloaded. If
231 `wmain' is not found, an object named `application' is looked for
232 instead. If found, it is used directly as the WSGI application
235 path = env["SCRIPT_FILENAME"]
239 log.error("Exception occurred when loading %s" % path, exc_info=sys.exc_info())
240 return wsgiutil.simpleerror(env, startreq, 500, "Internal Error", "Could not load WSGI handler.")
244 if hasattr(mod, "entry"):
247 if hasattr(mod.mod, "wmain"):
248 entry = mod.mod.wmain()
249 elif hasattr(mod.mod, "application"):
250 entry = mod.mod.application
252 if entry is not None:
253 return entry(env, startreq)
254 return wsgiutil.simpleerror(env, startreq, 500, "Internal Error", "Invalid WSGI handler.")
256 application = handler().handle