"""Installation utilities for Python ISAPI filters and extensions."""

# this code adapted from "Tomcat JK2 ISAPI redirector", part of Apache
# Created July 2004, Mark Hammond.
from __future__ import annotations

import importlib.machinery
import os
import shutil
import stat
import sys
import traceback
from collections.abc import Mapping

import pythoncom
import win32api
import winerror
from win32com.client import GetObject

_APP_INPROC = 0
_APP_OUTPROC = 1
_APP_POOLED = 2
_IIS_OBJECT = "IIS://LocalHost/W3SVC"
_IIS_SERVER = "IIsWebServer"
_IIS_WEBDIR = "IIsWebDirectory"
_IIS_WEBVIRTUALDIR = "IIsWebVirtualDir"
_IIS_FILTERS = "IIsFilters"
_IIS_FILTER = "IIsFilter"

_DEFAULT_SERVER_NAME = "Default Web Site"
_DEFAULT_HEADERS = "X-Powered-By: Python"
_DEFAULT_PROTECTION = _APP_POOLED

# Default is for 'execute' only access - ie, only the extension
# can be used.  This can be overridden via your install script.
_DEFAULT_ACCESS_EXECUTE = True
_DEFAULT_ACCESS_READ = False
_DEFAULT_ACCESS_WRITE = False
_DEFAULT_ACCESS_SCRIPT = False
_DEFAULT_CONTENT_INDEXED = False
_DEFAULT_ENABLE_DIR_BROWSING = False
_DEFAULT_ENABLE_DEFAULT_DOC = False

this_dir = os.path.abspath(os.path.dirname(__file__))


class FilterParameters:
    Name = None
    Description = None
    Path = None
    Server = None
    # Params that control if/how AddExtensionFile is called.
    AddExtensionFile = True
    AddExtensionFile_Enabled = True
    AddExtensionFile_GroupID = None  # defaults to Name
    AddExtensionFile_CanDelete = True
    AddExtensionFile_Description = None  # defaults to Description.

    def __init__(self, **kw):
        self.__dict__.update(kw)


class VirtualDirParameters:
    Name = None  # Must be provided.
    Description = None  # defaults to Name
    AppProtection = _DEFAULT_PROTECTION
    Headers = _DEFAULT_HEADERS
    Path = None  # defaults to WWW root.
    Type = _IIS_WEBVIRTUALDIR
    AccessExecute = _DEFAULT_ACCESS_EXECUTE
    AccessRead = _DEFAULT_ACCESS_READ
    AccessWrite = _DEFAULT_ACCESS_WRITE
    AccessScript = _DEFAULT_ACCESS_SCRIPT
    ContentIndexed = _DEFAULT_CONTENT_INDEXED
    EnableDirBrowsing = _DEFAULT_ENABLE_DIR_BROWSING
    EnableDefaultDoc = _DEFAULT_ENABLE_DEFAULT_DOC
    DefaultDoc = None  # Only set in IIS if not None
    ScriptMaps: list[ScriptMapParams] = []
    ScriptMapUpdate = "end"  # can be 'start', 'end', 'replace'
    Server = None

    def __init__(self, **kw):
        self.__dict__.update(kw)

    def is_root(self):
        "This virtual directory is a root directory if parent and name are blank"
        parent, name = self.split_path()
        return not parent and not name

    def split_path(self):
        return split_path(self.Name)


class ScriptMapParams:
    Extension = None
    Module = None
    Flags = 5
    Verbs = ""
    # Params that control if/how AddExtensionFile is called.
    AddExtensionFile = True
    AddExtensionFile_Enabled = True
    AddExtensionFile_GroupID = None  # defaults to Name
    AddExtensionFile_CanDelete = True
    AddExtensionFile_Description = None  # defaults to Description.

    def __init__(self, **kw):
        self.__dict__.update(kw)

    def __str__(self):
        "Format this parameter suitable for IIS"
        items = [self.Extension, self.Module, self.Flags]
        # IIS gets upset if there is a trailing verb comma, but no verbs
        if self.Verbs:
            items.append(self.Verbs)
        items = [str(item) for item in items]
        return ",".join(items)


