# General purpose service utilities, both for standard Python scripts,
# and for for Python programs which run as services...
#
# Note that most utility functions here will raise win32api.error's
# (which is win32service.error, pywintypes.error, etc)
# when things go wrong - eg, not enough permissions to hit the
# registry etc.

import importlib.machinery
import os
import sys
import warnings

import pywintypes
import win32api
import win32con
import win32service
import winerror

error = RuntimeError  # Re-exported alias


# Returns the full path to an executable for hosting a Python service - typically
# 'pythonservice.exe'
# * If you pass a param and it exists as a file, you'll get the abs path back
# * Otherwise we'll use the param instead of 'pythonservice.exe', and we will
#   look for it.
def LocatePythonServiceExe(exe=None):
    if not exe and hasattr(sys, "frozen"):
        # If py2exe etc calls this with no exe, default is current exe,
        # and all setup is their problem :)
        return sys.executable

    if exe and os.path.isfile(exe):
        return win32api.GetFullPathName(exe)

    suffix = "_d" if "_d.pyd" in importlib.machinery.EXTENSION_SUFFIXES else ""

    # We are confused if we aren't now looking for our default. But if that
    # exists as specified we assume it's good.
    exe = f"pythonservice{suffix}.exe"
    if os.path.isfile(exe):
        return win32api.GetFullPathName(exe)

    # Now we are searching for the .exe
    # We are going to want it here.
    correct = os.path.join(sys.exec_prefix, exe)
    # Even if that file already exists, we copy the one installed by pywin32
    # in-case it was upgraded.
    # pywin32 installed it next to win32service.pyd (but we can't run it from there)
    maybe = os.path.join(os.path.dirname(win32service.__file__), exe)
    if os.path.exists(maybe):
        print(f"moving host exe '{maybe}' -> '{correct}'")
        # Handle case where MoveFile() fails. Particularly if destination file
        # has a resource lock and can't be replaced by src file
        try:
            win32api.MoveFileEx(maybe, correct, win32con.MOVEFILE_REPLACE_EXISTING)
        except win32api.error as exc:
            print(f"Failed to move host exe '{exc}'")

    if not os.path.exists(correct):
        raise error(f"Can't find '{correct}'")

    # If pywintypes.dll isn't next to us, or at least next to pythonXX.dll,
    # there's a good chance the service will not run. That's usually copied by
    # `pywin32_postinstall`, but putting it next to the python DLL seems reasonable.
    # (Unlike the .exe above, we don't unconditionally copy this, and possibly
    # copy it to a different place. Doesn't seem a good reason for that!?)
    python_dll = win32api.GetModuleFileName(sys.dllhandle)
    pyw = f"pywintypes{sys.version_info.major}{sys.version_info.minor}{suffix}.dll"
    correct_pyw = os.path.join(os.path.dirname(python_dll), pyw)

    if not os.path.exists(correct_pyw):
        print(f"copying helper dll '{pywintypes.__file__}' -> '{correct_pyw}'")
        win32api.CopyFile(pywintypes.__file__, correct_pyw)

    return correct


def _GetServiceShortName(longName):
    # looks up a services name
    # from the display name
    # Thanks to Andy McKay for this code.
    access = (
        win32con.KEY_READ | win32con.KEY_ENUMERATE_SUB_KEYS | win32con.KEY_QUERY_VALUE
    )
    hkey = win32api.RegOpenKey(
        win32con.HKEY_LOCAL_MACHINE, "SYSTEM\\CurrentControlSet\\Services", 0, access
    )
    num = win32api.RegQueryInfoKey(hkey)[0]
    longName = longName.lower()
    # loop through number of subkeys
    for x in range(0, num):
        # find service name, open subkey
        svc = win32api.RegEnumKey(hkey, x)
        skey = win32api.RegOpenKey(hkey, svc, 0, access)
        try:
            # find display name
            thisName = str(win32api.RegQueryValueEx(skey, "DisplayName")[0])
            if thisName.lower() == longName:
                return svc
        except win32api.error:
            # in case there is no key called DisplayName
            pass
    return None


# Open a service given either it's long or short name.
def SmartOpenService(hscm, name, access):
    try:
        return win32service.OpenService(hscm, name, access)
    except win32api.error as details:
        if details.winerror not in [
            winerror.ERROR_SERVICE_DOES_NOT_EXIST,
            winerror.ERROR_INVALID_NAME,
        ]:
            raise
    name = win32service.GetServiceKeyName(hscm, name)
    return win32service.OpenService(hscm, name, access)


