python: Cleaned up dispatching in wsgidir.
[ashd.git] / python / ashd / wsgidir.py
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
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.
38 """
39
40 import os, threading, types
41 import wsgiutil
42
43 __all__ = ["application", "wmain", "getmod", "cachedmod"]
44
45 class 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
63 modcache = {}
64 cachelock = threading.Lock()
65
66 def mangle(path):
67     ret = ""
68     for c in path:
69         if c.isalnum():
70             ret += c
71         else:
72             ret += "_"
73     return ret
74
75 def 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
116 class 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
157 def 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
170 def 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
191 application = handler().handle