class ISAPIParameters:
    ServerName = _DEFAULT_SERVER_NAME
    # Description = None
    Filters: list[FilterParameters] = []
    VirtualDirs: list[VirtualDirParameters] = []

    def __init__(self, **kw):
        self.__dict__.update(kw)


verbose = 1  # The level - 0 is quiet.


def log(level, what):
    if verbose >= level:
        print(what)


# Convert an ADSI COM exception to the Win32 error code embedded in it.
def _GetWin32ErrorCode(com_exc):
    hr = com_exc.hresult
    # If we have more details in the 'excepinfo' struct, use it.
    if com_exc.excepinfo:
        hr = com_exc.excepinfo[-1]
    if winerror.HRESULT_FACILITY(hr) != winerror.FACILITY_WIN32:
        raise
    return winerror.SCODE_CODE(hr)


class InstallationError(Exception):
    pass


class ItemNotFound(InstallationError):
    pass


class ConfigurationError(InstallationError):
    pass


def FindPath(options, server, name):
    if name.lower().startswith("iis://"):
        return name
    else:
        if name and name[0] != "/":
            name = "/" + name
        return FindWebServer(options, server) + "/ROOT" + name


def LocateWebServerPath(description):
    """
    Find an IIS web server whose name or comment matches the provided
    description (case-insensitive).

    >>> LocateWebServerPath('Default Web Site') # doctest: +SKIP

    or

    >>> LocateWebServerPath('1') #doctest: +SKIP
    """
    assert len(description) >= 1, "Server name or comment is required"
    iis = GetObject(_IIS_OBJECT)
    description = description.lower().strip()
    for site in iis:
        # Name is generally a number, but no need to assume that.
        site_attributes = [
            getattr(site, attr, "").lower().strip()
            for attr in ("Name", "ServerComment")
        ]
        if description in site_attributes:
            return site.AdsPath
    msg = "No web sites match the description '%s'" % description
    raise ItemNotFound(msg)


def GetWebServer(description=None):
    """
    Load the web server instance (COM object) for a given instance
    or description.
    If None is specified, the default website is retrieved (indicated
    by the identifier 1.
    """
    description = description or "1"
    path = LocateWebServerPath(description)
    server = LoadWebServer(path)
    return server


def LoadWebServer(path):
    try:
        server = GetObject(path)
    except pythoncom.com_error as exc:
        msg = exc.strerror
        if exc.excepinfo and exc.excepinfo[2]:
            msg = exc.excepinfo[2]
        msg = f"WebServer {path}: {msg}"
        raise ItemNotFound(msg)
    return server


def FindWebServer(options, server_desc):
    """
    Legacy function to allow options to define a .server property
    to override the other parameter.  Use GetWebServer instead.
    """
    # options takes precedence
    server_desc = options.server or server_desc
    # make sure server_desc is unicode (could be mbcs if passed in
    #  sys.argv).
    if server_desc and not isinstance(server_desc, str):
        server_desc = server_desc.decode("mbcs")

    # get the server (if server_desc is None, the default site is acquired)
    server = GetWebServer(server_desc)
    return server.adsPath


def split_path(path):
    """
    Get the parent path and basename.

    >>> split_path('/')
    ['', '']

    >>> split_path('')
    ['', '']

    >>> split_path('foo')
    ['', 'foo']

    >>> split_path('/foo')
    ['', 'foo']

    >>> split_path('/foo/bar')
    ['/foo', 'bar']

    >>> split_path('foo/bar')
    ['/foo', 'bar']
    """

    if not path.startswith("/"):
        path = "/" + path
    return path.rsplit("/", 1)