def LocateSpecificServiceExe(serviceName):
    # Return the .exe name of any service.
    hkey = win32api.RegOpenKey(
        win32con.HKEY_LOCAL_MACHINE,
        "SYSTEM\\CurrentControlSet\\Services\\%s" % (serviceName),
        0,
        win32con.KEY_QUERY_VALUE,
    )
    try:
        return win32api.RegQueryValueEx(hkey, "ImagePath")[0]
    finally:
        hkey.Close()


def InstallPerfmonForService(serviceName, iniName, dllName=None):
    # If no DLL name, look it up in the INI file name
    if not dllName:  # May be empty string!
        dllName = win32api.GetProfileVal("Python", "dll", "", iniName)
    # Still not found - look for the standard one in the same dir as win32service.pyd
    if not dllName:
        try:
            tryName = os.path.join(
                os.path.split(win32service.__file__)[0], "perfmondata.dll"
            )
            if os.path.isfile(tryName):
                dllName = tryName
        except AttributeError:
            # Frozen app? - anyway, can't find it!
            pass
    if not dllName:
        raise ValueError("The name of the performance DLL must be available")
    dllName = win32api.GetFullPathName(dllName)
    # Now setup all the required "Performance" entries.
    hkey = win32api.RegOpenKey(
        win32con.HKEY_LOCAL_MACHINE,
        "SYSTEM\\CurrentControlSet\\Services\\%s" % (serviceName),
        0,
        win32con.KEY_ALL_ACCESS,
    )
    try:
        subKey = win32api.RegCreateKey(hkey, "Performance")
        try:
            win32api.RegSetValueEx(subKey, "Library", 0, win32con.REG_SZ, dllName)
            win32api.RegSetValueEx(
                subKey, "Open", 0, win32con.REG_SZ, "OpenPerformanceData"
            )
            win32api.RegSetValueEx(
                subKey, "Close", 0, win32con.REG_SZ, "ClosePerformanceData"
            )
            win32api.RegSetValueEx(
                subKey, "Collect", 0, win32con.REG_SZ, "CollectPerformanceData"
            )
        finally:
            win32api.RegCloseKey(subKey)
    finally:
        win32api.RegCloseKey(hkey)
    # Now do the "Lodctr" thang...

    try:
        import perfmon

        path, fname = os.path.split(iniName)
        oldPath = os.getcwd()
        if path:
            os.chdir(path)
        try:
            perfmon.LoadPerfCounterTextStrings("python.exe " + fname)
        finally:
            os.chdir(oldPath)
    except win32api.error as details:
        print("The service was installed OK, but the performance monitor")
        print("data could not be loaded.", details)


def _GetCommandLine(exeName, exeArgs):
    if exeArgs is not None:
        return exeName + " " + exeArgs
    else:
        return exeName


def InstallService(
    pythonClassString,
    serviceName,
    displayName,
    startType=None,
    errorControl=None,
    bRunInteractive=0,
    serviceDeps=None,
    userName=None,
    password=None,
    exeName=None,
    perfMonIni=None,
    perfMonDll=None,
    exeArgs=None,
    description=None,
    delayedstart=None,
):
    # Handle the default arguments.
    if startType is None:
        startType = win32service.SERVICE_DEMAND_START
    serviceType = win32service.SERVICE_WIN32_OWN_PROCESS
    if bRunInteractive:
        serviceType |= win32service.SERVICE_INTERACTIVE_PROCESS
    if errorControl is None:
        errorControl = win32service.SERVICE_ERROR_NORMAL

    exeName = '"%s"' % LocatePythonServiceExe(exeName)
    commandLine = _GetCommandLine(exeName, exeArgs)
    hscm = win32service.OpenSCManager(None, None, win32service.SC_MANAGER_ALL_ACCESS)
    try:
        hs = win32service.CreateService(
            hscm,
            serviceName,
            displayName,
            win32service.SERVICE_ALL_ACCESS,  # desired access
            serviceType,  # service type
            startType,
            errorControl,  # error control type
            commandLine,
            None,
            0,
            serviceDeps,
            userName,
            password,
        )
        if description is not None:
            win32service.ChangeServiceConfig2(
                hs, win32service.SERVICE_CONFIG_DESCRIPTION, description
            )
        if delayedstart is not None:
            try:
                win32service.ChangeServiceConfig2(
                    hs,
                    win32service.SERVICE_CONFIG_DELAYED_AUTO_START_INFO,
                    delayedstart,
                )
            except win32service.error as exc:
                # Changing Delayed Auto-Start config may be restricted
                # Warn only if trying to set delayed to True
                if delayedstart:
                    warnings.warn(exc.strerror, stacklevel=2)
        win32service.CloseServiceHandle(hs)
    finally:
        win32service.CloseServiceHandle(hscm)
    InstallPythonClassString(pythonClassString, serviceName)
    # If I have performance monitor info to install, do that.
    if perfMonIni is not None:
        InstallPerfmonForService(serviceName, perfMonIni, perfMonDll)


