accesslog: Remember to flush request-body.
[ashd.git] / python3 / 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 `.wsgi3' 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, importlib, getopt
37 from . 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 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
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)
101     with cachelock:
102         if path in modcache:
103             entry = modcache[path]
104         else:
105             entry = [threading.Lock(), None]
106             modcache[path] = entry
107     with entry[0]:
108         if entry[1] is None or sb.st_mtime > entry[1].mtime:
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
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)
124         return entry[1]
125
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
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:
174             log.error("wsgidir called without SCRIPT_FILENAME set")
175             return wsgiutil.simpleerror(env, startreq, 500, "Internal Error", "The server is erroneously configured.")
176         path = env["SCRIPT_FILENAME"]
177         if not os.access(path, os.R_OK):
178             log.error("%s: not readable" % path)
179             return wsgiutil.simpleerror(env, startreq, 500, "Internal Error", "The server is erroneously configured.")
180         if "HTTP_X_ASH_PYTHON_HANDLER" in env:
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.")
186         else:
187             base = os.path.basename(path)
188             p = base.rfind('.')
189             if p < 0:
190                 log.error("wsgidir called with neither X-Ash-Python-Handler nor a file extension: %s" % path)
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:
194                 log.error("unregistered file extension: %s" % ext)
195                 return wsgiutil.simpleerror(env, startreq, 500, "Internal Error", "The server is erroneously configured.")
196             handler = self.exts[ext]
197         return handler(env, startreq)
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     """
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:
215         if arg[0] == '.':
216             p = arg.index('=')
217             hnd.addext(arg[1:p], arg[p + 1:])
218     return ret
219
220 def chain(env, startreq):
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     """
235     path = env["SCRIPT_FILENAME"]
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.")
241     entry = None
242     if mod is not None:
243         with mod.lock:
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
252     if entry is not None:
253         return entry(env, startreq)
254     return wsgiutil.simpleerror(env, startreq, 500, "Internal Error", "Invalid WSGI handler.")
255
256 application = handler().handle