Format: text/x-python
⬇ Download File (27.4 KB)
   1  """Installation utilities for Python ISAPI filters and extensions."""
   2  
   3  # this code adapted from "Tomcat JK2 ISAPI redirector", part of Apache
   4  # Created July 2004, Mark Hammond.
   5  from __future__ import annotations
   6  
   7  import importlib.machinery
   8  import os
   9  import shutil
  10  import stat
  11  import sys
  12  import traceback
  13  from collections.abc import Mapping
  14  
  15  import pythoncom
  16  import win32api
  17  import winerror
  18  from win32com.client import GetObject
  19  
  20  _APP_INPROC = 0
  21  _APP_OUTPROC = 1
  22  _APP_POOLED = 2
  23  _IIS_OBJECT = "IIS://LocalHost/W3SVC"
  24  _IIS_SERVER = "IIsWebServer"
  25  _IIS_WEBDIR = "IIsWebDirectory"
  26  _IIS_WEBVIRTUALDIR = "IIsWebVirtualDir"
  27  _IIS_FILTERS = "IIsFilters"
  28  _IIS_FILTER = "IIsFilter"
  29  
  30  _DEFAULT_SERVER_NAME = "Default Web Site"
  31  _DEFAULT_HEADERS = "X-Powered-By: Python"
  32  _DEFAULT_PROTECTION = _APP_POOLED
  33  
  34  # Default is for 'execute' only access - ie, only the extension
  35  # can be used.  This can be overridden via your install script.
  36  _DEFAULT_ACCESS_EXECUTE = True
  37  _DEFAULT_ACCESS_READ = False
  38  _DEFAULT_ACCESS_WRITE = False
  39  _DEFAULT_ACCESS_SCRIPT = False
  40  _DEFAULT_CONTENT_INDEXED = False
  41  _DEFAULT_ENABLE_DIR_BROWSING = False
  42  _DEFAULT_ENABLE_DEFAULT_DOC = False
  43  
  44  this_dir = os.path.abspath(os.path.dirname(__file__))
  45  
  46  
  47  class FilterParameters:
  48      Name = None
  49      Description = None
  50      Path = None
  51      Server = None
  52      # Params that control if/how AddExtensionFile is called.
  53      AddExtensionFile = True
  54      AddExtensionFile_Enabled = True
  55      AddExtensionFile_GroupID = None  # defaults to Name
  56      AddExtensionFile_CanDelete = True
  57      AddExtensionFile_Description = None  # defaults to Description.
  58  
  59      def __init__(self, **kw):
  60          self.__dict__.update(kw)
  61  
  62  
  63  class VirtualDirParameters:
  64      Name = None  # Must be provided.
  65      Description = None  # defaults to Name
  66      AppProtection = _DEFAULT_PROTECTION
  67      Headers = _DEFAULT_HEADERS
  68      Path = None  # defaults to WWW root.
  69      Type = _IIS_WEBVIRTUALDIR
  70      AccessExecute = _DEFAULT_ACCESS_EXECUTE
  71      AccessRead = _DEFAULT_ACCESS_READ
  72      AccessWrite = _DEFAULT_ACCESS_WRITE
  73      AccessScript = _DEFAULT_ACCESS_SCRIPT
  74      ContentIndexed = _DEFAULT_CONTENT_INDEXED
  75      EnableDirBrowsing = _DEFAULT_ENABLE_DIR_BROWSING
  76      EnableDefaultDoc = _DEFAULT_ENABLE_DEFAULT_DOC
  77      DefaultDoc = None  # Only set in IIS if not None
  78      ScriptMaps: list[ScriptMapParams] = []
  79      ScriptMapUpdate = "end"  # can be 'start', 'end', 'replace'
  80      Server = None
  81  
  82      def __init__(self, **kw):
  83          self.__dict__.update(kw)
  84  
  85      def is_root(self):
  86          "This virtual directory is a root directory if parent and name are blank"
  87          parent, name = self.split_path()
  88          return not parent and not name
  89  
  90      def split_path(self):
  91          return split_path(self.Name)
  92  
  93  
  94  class ScriptMapParams:
  95      Extension = None
  96      Module = None
  97      Flags = 5
  98      Verbs = ""
  99      # Params that control if/how AddExtensionFile is called.
 100      AddExtensionFile = True
 101      AddExtensionFile_Enabled = True
 102      AddExtensionFile_GroupID = None  # defaults to Name
 103      AddExtensionFile_CanDelete = True
 104      AddExtensionFile_Description = None  # defaults to Description.
 105  
 106      def __init__(self, **kw):
 107          self.__dict__.update(kw)
 108  
 109      def __str__(self):
 110          "Format this parameter suitable for IIS"
 111          items = [self.Extension, self.Module, self.Flags]
 112          # IIS gets upset if there is a trailing verb comma, but no verbs
 113          if self.Verbs:
 114              items.append(self.Verbs)
 115          items = [str(item) for item in items]
 116          return ",".join(items)
 117  
 118  
 119  class ISAPIParameters:
 120      ServerName = _DEFAULT_SERVER_NAME
 121      # Description = None
 122      Filters: list[FilterParameters] = []
 123      VirtualDirs: list[VirtualDirParameters] = []
 124  
 125      def __init__(self, **kw):
 126          self.__dict__.update(kw)
 127  
 128  
 129  verbose = 1  # The level - 0 is quiet.
 130  
 131  
 132  def log(level, what):
 133      if verbose >= level:
 134          print(what)
 135  
 136  
 137  # Convert an ADSI COM exception to the Win32 error code embedded in it.
 138  def _GetWin32ErrorCode(com_exc):
 139      hr = com_exc.hresult
 140      # If we have more details in the 'excepinfo' struct, use it.
 141      if com_exc.excepinfo:
 142          hr = com_exc.excepinfo[-1]
 143      if winerror.HRESULT_FACILITY(hr) != winerror.FACILITY_WIN32:
 144          raise
 145      return winerror.SCODE_CODE(hr)
 146  
 147  
 148  class InstallationError(Exception):
 149      pass
 150  
 151  
 152  class ItemNotFound(InstallationError):
 153      pass
 154  
 155  
 156  class ConfigurationError(InstallationError):
 157      pass
 158  
 159  
 160  def FindPath(options, server, name):
 161      if name.lower().startswith("iis://"):
 162          return name
 163      else:
 164          if name and name[0] != "/":
 165              name = "/" + name
 166          return FindWebServer(options, server) + "/ROOT" + name
 167  
 168  
 169  def LocateWebServerPath(description):
 170      """
 171      Find an IIS web server whose name or comment matches the provided
 172      description (case-insensitive).
 173  
 174      >>> LocateWebServerPath('Default Web Site') # doctest: +SKIP
 175  
 176      or
 177  
 178      >>> LocateWebServerPath('1') #doctest: +SKIP
 179      """
 180      assert len(description) >= 1, "Server name or comment is required"
 181      iis = GetObject(_IIS_OBJECT)
 182      description = description.lower().strip()
 183      for site in iis:
 184          # Name is generally a number, but no need to assume that.
 185          site_attributes = [
 186              getattr(site, attr, "").lower().strip()
 187              for attr in ("Name", "ServerComment")
 188          ]
 189          if description in site_attributes:
 190              return site.AdsPath
 191      msg = "No web sites match the description '%s'" % description
 192      raise ItemNotFound(msg)
 193  
 194  
 195  def GetWebServer(description=None):
 196      """
 197      Load the web server instance (COM object) for a given instance
 198      or description.
 199      If None is specified, the default website is retrieved (indicated
 200      by the identifier 1.
 201      """
 202      description = description or "1"
 203      path = LocateWebServerPath(description)
 204      server = LoadWebServer(path)
 205      return server
 206  
 207  
 208  def LoadWebServer(path):
 209      try:
 210          server = GetObject(path)
 211      except pythoncom.com_error as exc:
 212          msg = exc.strerror
 213          if exc.excepinfo and exc.excepinfo[2]:
 214              msg = exc.excepinfo[2]
 215          msg = f"WebServer {path}: {msg}"
 216          raise ItemNotFound(msg)
 217      return server
 218  
 219  
 220  def FindWebServer(options, server_desc):
 221      """
 222      Legacy function to allow options to define a .server property
 223      to override the other parameter.  Use GetWebServer instead.
 224      """
 225      # options takes precedence
 226      server_desc = options.server or server_desc
 227      # make sure server_desc is unicode (could be mbcs if passed in
 228      #  sys.argv).
 229      if server_desc and not isinstance(server_desc, str):
 230          server_desc = server_desc.decode("mbcs")
 231  
 232      # get the server (if server_desc is None, the default site is acquired)
 233      server = GetWebServer(server_desc)
 234      return server.adsPath
 235  
 236  
 237  def split_path(path):
 238      """
 239      Get the parent path and basename.
 240  
 241      >>> split_path('/')
 242      ['', '']
 243  
 244      >>> split_path('')
 245      ['', '']
 246  
 247      >>> split_path('foo')
 248      ['', 'foo']
 249  
 250      >>> split_path('/foo')
 251      ['', 'foo']
 252  
 253      >>> split_path('/foo/bar')
 254      ['/foo', 'bar']
 255  
 256      >>> split_path('foo/bar')
 257      ['/foo', 'bar']
 258      """
 259  
 260      if not path.startswith("/"):
 261          path = "/" + path
 262      return path.rsplit("/", 1)
 263  
 264  
 265  def _CreateDirectory(iis_dir, name, params):
 266      # We used to go to lengths to keep an existing virtual directory
 267      # in place.  However, in some cases the existing directories got
 268      # into a bad state, and an update failed to get them working.
 269      # So we nuke it first.  If this is a problem, we could consider adding
 270      # a --keep-existing option.
 271      try:
 272          # Also seen the Class change to a generic IISObject - so nuke
 273          # *any* existing object, regardless of Class
 274          assert name.strip("/"), "mustn't delete the root!"
 275          iis_dir.Delete("", name)
 276          log(2, f"Deleted old directory '{name}'")
 277      except pythoncom.com_error:
 278          pass
 279  
 280      newDir = iis_dir.Create(params.Type, name)
 281      log(2, f"Creating new directory '{name}' in {iis_dir.Name}...")
 282  
 283      friendly = params.Description or params.Name
 284      newDir.AppFriendlyName = friendly
 285  
 286      # Note that the new directory won't be visible in the IIS UI
 287      # unless the directory exists on the filesystem.
 288      try:
 289          path = params.Path or iis_dir.Path
 290          newDir.Path = path
 291      except AttributeError:
 292          # If params.Type is IIS_WEBDIRECTORY, an exception is thrown
 293          pass
 294      newDir.AppCreate2(params.AppProtection)
 295      # XXX - note that these Headers only work in IIS6 and earlier.  IIS7
 296      # only supports them on the w3svc node - not even on individial sites,
 297      # let alone individual extensions in the site!
 298      if params.Headers:
 299          newDir.HttpCustomHeaders = params.Headers
 300  
 301      log(2, "Setting directory options...")
 302      newDir.AccessExecute = params.AccessExecute
 303      newDir.AccessRead = params.AccessRead
 304      newDir.AccessWrite = params.AccessWrite
 305      newDir.AccessScript = params.AccessScript
 306      newDir.ContentIndexed = params.ContentIndexed
 307      newDir.EnableDirBrowsing = params.EnableDirBrowsing
 308      newDir.EnableDefaultDoc = params.EnableDefaultDoc
 309      if params.DefaultDoc is not None:
 310          newDir.DefaultDoc = params.DefaultDoc
 311      newDir.SetInfo()
 312      return newDir
 313  
 314  
 315  def CreateDirectory(params, options):
 316      _CallHook(params, "PreInstall", options)
 317      if not params.Name:
 318          raise ConfigurationError("No Name param")
 319      parent, name = params.split_path()
 320      target_dir = GetObject(FindPath(options, params.Server, parent))
 321  
 322      if not params.is_root():
 323          target_dir = _CreateDirectory(target_dir, name, params)
 324  
 325      AssignScriptMaps(params.ScriptMaps, target_dir, params.ScriptMapUpdate)
 326  
 327      _CallHook(params, "PostInstall", options, target_dir)
 328      log(1, f"Configured Virtual Directory: {params.Name}")
 329      return target_dir
 330  
 331  
 332  def AssignScriptMaps(script_maps, target, update="replace"):
 333      """Updates IIS with the supplied script map information.
 334  
 335      script_maps is a list of ScriptMapParameter objects
 336  
 337      target is an IIS Virtual Directory to assign the script maps to
 338  
 339      update is a string indicating how to update the maps, one of  ('start',
 340      'end', or 'replace')
 341      """
 342      # determine which function to use to assign script maps
 343      script_map_func = "_AssignScriptMaps" + update.capitalize()
 344      try:
 345          script_map_func = eval(script_map_func)
 346      except NameError:
 347          msg = "Unknown ScriptMapUpdate option '%s'" % update
 348          raise ConfigurationError(msg)
 349      # use the str method to format the script maps for IIS
 350      script_maps = [str(s) for s in script_maps]
 351      # call the correct function
 352      script_map_func(target, script_maps)
 353      target.SetInfo()
 354  
 355  
 356  def get_unique_items(sequence, reference):
 357      "Return items in sequence that can't be found in reference."
 358      return tuple([item for item in sequence if item not in reference])
 359  
 360  
 361  def _AssignScriptMapsReplace(target, script_maps):
 362      target.ScriptMaps = script_maps
 363  
 364  
 365  def _AssignScriptMapsEnd(target, script_maps):
 366      unique_new_maps = get_unique_items(script_maps, target.ScriptMaps)
 367      target.ScriptMaps += unique_new_maps
 368  
 369  
 370  def _AssignScriptMapsStart(target, script_maps):
 371      unique_new_maps = get_unique_items(script_maps, target.ScriptMaps)
 372      target.ScriptMaps = unique_new_maps + target.ScriptMaps
 373  
 374  
 375  def CreateISAPIFilter(filterParams, options):
 376      server = FindWebServer(options, filterParams.Server)
 377      _CallHook(filterParams, "PreInstall", options)
 378      try:
 379          filters = GetObject(server + "/Filters")
 380      except pythoncom.com_error as exc:
 381          # Brand new sites don't have the '/Filters' collection - create it.
 382          # Any errors other than 'not found' we shouldn't ignore.
 383          if (
 384              winerror.HRESULT_FACILITY(exc.hresult) != winerror.FACILITY_WIN32
 385              or winerror.HRESULT_CODE(exc.hresult) != winerror.ERROR_PATH_NOT_FOUND
 386          ):
 387              raise
 388          server_ob = GetObject(server)
 389          filters = server_ob.Create(_IIS_FILTERS, "Filters")
 390          filters.FilterLoadOrder = ""
 391          filters.SetInfo()
 392  
 393      # As for VirtualDir, delete an existing one.
 394      assert filterParams.Name.strip("/"), "mustn't delete the root!"
 395      try:
 396          filters.Delete(_IIS_FILTER, filterParams.Name)
 397          log(2, f"Deleted old filter '{filterParams.Name}'")
 398      except pythoncom.com_error:
 399          pass
 400      newFilter = filters.Create(_IIS_FILTER, filterParams.Name)
 401      log(2, "Created new ISAPI filter...")
 402      assert os.path.isfile(filterParams.Path)
 403      newFilter.FilterPath = filterParams.Path
 404      newFilter.FilterDescription = filterParams.Description
 405      newFilter.SetInfo()
 406      load_order = [b.strip() for b in filters.FilterLoadOrder.split(",") if b]
 407      if filterParams.Name not in load_order:
 408          load_order.append(filterParams.Name)
 409          filters.FilterLoadOrder = ",".join(load_order)
 410          filters.SetInfo()
 411      _CallHook(filterParams, "PostInstall", options, newFilter)
 412      log(1, f"Configured Filter: {filterParams.Name}")
 413      return newFilter
 414  
 415  
 416  def DeleteISAPIFilter(filterParams, options):
 417      _CallHook(filterParams, "PreRemove", options)
 418      server = FindWebServer(options, filterParams.Server)
 419      ob_path = server + "/Filters"
 420      try:
 421          filters = GetObject(ob_path)
 422      except pythoncom.com_error as details:
 423          # failure to open the filters just means a totally clean IIS install
 424          # (IIS5 at least has no 'Filters' key when freshly installed).
 425          log(2, f"ISAPI filter path '{ob_path}' did not exist.")
 426          return
 427      try:
 428          assert filterParams.Name.strip("/"), "mustn't delete the root!"
 429          filters.Delete(_IIS_FILTER, filterParams.Name)
 430          log(2, f"Deleted ISAPI filter '{filterParams.Name}'")
 431      except pythoncom.com_error as details:
 432          rc = _GetWin32ErrorCode(details)
 433          if rc != winerror.ERROR_PATH_NOT_FOUND:
 434              raise
 435          log(2, f"ISAPI filter '{filterParams.Name}' did not exist.")
 436      # Remove from the load order
 437      load_order = [b.strip() for b in filters.FilterLoadOrder.split(",") if b]
 438      if filterParams.Name in load_order:
 439          load_order.remove(filterParams.Name)
 440          filters.FilterLoadOrder = ",".join(load_order)
 441          filters.SetInfo()
 442      _CallHook(filterParams, "PostRemove", options)
 443      log(1, f"Deleted Filter: {filterParams.Name}")
 444  
 445  
 446  def _AddExtensionFile(module, def_groupid, def_desc, params, options):
 447      group_id = params.AddExtensionFile_GroupID or def_groupid
 448      desc = params.AddExtensionFile_Description or def_desc
 449      try:
 450          ob = GetObject(_IIS_OBJECT)
 451          ob.AddExtensionFile(
 452              module,
 453              params.AddExtensionFile_Enabled,
 454              group_id,
 455              params.AddExtensionFile_CanDelete,
 456              desc,
 457          )
 458          log(2, f"Added extension file '{module}' ({desc})")
 459      except (pythoncom.com_error, AttributeError) as details:
 460          # IIS5 always fails.  Probably should upgrade this to
 461          # complain more loudly if IIS6 fails.
 462          log(2, f"Failed to add extension file '{module}': {details}")
 463  
 464  
 465  def AddExtensionFiles(params, options):
 466      """Register the modules used by the filters/extensions as a trusted
 467      'extension module' - required by the default IIS6 security settings."""
 468      # Add each module only once.
 469      added = {}
 470      for vd in params.VirtualDirs:
 471          for smp in vd.ScriptMaps:
 472              if smp.Module not in added and smp.AddExtensionFile:
 473                  _AddExtensionFile(smp.Module, vd.Name, vd.Description, smp, options)
 474                  added[smp.Module] = True
 475  
 476      for fd in params.Filters:
 477          if fd.Path not in added and fd.AddExtensionFile:
 478              _AddExtensionFile(fd.Path, fd.Name, fd.Description, fd, options)
 479              added[fd.Path] = True
 480  
 481  
 482  def _DeleteExtensionFileRecord(module, options):
 483      try:
 484          ob = GetObject(_IIS_OBJECT)
 485          ob.DeleteExtensionFileRecord(module)
 486          log(2, "Deleted extension file record for '%s'" % module)
 487      except (pythoncom.com_error, AttributeError) as details:
 488          log(2, f"Failed to remove extension file '{module}': {details}")
 489  
 490  
 491  def DeleteExtensionFileRecords(params, options):
 492      deleted = {}  # only remove each .dll once.
 493      for vd in params.VirtualDirs:
 494          for smp in vd.ScriptMaps:
 495              if smp.Module not in deleted and smp.AddExtensionFile:
 496                  _DeleteExtensionFileRecord(smp.Module, options)
 497                  deleted[smp.Module] = True
 498  
 499      for filter_def in params.Filters:
 500          if filter_def.Path not in deleted and filter_def.AddExtensionFile:
 501              _DeleteExtensionFileRecord(filter_def.Path, options)
 502              deleted[filter_def.Path] = True
 503  
 504  
 505  def CheckLoaderModule(dll_name):
 506      suffix = "_d" if "_d.pyd" in importlib.machinery.EXTENSION_SUFFIXES else ""
 507      template = os.path.join(this_dir, "PyISAPI_loader" + suffix + ".dll")
 508      if not os.path.isfile(template):
 509          raise ConfigurationError(f"Template loader '{template}' does not exist")
 510      # We can't do a simple "is newer" check, as the DLL is specific to the
 511      # Python version.  So we check the date-time and size are identical,
 512      # and skip the copy in that case.
 513      src_stat = os.stat(template)
 514      try:
 515          dest_stat = os.stat(dll_name)
 516      except OSError:
 517          same = 0
 518      else:
 519          same = (
 520              src_stat[stat.ST_SIZE] == dest_stat[stat.ST_SIZE]
 521              and src_stat[stat.ST_MTIME] == dest_stat[stat.ST_MTIME]
 522          )
 523      if not same:
 524          log(2, f"Updating {template}->{dll_name}")
 525          shutil.copyfile(template, dll_name)
 526          shutil.copystat(template, dll_name)
 527      else:
 528          log(2, f"{dll_name} is up to date.")
 529  
 530  
 531  def _CallHook(ob, hook_name, options, *extra_args):
 532      func = getattr(ob, hook_name, None)
 533      if func is not None:
 534          args = (ob, options) + extra_args
 535          func(*args)
 536  
 537  
 538  def Install(params, options):
 539      _CallHook(params, "PreInstall", options)
 540      for vd in params.VirtualDirs:
 541          CreateDirectory(vd, options)
 542  
 543      for filter_def in params.Filters:
 544          CreateISAPIFilter(filter_def, options)
 545  
 546      AddExtensionFiles(params, options)
 547  
 548      _CallHook(params, "PostInstall", options)
 549  
 550  
 551  def RemoveDirectory(params, options):
 552      if params.is_root():
 553          return
 554      try:
 555          directory = GetObject(FindPath(options, params.Server, params.Name))
 556      except pythoncom.com_error as details:
 557          rc = _GetWin32ErrorCode(details)
 558          if rc != winerror.ERROR_PATH_NOT_FOUND:
 559              raise
 560          log(2, "VirtualDirectory '%s' did not exist" % params.Name)
 561          directory = None
 562      if directory is not None:
 563          # Be robust should IIS get upset about unloading.
 564          try:
 565              directory.AppUnLoad()
 566          except:
 567              exc_val = sys.exc_info()[1]
 568              log(2, f"AppUnLoad() for {params.Name} failed: {exc_val}")
 569          # Continue trying to delete it.
 570          try:
 571              parent = GetObject(directory.Parent)
 572              parent.Delete(directory.Class, directory.Name)
 573              log(1, f"Deleted Virtual Directory: {params.Name}")
 574          except:
 575              exc_val = sys.exc_info()[1]
 576              log(1, f"Failed to remove directory {params.Name}: {exc_val}")
 577  
 578  
 579  def RemoveScriptMaps(vd_params, options):
 580      "Remove script maps from the already installed virtual directory"
 581      parent, name = vd_params.split_path()
 582      target_dir = GetObject(FindPath(options, vd_params.Server, parent))
 583      installed_maps = list(target_dir.ScriptMaps)
 584      for _map in map(str, vd_params.ScriptMaps):
 585          if _map in installed_maps:
 586              installed_maps.remove(_map)
 587      target_dir.ScriptMaps = installed_maps
 588      target_dir.SetInfo()
 589  
 590  
 591  def Uninstall(params, options):
 592      _CallHook(params, "PreRemove", options)
 593  
 594      DeleteExtensionFileRecords(params, options)
 595  
 596      for vd in params.VirtualDirs:
 597          _CallHook(vd, "PreRemove", options)
 598  
 599          RemoveDirectory(vd, options)
 600          if vd.is_root():
 601              # if this is installed to the root virtual directory, we can't delete it
 602              #  so remove the script maps.
 603              RemoveScriptMaps(vd, options)
 604  
 605          _CallHook(vd, "PostRemove", options)
 606  
 607      for filter_def in params.Filters:
 608          DeleteISAPIFilter(filter_def, options)
 609      _CallHook(params, "PostRemove", options)
 610  
 611  
 612  # Patch up any missing module names in the params, replacing them with
 613  # the DLL name that hosts this extension/filter.
 614  def _PatchParamsModule(params, dll_name, file_must_exist=True):
 615      if file_must_exist:
 616          if not os.path.isfile(dll_name):
 617              raise ConfigurationError(f"{dll_name} does not exist")
 618  
 619      # Patch up all references to the DLL.
 620      for f in params.Filters:
 621          if f.Path is None:
 622              f.Path = dll_name
 623      for d in params.VirtualDirs:
 624          for sm in d.ScriptMaps:
 625              if sm.Module is None:
 626                  sm.Module = dll_name
 627  
 628  
 629  def GetLoaderModuleName(mod_name, check_module=None):
 630      # find the name of the DLL hosting us.
 631      # By default, this is "_{module_base_name}.dll"
 632      if hasattr(sys, "frozen"):
 633          # What to do?  The .dll knows its name, but this is likely to be
 634          # executed via a .exe, which does not know.
 635          base, ext = os.path.splitext(mod_name)
 636          path, base = os.path.split(base)
 637          # handle the common case of 'foo.exe'/'foow.exe'
 638          if base.endswith("w"):
 639              base = base[:-1]
 640          # For py2exe, we have '_foo.dll' as the standard pyisapi loader - but
 641          # 'foo.dll' is what we use (it just delegates).
 642          # So no leading '_' on the installed name.
 643          dll_name = os.path.abspath(os.path.join(path, base + ".dll"))
 644      else:
 645          base, ext = os.path.splitext(mod_name)
 646          path, base = os.path.split(base)
 647          dll_name = os.path.abspath(os.path.join(path, "_" + base + ".dll"))
 648      # Check we actually have it.
 649      if check_module is None:
 650          check_module = not hasattr(sys, "frozen")
 651      if check_module:
 652          CheckLoaderModule(dll_name)
 653      return dll_name
 654  
 655  
 656  # Note the 'log' params to these 'builtin' args - old versions of pywin32
 657  # didn't log at all in this function (by intent; anyone calling this was
 658  # responsible). So existing code that calls this function with the old
 659  # signature (ie, without a 'log' param) still gets the same behaviour as
 660  # before...
 661  
 662  
 663  def InstallModule(conf_module_name, params, options, log=lambda *args: None):
 664      "Install the extension"
 665      if not hasattr(sys, "frozen"):
 666          conf_module_name = os.path.abspath(conf_module_name)
 667          if not os.path.isfile(conf_module_name):
 668              raise ConfigurationError(f"{conf_module_name} does not exist")
 669  
 670      loader_dll = GetLoaderModuleName(conf_module_name)
 671      _PatchParamsModule(params, loader_dll)
 672      Install(params, options)
 673      log(1, "Installation complete.")
 674  
 675  
 676  def UninstallModule(conf_module_name, params, options, log=lambda *args: None):
 677      "Remove the extension"
 678      loader_dll = GetLoaderModuleName(conf_module_name, False)
 679      _PatchParamsModule(params, loader_dll, False)
 680      Uninstall(params, options)
 681      log(1, "Uninstallation complete.")
 682  
 683  
 684  standard_arguments = {
 685      "install": InstallModule,
 686      "remove": UninstallModule,
 687  }
 688  
 689  
 690  def build_usage(handler_map: Mapping[str, object]) -> str:
 691      arg_names = "|".join(handler_map)
 692      lines = [
 693          " %-10s: %s" % (arg, handler.__doc__) for arg, handler in handler_map.items()
 694      ]
 695      return f"%prog [options] [{arg_names}]\ncommands:\n" + "\n".join(lines)
 696  
 697  
 698  def MergeStandardOptions(options, params):
 699      """
 700      Take an options object generated by the command line and merge
 701      the values into the IISParameters object.
 702      """
 703      pass
 704  
 705  
 706  # We support 2 ways of extending our command-line/install support.
 707  # * Many of the installation items allow you to specify "PreInstall",
 708  #   "PostInstall", "PreRemove" and "PostRemove" hooks
 709  #   All hooks are called with the 'params' object being operated on, and
 710  #   the 'optparser' options for this session (ie, the command-line options)
 711  #   PostInstall for VirtualDirectories and Filters both have an additional
 712  #   param - the ADSI object just created.
 713  # * You can pass your own option parser for us to use, and/or define a map
 714  #   with your own custom arg handlers.  It is a map of 'arg'->function.
 715  #   The function is called with (options, log_fn, arg).  The function's
 716  #   docstring is used in the usage output.
 717  def HandleCommandLine(
 718      params,
 719      argv=None,
 720      conf_module_name=None,
 721      default_arg="install",
 722      opt_parser=None,
 723      custom_arg_handlers={},
 724  ):
 725      """Perform installation or removal of an ISAPI filter or extension.
 726  
 727      This module handles standard command-line options and configuration
 728      information, and installs, removes or updates the configuration of an
 729      ISAPI filter or extension.
 730  
 731      You must pass your configuration information in params - all other
 732      arguments are optional, and allow you to configure the installation
 733      process.
 734      """
 735      global verbose
 736      from optparse import OptionParser
 737  
 738      argv = argv or sys.argv
 739      if not conf_module_name:
 740          conf_module_name = sys.argv[0]
 741          # convert to a long name so that if we were somehow registered with
 742          # the "short" version but unregistered with the "long" version we
 743          # still work (that will depend on exactly how the installer was
 744          # started)
 745          try:
 746              conf_module_name = win32api.GetLongPathName(conf_module_name)
 747          except win32api.error as exc:
 748              log(
 749                  2,
 750                  f"Couldn't determine the long name for {conf_module_name!r}: {exc}",
 751              )
 752  
 753      if opt_parser is None:
 754          # Build our own parser.
 755          parser = OptionParser(usage="")
 756      else:
 757          # The caller is providing their own filter, presumably with their
 758          # own options all setup.
 759          parser = opt_parser
 760  
 761      # build a usage string if we don't have one.
 762      if not parser.get_usage():
 763          all_handlers = standard_arguments.copy()
 764          all_handlers.update(custom_arg_handlers)
 765          parser.set_usage(build_usage(all_handlers))
 766  
 767      # allow the user to use uninstall as a synonym for remove if it wasn't
 768      #  defined by the custom arg handlers.
 769      all_handlers.setdefault("uninstall", all_handlers["remove"])
 770  
 771      parser.add_option(
 772          "-q",
 773          "--quiet",
 774          action="store_false",
 775          dest="verbose",
 776          default=True,
 777          help="don't print status messages to stdout",
 778      )
 779      parser.add_option(
 780          "-v",
 781          "--verbosity",
 782          action="count",
 783          dest="verbose",
 784          default=1,
 785          help="increase the verbosity of status messages",
 786      )
 787      parser.add_option(
 788          "",
 789          "--server",
 790          action="store",
 791          help="Specifies the IIS server to install/uninstall on."
 792          f" Default is '{_IIS_OBJECT}/1'",
 793      )
 794  
 795      (options, args) = parser.parse_args(argv[1:])
 796      MergeStandardOptions(options, params)
 797      verbose = options.verbose
 798      if not args:
 799          args = [default_arg]
 800      try:
 801          for arg in args:
 802              handler = all_handlers[arg]
 803              handler(conf_module_name, params, options, log)
 804      except (ItemNotFound, InstallationError) as details:
 805          if options.verbose > 1:
 806              traceback.print_exc()
 807          print(f"{details.__class__.__name__}: {details}")
 808      except KeyError:
 809          parser.error("Invalid arg '%s'" % arg)
 810