def ChangeServiceConfig(
    pythonClassString,
    serviceName,
    startType=None,
    errorControl=None,
    bRunInteractive=0,
    serviceDeps=None,
    userName=None,
    password=None,
    exeName=None,
    displayName=None,
    perfMonIni=None,
    perfMonDll=None,
    exeArgs=None,
    description=None,
    delayedstart=None,
):
    # Before doing anything, remove any perfmon counters.
    try:
        import perfmon

        perfmon.UnloadPerfCounterTextStrings("python.exe " + serviceName)
    except (ImportError, win32api.error):
        pass

    # The EXE location may have changed
    exeName = '"%s"' % LocatePythonServiceExe(exeName)

    # Handle the default arguments.
    if startType is None:
        startType = win32service.SERVICE_NO_CHANGE
    if errorControl is None:
        errorControl = win32service.SERVICE_NO_CHANGE

    hscm = win32service.OpenSCManager(None, None, win32service.SC_MANAGER_ALL_ACCESS)
    serviceType = win32service.SERVICE_WIN32_OWN_PROCESS
    if bRunInteractive:
        serviceType |= win32service.SERVICE_INTERACTIVE_PROCESS
    commandLine = _GetCommandLine(exeName, exeArgs)
    try:
        hs = SmartOpenService(hscm, serviceName, win32service.SERVICE_ALL_ACCESS)
        try:
            win32service.ChangeServiceConfig(
                hs,
                serviceType,  # service type
                startType,
                errorControl,  # error control type
                commandLine,
                None,
                0,
                serviceDeps,
                userName,
                password,
                displayName,
            )
            if description is not None:
                win32service.ChangeServiceConfig2(
                    hs, win32service.SERVICE_CONFIG_DESCRIPTION, description
                )
            if delayedstart is not None:
                try:
                    win32service.ChangeServiceConfig2(
                        hs,
                        win32service.SERVICE_CONFIG_DELAYED_AUTO_START_INFO,
                        delayedstart,
                    )
                except win32service.error as exc:
                    # Changing Delayed Auto-Start config may be restricted
                    # Warn only if trying to set delayed to True
                    if delayedstart:
                        warnings.warn(exc.strerror, stacklevel=2)
        finally:
            win32service.CloseServiceHandle(hs)
    finally:
        win32service.CloseServiceHandle(hscm)
    InstallPythonClassString(pythonClassString, serviceName)
    # If I have performance monitor info to install, do that.
    if perfMonIni is not None:
        InstallPerfmonForService(serviceName, perfMonIni, perfMonDll)


def InstallPythonClassString(pythonClassString, serviceName):
    # Now setup our Python specific entries.
    if pythonClassString:
        key = win32api.RegCreateKey(
            win32con.HKEY_LOCAL_MACHINE,
            "System\\CurrentControlSet\\Services\\%s\\PythonClass" % serviceName,
        )
        try:
            win32api.RegSetValue(key, None, win32con.REG_SZ, pythonClassString)
        finally:
            win32api.RegCloseKey(key)


# Utility functions for Services, to allow persistant properties.
def SetServiceCustomOption(serviceName, option, value):
    try:
        serviceName = serviceName._svc_name_
    except AttributeError:
        pass
    key = win32api.RegCreateKey(
        win32con.HKEY_LOCAL_MACHINE,
        "System\\CurrentControlSet\\Services\\%s\\Parameters" % serviceName,
    )
    try:
        if isinstance(value, int):
            win32api.RegSetValueEx(key, option, 0, win32con.REG_DWORD, value)
        else:
            win32api.RegSetValueEx(key, option, 0, win32con.REG_SZ, value)
    finally:
        win32api.RegCloseKey(key)


