callcgi: Fixed possible deadlock problem on aborted requests.
[ashd.git] / python3 / ashd / wsgidir.py
CommitLineData
173e0e9e
FT
1"""WSGI handler for serving chained WSGI modules from physical files
2
7ed9b82b
FT
3The WSGI handler in this module ensures that the SCRIPT_FILENAME
4variable is properly set in every request and points out a file that
5exists and is readable. It then dispatches the request in one of two
6ways: If the header X-Ash-Python-Handler is set in the request, its
7value is used as the name of a handler object to dispatch the request
8to; otherwise, the file extension of the SCRIPT_FILENAME is used to
9determine the handler object.
10
11The name of a handler object is specified as a string, which is split
12along its last constituent dot. The part left of the dot is the name
13of a module, which is imported; and the part right of the dot is the
14name of an object in that module, which should be a callable adhering
15to the WSGI specification. Alternatively, the module part may be
16omitted (such that the name is a string with no dots), in which case
17the handler object is looked up from this module.
18
19By default, this module will handle files with the extensions `.wsgi'
2d4ab435 20or `.wsgi3' using the `chain' handler, which chainloads such files and
7ed9b82b
FT
21runs them as independent WSGI applications. See its documentation for
22details.
173e0e9e
FT
23
24This module itself contains both an `application' and a `wmain'
25object. If this module is used by ashd-wsgi(1) or scgi-wsgi(1) so that
26its wmain function is called, arguments can be specified to it to
27install handlers for other file extensions. Such arguments take the
7ed9b82b
FT
28form `.EXT=HANDLER', where EXT is the file extension to be handled,
29and HANDLER is a handler name, as described above. For example, the
30argument `.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
33functions, you may want to use the getmod() function in this module.
173e0e9e
FT
34"""
35
58ee5c4a 36import sys, os, threading, types, logging, importlib, getopt
173e0e9e
FT
37from . import wsgiutil
38
5c1a2105 39__all__ = ["application", "wmain", "getmod", "cachedmod", "chain"]
173e0e9e 40
58ee5c4a
FT
41log = logging.getLogger("wsgidir")
42
173e0e9e
FT
43class 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 """
7fe08a6f 56 def __init__(self, mod = None, mtime = -1):
173e0e9e
FT
57 self.lock = threading.Lock()
58 self.mod = mod
59 self.mtime = mtime
60
10f3ffeb
FT
61class 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
173e0e9e
FT
75modcache = {}
76cachelock = threading.Lock()
77
78def mangle(path):
79 ret = ""
80 for c in path:
81 if c.isalnum():
82 ret += c
83 else:
84 ret += "_"
85 return ret
86
87def 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)
2037cee2 101 with cachelock:
173e0e9e
FT
102 if path in modcache:
103 entry = modcache[path]
7fe08a6f 104 else:
b8d56e8f 105 entry = [threading.Lock(), None]
7fe08a6f 106 modcache[path] = entry
b8d56e8f
FT
107 with entry[0]:
108 if entry[1] is None or sb.st_mtime > entry[1].mtime:
7fe08a6f
FT
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
10f3ffeb
FT
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)
b8d56e8f 124 return entry[1]
173e0e9e 125
56e8f0f5
FT
126def 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
e9817fee
FT
147class 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:
5f0c1cd6 174 log.error("wsgidir called without SCRIPT_FILENAME set")
e9817fee
FT
175 return wsgiutil.simpleerror(env, startreq, 500, "Internal Error", "The server is erroneously configured.")
176 path = env["SCRIPT_FILENAME"]
7ed9b82b 177 if not os.access(path, os.R_OK):
5f0c1cd6 178 log.error("%s: not readable" % path)
e9817fee 179 return wsgiutil.simpleerror(env, startreq, 500, "Internal Error", "The server is erroneously configured.")
7ed9b82b 180 if "HTTP_X_ASH_PYTHON_HANDLER" in env:
5f0c1cd6
FT
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.")
7ed9b82b
FT
186 else:
187 base = os.path.basename(path)
188 p = base.rfind('.')
189 if p < 0:
5f0c1cd6 190 log.error("wsgidir called with neither X-Ash-Python-Handler nor a file extension: %s" % path)
7ed9b82b
FT
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:
5f0c1cd6 194 log.error("unregistered file extension: %s" % ext)
7ed9b82b
FT
195 return wsgiutil.simpleerror(env, startreq, 500, "Internal Error", "The server is erroneously configured.")
196 handler = self.exts[ext]
197 return handler(env, startreq)
e9817fee
FT
198
199def 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 """
6085469b
FT
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:
e9817fee
FT
215 if arg[0] == '.':
216 p = arg.index('=')
6085469b
FT
217 hnd.addext(arg[1:p], arg[p + 1:])
218 return ret
e9817fee 219
173e0e9e 220def chain(env, startreq):
7ed9b82b
FT
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 """
173e0e9e 235 path = env["SCRIPT_FILENAME"]
58ee5c4a
FT
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.")
173e0e9e
FT
241 entry = None
242 if mod is not None:
2037cee2 243 with mod.lock:
173e0e9e
FT
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
173e0e9e
FT
252 if entry is not None:
253 return entry(env, startreq)
254 return wsgiutil.simpleerror(env, startreq, 500, "Internal Error", "Invalid WSGI handler.")
173e0e9e 255
e9817fee 256application = handler().handle