def _CreateDirectory(iis_dir, name, params):
    # We used to go to lengths to keep an existing virtual directory
    # in place.  However, in some cases the existing directories got
    # into a bad state, and an update failed to get them working.
    # So we nuke it first.  If this is a problem, we could consider adding
    # a --keep-existing option.
    try:
        # Also seen the Class change to a generic IISObject - so nuke
        # *any* existing object, regardless of Class
        assert name.strip("/"), "mustn't delete the root!"
        iis_dir.Delete("", name)
        log(2, f"Deleted old directory '{name}'")
    except pythoncom.com_error:
        pass

    newDir = iis_dir.Create(params.Type, name)
    log(2, f"Creating new directory '{name}' in {iis_dir.Name}...")

    friendly = params.Description or params.Name
    newDir.AppFriendlyName = friendly

    # Note that the new directory won't be visible in the IIS UI
    # unless the directory exists on the filesystem.
    try:
        path = params.Path or iis_dir.Path
        newDir.Path = path
    except AttributeError:
        # If params.Type is IIS_WEBDIRECTORY, an exception is thrown
        pass
    newDir.AppCreate2(params.AppProtection)
    # XXX - note that these Headers only work in IIS6 and earlier.  IIS7
    # only supports them on the w3svc node - not even on individial sites,
    # let alone individual extensions in the site!
    if params.Headers:
        newDir.HttpCustomHeaders = params.Headers

    log(2, "Setting directory options...")
    newDir.AccessExecute = params.AccessExecute
    newDir.AccessRead = params.AccessRead
    newDir.AccessWrite = params.AccessWrite
    newDir.AccessScript = params.AccessScript
    newDir.ContentIndexed = params.ContentIndexed
    newDir.EnableDirBrowsing = params.EnableDirBrowsing
    newDir.EnableDefaultDoc = params.EnableDefaultDoc
    if params.DefaultDoc is not None:
        newDir.DefaultDoc = params.DefaultDoc
    newDir.SetInfo()
    return newDir


def CreateDirectory(params, options):
    _CallHook(params, "PreInstall", options)
    if not params.Name:
        raise ConfigurationError("No Name param")
    parent, name = params.split_path()
    target_dir = GetObject(FindPath(options, params.Server, parent))

    if not params.is_root():
        target_dir = _CreateDirectory(target_dir, name, params)

    AssignScriptMaps(params.ScriptMaps, target_dir, params.ScriptMapUpdate)

    _CallHook(params, "PostInstall", options, target_dir)
    log(1, f"Configured Virtual Directory: {params.Name}")
    return target_dir


def AssignScriptMaps(script_maps, target, update="replace"):
    """Updates IIS with the supplied script map information.

    script_maps is a list of ScriptMapParameter objects

    target is an IIS Virtual Directory to assign the script maps to

    update is a string indicating how to update the maps, one of  ('start',
    'end', or 'replace')
    """
    # determine which function to use to assign script maps
    script_map_func = "_AssignScriptMaps" + update.capitalize()
    try:
        script_map_func = eval(script_map_func)
    except NameError:
        msg = "Unknown ScriptMapUpdate option '%s'" % update
        raise ConfigurationError(msg)
    # use the str method to format the script maps for IIS
    script_maps = [str(s) for s in script_maps]
    # call the correct function
    script_map_func(target, script_maps)
    target.SetInfo()


def get_unique_items(sequence, reference):
    "Return items in sequence that can't be found in reference."
    return tuple([item for item in sequence if item not in reference])


def _AssignScriptMapsReplace(target, script_maps):
    target.ScriptMaps = script_maps


def _AssignScriptMapsEnd(target, script_maps):
    unique_new_maps = get_unique_items(script_maps, target.ScriptMaps)
    target.ScriptMaps += unique_new_maps


def _AssignScriptMapsStart(target, script_maps):
    unique_new_maps = get_unique_items(script_maps, target.ScriptMaps)
    target.ScriptMaps = unique_new_maps + target.ScriptMaps