def GetServiceCustomOption(serviceName, option, defaultValue=None):
    # First param may also be a service class/instance.
    # This allows services to pass "self"
    try:
        serviceName = serviceName._svc_name_
    except AttributeError:
        pass
    key = win32api.RegCreateKey(
        win32con.HKEY_LOCAL_MACHINE,
        "System\\CurrentControlSet\\Services\\%s\\Parameters" % serviceName,
    )
    try:
        try:
            return win32api.RegQueryValueEx(key, option)[0]
        except win32api.error:  # No value.
            return defaultValue
    finally:
        win32api.RegCloseKey(key)


def RemoveService(serviceName):
    try:
        import perfmon

        perfmon.UnloadPerfCounterTextStrings("python.exe " + serviceName)
    except (ImportError, win32api.error):
        pass

    hscm = win32service.OpenSCManager(None, None, win32service.SC_MANAGER_ALL_ACCESS)
    try:
        hs = SmartOpenService(hscm, serviceName, win32service.SERVICE_ALL_ACCESS)
        win32service.DeleteService(hs)
        win32service.CloseServiceHandle(hs)
    finally:
        win32service.CloseServiceHandle(hscm)

    import win32evtlogutil

    try:
        win32evtlogutil.RemoveSourceFromRegistry(serviceName)
    except win32api.error:
        pass


def ControlService(serviceName, code, machine=None):
    hscm = win32service.OpenSCManager(machine, None, win32service.SC_MANAGER_ALL_ACCESS)
    try:
        hs = SmartOpenService(hscm, serviceName, win32service.SERVICE_ALL_ACCESS)
        try:
            status = win32service.ControlService(hs, code)
        finally:
            win32service.CloseServiceHandle(hs)
    finally:
        win32service.CloseServiceHandle(hscm)
    return status


def __FindSvcDeps(findName):
    dict = {}
    k = win32api.RegOpenKey(
        win32con.HKEY_LOCAL_MACHINE, "SYSTEM\\CurrentControlSet\\Services"
    )
    num = 0
    while 1:
        try:
            svc = win32api.RegEnumKey(k, num)
        except win32api.error:
            break
        num += 1
        sk = win32api.RegOpenKey(k, svc)
        try:
            deps, typ = win32api.RegQueryValueEx(sk, "DependOnService")
        except win32api.error:
            deps = ()
        for dep in deps:
            dep = dep.lower()
            dep_on = dict.get(dep, [])
            dep_on.append(svc)
            dict[dep] = dep_on

    return __ResolveDeps(findName, dict)


def __ResolveDeps(findName, dict):
    items = dict.get(findName.lower(), [])
    retList = []
    for svc in items:
        retList.insert(0, svc)
        retList = __ResolveDeps(svc, dict) + retList
    return retList


def WaitForServiceStatus(serviceName, status, waitSecs, machine=None):
    """Waits for the service to return the specified status.  You
    should have already requested the service to enter that state"""
    for i in range(waitSecs * 4):
        now_status = QueryServiceStatus(serviceName, machine)[1]
        if now_status == status:
            break
        win32api.Sleep(250)
    else:
        raise pywintypes.error(
            winerror.ERROR_SERVICE_REQUEST_TIMEOUT,
            "QueryServiceStatus",
            win32api.FormatMessage(winerror.ERROR_SERVICE_REQUEST_TIMEOUT)[:-2],
        )


def __StopServiceWithTimeout(hs, waitSecs=30):
    try:
        status = win32service.ControlService(hs, win32service.SERVICE_CONTROL_STOP)
    except pywintypes.error as exc:
        if exc.winerror != winerror.ERROR_SERVICE_NOT_ACTIVE:
            raise
    for i in range(waitSecs):
        status = win32service.QueryServiceStatus(hs)
        if status[1] == win32service.SERVICE_STOPPED:
            break
        win32api.Sleep(1000)
    else:
        raise pywintypes.error(
            winerror.ERROR_SERVICE_REQUEST_TIMEOUT,
            "ControlService",
            win32api.FormatMessage(winerror.ERROR_SERVICE_REQUEST_TIMEOUT)[:-2],
        )


