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, "<no such variable>") 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