python: Added XBitHack-style caching to SSI handler.
[ashd.git] / python / ashd / wsgidir.py
CommitLineData
fc2aa563
FT
1"""WSGI handler for serving chained WSGI modules from physical files
2
7ed9b82b
FT
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.
fc2aa563
FT
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
7ed9b82b
FT
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.
fc2aa563
FT
34"""
35
6085469b 36import os, threading, types, getopt
173e0e9e 37import wsgiutil
c06db49a 38
5c1a2105 39__all__ = ["application", "wmain", "getmod", "cachedmod", "chain"]
fc2aa563 40
f677567f 41class cachedmod(object):
fc2aa563
FT
42 """Cache entry for modules loaded by getmod()
43
44 Instances of this class are returned by the getmod()
45 function. They contain three data attributes:
46 * mod - The loaded module
47 * lock - A threading.Lock object, which can be used for
48 manipulating this instance in a thread-safe manner
49 * mtime - The time the file was last modified
50
51 Additional data attributes can be arbitrarily added for recording
52 any meta-data about the module.
53 """
7fe08a6f 54 def __init__(self, mod = None, mtime = -1):
14ef1e0e
FT
55 self.lock = threading.Lock()
56 self.mod = mod
57 self.mtime = mtime
58
c06db49a
FT
59modcache = {}
60cachelock = threading.Lock()
61
62def mangle(path):
63 ret = ""
64 for c in path:
65 if c.isalnum():
66 ret += c
67 else:
68 ret += "_"
69 return ret
70
71def getmod(path):
fc2aa563
FT
72 """Load the given file as a module, caching it appropriately
73
74 The given file is loaded and compiled into a Python module. The
75 compiled module is cached and returned upon subsequent requests
76 for the same file, unless the file has changed (as determined by
77 its mtime), in which case the cached module is discarded and the
78 new file contents are reloaded in its place.
79
80 The return value is an instance of the cachedmod class, which can
81 be used for locking purposes and for storing arbitrary meta-data
82 about the module. See its documentation for details.
83 """
c06db49a
FT
84 sb = os.stat(path)
85 cachelock.acquire()
86 try:
87 if path in modcache:
14ef1e0e 88 entry = modcache[path]
7fe08a6f 89 else:
b8d56e8f 90 entry = [threading.Lock(), None]
7fe08a6f 91 modcache[path] = entry
c06db49a
FT
92 finally:
93 cachelock.release()
b8d56e8f 94 entry[0].acquire()
7fe08a6f 95 try:
b8d56e8f 96 if entry[1] is None or sb.st_mtime > entry[1].mtime:
7fe08a6f
FT
97 f = open(path, "r")
98 try:
99 text = f.read()
100 finally:
101 f.close()
102 code = compile(text, path, "exec")
103 mod = types.ModuleType(mangle(path))
104 mod.__file__ = path
105 exec code in mod.__dict__
b8d56e8f
FT
106 entry[1] = cachedmod(mod, sb.st_mtime)
107 return entry[1]
7fe08a6f 108 finally:
b8d56e8f 109 entry[0].release()
c06db49a 110
e9817fee
FT
111class handler(object):
112 def __init__(self):
113 self.lock = threading.Lock()
114 self.handlers = {}
115 self.exts = {}
116 self.addext("wsgi", "chain")
117 self.addext("wsgi2", "chain")
118
119 def resolve(self, name):
120 self.lock.acquire()
121 try:
122 if name in self.handlers:
123 return self.handlers[name]
124 p = name.rfind('.')
125 if p < 0:
126 return globals()[name]
127 mname = name[:p]
128 hname = name[p + 1:]
129 mod = __import__(mname, fromlist = ["dummy"])
130 ret = getattr(mod, hname)
131 self.handlers[name] = ret
132 return ret
133 finally:
134 self.lock.release()
135
136 def addext(self, ext, handler):
137 self.exts[ext] = self.resolve(handler)
138
139 def handle(self, env, startreq):
140 if not "SCRIPT_FILENAME" in env:
141 return wsgiutil.simpleerror(env, startreq, 500, "Internal Error", "The server is erroneously configured.")
142 path = env["SCRIPT_FILENAME"]
7ed9b82b 143 if not os.access(path, os.R_OK):
e9817fee 144 return wsgiutil.simpleerror(env, startreq, 500, "Internal Error", "The server is erroneously configured.")
7ed9b82b
FT
145 if "HTTP_X_ASH_PYTHON_HANDLER" in env:
146 handler = self.resolve(env["HTTP_X_ASH_PYTHON_HANDLER"])
147 else:
148 base = os.path.basename(path)
149 p = base.rfind('.')
150 if p < 0:
151 return wsgiutil.simpleerror(env, startreq, 500, "Internal Error", "The server is erroneously configured.")
152 ext = base[p + 1:]
153 if not ext in self.exts:
154 return wsgiutil.simpleerror(env, startreq, 500, "Internal Error", "The server is erroneously configured.")
155 handler = self.exts[ext]
156 return handler(env, startreq)
e9817fee
FT
157
158def wmain(*argv):
159 """Main function for ashd(7)-compatible WSGI handlers
160
161 Returns the `application' function. If any arguments are given,
162 they are parsed according to the module documentation.
163 """
6085469b
FT
164 hnd = handler()
165 ret = hnd.handle
166
167 opts, args = getopt.getopt(argv, "V")
168 for o, a in opts:
169 if o == "-V":
170 import wsgiref.validate
171 ret = wsgiref.validate.validator(ret)
172
173 for arg in args:
e9817fee
FT
174 if arg[0] == '.':
175 p = arg.index('=')
6085469b
FT
176 hnd.addext(arg[1:p], arg[p + 1:])
177 return ret
e9817fee 178
9b417336 179def chain(env, startreq):
7ed9b82b
FT
180 """Chain-loading WSGI handler
181
182 This handler loads requested files, compiles them and loads them
183 into their own modules. The compiled modules are cached and reused
184 until the file is modified, in which case the previous module is
185 discarded and the new file contents are loaded into a new module
186 in its place. When chaining such modules, an object named `wmain'
187 is first looked for and called with no arguments if found. The
188 object it returns is then used as the WSGI application object for
189 that module, which is reused until the module is reloaded. If
190 `wmain' is not found, an object named `application' is looked for
191 instead. If found, it is used directly as the WSGI application
192 object.
193 """
9b417336 194 path = env["SCRIPT_FILENAME"]
c06db49a 195 mod = getmod(path)
14ef1e0e
FT
196 entry = None
197 if mod is not None:
198 mod.lock.acquire()
199 try:
200 if hasattr(mod, "entry"):
201 entry = mod.entry
202 else:
203 if hasattr(mod.mod, "wmain"):
adb11d5f 204 entry = mod.mod.wmain()
14ef1e0e
FT
205 elif hasattr(mod.mod, "application"):
206 entry = mod.mod.application
207 mod.entry = entry
208 finally:
209 mod.lock.release()
210 if entry is not None:
211 return entry(env, startreq)
36ea06a6 212 return wsgiutil.simpleerror(env, startreq, 500, "Internal Error", "Invalid WSGI handler.")
c06db49a 213
e9817fee 214application = handler().handle