advanced.py SampleΒΆ

  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, "&lt;no such variable&gt;")
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    )