def CreateISAPIFilter(filterParams, options):
    server = FindWebServer(options, filterParams.Server)
    _CallHook(filterParams, "PreInstall", options)
    try:
        filters = GetObject(server + "/Filters")
    except pythoncom.com_error as exc:
        # Brand new sites don't have the '/Filters' collection - create it.
        # Any errors other than 'not found' we shouldn't ignore.
        if (
            winerror.HRESULT_FACILITY(exc.hresult) != winerror.FACILITY_WIN32
            or winerror.HRESULT_CODE(exc.hresult) != winerror.ERROR_PATH_NOT_FOUND
        ):
            raise
        server_ob = GetObject(server)
        filters = server_ob.Create(_IIS_FILTERS, "Filters")
        filters.FilterLoadOrder = ""
        filters.SetInfo()

    # As for VirtualDir, delete an existing one.
    assert filterParams.Name.strip("/"), "mustn't delete the root!"
    try:
        filters.Delete(_IIS_FILTER, filterParams.Name)
        log(2, f"Deleted old filter '{filterParams.Name}'")
    except pythoncom.com_error:
        pass
    newFilter = filters.Create(_IIS_FILTER, filterParams.Name)
    log(2, "Created new ISAPI filter...")
    assert os.path.isfile(filterParams.Path)
    newFilter.FilterPath = filterParams.Path
    newFilter.FilterDescription = filterParams.Description
    newFilter.SetInfo()
    load_order = [b.strip() for b in filters.FilterLoadOrder.split(",") if b]
    if filterParams.Name not in load_order:
        load_order.append(filterParams.Name)
        filters.FilterLoadOrder = ",".join(load_order)
        filters.SetInfo()
    _CallHook(filterParams, "PostInstall", options, newFilter)
    log(1, f"Configured Filter: {filterParams.Name}")
    return newFilter


def DeleteISAPIFilter(filterParams, options):
    _CallHook(filterParams, "PreRemove", options)
    server = FindWebServer(options, filterParams.Server)
    ob_path = server + "/Filters"
    try:
        filters = GetObject(ob_path)
    except pythoncom.com_error as details:
        # failure to open the filters just means a totally clean IIS install
        # (IIS5 at least has no 'Filters' key when freshly installed).
        log(2, f"ISAPI filter path '{ob_path}' did not exist.")
        return
    try:
        assert filterParams.Name.strip("/"), "mustn't delete the root!"
        filters.Delete(_IIS_FILTER, filterParams.Name)
        log(2, f"Deleted ISAPI filter '{filterParams.Name}'")
    except pythoncom.com_error as details:
        rc = _GetWin32ErrorCode(details)
        if rc != winerror.ERROR_PATH_NOT_FOUND:
            raise
        log(2, f"ISAPI filter '{filterParams.Name}' did not exist.")
    # Remove from the load order
    load_order = [b.strip() for b in filters.FilterLoadOrder.split(",") if b]
    if filterParams.Name in load_order:
        load_order.remove(filterParams.Name)
        filters.FilterLoadOrder = ",".join(load_order)
        filters.SetInfo()
    _CallHook(filterParams, "PostRemove", options)
    log(1, f"Deleted Filter: {filterParams.Name}")


def _AddExtensionFile(module, def_groupid, def_desc, params, options):
    group_id = params.AddExtensionFile_GroupID or def_groupid
    desc = params.AddExtensionFile_Description or def_desc
    try:
        ob = GetObject(_IIS_OBJECT)
        ob.AddExtensionFile(
            module,
            params.AddExtensionFile_Enabled,
            group_id,
            params.AddExtensionFile_CanDelete,
            desc,
        )
        log(2, f"Added extension file '{module}' ({desc})")
    except (pythoncom.com_error, AttributeError) as details:
        # IIS5 always fails.  Probably should upgrade this to
        # complain more loudly if IIS6 fails.
        log(2, f"Failed to add extension file '{module}': {details}")


