python: Added more useful logging to wsgidir.
[ashd.git] / python / ashd / wsgidir.py
... / ...
CommitLineData
1"""WSGI handler for serving chained WSGI modules from physical files
2
3The WSGI handler in this module ensures that the SCRIPT_FILENAME
4variable is properly set in every request and points out a file that
5exists and is readable. It then dispatches the request in one of two
6ways: If the header X-Ash-Python-Handler is set in the request, its
7value is used as the name of a handler object to dispatch the request
8to; otherwise, the file extension of the SCRIPT_FILENAME is used to
9determine the handler object.
10
11The name of a handler object is specified as a string, which is split
12along its last constituent dot. The part left of the dot is the name
13of a module, which is imported; and the part right of the dot is the
14name of an object in that module, which should be a callable adhering
15to the WSGI specification. Alternatively, the module part may be
16omitted (such that the name is a string with no dots), in which case
17the handler object is looked up from this module.
18
19By default, this module will handle files with the extensions `.wsgi'
20or `.wsgi2' using the `chain' handler, which chainloads such files and
21runs them as independent WSGI applications. See its documentation for
22details.
23
24This module itself contains both an `application' and a `wmain'
25object. If this module is used by ashd-wsgi(1) or scgi-wsgi(1) so that
26its wmain function is called, arguments can be specified to it to
27install handlers for other file extensions. Such arguments take the
28form `.EXT=HANDLER', where EXT is the file extension to be handled,
29and HANDLER is a handler name, as described above. For example, the
30argument `.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
33functions, you may want to use the getmod() function in this module.
34"""
35
36import sys, os, threading, types, logging, getopt
37import wsgiutil
38
39__all__ = ["application", "wmain", "getmod", "cachedmod", "chain"]
40
41log = logging.getLogger("wsgidir")
42
43class cachedmod(object):
44 """Cache entry for modules loaded by getmod()
45
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
52
53 Additional data attributes can be arbitrarily added for recording
54 any meta-data about the module.
55 """
56 def __init__(self, mod = None, mtime = -1):
57 self.lock = threading.Lock()
58 self.mod = mod
59 self.mtime = mtime
60
61modcache = {}
62cachelock = threading.Lock()
63
64def mangle(path):
65 ret = ""
66 for c in path:
67 if c.isalnum():
68 ret += c
69 else:
70 ret += "_"
71 return ret
72
73def getmod(path):
74 """Load the given file as a module, caching it appropriately
75
76 The given file is loaded and compiled into a Python module. The
77 compiled module is cached and returned upon subsequent requests
78 for the same file, unless the file has changed (as determined by
79 its mtime), in which case the cached module is discarded and the
80 new file contents are reloaded in its place.
81
82 The return value is an instance of the cachedmod class, which can
83 be used for locking purposes and for storing arbitrary meta-data
84 about the module. See its documentation for details.
85 """
86 sb = os.stat(path)
87 cachelock.acquire()
88 try:
89 if path in modcache:
90 entry = modcache[path]
91 else:
92 entry = [threading.Lock(), None]
93 modcache[path] = entry
94 finally:
95 cachelock.release()
96 entry[0].acquire()
97 try:
98 if entry[1] is None or sb.st_mtime > entry[1].mtime:
99 f = open(path, "r")
100 try:
101 text = f.read()
102 finally:
103 f.close()
104 code = compile(text, path, "exec")
105 mod = types.ModuleType(mangle(path))
106 mod.__file__ = path
107 exec code in mod.__dict__
108 entry[1] = cachedmod(mod, sb.st_mtime)
109 return entry[1]
110 finally:
111 entry[0].release()
112
113class handler(object):
114 def __init__(self):
115 self.lock = threading.Lock()
116 self.handlers = {}
117 self.exts = {}
118 self.addext("wsgi", "chain")
119 self.addext("wsgi2", "chain")
120
121 def resolve(self, name):
122 self.lock.acquire()
123 try:
124 if name in self.handlers:
125 return self.handlers[name]
126 p = name.rfind('.')
127 if p < 0:
128 return globals()[name]
129 mname = name[:p]
130 hname = name[p + 1:]
131 mod = __import__(mname, fromlist = ["dummy"])
132 ret = getattr(mod, hname)
133 self.handlers[name] = ret
134 return ret
135 finally:
136 self.lock.release()
137
138 def addext(self, ext, handler):
139 self.exts[ext] = self.resolve(handler)
140
141 def handle(self, env, startreq):
142 if not "SCRIPT_FILENAME" in env:
143 log.error("wsgidir called without SCRIPT_FILENAME set")
144 return wsgiutil.simpleerror(env, startreq, 500, "Internal Error", "The server is erroneously configured.")
145 path = env["SCRIPT_FILENAME"]
146 if not os.access(path, os.R_OK):
147 log.error("%s: not readable" % path)
148 return wsgiutil.simpleerror(env, startreq, 500, "Internal Error", "The server is erroneously configured.")
149 if "HTTP_X_ASH_PYTHON_HANDLER" in env:
150 try:
151 handler = self.resolve(env["HTTP_X_ASH_PYTHON_HANDLER"])
152 except Exception:
153 log.error("could not load handler %s" % env["HTTP_X_ASH_PYTHON_HANDLER"], exc_info=sys.exc_info())
154 return wsgiutil.simpleerror(env, startreq, 500, "Internal Error", "The server is erroneously configured.")
155 else:
156 base = os.path.basename(path)
157 p = base.rfind('.')
158 if p < 0:
159 log.error("wsgidir called with neither X-Ash-Python-Handler nor a file extension: %s" % path)
160 return wsgiutil.simpleerror(env, startreq, 500, "Internal Error", "The server is erroneously configured.")
161 ext = base[p + 1:]
162 if not ext in self.exts:
163 log.error("unregistered file extension: %s" % ext)
164 return wsgiutil.simpleerror(env, startreq, 500, "Internal Error", "The server is erroneously configured.")
165 handler = self.exts[ext]
166 return handler(env, startreq)
167
168def wmain(*argv):
169 """Main function for ashd(7)-compatible WSGI handlers
170
171 Returns the `application' function. If any arguments are given,
172 they are parsed according to the module documentation.
173 """
174 hnd = handler()
175 ret = hnd.handle
176
177 opts, args = getopt.getopt(argv, "V")
178 for o, a in opts:
179 if o == "-V":
180 import wsgiref.validate
181 ret = wsgiref.validate.validator(ret)
182
183 for arg in args:
184 if arg[0] == '.':
185 p = arg.index('=')
186 hnd.addext(arg[1:p], arg[p + 1:])
187 return ret
188
189def chain(env, startreq):
190 """Chain-loading WSGI handler
191
192 This handler loads requested files, compiles them and loads them
193 into their own modules. The compiled modules are cached and reused
194 until the file is modified, in which case the previous module is
195 discarded and the new file contents are loaded into a new module
196 in its place. When chaining such modules, an object named `wmain'
197 is first looked for and called with no arguments if found. The
198 object it returns is then used as the WSGI application object for
199 that module, which is reused until the module is reloaded. If
200 `wmain' is not found, an object named `application' is looked for
201 instead. If found, it is used directly as the WSGI application
202 object.
203 """
204 path = env["SCRIPT_FILENAME"]
205 try:
206 mod = getmod(path)
207 except Exception:
208 log.error("Exception occurred when loading %s" % path, exc_info=sys.exc_info())
209 return wsgiutil.simpleerror(env, startreq, 500, "Internal Error", "Could not load WSGI handler.")
210 entry = None
211 if mod is not None:
212 mod.lock.acquire()
213 try:
214 if hasattr(mod, "entry"):
215 entry = mod.entry
216 else:
217 if hasattr(mod.mod, "wmain"):
218 entry = mod.mod.wmain()
219 elif hasattr(mod.mod, "application"):
220 entry = mod.mod.application
221 mod.entry = entry
222 finally:
223 mod.lock.release()
224 if entry is not None:
225 return entry(env, startreq)
226 return wsgiutil.simpleerror(env, startreq, 500, "Internal Error", "Invalid WSGI handler.")
227
228application = handler().handle