python: Fixed wsgidir caching problem.
[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
7fe08a6f 36import os, threading, types, importlib
173e0e9e
FT
37from . import wsgiutil
38
5c1a2105 39__all__ = ["application", "wmain", "getmod", "cachedmod", "chain"]
173e0e9e
FT
40
41class cachedmod(object):
42 """Cache entry for modules loaded by getmod()
43
44 Instances of this class are returned by the getmod()
45 function. They contain three data attributes:
46 * mod - The loaded module
47 * lock - A threading.Lock object, which can be used for
48 manipulating this instance in a thread-safe manner
49 * mtime - The time the file was last modified
50
51 Additional data attributes can be arbitrarily added for recording
52 any meta-data about the module.
53 """
7fe08a6f 54 def __init__(self, mod = None, mtime = -1):
173e0e9e
FT
55 self.lock = threading.Lock()
56 self.mod = mod
57 self.mtime = mtime
58
173e0e9e
FT
59modcache = {}
60cachelock = threading.Lock()
61
62def mangle(path):
63 ret = ""
64 for c in path:
65 if c.isalnum():
66 ret += c
67 else:
68 ret += "_"
69 return ret
70
71def getmod(path):
72 """Load the given file as a module, caching it appropriately
73
74 The given file is loaded and compiled into a Python module. The
75 compiled module is cached and returned upon subsequent requests
76 for the same file, unless the file has changed (as determined by
77 its mtime), in which case the cached module is discarded and the
78 new file contents are reloaded in its place.
79
80 The return value is an instance of the cachedmod class, which can
81 be used for locking purposes and for storing arbitrary meta-data
82 about the module. See its documentation for details.
83 """
84 sb = os.stat(path)
2037cee2 85 with cachelock:
173e0e9e
FT
86 if path in modcache:
87 entry = modcache[path]
7fe08a6f 88 else:
b8d56e8f 89 entry = [threading.Lock(), None]
7fe08a6f 90 modcache[path] = entry
b8d56e8f
FT
91 with entry[0]:
92 if entry[1] is None or sb.st_mtime > entry[1].mtime:
7fe08a6f
FT
93 with open(path, "rb") as f:
94 text = f.read()
95 code = compile(text, path, "exec")
96 mod = types.ModuleType(mangle(path))
97 mod.__file__ = path
98 exec(code, mod.__dict__)
b8d56e8f
FT
99 entry[1] = cachedmod(mod, sb.st_mtime)
100 return entry[1]
173e0e9e 101
e9817fee
FT
102class handler(object):
103 def __init__(self):
104 self.lock = threading.Lock()
105 self.handlers = {}
106 self.exts = {}
107 self.addext("wsgi", "chain")
108 self.addext("wsgi3", "chain")
109
110 def resolve(self, name):
111 with self.lock:
112 if name in self.handlers:
113 return self.handlers[name]
114 p = name.rfind('.')
115 if p < 0:
116 return globals()[name]
117 mname = name[:p]
118 hname = name[p + 1:]
119 mod = importlib.import_module(mname)
120 ret = getattr(mod, hname)
121 self.handlers[name] = ret
122 return ret
123
124 def addext(self, ext, handler):
125 self.exts[ext] = self.resolve(handler)
126
127 def handle(self, env, startreq):
128 if not "SCRIPT_FILENAME" in env:
129 return wsgiutil.simpleerror(env, startreq, 500, "Internal Error", "The server is erroneously configured.")
130 path = env["SCRIPT_FILENAME"]
7ed9b82b 131 if not os.access(path, os.R_OK):
e9817fee 132 return wsgiutil.simpleerror(env, startreq, 500, "Internal Error", "The server is erroneously configured.")
7ed9b82b
FT
133 if "HTTP_X_ASH_PYTHON_HANDLER" in env:
134 handler = self.resolve(env["HTTP_X_ASH_PYTHON_HANDLER"])
135 else:
136 base = os.path.basename(path)
137 p = base.rfind('.')
138 if p < 0:
139 return wsgiutil.simpleerror(env, startreq, 500, "Internal Error", "The server is erroneously configured.")
140 ext = base[p + 1:]
141 if not ext in self.exts:
142 return wsgiutil.simpleerror(env, startreq, 500, "Internal Error", "The server is erroneously configured.")
143 handler = self.exts[ext]
144 return handler(env, startreq)
e9817fee
FT
145
146def wmain(*argv):
147 """Main function for ashd(7)-compatible WSGI handlers
148
149 Returns the `application' function. If any arguments are given,
150 they are parsed according to the module documentation.
151 """
152 ret = handler()
153 for arg in argv:
154 if arg[0] == '.':
155 p = arg.index('=')
156 ret.addext(arg[1:p], arg[p + 1:])
157 return ret.handle
158
173e0e9e 159def chain(env, startreq):
7ed9b82b
FT
160 """Chain-loading WSGI handler
161
162 This handler loads requested files, compiles them and loads them
163 into their own modules. The compiled modules are cached and reused
164 until the file is modified, in which case the previous module is
165 discarded and the new file contents are loaded into a new module
166 in its place. When chaining such modules, an object named `wmain'
167 is first looked for and called with no arguments if found. The
168 object it returns is then used as the WSGI application object for
169 that module, which is reused until the module is reloaded. If
170 `wmain' is not found, an object named `application' is looked for
171 instead. If found, it is used directly as the WSGI application
172 object.
173 """
173e0e9e
FT
174 path = env["SCRIPT_FILENAME"]
175 mod = getmod(path)
176 entry = None
177 if mod is not None:
2037cee2 178 with mod.lock:
173e0e9e
FT
179 if hasattr(mod, "entry"):
180 entry = mod.entry
181 else:
182 if hasattr(mod.mod, "wmain"):
183 entry = mod.mod.wmain()
184 elif hasattr(mod.mod, "application"):
185 entry = mod.mod.application
186 mod.entry = entry
173e0e9e
FT
187 if entry is not None:
188 return entry(env, startreq)
189 return wsgiutil.simpleerror(env, startreq, 500, "Internal Error", "Invalid WSGI handler.")
173e0e9e 190
e9817fee 191application = handler().handle