def AddExtensionFiles(params, options):
    """Register the modules used by the filters/extensions as a trusted
    'extension module' - required by the default IIS6 security settings."""
    # Add each module only once.
    added = {}
    for vd in params.VirtualDirs:
        for smp in vd.ScriptMaps:
            if smp.Module not in added and smp.AddExtensionFile:
                _AddExtensionFile(smp.Module, vd.Name, vd.Description, smp, options)
                added[smp.Module] = True

    for fd in params.Filters:
        if fd.Path not in added and fd.AddExtensionFile:
            _AddExtensionFile(fd.Path, fd.Name, fd.Description, fd, options)
            added[fd.Path] = True


def _DeleteExtensionFileRecord(module, options):
    try:
        ob = GetObject(_IIS_OBJECT)
        ob.DeleteExtensionFileRecord(module)
        log(2, "Deleted extension file record for '%s'" % module)
    except (pythoncom.com_error, AttributeError) as details:
        log(2, f"Failed to remove extension file '{module}': {details}")


def DeleteExtensionFileRecords(params, options):
    deleted = {}  # only remove each .dll once.
    for vd in params.VirtualDirs:
        for smp in vd.ScriptMaps:
            if smp.Module not in deleted and smp.AddExtensionFile:
                _DeleteExtensionFileRecord(smp.Module, options)
                deleted[smp.Module] = True

    for filter_def in params.Filters:
        if filter_def.Path not in deleted and filter_def.AddExtensionFile:
            _DeleteExtensionFileRecord(filter_def.Path, options)
            deleted[filter_def.Path] = True


def CheckLoaderModule(dll_name):
    suffix = "_d" if "_d.pyd" in importlib.machinery.EXTENSION_SUFFIXES else ""
    template = os.path.join(this_dir, "PyISAPI_loader" + suffix + ".dll")
    if not os.path.isfile(template):
        raise ConfigurationError(f"Template loader '{template}' does not exist")
    # We can't do a simple "is newer" check, as the DLL is specific to the
    # Python version.  So we check the date-time and size are identical,
    # and skip the copy in that case.
    src_stat = os.stat(template)
    try:
        dest_stat = os.stat(dll_name)
    except OSError:
        same = 0
    else:
        same = (
            src_stat[stat.ST_SIZE] == dest_stat[stat.ST_SIZE]
            and src_stat[stat.ST_MTIME] == dest_stat[stat.ST_MTIME]
        )
    if not same:
        log(2, f"Updating {template}->{dll_name}")
        shutil.copyfile(template, dll_name)
        shutil.copystat(template, dll_name)
    else:
        log(2, f"{dll_name} is up to date.")


def _CallHook(ob, hook_name, options, *extra_args):
    func = getattr(ob, hook_name, None)
    if func is not None:
        args = (ob, options) + extra_args
        func(*args)


def Install(params, options):
    _CallHook(params, "PreInstall", options)
    for vd in params.VirtualDirs:
        CreateDirectory(vd, options)

    for filter_def in params.Filters:
        CreateISAPIFilter(filter_def, options)

    AddExtensionFiles(params, options)

    _CallHook(params, "PostInstall", options)


def RemoveDirectory(params, options):
    if params.is_root():
        return
    try:
        directory = GetObject(FindPath(options, params.Server, params.Name))
    except pythoncom.com_error as details:
        rc = _GetWin32ErrorCode(details)
        if rc != winerror.ERROR_PATH_NOT_FOUND:
            raise
        log(2, "VirtualDirectory '%s' did not exist" % params.Name)
        directory = None
    if directory is not None:
        # Be robust should IIS get upset about unloading.
        try:
            directory.AppUnLoad()
        except:
            exc_val = sys.exc_info()[1]
            log(2, f"AppUnLoad() for {params.Name} failed: {exc_val}")
        # Continue trying to delete it.
        try:
            parent = GetObject(directory.Parent)
            parent.Delete(directory.Class, directory.Name)
            log(1, f"Deleted Virtual Directory: {params.Name}")
        except:
            exc_val = sys.exc_info()[1]
            log(1, f"Failed to remove directory {params.Name}: {exc_val}")


