1# This extension demonstrates some advanced features of the Python ISAPI
2# framework.
3# We demonstrate:
4# * Reloading your Python module without shutting down IIS (eg, when your
5# .py implementation file changes.)
6# * Custom command-line handling - both additional options and commands.
7# * Using a query string - any part of the URL after a '?' is assumed to
8# be "variable names" separated by '&' - we will print the values of
9# these server variables.
10# * If the tail portion of the URL is "ReportUnhealthy", IIS will be
11# notified we are unhealthy via a HSE_REQ_REPORT_UNHEALTHY request.
12# Whether this is acted upon depends on if the IIS health-checking
13# tools are installed, but you should always see the reason written
14# to the Windows event log - see the IIS documentation for more.
15
16from isapi import isapicon
17from isapi.simple import SimpleExtension
18import sys, os, stat
19
20if hasattr(sys, "isapidllhandle"):
21 import win32traceutil
22
23# Notes on reloading
24# If your HttpFilterProc or HttpExtensionProc functions raises
25# 'isapi.InternalReloadException', the framework will not treat it
26# as an error but instead will terminate your extension, reload your
27# extension module, re-initialize the instance, and re-issue the request.
28# The Initialize functions are called with None as their param. The
29# return code from the terminate function is ignored.
30#
31# This is all the framework does to help you. It is up to your code
32# when you raise this exception. This sample uses a Win32 "find
33# notification". Whenever windows tells us one of the files in the
34# directory has changed, we check if the time of our source-file has
35# changed, and set a flag. Next imcoming request, we check the flag and
36# raise the special exception if set.
37#
38# The end result is that the module is automatically reloaded whenever
39# the source-file changes - you need take no further action to see your
40# changes reflected in the running server.
41
42# The framework only reloads your module - if you have libraries you
43# depend on and also want reloaded, you must arrange for this yourself.
44# One way of doing this would be to special case the import of these
45# modules. Eg:
46# --
47# try:
48# my_module = reload(my_module) # module already imported - reload it
49# except NameError:
50# import my_module # first time around - import it.
51# --
52# When your module is imported for the first time, the NameError will
53# be raised, and the module imported. When the ISAPI framework reloads
54# your module, the existing module will avoid the NameError, and allow
55# you to reload that module.
56
57from isapi import InternalReloadException
58import win32event, win32file, winerror, win32con, threading
59
60try:
61 reload_counter += 1
62except NameError:
63 reload_counter = 0
64
65# A watcher thread that checks for __file__ changing.
66# When it detects it, it simply sets "change_detected" to true.
67class ReloadWatcherThread(threading.Thread):
68 def __init__(self):
69 self.change_detected = False
70 self.filename = __file__
71 if self.filename.endswith("c") or self.filename.endswith("o"):
72 self.filename = self.filename[:-1]
73 self.handle = win32file.FindFirstChangeNotification(
74 os.path.dirname(self.filename),
75 False, # watch tree?
76 win32con.FILE_NOTIFY_CHANGE_LAST_WRITE,
77 )
78 threading.Thread.__init__(self)
79
80 def run(self):
81 last_time = os.stat(self.filename)[stat.ST_MTIME]
82 while 1:
83 try:
84 rc = win32event.WaitForSingleObject(self.handle, win32event.INFINITE)
85 win32file.FindNextChangeNotification(self.handle)
86 except win32event.error as details:
87 # handle closed - thread should terminate.
88 if details.winerror != winerror.ERROR_INVALID_HANDLE:
89 raise
90 break
91 this_time = os.stat(self.filename)[stat.ST_MTIME]
92 if this_time != last_time:
93 print("Detected file change - flagging for reload.")
94 self.change_detected = True
95 last_time = this_time
96
97 def stop(self):
98 win32file.FindCloseChangeNotification(self.handle)
99
100
101# The ISAPI extension - handles requests in our virtual dir, and sends the
102# response to the client.
103class Extension(SimpleExtension):
104 "Python advanced sample Extension"
105
106 def __init__(self):
107 self.reload_watcher = ReloadWatcherThread()
108 self.reload_watcher.start()
109
110 def HttpExtensionProc(self, ecb):
111 # NOTE: If you use a ThreadPoolExtension, you must still perform
112 # this check in HttpExtensionProc - raising the exception from
113 # The "Dispatch" method will just cause the exception to be
114 # rendered to the browser.
115 if self.reload_watcher.change_detected:
116 print("Doing reload")
117 raise InternalReloadException
118
119 url = ecb.GetServerVariable("UNICODE_URL")
120 if url.endswith("ReportUnhealthy"):
121 ecb.ReportUnhealthy("I'm a little sick")
122
123 ecb.SendResponseHeaders("200 OK", "Content-Type: text/html\r\n\r\n", 0)
124 print("<HTML><BODY>", file=ecb)
125
126 qs = ecb.GetServerVariable("QUERY_STRING")
127 if qs:
128 queries = qs.split("&")
129 print("<PRE>", file=ecb)
130 for q in queries:
131 val = ecb.GetServerVariable(q, "<no such variable>")
132 print("%s=%r" % (q, val), file=ecb)
133 print("</PRE><P/>", file=ecb)
134
135 print("This module has been imported", file=ecb)
136 print("%d times" % (reload_counter,), file=ecb)
137 print("</BODY></HTML>", file=ecb)
138 ecb.close()
139 return isapicon.HSE_STATUS_SUCCESS
140
141 def TerminateExtension(self, status):
142 self.reload_watcher.stop()
143
144
145# The entry points for the ISAPI extension.
146def __ExtensionFactory__():
147 return Extension()
148
149
150# Our special command line customization.
151# Pre-install hook for our virtual directory.
152def PreInstallDirectory(params, options):
153 # If the user used our special '--description' option,
154 # then we override our default.
155 if options.description:
156 params.Description = options.description
157
158
159# Post install hook for our entire script
160def PostInstall(params, options):
161 print()
162 print("The sample has been installed.")
163 print("Point your browser to /AdvancedPythonSample")
164 print("If you modify the source file and reload the page,")
165 print("you should see the reload counter increment")
166
167
168# Handler for our custom 'status' argument.
169def status_handler(options, log, arg):
170 "Query the status of something"
171 print("Everything seems to be fine!")
172
173
174custom_arg_handlers = {"status": status_handler}
175
176if __name__ == "__main__":
177 # If run from the command-line, install ourselves.
178 from isapi.install import *
179
180 params = ISAPIParameters(PostInstall=PostInstall)
181 # Setup the virtual directories - this is a list of directories our
182 # extension uses - in this case only 1.
183 # Each extension has a "script map" - this is the mapping of ISAPI
184 # extensions.
185 sm = [ScriptMapParams(Extension="*", Flags=0)]
186 vd = VirtualDirParameters(
187 Name="AdvancedPythonSample",
188 Description=Extension.__doc__,
189 ScriptMaps=sm,
190 ScriptMapUpdate="replace",
191 # specify the pre-install hook.
192 PreInstall=PreInstallDirectory,
193 )
194 params.VirtualDirs = [vd]
195 # Setup our custom option parser.
196 from optparse import OptionParser
197
198 parser = OptionParser("") # blank usage, so isapi sets it.
199 parser.add_option(
200 "",
201 "--description",
202 action="store",
203 help="custom description to use for the virtual directory",
204 )
205
206 HandleCommandLine(
207 params, opt_parser=parser, custom_arg_handlers=custom_arg_handlers
208 )