etc: Add environment option to run init.d/ashd silently.
[ashd.git] / python / ashd / wsgidir.py
CommitLineData
fc2aa563
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'
20or `.wsgi2' using the `chain' handler, which chainloads such files and
21runs them as independent WSGI applications. See its documentation for
22details.
fc2aa563
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.
fc2aa563
FT
34"""
35
58ee5c4a 36import sys, os, threading, types, logging, getopt
173e0e9e 37import wsgiutil
c06db49a 38
5c1a2105 39__all__ = ["application", "wmain", "getmod", "cachedmod", "chain"]
fc2aa563 40
58ee5c4a
FT
41log = logging.getLogger("wsgidir")
42
f677567f 43class cachedmod(object):
fc2aa563
FT
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):
14ef1e0e
FT
57 self.lock = threading.Lock()
58 self.mod = mod
59 self.mtime = mtime
60
c06db49a
FT
61modcache = {}
62cachelock = threading.Lock()
63
64def mangle(path):
65 ret = ""
66 for c in path:
67 if c.isalnum():
68 ret += c
69 else:
70 ret += "_"
71 return ret
72
73def getmod(path):
fc2aa563
FT
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 """
c06db49a
FT
86 sb = os.stat(path)
87 cachelock.acquire()
88 try:
89 if path in modcache:
14ef1e0e 90 entry = modcache[path]
7fe08a6f 91 else:
b8d56e8f 92 entry = [threading.Lock(), None]
7fe08a6f 93 modcache[path] = entry
c06db49a
FT
94 finally:
95 cachelock.release()
b8d56e8f 96 entry[0].acquire()
7fe08a6f 97 try:
b8d56e8f 98 if entry[1] is None or sb.st_mtime > entry[1].mtime:
7fe08a6f
FT
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__
b8d56e8f
FT
108 entry[1] = cachedmod(mod, sb.st_mtime)
109 return entry[1]
7fe08a6f 110 finally:
b8d56e8f 111 entry[0].release()
c06db49a 112
56e8f0f5
FT
113def 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
e9817fee
FT
134class 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:
5f0c1cd6 164 log.error("wsgidir called without SCRIPT_FILENAME set")
e9817fee
FT
165 return wsgiutil.simpleerror(env, startreq, 500, "Internal Error", "The server is erroneously configured.")
166 path = env["SCRIPT_FILENAME"]
7ed9b82b 167 if not os.access(path, os.R_OK):
5f0c1cd6 168 log.error("%s: not readable" % path)
e9817fee 169 return wsgiutil.simpleerror(env, startreq, 500, "Internal Error", "The server is erroneously configured.")
7ed9b82b 170 if "HTTP_X_ASH_PYTHON_HANDLER" in env:
5f0c1cd6
FT
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.")
7ed9b82b
FT
176 else:
177 base = os.path.basename(path)
178 p = base.rfind('.')
179 if p < 0:
5f0c1cd6 180 log.error("wsgidir called with neither X-Ash-Python-Handler nor a file extension: %s" % path)
7ed9b82b
FT
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:
5f0c1cd6 184 log.error("unregistered file extension: %s" % ext)
7ed9b82b
FT
185 return wsgiutil.simpleerror(env, startreq, 500, "Internal Error", "The server is erroneously configured.")
186 handler = self.exts[ext]
187 return handler(env, startreq)
e9817fee
FT
188
189def 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 """
6085469b
FT
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:
e9817fee
FT
205 if arg[0] == '.':
206 p = arg.index('=')
6085469b
FT
207 hnd.addext(arg[1:p], arg[p + 1:])
208 return ret
e9817fee 209
9b417336 210def chain(env, startreq):
7ed9b82b
FT
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 """
9b417336 225 path = env["SCRIPT_FILENAME"]
58ee5c4a
FT
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.")
14ef1e0e
FT
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"):
adb11d5f 239 entry = mod.mod.wmain()
14ef1e0e
FT
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)
36ea06a6 247 return wsgiutil.simpleerror(env, startreq, 500, "Internal Error", "Invalid WSGI handler.")
c06db49a 248
e9817fee 249application = handler().handle