Commit | Line | Data |
---|---|---|
173e0e9e FT |
1 | """WSGI handler for serving chained WSGI modules from physical files |
2 | ||
7ed9b82b FT |
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. | |
10 | ||
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. | |
18 | ||
19 | By default, this module will handle files with the extensions `.wsgi' | |
2d4ab435 | 20 | or `.wsgi3' using the `chain' handler, which chainloads such files and |
7ed9b82b FT |
21 | runs them as independent WSGI applications. See its documentation for |
22 | details. | |
173e0e9e FT |
23 | |
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 | |
7ed9b82b FT |
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. | |
173e0e9e FT |
34 | """ |
35 | ||
58ee5c4a | 36 | import sys, os, threading, types, logging, importlib, getopt |
173e0e9e FT |
37 | from . import wsgiutil |
38 | ||
5c1a2105 | 39 | __all__ = ["application", "wmain", "getmod", "cachedmod", "chain"] |
173e0e9e | 40 | |
58ee5c4a FT |
41 | log = logging.getLogger("wsgidir") |
42 | ||
173e0e9e FT |
43 | class 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 | """ | |
7fe08a6f | 56 | def __init__(self, mod = None, mtime = -1): |
173e0e9e FT |
57 | self.lock = threading.Lock() |
58 | self.mod = mod | |
59 | self.mtime = mtime | |
60 | ||
10f3ffeb FT |
61 | class current(object): |
62 | def __init__(self): | |
63 | self.cond = threading.Condition() | |
64 | self.current = True | |
65 | def wait(self, timeout=None): | |
66 | with self.cond: | |
67 | self.cond.wait(timeout) | |
68 | def uncurrent(self): | |
69 | with self.cond: | |
70 | self.current = False | |
71 | self.cond.notify_all() | |
72 | def __bool__(self): | |
73 | return self.current | |
74 | ||
173e0e9e FT |
75 | modcache = {} |
76 | cachelock = threading.Lock() | |
77 | ||
78 | def mangle(path): | |
79 | ret = "" | |
80 | for c in path: | |
81 | if c.isalnum(): | |
82 | ret += c | |
83 | else: | |
84 | ret += "_" | |
85 | return ret | |
86 | ||
87 | def getmod(path): | |
88 | """Load the given file as a module, caching it appropriately | |
89 | ||
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. | |
95 | ||
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. | |
99 | """ | |
100 | sb = os.stat(path) | |
2037cee2 | 101 | with cachelock: |
173e0e9e FT |
102 | if path in modcache: |
103 | entry = modcache[path] | |
7fe08a6f | 104 | else: |
b8d56e8f | 105 | entry = [threading.Lock(), None] |
7fe08a6f | 106 | modcache[path] = entry |
b8d56e8f FT |
107 | with entry[0]: |
108 | if entry[1] is None or sb.st_mtime > entry[1].mtime: | |
7fe08a6f FT |
109 | with open(path, "rb") as f: |
110 | text = f.read() | |
111 | code = compile(text, path, "exec") | |
112 | mod = types.ModuleType(mangle(path)) | |
113 | mod.__file__ = path | |
10f3ffeb FT |
114 | mod.__current__ = current() |
115 | try: | |
116 | exec(code, mod.__dict__) | |
117 | except: | |
118 | mod.__current__.uncurrent() | |
119 | raise | |
120 | else: | |
121 | if entry[1] is not None: | |
122 | entry[1].mod.__current__.uncurrent() | |
123 | entry[1] = cachedmod(mod, sb.st_mtime) | |
b8d56e8f | 124 | return entry[1] |
173e0e9e | 125 | |
56e8f0f5 FT |
126 | def importlocal(filename): |
127 | import inspect | |
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): | |
138 | path += ext | |
139 | break | |
140 | else: | |
141 | raise ImportError("could not resolve file: " + filename) | |
142 | else: | |
143 | if not os.path.exists(cfile): | |
144 | raise ImportError("no such file: " + filename) | |
145 | return getmod(path).mod | |
146 | ||
e9817fee FT |
147 | class handler(object): |
148 | def __init__(self): | |
149 | self.lock = threading.Lock() | |
150 | self.handlers = {} | |
151 | self.exts = {} | |
152 | self.addext("wsgi", "chain") | |
153 | self.addext("wsgi3", "chain") | |
154 | ||
155 | def resolve(self, name): | |
156 | with self.lock: | |
157 | if name in self.handlers: | |
158 | return self.handlers[name] | |
159 | p = name.rfind('.') | |
160 | if p < 0: | |
161 | return globals()[name] | |
162 | mname = name[:p] | |
163 | hname = name[p + 1:] | |
164 | mod = importlib.import_module(mname) | |
165 | ret = getattr(mod, hname) | |
166 | self.handlers[name] = ret | |
167 | return ret | |
168 | ||
169 | def addext(self, ext, handler): | |
170 | self.exts[ext] = self.resolve(handler) | |
171 | ||
172 | def handle(self, env, startreq): | |
173 | if not "SCRIPT_FILENAME" in env: | |
5f0c1cd6 | 174 | log.error("wsgidir called without SCRIPT_FILENAME set") |
e9817fee FT |
175 | return wsgiutil.simpleerror(env, startreq, 500, "Internal Error", "The server is erroneously configured.") |
176 | path = env["SCRIPT_FILENAME"] | |
7ed9b82b | 177 | if not os.access(path, os.R_OK): |
5f0c1cd6 | 178 | log.error("%s: not readable" % path) |
e9817fee | 179 | return wsgiutil.simpleerror(env, startreq, 500, "Internal Error", "The server is erroneously configured.") |
7ed9b82b | 180 | if "HTTP_X_ASH_PYTHON_HANDLER" in env: |
5f0c1cd6 FT |
181 | try: |
182 | handler = self.resolve(env["HTTP_X_ASH_PYTHON_HANDLER"]) | |
183 | except Exception: | |
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.") | |
7ed9b82b FT |
186 | else: |
187 | base = os.path.basename(path) | |
188 | p = base.rfind('.') | |
189 | if p < 0: | |
5f0c1cd6 | 190 | log.error("wsgidir called with neither X-Ash-Python-Handler nor a file extension: %s" % path) |
7ed9b82b FT |
191 | return wsgiutil.simpleerror(env, startreq, 500, "Internal Error", "The server is erroneously configured.") |
192 | ext = base[p + 1:] | |
193 | if not ext in self.exts: | |
5f0c1cd6 | 194 | log.error("unregistered file extension: %s" % ext) |
7ed9b82b FT |
195 | return wsgiutil.simpleerror(env, startreq, 500, "Internal Error", "The server is erroneously configured.") |
196 | handler = self.exts[ext] | |
197 | return handler(env, startreq) | |
e9817fee FT |
198 | |
199 | def wmain(*argv): | |
200 | """Main function for ashd(7)-compatible WSGI handlers | |
201 | ||
202 | Returns the `application' function. If any arguments are given, | |
203 | they are parsed according to the module documentation. | |
204 | """ | |
6085469b FT |
205 | hnd = handler() |
206 | ret = hnd.handle | |
207 | ||
208 | opts, args = getopt.getopt(argv, "-V") | |
209 | for o, a in opts: | |
210 | if o == "-V": | |
211 | import wsgiref.validate | |
212 | ret = wsgiref.validate.validator(ret) | |
213 | ||
214 | for arg in args: | |
e9817fee FT |
215 | if arg[0] == '.': |
216 | p = arg.index('=') | |
6085469b FT |
217 | hnd.addext(arg[1:p], arg[p + 1:]) |
218 | return ret | |
e9817fee | 219 | |
173e0e9e | 220 | def chain(env, startreq): |
7ed9b82b FT |
221 | """Chain-loading WSGI handler |
222 | ||
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 | |
233 | object. | |
234 | """ | |
173e0e9e | 235 | path = env["SCRIPT_FILENAME"] |
58ee5c4a FT |
236 | try: |
237 | mod = getmod(path) | |
238 | except Exception: | |
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.") | |
173e0e9e FT |
241 | entry = None |
242 | if mod is not None: | |
2037cee2 | 243 | with mod.lock: |
173e0e9e FT |
244 | if hasattr(mod, "entry"): |
245 | entry = mod.entry | |
246 | else: | |
247 | if hasattr(mod.mod, "wmain"): | |
248 | entry = mod.mod.wmain() | |
249 | elif hasattr(mod.mod, "application"): | |
250 | entry = mod.mod.application | |
251 | mod.entry = entry | |
173e0e9e FT |
252 | if entry is not None: |
253 | return entry(env, startreq) | |
254 | return wsgiutil.simpleerror(env, startreq, 500, "Internal Error", "Invalid WSGI handler.") | |
173e0e9e | 255 | |
e9817fee | 256 | application = handler().handle |