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