def StopServiceWithDeps(serviceName, machine=None, waitSecs=30):
    # Stop a service recursively looking for dependant services
    hscm = win32service.OpenSCManager(machine, None, win32service.SC_MANAGER_ALL_ACCESS)
    try:
        deps = __FindSvcDeps(serviceName)
        for dep in deps:
            hs = win32service.OpenService(hscm, dep, win32service.SERVICE_ALL_ACCESS)
            try:
                __StopServiceWithTimeout(hs, waitSecs)
            finally:
                win32service.CloseServiceHandle(hs)
        # Now my service!
        hs = win32service.OpenService(
            hscm, serviceName, win32service.SERVICE_ALL_ACCESS
        )
        try:
            __StopServiceWithTimeout(hs, waitSecs)
        finally:
            win32service.CloseServiceHandle(hs)

    finally:
        win32service.CloseServiceHandle(hscm)


def StopService(serviceName, machine=None):
    return ControlService(serviceName, win32service.SERVICE_CONTROL_STOP, machine)


def StartService(serviceName, args=None, machine=None):
    hscm = win32service.OpenSCManager(machine, None, win32service.SC_MANAGER_ALL_ACCESS)
    try:
        hs = SmartOpenService(hscm, serviceName, win32service.SERVICE_ALL_ACCESS)
        try:
            win32service.StartService(hs, args)
        finally:
            win32service.CloseServiceHandle(hs)
    finally:
        win32service.CloseServiceHandle(hscm)


def RestartService(serviceName, args=None, waitSeconds=30, machine=None):
    "Stop the service, and then start it again (with some tolerance for allowing it to stop.)"
    try:
        StopService(serviceName, machine)
    except pywintypes.error as exc:
        # Allow only "service not running" error
        if exc.winerror != winerror.ERROR_SERVICE_NOT_ACTIVE:
            raise
    # Give it a few goes, as the service may take time to stop
    for i in range(waitSeconds):
        try:
            StartService(serviceName, args, machine)
            break
        except pywintypes.error as exc:
            if exc.winerror != winerror.ERROR_SERVICE_ALREADY_RUNNING:
                raise
            win32api.Sleep(1000)
    else:
        print("Gave up waiting for the old service to stop!")


def _DebugCtrlHandler(evt):
    if evt in (win32con.CTRL_C_EVENT, win32con.CTRL_BREAK_EVENT):
        assert g_debugService
        print("Stopping debug service.")
        g_debugService.SvcStop()
        return True
    return False


def DebugService(cls, argv=[]):
    # Run a service in "debug" mode.  Re-implements what pythonservice.exe
    # does when it sees a "-debug" param.
    # Currently only used by "frozen" (ie, py2exe) programs (but later may
    # end up being used for all services should we ever remove
    # pythonservice.exe)
    import servicemanager

    global g_debugService

    print(f"Debugging service {cls._svc_name_} - press Ctrl+C to stop.")
    servicemanager.Debugging(True)
    servicemanager.PrepareToHostSingle(cls)
    g_debugService = cls(argv)
    # Setup a ctrl+c handler to simulate a "stop"
    win32api.SetConsoleCtrlHandler(_DebugCtrlHandler, True)
    try:
        g_debugService.SvcRun()
    finally:
        win32api.SetConsoleCtrlHandler(_DebugCtrlHandler, False)
        servicemanager.Debugging(False)
        g_debugService = None


def GetServiceClassString(cls, argv=None):
    if argv is None:
        argv = sys.argv
    import pickle

    modName = pickle.whichmodule(cls, cls.__name__)
    if modName == "__main__":
        try:
            fname = win32api.GetFullPathName(argv[0])
            path = os.path.split(fname)[0]
            # Eaaaahhhh - sometimes this will be a short filename, which causes
            # problems with 1.5.1 and the silly filename case rule.
            filelist = win32api.FindFiles(fname)
            # win32api.FindFiles will not detect files in a zip or exe. If list is empty,
            # skip the test and hope the file really exists.
            if len(filelist) != 0:
                # Get the long name
                fname = os.path.join(path, filelist[0][8])
        except win32api.error:
            raise error(
                "Could not resolve the path name '%s' to a full path" % (argv[0])
            )
        modName = os.path.splitext(fname)[0]
    return modName + "." + cls.__name__


def QueryServiceStatus(serviceName, machine=None):
    hscm = win32service.OpenSCManager(machine, None, win32service.SC_MANAGER_CONNECT)
    try:
        hs = SmartOpenService(hscm, serviceName, win32service.SERVICE_QUERY_STATUS)
        try:
            status = win32service.QueryServiceStatus(hs)
        finally:
            win32service.CloseServiceHandle(hs)
    finally:
        win32service.CloseServiceHandle(hscm)
    return status