def RemoveScriptMaps(vd_params, options):
    "Remove script maps from the already installed virtual directory"
    parent, name = vd_params.split_path()
    target_dir = GetObject(FindPath(options, vd_params.Server, parent))
    installed_maps = list(target_dir.ScriptMaps)
    for _map in map(str, vd_params.ScriptMaps):
        if _map in installed_maps:
            installed_maps.remove(_map)
    target_dir.ScriptMaps = installed_maps
    target_dir.SetInfo()


def Uninstall(params, options):
    _CallHook(params, "PreRemove", options)

    DeleteExtensionFileRecords(params, options)

    for vd in params.VirtualDirs:
        _CallHook(vd, "PreRemove", options)

        RemoveDirectory(vd, options)
        if vd.is_root():
            # if this is installed to the root virtual directory, we can't delete it
            #  so remove the script maps.
            RemoveScriptMaps(vd, options)

        _CallHook(vd, "PostRemove", options)

    for filter_def in params.Filters:
        DeleteISAPIFilter(filter_def, options)
    _CallHook(params, "PostRemove", options)


# Patch up any missing module names in the params, replacing them with
# the DLL name that hosts this extension/filter.
def _PatchParamsModule(params, dll_name, file_must_exist=True):
    if file_must_exist:
        if not os.path.isfile(dll_name):
            raise ConfigurationError(f"{dll_name} does not exist")

    # Patch up all references to the DLL.
    for f in params.Filters:
        if f.Path is None:
            f.Path = dll_name
    for d in params.VirtualDirs:
        for sm in d.ScriptMaps:
            if sm.Module is None:
                sm.Module = dll_name


def GetLoaderModuleName(mod_name, check_module=None):
    # find the name of the DLL hosting us.
    # By default, this is "_{module_base_name}.dll"
    if hasattr(sys, "frozen"):
        # What to do?  The .dll knows its name, but this is likely to be
        # executed via a .exe, which does not know.
        base, ext = os.path.splitext(mod_name)
        path, base = os.path.split(base)
        # handle the common case of 'foo.exe'/'foow.exe'
        if base.endswith("w"):
            base = base[:-1]
        # For py2exe, we have '_foo.dll' as the standard pyisapi loader - but
        # 'foo.dll' is what we use (it just delegates).
        # So no leading '_' on the installed name.
        dll_name = os.path.abspath(os.path.join(path, base + ".dll"))
    else:
        base, ext = os.path.splitext(mod_name)
        path, base = os.path.split(base)
        dll_name = os.path.abspath(os.path.join(path, "_" + base + ".dll"))
    # Check we actually have it.
    if check_module is None:
        check_module = not hasattr(sys, "frozen")
    if check_module:
        CheckLoaderModule(dll_name)
    return dll_name


# Note the 'log' params to these 'builtin' args - old versions of pywin32
# didn't log at all in this function (by intent; anyone calling this was
# responsible). So existing code that calls this function with the old
# signature (ie, without a 'log' param) still gets the same behaviour as
# before...


def InstallModule(conf_module_name, params, options, log=lambda *args: None):
    "Install the extension"
    if not hasattr(sys, "frozen"):
        conf_module_name = os.path.abspath(conf_module_name)
        if not os.path.isfile(conf_module_name):
            raise ConfigurationError(f"{conf_module_name} does not exist")

    loader_dll = GetLoaderModuleName(conf_module_name)
    _PatchParamsModule(params, loader_dll)
    Install(params, options)
    log(1, "Installation complete.")


def UninstallModule(conf_module_name, params, options, log=lambda *args: None):
    "Remove the extension"
    loader_dll = GetLoaderModuleName(conf_module_name, False)
    _PatchParamsModule(params, loader_dll, False)
    Uninstall(params, options)
    log(1, "Uninstallation complete.")


