# encoding: ascii
# api: powershell
# title: Stop-ServiceandProcess
# description: Stop service and associated Pid. Sometimes homegrown win32 services has its Pid still terminating gracefully in the background for quite a while. This function allows to wait indefinitely, or after some time, kill the associated Pid.
# version: 0.1
# type: function
# author: Daniel Cheng
# license: CC0
# function: Stop-ServiceandProcess
# x-poshcode-id: 5607
# x-archived: 2015-01-31T20:33:38
# x-published: 2015-11-20T18:31:00
#
# Updated to support remote processID kills, via WMI.
#
# daniel.cheng@thomsonreuters.com
# Function: Stop service and its associated processID (PID).
#           Sometimes homegrown services has its PID still terminating gracefully in the background, but the service is reported as stopped.

function Stop-ServiceandProcess {
    param (
        [string[]]$servers = 'localhost',
        [parameter(Mandatory)][string[]]$services,
        [int]$InterrogateServiceDelay = 1, # in seconds
        [int]$killDelay = $null,
        [switch]$terse = $false, # shows terseIndicator string if true, otherwise, display request/response line from InterrogateService()
        [string]$terseIndicator = '.',
        [switch]$showEndStatus = $false
    )

    $localhost = 'localhost' # $env:computername

    # http://msdn.microsoft.com/en-us/library/aa393660(v=vs.85).aspx
    $serviceState = @{
        00 = 'The request was accepted.';
        01 = 'The request is not supported.';
        02 = 'The user did not have the necessary access.';
        03 = 'The service cannot be stopped because other services that are running are dependent on it.';
        04 = 'The requested control code is not valid, or it is unacceptable to the service.';
        05 = 'The requested control code cannot be sent to the service because the state of the service (Win32_BaseService State property) is equal to 0, 1, or 2.';
        06 = 'The service has not been started.';
        07 = 'The service did not respond to the start request in a timely fashion.';
        08 = 'Unknown failure when starting the service.';
        09 = 'The directory path to the service executable file was not found.';
        10 = 'The service is already running.';
        11 = 'The database to add a new service is locked.';
        12 = 'A dependency this service relies on has been removed from the system.';
        13 = 'The service failed to find the service needed from a dependent service.';
        14 = 'The service has been disabled from the system.';
        15 = 'The service does not have the correct authentication to run on the system.';
        16 = 'This service is being removed from the system.';
        17 = 'The service has no execution thread.';
        18 = 'The service has circular dependencies when it starts.';
        19 = 'A service is running under the same name.';
        20 = 'The service name has invalid characters.';
        21 = 'Invalid parameters have been passed to the service.';
        22 = 'The account under which this service runs is either invalid or lacks the permissions to run the service.';
        23 = 'The service exists in the database of services available from the system.';
        24 = 'The service is currently paused in the system.';
    }

    # http://msdn.microsoft.com/en-us/library/aa393907(v=vs.85).aspx
    $terminateReturnCode = @{
        00 = 'Successful Completion';
        02 = 'Access Denied';
        03 = 'Insufficient Privilege';
        08 = 'Unknown Failure';
        09 = 'Path Not Found';
        21 = 'Invalid Parameter';
    }

    $acceptableConditions = [ordered]@{
        AcceptStop = 'True';
        Status = 'OK';
        State = @('Running', 'Paused');
        StartMode = @('Auto','Manual');
        ServiceType = @('Own Process'); # excludes 'Share Process'!
    }

    filter Get-FormattedTimestamp {return Get-Date -Format s}

    foreach ($server in $servers){
        foreach ($service in $services){
            $currentService = gwmi -ComputerName $server -Class win32_service -Filter "name='$service'" -ea SilentlyContinue
            $processId = $currentService.ProcessId

            if ($currentService -isnot [object]){
                Write-Warning "Error with creating server object ($server) for service ($service)!"
            } else {
                foreach ($condition in $acceptableConditions.Keys){
                    if (-not ($acceptableConditions.$condition -contains $currentService.$condition)){
                        "{0}: {1} -- {2}: service ({3}, pid={4}); message: {5}" -f (Get-FormattedTimestamp), $localhost, $server, $service, $processId, "Error! $($currentService.Name).$condition = '$($currentService.$condition)'.. skipping!"
                        $condition = $false
                        break
                    }
                }
            }
            if ($condition){
                "{0}: {1} -> {2}: service ({3}, pid={4}); message: {5}" -f (Get-FormattedTimestamp), $localhost, $server, $service, $processId, 'Stop command sent.'
                "{0}: {1} <- {2}: service ({3}, pid={4}); message: {5}" -f (Get-FormattedTimestamp), $localhost, $server, $service, $processId, $serviceState.$([int]$currentService.StopService().ReturnValue)

                $timeElapsed = [System.Diagnostics.Stopwatch]::StartNew()
                do {
                    $exitCode = $([int]$currentService.InterrogateService().ReturnValue)
                    if ($terse){
                        Write-Host -NoNewline $terseIndicator
                    } else {
                        "{0}: {1} -> {2}: service ({3}, pid={4}); message: {5}" -f (Get-FormattedTimestamp), $localhost, $server, $service, $processId, "InterrogateService()"
                        "{0}: {1} <- {2}: service ({3}, pid={4}); message: {5}" -f (Get-FormattedTimestamp), $localhost, $server, $service, $processId, "Exit code = $exitCode, $($serviceState.$exitCode)"
                    }
                    if (([bool]$killDelay) -and ($timeElapsed.Elapsed.Seconds -ge $killDelay)){
                        "{0}: {1} -> {2}: service ({3}, pid={4}); message: {5}" -f (Get-FormattedTimestamp), $localhost, $server, $service, $processId, "Time threshold ($killDelay second(s)) expired, sending kill command."
                        $terminateStatus = (gwmi -ea SilentlyContinue -ComputerName $server -Class win32_process -Filter "handle='$processId'").Terminate()
                    }
                    Start-Sleep -Seconds $InterrogateServiceDelay
                } until (
                    ([bool]!(Get-Process -ComputerName $server -Id $currentService.ProcessId -ea SilentlyContinue).Id) -and
                    ((gwmi -ComputerName $server -Class win32_service -Filter "name='$service'" -ea SilentlyContinue).State -eq 'Stopped')
                )
                "{0}: {1} <- {2}: service ({3}, pid={4}); message: {5}" -f (Get-FormattedTimestamp), $localhost, $server, $service, $processId, $terminateReturnCode.([int]$($terminateStatus.ReturnValue))
                "{0}: {1} -- {2}: service ({3}, pid={4}); message: {5}" -f (Get-FormattedTimestamp), $localhost, $server, $service, $processId, "Operation took $($timeElapsed.Elapsed.TotalSeconds) seconds."
                $timeElapsed.Stop()
            }
            if ($showEndStatus){gwmi -ComputerName $server -Class win32_service -Filter "name='$service'" | ft PSComputerName, Name, ServiceType, StartMode, State, ProcessId, PathName -AutoSize}
        }
    }
}