def usage():
    try:
        fname = os.path.split(sys.argv[0])[1]
    except:
        fname = sys.argv[0]
    print(
        "Usage: '%s [options] install|update|remove|start [...]|stop|restart [...]|debug [...]'"
        % fname
    )
    print("Options for 'install' and 'update' commands only:")
    print(" --username domain\\username : The Username the service is to run under")
    print(" --password password : The password for the username")
    print(
        " --startup [manual|auto|disabled|delayed] : How the service starts, default = manual"
    )
    print(" --interactive : Allow the service to interact with the desktop.")
    print(
        " --perfmonini file: .ini file to use for registering performance monitor data"
    )
    print(" --perfmondll file: .dll file to use when querying the service for")
    print("   performance data, default = perfmondata.dll")
    print("Options for 'start' and 'stop' commands only:")
    print(" --wait seconds: Wait for the service to actually start or stop.")
    print("                 If you specify --wait with the 'stop' option, the service")
    print("                 and all dependent services will be stopped, each waiting")
    print("                 the specified period.")
    sys.exit(1)


def HandleCommandLine(
    cls,
    serviceClassString=None,
    argv=None,
    customInstallOptions="",
    customOptionHandler=None,
):
    """Utility function allowing services to process the command line.

    Allows standard commands such as 'start', 'stop', 'debug', 'install' etc.

    Install supports 'standard' command line options prefixed with '--', such as
    --username, --password, etc.  In addition,
    the function allows custom command line options to be handled by the calling function.
    """
    err = 0

    if argv is None:
        argv = sys.argv

    if len(argv) <= 1:
        usage()

    serviceName = cls._svc_name_
    serviceDisplayName = cls._svc_display_name_
    if serviceClassString is None:
        serviceClassString = GetServiceClassString(cls)

    # Pull apart the command line
    import getopt

    try:
        opts, args = getopt.getopt(
            argv[1:],
            customInstallOptions,
            [
                "password=",
                "username=",
                "startup=",
                "perfmonini=",
                "perfmondll=",
                "interactive",
                "wait=",
            ],
        )
    except getopt.error as details:
        print(details)
        usage()
    userName = None
    password = None
    perfMonIni = perfMonDll = None
    startup = None
    delayedstart = None
    interactive = None
    waitSecs = 0
    for opt, val in opts:
        if opt == "--username":
            userName = val
        elif opt == "--password":
            password = val
        elif opt == "--perfmonini":
            perfMonIni = val
        elif opt == "--perfmondll":
            perfMonDll = val
        elif opt == "--interactive":
            interactive = 1
        elif opt == "--startup":
            map = {
                "manual": win32service.SERVICE_DEMAND_START,
                "auto": win32service.SERVICE_AUTO_START,
                "delayed": win32service.SERVICE_AUTO_START,  ## ChangeServiceConfig2 called later
                "disabled": win32service.SERVICE_DISABLED,
            }
            startup = map.get(val.lower())
            if not startup:
                print(f"{val!r} is not a valid startup option")
            if val.lower() == "delayed":
                delayedstart = True
            elif val.lower() == "auto":
                delayedstart = False
            ## else no change
        elif opt == "--wait":
            try:
                waitSecs = int(val)
            except ValueError:
                print("--wait must specify an integer number of seconds.")
                usage()

    arg = args[0]
    knownArg = 0
    # First we process all arguments which pass additional args on
    if arg == "start":
        knownArg = 1
        print("Starting service %s" % (serviceName))
        try:
            StartService(serviceName, args[1:])
            if waitSecs:
                WaitForServiceStatus(
                    serviceName, win32service.SERVICE_RUNNING, waitSecs
                )
        except win32service.error as exc:
            print("Error starting service: %s" % exc.strerror)
            err = exc.winerror

    elif arg == "restart":
        knownArg = 1
        print("Restarting service %s" % (serviceName))
        RestartService(serviceName, args[1:])
        if waitSecs:
            WaitForServiceStatus(serviceName, win32service.SERVICE_RUNNING, waitSecs)

    elif arg == "debug":
        knownArg = 1
        if not hasattr(sys, "frozen"):
            # non-frozen services use pythonservice.exe which handles a
            # -debug option
            svcArgs = " ".join(args[1:])
            try:
                exeName = LocateSpecificServiceExe(serviceName)
            except win32api.error as exc:
                if exc.winerror == winerror.ERROR_FILE_NOT_FOUND:
                    print("The service does not appear to be installed.")
                    print("Please install the service before debugging it.")
                    sys.exit(1)
                raise
            try:
                os.system(f"{exeName} -debug {serviceName} {svcArgs}")
            # ^C is used to kill the debug service.  Sometimes Python also gets
            # interrupted - ignore it...
            except KeyboardInterrupt:
                pass
        else:
            # py2exe services don't use pythonservice - so we simulate
            # debugging here.
            DebugService(cls, args)

    if not knownArg and len(args) != 1:
        usage()  # the rest of the cmds don't take addn args

    if arg == "install":
        knownArg = 1
        try:
            serviceDeps = cls._svc_deps_
        except AttributeError:
            serviceDeps = None
        try:
            exeName = cls._exe_name_
        except AttributeError:
            exeName = None  # Default to PythonService.exe
        try:
            exeArgs = cls._exe_args_
        except AttributeError:
            exeArgs = None
        try:
            description = cls._svc_description_
        except AttributeError:
            description = None
        print(f"Installing service {serviceName}")
        # Note that we install the service before calling the custom option
        # handler, so if the custom handler fails, we have an installed service (from NT's POV)
        # but is unlikely to work, as the Python code controlling it failed.  Therefore
        # we remove the service if the first bit works, but the second doesn't!
        try:
            InstallService(
                serviceClassString,
                serviceName,
                serviceDisplayName,
                serviceDeps=serviceDeps,
                startType=startup,
                bRunInteractive=interactive,
                userName=userName,
                password=password,
                exeName=exeName,
                perfMonIni=perfMonIni,
                perfMonDll=perfMonDll,
                exeArgs=exeArgs,
                description=description,
                delayedstart=delayedstart,
            )
            if customOptionHandler:
                customOptionHandler(*(opts,))
            print("Service installed")
        except win32service.error as exc:
            if exc.winerror == winerror.ERROR_SERVICE_EXISTS:
                arg = "update"  # Fall through to the "update" param!
            else:
                print(
                    "Error installing service: %s (%d)" % (exc.strerror, exc.winerror)
                )
                err = exc.winerror
        except ValueError as msg:  # Can be raised by custom option handler.
            print("Error installing service: %s" % str(msg))
            err = -1
            # xxx - maybe I should remove after _any_ failed install - however,
            # xxx - it may be useful to help debug to leave the service as it failed.
            # xxx - We really _must_ remove as per the comments above...
            # As we failed here, remove the service, so the next installation
            # attempt works.
            try:
                RemoveService(serviceName)
            except win32api.error:
                print("Warning - could not remove the partially installed service.")

    if arg == "update":
        knownArg = 1
        try:
            serviceDeps = cls._svc_deps_
        except AttributeError:
            serviceDeps = None
        try:
            exeName = cls._exe_name_
        except AttributeError:
            exeName = None  # Default to PythonService.exe
        try:
            exeArgs = cls._exe_args_
        except AttributeError:
            exeArgs = None
        try:
            description = cls._svc_description_
        except AttributeError:
            description = None
        print("Changing service configuration")
        try:
            ChangeServiceConfig(
                serviceClassString,
                serviceName,
                serviceDeps=serviceDeps,
                startType=startup,
                bRunInteractive=interactive,
                userName=userName,
                password=password,
                exeName=exeName,
                displayName=serviceDisplayName,
                perfMonIni=perfMonIni,
                perfMonDll=perfMonDll,
                exeArgs=exeArgs,
                description=description,
                delayedstart=delayedstart,
            )
            if customOptionHandler:
                customOptionHandler(*(opts,))
            print("Service updated")
        except win32service.error as exc:
            print(
                "Error changing service configuration: %s (%d)"
                % (exc.strerror, exc.winerror)
            )
            err = exc.winerror

    elif arg == "remove":
        knownArg = 1
        print("Removing service %s" % (serviceName))
        try:
            RemoveService(serviceName)
            print("Service removed")
        except win32service.error as exc:
            print("Error removing service: %s (%d)" % (exc.strerror, exc.winerror))
            err = exc.winerror
    elif arg == "stop":
        knownArg = 1
        print("Stopping service %s" % (serviceName))
        try:
            if waitSecs:
                StopServiceWithDeps(serviceName, waitSecs=waitSecs)
            else:
                StopService(serviceName)
        except win32service.error as exc:
            print("Error stopping service: %s (%d)" % (exc.strerror, exc.winerror))
            err = exc.winerror
    if not knownArg:
        err = -1
        print("Unknown command - '%s'" % arg)
        usage()
    return err