standard_arguments = {
    "install": InstallModule,
    "remove": UninstallModule,
}


def build_usage(handler_map: Mapping[str, object]) -> str:
    arg_names = "|".join(handler_map)
    lines = [
        " %-10s: %s" % (arg, handler.__doc__) for arg, handler in handler_map.items()
    ]
    return f"%prog [options] [{arg_names}]\ncommands:\n" + "\n".join(lines)


def MergeStandardOptions(options, params):
    """
    Take an options object generated by the command line and merge
    the values into the IISParameters object.
    """
    pass


# We support 2 ways of extending our command-line/install support.
# * Many of the installation items allow you to specify "PreInstall",
#   "PostInstall", "PreRemove" and "PostRemove" hooks
#   All hooks are called with the 'params' object being operated on, and
#   the 'optparser' options for this session (ie, the command-line options)
#   PostInstall for VirtualDirectories and Filters both have an additional
#   param - the ADSI object just created.
# * You can pass your own option parser for us to use, and/or define a map
#   with your own custom arg handlers.  It is a map of 'arg'->function.
#   The function is called with (options, log_fn, arg).  The function's
#   docstring is used in the usage output.
def HandleCommandLine(
    params,
    argv=None,
    conf_module_name=None,
    default_arg="install",
    opt_parser=None,
    custom_arg_handlers={},
):
    """Perform installation or removal of an ISAPI filter or extension.

    This module handles standard command-line options and configuration
    information, and installs, removes or updates the configuration of an
    ISAPI filter or extension.

    You must pass your configuration information in params - all other
    arguments are optional, and allow you to configure the installation
    process.
    """
    global verbose
    from optparse import OptionParser

    argv = argv or sys.argv
    if not conf_module_name:
        conf_module_name = sys.argv[0]
        # convert to a long name so that if we were somehow registered with
        # the "short" version but unregistered with the "long" version we
        # still work (that will depend on exactly how the installer was
        # started)
        try:
            conf_module_name = win32api.GetLongPathName(conf_module_name)
        except win32api.error as exc:
            log(
                2,
                f"Couldn't determine the long name for {conf_module_name!r}: {exc}",
            )

    if opt_parser is None:
        # Build our own parser.
        parser = OptionParser(usage="")
    else:
        # The caller is providing their own filter, presumably with their
        # own options all setup.
        parser = opt_parser

    # build a usage string if we don't have one.
    if not parser.get_usage():
        all_handlers = standard_arguments.copy()
        all_handlers.update(custom_arg_handlers)
        parser.set_usage(build_usage(all_handlers))

    # allow the user to use uninstall as a synonym for remove if it wasn't
    #  defined by the custom arg handlers.
    all_handlers.setdefault("uninstall", all_handlers["remove"])

    parser.add_option(
        "-q",
        "--quiet",
        action="store_false",
        dest="verbose",
        default=True,
        help="don't print status messages to stdout",
    )
    parser.add_option(
        "-v",
        "--verbosity",
        action="count",
        dest="verbose",
        default=1,
        help="increase the verbosity of status messages",
    )
    parser.add_option(
        "",
        "--server",
        action="store",
        help="Specifies the IIS server to install/uninstall on."
        f" Default is '{_IIS_OBJECT}/1'",
    )

    (options, args) = parser.parse_args(argv[1:])
    MergeStandardOptions(options, params)
    verbose = options.verbose
    if not args:
        args = [default_arg]
    try:
        for arg in args:
            handler = all_handlers[arg]
            handler(conf_module_name, params, options, log)
    except (ItemNotFound, InstallationError) as details:
        if options.verbose > 1:
            traceback.print_exc()
        print(f"{details.__class__.__name__}: {details}")
    except KeyError:
        parser.error("Invalid arg '%s'" % arg)
