Commit | Line | Data |
---|---|---|
fc2aa563 FT |
1 | """WSGI handler for serving chained WSGI modules from physical files |
2 | ||
3 | The WSGI handler in this module examines the SCRIPT_FILENAME variable | |
4 | of the requests it handles -- that is, the physical file corresponding | |
5 | to the request, as determined by the webserver -- determining what to | |
6 | do with the request based on the extension of that file. | |
7 | ||
8 | By default, it handles files named `.wsgi' by compiling them into | |
9 | Python modules and using them, in turn, as chained WSGI handlers, but | |
10 | handlers for other extensions can be installed as well. | |
11 | ||
12 | When handling `.wsgi' files, the compiled modules are cached and | |
13 | reused until the file is modified, in which case the previous module | |
14 | is discarded and the new file contents are loaded into a new module in | |
15 | its place. When chaining such modules, an object named `wmain' is | |
16 | first looked for and called with no arguments if found. The object it | |
17 | returns is then used as the WSGI application object for that module, | |
18 | which is reused until the module is reloaded. If `wmain' is not found, | |
19 | an object named `application' is looked for instead. If found, it is | |
20 | used directly as the WSGI application object. | |
21 | ||
22 | This module itself contains both an `application' and a `wmain' | |
23 | object. If this module is used by ashd-wsgi(1) or scgi-wsgi(1) so that | |
24 | its wmain function is called, arguments can be specified to it to | |
25 | install handlers for other file extensions. Such arguments take the | |
26 | form `.EXT=MODULE.HANDLER', where EXT is the file extension to be | |
27 | handled, and the MODULE.HANDLER string is treated by splitting it | |
28 | along its last constituent dot. The part left of the dot is the name | |
29 | of a module which is imported, and the part right of the dot is the | |
9b417336 FT |
30 | name of an object in that module, which should be a callable adhering |
31 | to the WSGI specification. When called, this module will have made | |
32 | sure that the WSGI environment contains the SCRIPT_FILENAME parameter | |
33 | and that it is properly working. For example, the argument | |
34 | `.fpy=my.module.foohandler' can be given to pass requests for `.fpy' | |
35 | files to the function `foohandler' in the module `my.module' (which | |
36 | must, of course, be importable). When writing such handler functions, | |
37 | you will probably want to use the getmod() function in this module. | |
fc2aa563 FT |
38 | """ |
39 | ||
c06db49a FT |
40 | import os, threading, types |
41 | import wsgiutil | |
42 | ||
fc2aa563 FT |
43 | __all__ = ["application", "wmain", "getmod", "cachedmod"] |
44 | ||
f677567f | 45 | class cachedmod(object): |
fc2aa563 FT |
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 | """ | |
14ef1e0e FT |
58 | def __init__(self, mod, mtime): |
59 | self.lock = threading.Lock() | |
60 | self.mod = mod | |
61 | self.mtime = mtime | |
62 | ||
c06db49a FT |
63 | exts = {} |
64 | modcache = {} | |
65 | cachelock = threading.Lock() | |
66 | ||
67 | def mangle(path): | |
68 | ret = "" | |
69 | for c in path: | |
70 | if c.isalnum(): | |
71 | ret += c | |
72 | else: | |
73 | ret += "_" | |
74 | return ret | |
75 | ||
76 | def getmod(path): | |
fc2aa563 FT |
77 | """Load the given file as a module, caching it appropriately |
78 | ||
79 | The given file is loaded and compiled into a Python module. The | |
80 | compiled module is cached and returned upon subsequent requests | |
81 | for the same file, unless the file has changed (as determined by | |
82 | its mtime), in which case the cached module is discarded and the | |
83 | new file contents are reloaded in its place. | |
84 | ||
85 | The return value is an instance of the cachedmod class, which can | |
86 | be used for locking purposes and for storing arbitrary meta-data | |
87 | about the module. See its documentation for details. | |
88 | """ | |
c06db49a FT |
89 | sb = os.stat(path) |
90 | cachelock.acquire() | |
91 | try: | |
92 | if path in modcache: | |
14ef1e0e FT |
93 | entry = modcache[path] |
94 | if sb.st_mtime <= entry.mtime: | |
95 | return entry | |
96 | ||
c06db49a FT |
97 | f = open(path) |
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__ | |
14ef1e0e FT |
106 | entry = cachedmod(mod, sb.st_mtime) |
107 | modcache[path] = entry | |
108 | return entry | |
c06db49a FT |
109 | finally: |
110 | cachelock.release() | |
111 | ||
9b417336 FT |
112 | def chain(env, startreq): |
113 | path = env["SCRIPT_FILENAME"] | |
c06db49a | 114 | mod = getmod(path) |
14ef1e0e FT |
115 | entry = None |
116 | if mod is not None: | |
117 | mod.lock.acquire() | |
118 | try: | |
119 | if hasattr(mod, "entry"): | |
120 | entry = mod.entry | |
121 | else: | |
122 | if hasattr(mod.mod, "wmain"): | |
adb11d5f | 123 | entry = mod.mod.wmain() |
14ef1e0e FT |
124 | elif hasattr(mod.mod, "application"): |
125 | entry = mod.mod.application | |
126 | mod.entry = entry | |
127 | finally: | |
128 | mod.lock.release() | |
129 | if entry is not None: | |
130 | return entry(env, startreq) | |
36ea06a6 | 131 | return wsgiutil.simpleerror(env, startreq, 500, "Internal Error", "Invalid WSGI handler.") |
c06db49a FT |
132 | exts["wsgi"] = chain |
133 | ||
48a91dd9 FT |
134 | def addext(ext, handler): |
135 | p = handler.rindex('.') | |
136 | mname = handler[:p] | |
137 | hname = handler[p + 1:] | |
138 | mod = __import__(mname, fromlist = ["dummy"]) | |
139 | exts[ext] = getattr(mod, hname) | |
140 | ||
c06db49a | 141 | def application(env, startreq): |
fc2aa563 FT |
142 | """WSGI handler function |
143 | ||
144 | Handles WSGI requests as per the module documentation. | |
145 | """ | |
c06db49a | 146 | if not "SCRIPT_FILENAME" in env: |
36ea06a6 | 147 | return wsgiutil.simpleerror(env, startreq, 500, "Internal Error", "The server is erroneously configured.") |
c06db49a FT |
148 | path = env["SCRIPT_FILENAME"] |
149 | base = os.path.basename(path) | |
150 | p = base.rfind('.') | |
151 | if p < 0 or not os.access(path, os.R_OK): | |
36ea06a6 | 152 | return wsgiutil.simpleerror(env, startreq, 500, "Internal Error", "The server is erroneously configured.") |
c06db49a FT |
153 | ext = base[p + 1:] |
154 | if not ext in exts: | |
36ea06a6 | 155 | return wsgiutil.simpleerror(env, startreq, 500, "Internal Error", "The server is erroneously configured.") |
9b417336 | 156 | return(exts[ext](env, startreq)) |
c06db49a | 157 | |
adb11d5f | 158 | def wmain(*argv): |
fc2aa563 FT |
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 | """ | |
48a91dd9 FT |
164 | for arg in argv: |
165 | if arg[0] == '.': | |
166 | p = arg.index('=') | |
167 | addext(arg[1:p], arg[p + 1:]) | |
c06db49a | 168 | return application |