#
# Useful base class to build services from.
#
class ServiceFramework:
    # Required Attributes:
    # _svc_name_ = The service name
    # _svc_display_name_ = The service display name

    # Optional Attributes:
    _svc_deps_ = None  # sequence of service names on which this depends
    _exe_name_ = None  # Default to PythonService.exe
    _exe_args_ = None  # Default to no arguments
    _svc_description_ = None

    def __init__(self, args):
        import servicemanager

        self.ssh = servicemanager.RegisterServiceCtrlHandler(
            args[0], self.ServiceCtrlHandlerEx, True
        )
        servicemanager.SetEventSourceName(self._svc_name_)
        self.checkPoint = 0

    def GetAcceptedControls(self):
        # Setup the service controls we accept based on our attributes. Note
        # that if you need to handle controls via SvcOther[Ex](), you must
        # override this.
        accepted = 0
        if hasattr(self, "SvcStop"):
            accepted |= win32service.SERVICE_ACCEPT_STOP
        if hasattr(self, "SvcPause") and hasattr(self, "SvcContinue"):
            accepted |= win32service.SERVICE_ACCEPT_PAUSE_CONTINUE
        if hasattr(self, "SvcShutdown"):
            accepted |= win32service.SERVICE_ACCEPT_SHUTDOWN
        return accepted

    def ReportServiceStatus(
        self, serviceStatus, waitHint=5000, win32ExitCode=0, svcExitCode=0
    ):
        if self.ssh is None:  # Debugging!
            return
        if serviceStatus == win32service.SERVICE_START_PENDING:
            accepted = 0
        else:
            accepted = self.GetAcceptedControls()

        if serviceStatus in [
            win32service.SERVICE_RUNNING,
            win32service.SERVICE_STOPPED,
        ]:
            checkPoint = 0
        else:
            self.checkPoint += 1
            checkPoint = self.checkPoint

        # Now report the status to the control manager
        status = (
            win32service.SERVICE_WIN32_OWN_PROCESS,
            serviceStatus,
            accepted,  # dwControlsAccepted,
            win32ExitCode,  # dwWin32ExitCode;
            svcExitCode,  # dwServiceSpecificExitCode;
            checkPoint,  # dwCheckPoint;
            waitHint,
        )
        win32service.SetServiceStatus(self.ssh, status)

    def SvcInterrogate(self):
        # Assume we are running, and everyone is happy.
        self.ReportServiceStatus(win32service.SERVICE_RUNNING)

    def SvcOther(self, control):
        try:
            print("Unknown control status - %d" % control)
        except OSError:
            # services may not have a valid stdout!
            pass

    def ServiceCtrlHandler(self, control):
        return self.ServiceCtrlHandlerEx(control, 0, None)

    # The 'Ex' functions, which take additional params
    def SvcOtherEx(self, control, event_type, data):
        # The default here is to call self.SvcOther as that is the old behaviour.
        # If you want to take advantage of the extra data, override this method
        return self.SvcOther(control)

    def ServiceCtrlHandlerEx(self, control, event_type, data):
        if control == win32service.SERVICE_CONTROL_STOP:
            return self.SvcStop()
        elif control == win32service.SERVICE_CONTROL_PAUSE:
            return self.SvcPause()
        elif control == win32service.SERVICE_CONTROL_CONTINUE:
            return self.SvcContinue()
        elif control == win32service.SERVICE_CONTROL_INTERROGATE:
            return self.SvcInterrogate()
        elif control == win32service.SERVICE_CONTROL_SHUTDOWN:
            return self.SvcShutdown()
        else:
            return self.SvcOtherEx(control, event_type, data)

    def SvcRun(self):
        # This is the entry point the C framework calls when the Service is
        # started. Your Service class should implement SvcDoRun().
        # Or you can override this method for more control over the Service
        # statuses reported to the SCM.

        # If this method raises an exception, the C framework will detect this
        # and report a SERVICE_STOPPED status with a non-zero error code.

        self.ReportServiceStatus(win32service.SERVICE_RUNNING)
        self.SvcDoRun()
        # Once SvcDoRun terminates, the service has stopped.
        # We tell the SCM the service is still stopping - the C framework
        # will automatically tell the SCM it has stopped when this returns.
        self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
