python: Added a simple function for doing directory-local "imports".
[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 sys, os, threading, types, logging, getopt
37 import wsgiutil
38
39 __all__ = ["application", "wmain", "getmod", "cachedmod", "chain"]
40
41 log = logging.getLogger("wsgidir")
42
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     """
56     def __init__(self, mod = None, mtime = -1):
57         self.lock = threading.Lock()
58         self.mod = mod
59         self.mtime = mtime
60
61 modcache = {}
62 cachelock = threading.Lock()
63
64 def mangle(path):
65     ret = ""
66     for c in path:
67         if c.isalnum():
68             ret += c
69         else:
70             ret += "_"
71     return ret
72
73 def getmod(path):
74     """Load the given file as a module, caching it appropriately
75
76     The given file is loaded and compiled into a Python module. The
77     compiled module is cached and returned upon subsequent requests
78     for the same file, unless the file has changed (as determined by
79     its mtime), in which case the cached module is discarded and the
80     new file contents are reloaded in its place.
81
82     The return value is an instance of the cachedmod class, which can
83     be used for locking purposes and for storing arbitrary meta-data
84     about the module. See its documentation for details.
85     """
86     sb = os.stat(path)
87     cachelock.acquire()
88     try:
89         if path in modcache:
90             entry = modcache[path]
91         else:
92             entry = [threading.Lock(), None]
93             modcache[path] = entry
94     finally:
95         cachelock.release()
96     entry[0].acquire()
97     try:
98         if entry[1] is None or sb.st_mtime > entry[1].mtime:
99             f = open(path, "r")
100             try:
101                 text = f.read()
102             finally:
103                 f.close()
104             code = compile(text, path, "exec")
105             mod = types.ModuleType(mangle(path))
106             mod.__file__ = path
107             exec code in mod.__dict__
108             entry[1] = cachedmod(mod, sb.st_mtime)
109         return entry[1]
110     finally:
111         entry[0].release()
112
113 def importlocal(filename):
114     import inspect
115     cf = inspect.currentframe()
116     if cf is None: raise ImportError("could not get current frame")
117     if cf.f_back is None: raise ImportError("could not get caller frame")
118     cfile = cf.f_back.f_code.co_filename
119     if not os.path.exists(cfile):
120         raise ImportError("caller is not in a proper file")
121     path = os.path.realpath(os.path.join(os.path.dirname(cfile), filename))
122     if '.' not in os.path.basename(path):
123         for ext in [".pyl", ".py"]:
124             if os.path.exists(path + ext):
125                 path += ext
126                 break
127         else:
128             raise ImportError("could not resolve file: " + filename)
129     else:
130         if not os.path.exists(cfile):
131             raise ImportError("no such file: " + filename)
132     return getmod(path).mod
133
134 class handler(object):
135     def __init__(self):
136         self.lock = threading.Lock()
137         self.handlers = {}
138         self.exts = {}
139         self.addext("wsgi", "chain")
140         self.addext("wsgi2", "chain")
141
142     def resolve(self, name):
143         self.lock.acquire()
144         try:
145             if name in self.handlers:
146                 return self.handlers[name]
147             p = name.rfind('.')
148             if p < 0:
149                 return globals()[name]
150             mname = name[:p]
151             hname = name[p + 1:]
152             mod = __import__(mname, fromlist = ["dummy"])
153             ret = getattr(mod, hname)
154             self.handlers[name] = ret
155             return ret
156         finally:
157             self.lock.release()
158         
159     def addext(self, ext, handler):
160         self.exts[ext] = self.resolve(handler)
161
162     def handle(self, env, startreq):
163         if not "SCRIPT_FILENAME" in env:
164             log.error("wsgidir called without SCRIPT_FILENAME set")
165             return wsgiutil.simpleerror(env, startreq, 500, "Internal Error", "The server is erroneously configured.")
166         path = env["SCRIPT_FILENAME"]
167         if not os.access(path, os.R_OK):
168             log.error("%s: not readable" % path)
169             return wsgiutil.simpleerror(env, startreq, 500, "Internal Error", "The server is erroneously configured.")
170         if "HTTP_X_ASH_PYTHON_HANDLER" in env:
171             try:
172                 handler = self.resolve(env["HTTP_X_ASH_PYTHON_HANDLER"])
173             except Exception:
174                 log.error("could not load handler %s" % env["HTTP_X_ASH_PYTHON_HANDLER"], exc_info=sys.exc_info())
175                 return wsgiutil.simpleerror(env, startreq, 500, "Internal Error", "The server is erroneously configured.")
176         else:
177             base = os.path.basename(path)
178             p = base.rfind('.')
179             if p < 0:
180                 log.error("wsgidir called with neither X-Ash-Python-Handler nor a file extension: %s" % path)
181                 return wsgiutil.simpleerror(env, startreq, 500, "Internal Error", "The server is erroneously configured.")
182             ext = base[p + 1:]
183             if not ext in self.exts:
184                 log.error("unregistered file extension: %s" % ext)
185                 return wsgiutil.simpleerror(env, startreq, 500, "Internal Error", "The server is erroneously configured.")
186             handler = self.exts[ext]
187         return handler(env, startreq)
188
189 def wmain(*argv):
190     """Main function for ashd(7)-compatible WSGI handlers
191
192     Returns the `application' function. If any arguments are given,
193     they are parsed according to the module documentation.
194     """
195     hnd = handler()
196     ret = hnd.handle
197
198     opts, args = getopt.getopt(argv, "V")
199     for o, a in opts:
200         if o == "-V":
201             import wsgiref.validate
202             ret = wsgiref.validate.validator(ret)
203
204     for arg in args:
205         if arg[0] == '.':
206             p = arg.index('=')
207             hnd.addext(arg[1:p], arg[p + 1:])
208     return ret
209
210 def chain(env, startreq):
211     """Chain-loading WSGI handler
212     
213     This handler loads requested files, compiles them and loads them
214     into their own modules. The compiled modules are cached and reused
215     until the file is modified, in which case the previous module is
216     discarded and the new file contents are loaded into a new module
217     in its place. When chaining such modules, an object named `wmain'
218     is first looked for and called with no arguments if found. The
219     object it returns is then used as the WSGI application object for
220     that module, which is reused until the module is reloaded. If
221     `wmain' is not found, an object named `application' is looked for
222     instead. If found, it is used directly as the WSGI application
223     object.
224     """
225     path = env["SCRIPT_FILENAME"]
226     try:
227         mod = getmod(path)
228     except Exception:
229         log.error("Exception occurred when loading %s" % path, exc_info=sys.exc_info())
230         return wsgiutil.simpleerror(env, startreq, 500, "Internal Error", "Could not load WSGI handler.")
231     entry = None
232     if mod is not None:
233         mod.lock.acquire()
234         try:
235             if hasattr(mod, "entry"):
236                 entry = mod.entry
237             else:
238                 if hasattr(mod.mod, "wmain"):
239                     entry = mod.mod.wmain()
240                 elif hasattr(mod.mod, "application"):
241                     entry = mod.mod.application
242                 mod.entry = entry
243         finally:
244             mod.lock.release()
245     if entry is not None:
246         return entry(env, startreq)
247     return wsgiutil.simpleerror(env, startreq, 500, "Internal Error", "Invalid WSGI handler.")
248
249 application = handler().handle