python: Ensure greater concurrency is wsgidir.
[ashd.git] / python / ashd / wsgidir.py
CommitLineData
fc2aa563
FT
1"""WSGI handler for serving chained WSGI modules from physical files
2
3The WSGI handler in this module examines the SCRIPT_FILENAME variable
4of the requests it handles -- that is, the physical file corresponding
5to the request, as determined by the webserver -- determining what to
6do with the request based on the extension of that file.
7
8By default, it handles files named `.wsgi' by compiling them into
9Python modules and using them, in turn, as chained WSGI handlers, but
10handlers for other extensions can be installed as well.
11
12When handling `.wsgi' files, the compiled modules are cached and
13reused until the file is modified, in which case the previous module
14is discarded and the new file contents are loaded into a new module in
15its place. When chaining such modules, an object named `wmain' is
16first looked for and called with no arguments if found. The object it
17returns is then used as the WSGI application object for that module,
18which is reused until the module is reloaded. If `wmain' is not found,
19an object named `application' is looked for instead. If found, it is
20used directly as the WSGI application object.
21
22This module itself contains both an `application' and a `wmain'
23object. If this module is used by ashd-wsgi(1) or scgi-wsgi(1) so that
24its wmain function is called, arguments can be specified to it to
25install handlers for other file extensions. Such arguments take the
26form `.EXT=MODULE.HANDLER', where EXT is the file extension to be
27handled, and the MODULE.HANDLER string is treated by splitting it
28along its last constituent dot. The part left of the dot is the name
29of a module which is imported, and the part right of the dot is the
9b417336
FT
30name of an object in that module, which should be a callable adhering
31to the WSGI specification. When called, this module will have made
32sure that the WSGI environment contains the SCRIPT_FILENAME parameter
33and that it is properly working. For example, the argument
34`.fpy=my.module.foohandler' can be given to pass requests for `.fpy'
35files to the function `foohandler' in the module `my.module' (which
36must, of course, be importable). When writing such handler functions,
37you will probably want to use the getmod() function in this module.
fc2aa563
FT
38"""
39
c06db49a 40import os, threading, types
173e0e9e 41import wsgiutil
c06db49a 42
fc2aa563
FT
43__all__ = ["application", "wmain", "getmod", "cachedmod"]
44
f677567f 45class cachedmod(object):
fc2aa563
FT
46 """Cache entry for modules loaded by getmod()
47
48 Instances of this class are returned by the getmod()
49 function. They contain three data attributes:
50 * mod - The loaded module
51 * lock - A threading.Lock object, which can be used for
52 manipulating this instance in a thread-safe manner
53 * mtime - The time the file was last modified
54
55 Additional data attributes can be arbitrarily added for recording
56 any meta-data about the module.
57 """
7fe08a6f 58 def __init__(self, mod = None, mtime = -1):
14ef1e0e
FT
59 self.lock = threading.Lock()
60 self.mod = mod
61 self.mtime = mtime
62
c06db49a
FT
63exts = {}
64modcache = {}
65cachelock = threading.Lock()
66
67def mangle(path):
68 ret = ""
69 for c in path:
70 if c.isalnum():
71 ret += c
72 else:
73 ret += "_"
74 return ret
75
76def getmod(path):
fc2aa563
FT
77 """Load the given file as a module, caching it appropriately
78
79 The given file is loaded and compiled into a Python module. The
80 compiled module is cached and returned upon subsequent requests
81 for the same file, unless the file has changed (as determined by
82 its mtime), in which case the cached module is discarded and the
83 new file contents are reloaded in its place.
84
85 The return value is an instance of the cachedmod class, which can
86 be used for locking purposes and for storing arbitrary meta-data
87 about the module. See its documentation for details.
88 """
c06db49a
FT
89 sb = os.stat(path)
90 cachelock.acquire()
91 try:
92 if path in modcache:
14ef1e0e 93 entry = modcache[path]
7fe08a6f
FT
94 else:
95 entry = cachedmod()
96 modcache[path] = entry
c06db49a
FT
97 finally:
98 cachelock.release()
7fe08a6f
FT
99 entry.lock.acquire()
100 try:
101 if entry.mod is None or sb.st_mtime > entry.mtime:
102 f = open(path, "r")
103 try:
104 text = f.read()
105 finally:
106 f.close()
107 code = compile(text, path, "exec")
108 mod = types.ModuleType(mangle(path))
109 mod.__file__ = path
110 exec code in mod.__dict__
111 entry.mod = mod
112 entry.mtime = sb.st_mtime
113 return entry
114 finally:
115 entry.lock.release()
c06db49a 116
9b417336
FT
117def chain(env, startreq):
118 path = env["SCRIPT_FILENAME"]
c06db49a 119 mod = getmod(path)
14ef1e0e
FT
120 entry = None
121 if mod is not None:
122 mod.lock.acquire()
123 try:
124 if hasattr(mod, "entry"):
125 entry = mod.entry
126 else:
127 if hasattr(mod.mod, "wmain"):
adb11d5f 128 entry = mod.mod.wmain()
14ef1e0e
FT
129 elif hasattr(mod.mod, "application"):
130 entry = mod.mod.application
131 mod.entry = entry
132 finally:
133 mod.lock.release()
134 if entry is not None:
135 return entry(env, startreq)
36ea06a6 136 return wsgiutil.simpleerror(env, startreq, 500, "Internal Error", "Invalid WSGI handler.")
c06db49a
FT
137exts["wsgi"] = chain
138
48a91dd9
FT
139def addext(ext, handler):
140 p = handler.rindex('.')
141 mname = handler[:p]
142 hname = handler[p + 1:]
143 mod = __import__(mname, fromlist = ["dummy"])
144 exts[ext] = getattr(mod, hname)
145
c06db49a 146def application(env, startreq):
fc2aa563
FT
147 """WSGI handler function
148
149 Handles WSGI requests as per the module documentation.
150 """
c06db49a 151 if not "SCRIPT_FILENAME" in env:
36ea06a6 152 return wsgiutil.simpleerror(env, startreq, 500, "Internal Error", "The server is erroneously configured.")
c06db49a
FT
153 path = env["SCRIPT_FILENAME"]
154 base = os.path.basename(path)
155 p = base.rfind('.')
156 if p < 0 or not os.access(path, os.R_OK):
36ea06a6 157 return wsgiutil.simpleerror(env, startreq, 500, "Internal Error", "The server is erroneously configured.")
c06db49a
FT
158 ext = base[p + 1:]
159 if not ext in exts:
36ea06a6 160 return wsgiutil.simpleerror(env, startreq, 500, "Internal Error", "The server is erroneously configured.")
9b417336 161 return(exts[ext](env, startreq))
c06db49a 162
adb11d5f 163def wmain(*argv):
fc2aa563
FT
164 """Main function for ashd(7)-compatible WSGI handlers
165
166 Returns the `application' function. If any arguments are given,
167 they are parsed according to the module documentation.
168 """
48a91dd9
FT
169 for arg in argv:
170 if arg[0] == '.':
171 p = arg.index('=')
172 addext(arg[1:p], arg[p + 1:])
c06db49a 173 return application