python: Cleaned up dispatching in 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 examines the SCRIPT_FILENAME variable
4of the requests it handles -- that is, the physical file corresponding
5to the request, as determined by the webserver -- determining what to
6do with the request based on the extension of that file.
7
8By default, it handles files named `.wsgi' by compiling them into
9Python modules and using them, in turn, as chained WSGI handlers, but
10handlers for other extensions can be installed as well.
11
12When handling `.wsgi' files, the compiled modules are cached and
13reused until the file is modified, in which case the previous module
14is discarded and the new file contents are loaded into a new module in
15its place. When chaining such modules, an object named `wmain' is
16first looked for and called with no arguments if found. The object it
17returns is then used as the WSGI application object for that module,
18which is reused until the module is reloaded. If `wmain' is not found,
19an object named `application' is looked for instead. If found, it is
20used directly as the WSGI application object.
21
22This module itself contains both an `application' and a `wmain'
23object. If this module is used by ashd-wsgi(1) or scgi-wsgi(1) so that
24its wmain function is called, arguments can be specified to it to
25install handlers for other file extensions. Such arguments take the
26form `.EXT=MODULE.HANDLER', where EXT is the file extension to be
27handled, and the MODULE.HANDLER string is treated by splitting it
28along its last constituent dot. The part left of the dot is the name
29of a module which is imported, and the part right of the dot is the
30name of an object in that module, which should be a callable adhering
31to the WSGI specification. When called, this module will have made
32sure that the WSGI environment contains the SCRIPT_FILENAME parameter
33and that it is properly working. For example, the argument
34`.fpy=my.module.foohandler' can be given to pass requests for `.fpy'
35files to the function `foohandler' in the module `my.module' (which
36must, of course, be importable). When writing such handler functions,
37you will probably want to use the getmod() function in this module.
38"""
39
40import os, threading, types
41import wsgiutil
42
43__all__ = ["application", "wmain", "getmod", "cachedmod"]
44
45class cachedmod(object):
46 """Cache entry for modules loaded by getmod()
47
48 Instances of this class are returned by the getmod()
49 function. They contain three data attributes:
50 * mod - The loaded module
51 * lock - A threading.Lock object, which can be used for
52 manipulating this instance in a thread-safe manner
53 * mtime - The time the file was last modified
54
55 Additional data attributes can be arbitrarily added for recording
56 any meta-data about the module.
57 """
58 def __init__(self, mod = None, mtime = -1):
59 self.lock = threading.Lock()
60 self.mod = mod
61 self.mtime = mtime
62
63modcache = {}
64cachelock = threading.Lock()
65
66def mangle(path):
67 ret = ""
68 for c in path:
69 if c.isalnum():
70 ret += c
71 else:
72 ret += "_"
73 return ret
74
75def getmod(path):
76 """Load the given file as a module, caching it appropriately
77
78 The given file is loaded and compiled into a Python module. The
79 compiled module is cached and returned upon subsequent requests
80 for the same file, unless the file has changed (as determined by
81 its mtime), in which case the cached module is discarded and the
82 new file contents are reloaded in its place.
83
84 The return value is an instance of the cachedmod class, which can
85 be used for locking purposes and for storing arbitrary meta-data
86 about the module. See its documentation for details.
87 """
88 sb = os.stat(path)
89 cachelock.acquire()
90 try:
91 if path in modcache:
92 entry = modcache[path]
93 else:
94 entry = cachedmod()
95 modcache[path] = entry
96 finally:
97 cachelock.release()
98 entry.lock.acquire()
99 try:
100 if entry.mod is None or sb.st_mtime > entry.mtime:
101 f = open(path, "r")
102 try:
103 text = f.read()
104 finally:
105 f.close()
106 code = compile(text, path, "exec")
107 mod = types.ModuleType(mangle(path))
108 mod.__file__ = path
109 exec code in mod.__dict__
110 entry.mod = mod
111 entry.mtime = sb.st_mtime
112 return entry
113 finally:
114 entry.lock.release()
115
116class handler(object):
117 def __init__(self):
118 self.lock = threading.Lock()
119 self.handlers = {}
120 self.exts = {}
121 self.addext("wsgi", "chain")
122 self.addext("wsgi2", "chain")
123
124 def resolve(self, name):
125 self.lock.acquire()
126 try:
127 if name in self.handlers:
128 return self.handlers[name]
129 p = name.rfind('.')
130 if p < 0:
131 return globals()[name]
132 mname = name[:p]
133 hname = name[p + 1:]
134 mod = __import__(mname, fromlist = ["dummy"])
135 ret = getattr(mod, hname)
136 self.handlers[name] = ret
137 return ret
138 finally:
139 self.lock.release()
140
141 def addext(self, ext, handler):
142 self.exts[ext] = self.resolve(handler)
143
144 def handle(self, env, startreq):
145 if not "SCRIPT_FILENAME" in env:
146 return wsgiutil.simpleerror(env, startreq, 500, "Internal Error", "The server is erroneously configured.")
147 path = env["SCRIPT_FILENAME"]
148 base = os.path.basename(path)
149 p = base.rfind('.')
150 if p < 0 or not os.access(path, os.R_OK):
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 return(self.exts[ext](env, startreq))
156
157def wmain(*argv):
158 """Main function for ashd(7)-compatible WSGI handlers
159
160 Returns the `application' function. If any arguments are given,
161 they are parsed according to the module documentation.
162 """
163 ret = handler()
164 for arg in argv:
165 if arg[0] == '.':
166 p = arg.index('=')
167 ret.addext(arg[1:p], arg[p + 1:])
168 return ret.handle
169
170def chain(env, startreq):
171 path = env["SCRIPT_FILENAME"]
172 mod = getmod(path)
173 entry = None
174 if mod is not None:
175 mod.lock.acquire()
176 try:
177 if hasattr(mod, "entry"):
178 entry = mod.entry
179 else:
180 if hasattr(mod.mod, "wmain"):
181 entry = mod.mod.wmain()
182 elif hasattr(mod.mod, "application"):
183 entry = mod.mod.application
184 mod.entry = entry
185 finally:
186 mod.lock.release()
187 if entry is not None:
188 return entry(env, startreq)
189 return wsgiutil.simpleerror(env, startreq, 500, "Internal Error", "Invalid WSGI handler.")
190
191application = handler().handle