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