196 lines
7.9 KiB
Python
196 lines
7.9 KiB
Python
# This extension demonstrates some advanced features of the Python ISAPI
|
|
# framework.
|
|
# We demonstrate:
|
|
# * Reloading your Python module without shutting down IIS (eg, when your
|
|
# .py implementation file changes.)
|
|
# * Custom command-line handling - both additional options and commands.
|
|
# * Using a query string - any part of the URL after a '?' is assumed to
|
|
# be "variable names" separated by '&' - we will print the values of
|
|
# these server variables.
|
|
# * If the tail portion of the URL is "ReportUnhealthy", IIS will be
|
|
# notified we are unhealthy via a HSE_REQ_REPORT_UNHEALTHY request.
|
|
# Whether this is acted upon depends on if the IIS health-checking
|
|
# tools are installed, but you should always see the reason written
|
|
# to the Windows event log - see the IIS documentation for more.
|
|
|
|
from isapi import isapicon
|
|
from isapi.simple import SimpleExtension
|
|
import sys, os, stat
|
|
|
|
if hasattr(sys, "isapidllhandle"):
|
|
import win32traceutil
|
|
|
|
# Notes on reloading
|
|
# If your HttpFilterProc or HttpExtensionProc functions raises
|
|
# 'isapi.InternalReloadException', the framework will not treat it
|
|
# as an error but instead will terminate your extension, reload your
|
|
# extension module, re-initialize the instance, and re-issue the request.
|
|
# The Initialize functions are called with None as their param. The
|
|
# return code from the terminate function is ignored.
|
|
#
|
|
# This is all the framework does to help you. It is up to your code
|
|
# when you raise this exception. This sample uses a Win32 "find
|
|
# notification". Whenever windows tells us one of the files in the
|
|
# directory has changed, we check if the time of our source-file has
|
|
# changed, and set a flag. Next imcoming request, we check the flag and
|
|
# raise the special exception if set.
|
|
#
|
|
# The end result is that the module is automatically reloaded whenever
|
|
# the source-file changes - you need take no further action to see your
|
|
# changes reflected in the running server.
|
|
|
|
# The framework only reloads your module - if you have libraries you
|
|
# depend on and also want reloaded, you must arrange for this yourself.
|
|
# One way of doing this would be to special case the import of these
|
|
# modules. Eg:
|
|
# --
|
|
# try:
|
|
# my_module = reload(my_module) # module already imported - reload it
|
|
# except NameError:
|
|
# import my_module # first time around - import it.
|
|
# --
|
|
# When your module is imported for the first time, the NameError will
|
|
# be raised, and the module imported. When the ISAPI framework reloads
|
|
# your module, the existing module will avoid the NameError, and allow
|
|
# you to reload that module.
|
|
|
|
from isapi import InternalReloadException
|
|
import win32event, win32file, winerror, win32con, threading
|
|
|
|
try:
|
|
reload_counter += 1
|
|
except NameError:
|
|
reload_counter = 0
|
|
|
|
# A watcher thread that checks for __file__ changing.
|
|
# When it detects it, it simply sets "change_detected" to true.
|
|
class ReloadWatcherThread(threading.Thread):
|
|
def __init__(self):
|
|
self.change_detected = False
|
|
self.filename = __file__
|
|
if self.filename.endswith("c") or self.filename.endswith("o"):
|
|
self.filename = self.filename[:-1]
|
|
self.handle = win32file.FindFirstChangeNotification(
|
|
os.path.dirname(self.filename),
|
|
False, # watch tree?
|
|
win32con.FILE_NOTIFY_CHANGE_LAST_WRITE)
|
|
threading.Thread.__init__(self)
|
|
|
|
def run(self):
|
|
last_time = os.stat(self.filename)[stat.ST_MTIME]
|
|
while 1:
|
|
try:
|
|
rc = win32event.WaitForSingleObject(self.handle,
|
|
win32event.INFINITE)
|
|
win32file.FindNextChangeNotification(self.handle)
|
|
except win32event.error as details:
|
|
# handle closed - thread should terminate.
|
|
if details.winerror != winerror.ERROR_INVALID_HANDLE:
|
|
raise
|
|
break
|
|
this_time = os.stat(self.filename)[stat.ST_MTIME]
|
|
if this_time != last_time:
|
|
print("Detected file change - flagging for reload.")
|
|
self.change_detected = True
|
|
last_time = this_time
|
|
|
|
def stop(self):
|
|
win32file.FindCloseChangeNotification(self.handle)
|
|
|
|
# The ISAPI extension - handles requests in our virtual dir, and sends the
|
|
# response to the client.
|
|
class Extension(SimpleExtension):
|
|
"Python advanced sample Extension"
|
|
def __init__(self):
|
|
self.reload_watcher = ReloadWatcherThread()
|
|
self.reload_watcher.start()
|
|
|
|
def HttpExtensionProc(self, ecb):
|
|
# NOTE: If you use a ThreadPoolExtension, you must still perform
|
|
# this check in HttpExtensionProc - raising the exception from
|
|
# The "Dispatch" method will just cause the exception to be
|
|
# rendered to the browser.
|
|
if self.reload_watcher.change_detected:
|
|
print("Doing reload")
|
|
raise InternalReloadException
|
|
|
|
url = ecb.GetServerVariable("UNICODE_URL")
|
|
if url.endswith("ReportUnhealthy"):
|
|
ecb.ReportUnhealthy("I'm a little sick")
|
|
|
|
ecb.SendResponseHeaders("200 OK", "Content-Type: text/html\r\n\r\n", 0)
|
|
print("<HTML><BODY>", file=ecb)
|
|
|
|
qs = ecb.GetServerVariable("QUERY_STRING")
|
|
if qs:
|
|
queries = qs.split("&")
|
|
print("<PRE>", file=ecb)
|
|
for q in queries:
|
|
val = ecb.GetServerVariable(q, '<no such variable>')
|
|
print("%s=%r" % (q, val), file=ecb)
|
|
print("</PRE><P/>", file=ecb)
|
|
|
|
print("This module has been imported", file=ecb)
|
|
print("%d times" % (reload_counter,), file=ecb)
|
|
print("</BODY></HTML>", file=ecb)
|
|
ecb.close()
|
|
return isapicon.HSE_STATUS_SUCCESS
|
|
|
|
def TerminateExtension(self, status):
|
|
self.reload_watcher.stop()
|
|
|
|
# The entry points for the ISAPI extension.
|
|
def __ExtensionFactory__():
|
|
return Extension()
|
|
|
|
# Our special command line customization.
|
|
# Pre-install hook for our virtual directory.
|
|
def PreInstallDirectory(params, options):
|
|
# If the user used our special '--description' option,
|
|
# then we override our default.
|
|
if options.description:
|
|
params.Description = options.description
|
|
|
|
# Post install hook for our entire script
|
|
def PostInstall(params, options):
|
|
print()
|
|
print("The sample has been installed.")
|
|
print("Point your browser to /AdvancedPythonSample")
|
|
print("If you modify the source file and reload the page,")
|
|
print("you should see the reload counter increment")
|
|
|
|
# Handler for our custom 'status' argument.
|
|
def status_handler(options, log, arg):
|
|
"Query the status of something"
|
|
print("Everything seems to be fine!")
|
|
|
|
custom_arg_handlers = {"status": status_handler}
|
|
|
|
if __name__=='__main__':
|
|
# If run from the command-line, install ourselves.
|
|
from isapi.install import *
|
|
params = ISAPIParameters(PostInstall = PostInstall)
|
|
# Setup the virtual directories - this is a list of directories our
|
|
# extension uses - in this case only 1.
|
|
# Each extension has a "script map" - this is the mapping of ISAPI
|
|
# extensions.
|
|
sm = [
|
|
ScriptMapParams(Extension="*", Flags=0)
|
|
]
|
|
vd = VirtualDirParameters(Name="AdvancedPythonSample",
|
|
Description = Extension.__doc__,
|
|
ScriptMaps = sm,
|
|
ScriptMapUpdate = "replace",
|
|
# specify the pre-install hook.
|
|
PreInstall = PreInstallDirectory
|
|
)
|
|
params.VirtualDirs = [vd]
|
|
# Setup our custom option parser.
|
|
from optparse import OptionParser
|
|
parser = OptionParser('') # blank usage, so isapi sets it.
|
|
parser.add_option("", "--description",
|
|
action="store",
|
|
help="custom description to use for the virtual directory")
|
|
|
|
HandleCommandLine(params, opt_parser=parser,
|
|
custom_arg_handlers = custom_arg_handlers)
|