examples: Added an example for wsgidir usage.
[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 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'
20 or `.wsgi2' using the `chain' handler, which chainloads such files and
21 runs them as independent WSGI applications. See its documentation for
22 details.
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
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.
34 """
35
36 import os, threading, types
37 import wsgiutil
38
39 __all__ = ["application", "wmain", "getmod", "cachedmod", "chain"]
40
41 class cachedmod(object):
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     """
54     def __init__(self, mod = None, mtime = -1):
55         self.lock = threading.Lock()
56         self.mod = mod
57         self.mtime = mtime
58
59 modcache = {}
60 cachelock = threading.Lock()
61
62 def mangle(path):
63     ret = ""
64     for c in path:
65         if c.isalnum():
66             ret += c
67         else:
68             ret += "_"
69     return ret
70
71 def getmod(path):
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     """
84     sb = os.stat(path)
85     cachelock.acquire()
86     try:
87         if path in modcache:
88             entry = modcache[path]
89         else:
90             entry = [threading.Lock(), None]
91             modcache[path] = entry
92     finally:
93         cachelock.release()
94     entry[0].acquire()
95     try:
96         if entry[1] is None or sb.st_mtime > entry[1].mtime:
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__
106             entry[1] = cachedmod(mod, sb.st_mtime)
107         return entry[1]
108     finally:
109         entry[0].release()
110
111 class 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"]
143         if not os.access(path, os.R_OK):
144             return wsgiutil.simpleerror(env, startreq, 500, "Internal Error", "The server is erroneously configured.")
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)
157
158 def 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     """
164     ret = handler()
165     for arg in argv:
166         if arg[0] == '.':
167             p = arg.index('=')
168             ret.addext(arg[1:p], arg[p + 1:])
169     return ret.handle
170
171 def chain(env, startreq):
172     """Chain-loading WSGI handler
173     
174     This handler loads requested files, compiles them and loads them
175     into their own modules. The compiled modules are cached and reused
176     until the file is modified, in which case the previous module is
177     discarded and the new file contents are loaded into a new module
178     in its place. When chaining such modules, an object named `wmain'
179     is first looked for and called with no arguments if found. The
180     object it returns is then used as the WSGI application object for
181     that module, which is reused until the module is reloaded. If
182     `wmain' is not found, an object named `application' is looked for
183     instead. If found, it is used directly as the WSGI application
184     object.
185     """
186     path = env["SCRIPT_FILENAME"]
187     mod = getmod(path)
188     entry = None
189     if mod is not None:
190         mod.lock.acquire()
191         try:
192             if hasattr(mod, "entry"):
193                 entry = mod.entry
194             else:
195                 if hasattr(mod.mod, "wmain"):
196                     entry = mod.mod.wmain()
197                 elif hasattr(mod.mod, "application"):
198                     entry = mod.mod.application
199                 mod.entry = entry
200         finally:
201             mod.lock.release()
202     if entry is not None:
203         return entry(env, startreq)
204     return wsgiutil.simpleerror(env, startreq, 500, "Internal Error", "Invalid WSGI handler.")
205
206 application = handler().handle