psendfile: Fixed crash when files cannot be opened.
[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
173e0e9e
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):
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)
2037cee2 87 with cachelock:
173e0e9e
FT
88 if path in modcache:
89 entry = modcache[path]
7fe08a6f 90 else:
b8d56e8f 91 entry = [threading.Lock(), None]
7fe08a6f 92 modcache[path] = entry
b8d56e8f
FT
93 with entry[0]:
94 if entry[1] is None or sb.st_mtime > entry[1].mtime:
7fe08a6f
FT
95 with open(path, "rb") as f:
96 text = f.read()
97 code = compile(text, path, "exec")
98 mod = types.ModuleType(mangle(path))
99 mod.__file__ = path
100 exec(code, mod.__dict__)
b8d56e8f
FT
101 entry[1] = cachedmod(mod, sb.st_mtime)
102 return entry[1]
173e0e9e 103
56e8f0f5
FT
104def importlocal(filename):
105 import inspect
106 cf = inspect.currentframe()
107 if cf is None: raise ImportError("could not get current frame")
108 if cf.f_back is None: raise ImportError("could not get caller frame")
109 cfile = cf.f_back.f_code.co_filename
110 if not os.path.exists(cfile):
111 raise ImportError("caller is not in a proper file")
112 path = os.path.realpath(os.path.join(os.path.dirname(cfile), filename))
113 if '.' not in os.path.basename(path):
114 for ext in [".pyl", ".py"]:
115 if os.path.exists(path + ext):
116 path += ext
117 break
118 else:
119 raise ImportError("could not resolve file: " + filename)
120 else:
121 if not os.path.exists(cfile):
122 raise ImportError("no such file: " + filename)
123 return getmod(path).mod
124
e9817fee
FT
125class handler(object):
126 def __init__(self):
127 self.lock = threading.Lock()
128 self.handlers = {}
129 self.exts = {}
130 self.addext("wsgi", "chain")
131 self.addext("wsgi3", "chain")
132
133 def resolve(self, name):
134 with self.lock:
135 if name in self.handlers:
136 return self.handlers[name]
137 p = name.rfind('.')
138 if p < 0:
139 return globals()[name]
140 mname = name[:p]
141 hname = name[p + 1:]
142 mod = importlib.import_module(mname)
143 ret = getattr(mod, hname)
144 self.handlers[name] = ret
145 return ret
146
147 def addext(self, ext, handler):
148 self.exts[ext] = self.resolve(handler)
149
150 def handle(self, env, startreq):
151 if not "SCRIPT_FILENAME" in env:
5f0c1cd6 152 log.error("wsgidir called without SCRIPT_FILENAME set")
e9817fee
FT
153 return wsgiutil.simpleerror(env, startreq, 500, "Internal Error", "The server is erroneously configured.")
154 path = env["SCRIPT_FILENAME"]
7ed9b82b 155 if not os.access(path, os.R_OK):
5f0c1cd6 156 log.error("%s: not readable" % path)
e9817fee 157 return wsgiutil.simpleerror(env, startreq, 500, "Internal Error", "The server is erroneously configured.")
7ed9b82b 158 if "HTTP_X_ASH_PYTHON_HANDLER" in env:
5f0c1cd6
FT
159 try:
160 handler = self.resolve(env["HTTP_X_ASH_PYTHON_HANDLER"])
161 except Exception:
162 log.error("could not load handler %s" % env["HTTP_X_ASH_PYTHON_HANDLER"], exc_info=sys.exc_info())
163 return wsgiutil.simpleerror(env, startreq, 500, "Internal Error", "The server is erroneously configured.")
7ed9b82b
FT
164 else:
165 base = os.path.basename(path)
166 p = base.rfind('.')
167 if p < 0:
5f0c1cd6 168 log.error("wsgidir called with neither X-Ash-Python-Handler nor a file extension: %s" % path)
7ed9b82b
FT
169 return wsgiutil.simpleerror(env, startreq, 500, "Internal Error", "The server is erroneously configured.")
170 ext = base[p + 1:]
171 if not ext in self.exts:
5f0c1cd6 172 log.error("unregistered file extension: %s" % ext)
7ed9b82b
FT
173 return wsgiutil.simpleerror(env, startreq, 500, "Internal Error", "The server is erroneously configured.")
174 handler = self.exts[ext]
175 return handler(env, startreq)
e9817fee
FT
176
177def wmain(*argv):
178 """Main function for ashd(7)-compatible WSGI handlers
179
180 Returns the `application' function. If any arguments are given,
181 they are parsed according to the module documentation.
182 """
6085469b
FT
183 hnd = handler()
184 ret = hnd.handle
185
186 opts, args = getopt.getopt(argv, "-V")
187 for o, a in opts:
188 if o == "-V":
189 import wsgiref.validate
190 ret = wsgiref.validate.validator(ret)
191
192 for arg in args:
e9817fee
FT
193 if arg[0] == '.':
194 p = arg.index('=')
6085469b
FT
195 hnd.addext(arg[1:p], arg[p + 1:])
196 return ret
e9817fee 197
173e0e9e 198def chain(env, startreq):
7ed9b82b
FT
199 """Chain-loading WSGI handler
200
201 This handler loads requested files, compiles them and loads them
202 into their own modules. The compiled modules are cached and reused
203 until the file is modified, in which case the previous module is
204 discarded and the new file contents are loaded into a new module
205 in its place. When chaining such modules, an object named `wmain'
206 is first looked for and called with no arguments if found. The
207 object it returns is then used as the WSGI application object for
208 that module, which is reused until the module is reloaded. If
209 `wmain' is not found, an object named `application' is looked for
210 instead. If found, it is used directly as the WSGI application
211 object.
212 """
173e0e9e 213 path = env["SCRIPT_FILENAME"]
58ee5c4a
FT
214 try:
215 mod = getmod(path)
216 except Exception:
217 log.error("Exception occurred when loading %s" % path, exc_info=sys.exc_info())
218 return wsgiutil.simpleerror(env, startreq, 500, "Internal Error", "Could not load WSGI handler.")
173e0e9e
FT
219 entry = None
220 if mod is not None:
2037cee2 221 with mod.lock:
173e0e9e
FT
222 if hasattr(mod, "entry"):
223 entry = mod.entry
224 else:
225 if hasattr(mod.mod, "wmain"):
226 entry = mod.mod.wmain()
227 elif hasattr(mod.mod, "application"):
228 entry = mod.mod.application
229 mod.entry = entry
173e0e9e
FT
230 if entry is not None:
231 return entry(env, startreq)
232 return wsgiutil.simpleerror(env, startreq, 500, "Internal Error", "Invalid WSGI handler.")
173e0e9e 233
e9817fee 234application = handler().handle