ps/Modules/Alkami.PowerShell.Services/Public/Start-AlkamiService.ps1
2023-05-30 22:51:22 -07:00

171 lines
6.6 KiB
PowerShell

function Start-AlkamiService {
<#
.SYNOPSIS
Starts a service, and throws an error if the service does not start in -Timeout seconds.
.PARAMETER ServiceName
The name of the service to start
.PARAMETER Timeout
The maximum number of seconds to wait. Defaults to 60 seconds
.PARAMETER StatusUpdateIntervalSeconds
Number of seconds to wait between log statements during the "Timeout Loop" when waiting for the service to start. This is to avoid having 60 log statements for each service start attempt that times out
.PARAMETER Force
Ignore whether or not Get-Service returns a service for ServiceName, try to start it anyway. Primarily useful for testing.
#>
Param(
[Parameter(Mandatory = $true)]
[string]$ServiceName,
[Parameter(Mandatory = $false)]
[int]$Timeout = 60,
[Parameter(Mandatory = $false)]
[int]$StatusUpdateIntervalSeconds = 10,
[Parameter(Mandatory = $false)]
[switch]$Force
)
#region entry-guards
$logLead = Get-LogLeadName
$runningConstant = "Running"
$disabledConstant = "Disabled"
$startPendingConstant = "StartPending"
if ($Force) {
Write-Warning "$logLead : Force flag present! Unexepected behavior may occur AND log statements may not always be accurate, especially regarding existence or state of the service named [$ServiceName]"
}
Write-Host "$logLead : Get-Service -Name [$ServiceName]"
try {
$service = Get-Service -Name $ServiceName
} catch {
$caughtEx = $_
Write-Warning "$loglead : Service with name $ServiceName not found"
if ($Force) {
Write-Host "$loglead : Force flag enabled, continuing"
} else {
throw $caughtEx
}
}
if ($null -eq $service -and -not $Force) {
Write-Warning "$logLead : Service [$ServiceName] was not found"
return
}
if ($service.StartType -eq $disabledConstant) {
Write-Warning "$logLead : Service [$ServiceName] was disabled"
return
}
if ($service.Status -eq $runningConstant) {
Write-Host "$logLead : Service [$ServiceName] is already running."
return
}
if ([int]$Timeout -le 0) {
Write-Warning "$logLead : Invalid Timeout value [$Timeout], defaulting to 60"
$Timeout = 60
}
#endregion entry-guards
$startTime = Get-Date
Write-Host "$logLead : Starting [$ServiceName] at [$startTime]"
# Invoke Command With Retry timing.
$icwrTiming = @{
MaxRetries = 3
Seconds = 3
JitterMin = -500
JitterMax = 1500
}
$icwrArgs = @{
Timeout = $Timeout
StatusUpdateIntervalSeconds = $StatusUpdateIntervalSeconds
LogLead = $logLead
RunningConstant = $runningConstant
StartPendingConstant = $startPendingConstant
}
$scriptblockStartService = {
param($sbServiceName, $sbInputArgs)
$sbTimeout = $sbInputArgs.Timeout
$sbStatusUpdateIntervalSeconds = $sbInputArgs.StatusUpdateIntervalSeconds
$sbLogLead = "sb_$($sbInputArgs.LogLead)"
$sbRunningConstant = $sbInputArgs.RunningConstant
$sbStartPendingConstant = $sbInputArgs.StartPendingConstant
$sbPriorSvcStatus = (Get-Service -Name $sbServiceName -ErrorAction Ignore).Status
if ($sbPriorSvcStatus -eq $sbRunningConstant) {
Write-Warning "$sbLogLead : Service [$sbServiceName] already running; maybe a previously timed out attempt succeeded during backoff period. Exiting..."
return
}
if ($sbPriorSvcStatus -eq $sbStartPendingConstant) {
Write-Warning "$sbLoglead : service [$sbServiceName] is already in state [$sbStartPendingConstant]"
Write-Warning "$sbLogLead : Waiting for [$sbServiceName] to start instead of calling start command"
} else {
try {
Write-Host "$sbLogLead : start [$sbServiceName]"
Invoke-SCExe -Arguments @("start", $sbServiceName)
} catch {
# Is an error on start
$sbCaughtEx = $_
Write-Warning "$sbLogLead : start [$sbServiceName] failed due to immediate start failure"
Write-Warning "$sbLogLead : $($sbCaughEx.Exception.Message)"
throw $sbCaughtEx
}
}
# Slowly poll to make sure that the service is running.
Write-Host "$sbLogLead : waiting for [$sbServiceName] to enter state $sbRunningConstant (max $sbTimeout seconds)"
$sbLastStatus = ""
for ($i = 0; $i -lt $sbTimeout; $i++) {
$sbLastStatus = (Get-Service -Name $sbServiceName).Status
if ($sbLastStatus -eq $sbRunningConstant) {
Write-Host "$sbLogLead : Service [$sbServiceName] was started successfully"
return
}
# The following condition ALWAYS means the service entered the RUNNING state AND THEN LEFT IT
# For explanations:
# See https://docs.microsoft.com/en-us/windows/win32/services/service-status-transitions
# See https://jira.alkami.com/browse/SRE-18231?focusedCommentId=5158719&page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel#comment-5158719
if ($sbLastStatus -ne $sbStartPendingConstant) {
Write-Host "$sbLogLead : Current status: [$sbLastStatus]"
Write-Host "$sbLogLead : Expected status: [$sbStartPendingConstant]"
Write-Warning "$sbLogLead : [$sbServiceName] started but is not in status [$sbRunningConstant] - CHECK LOGS"
throw "$sbLogLead : Service [$sbServiceName] STOPPED RUNNING during the timeout period for some OTHER reason. Last status was [$sbLastStatus] - CHECK LOGS"
}
Start-Sleep -Seconds 1
$sbShouldWriteStatus = ($i + 1) % $sbStatusUpdateIntervalSeconds -eq 0
if ($sbShouldWriteStatus) {
Write-Host "$sbLogLead : still waiting to make sure [$sbServiceName] has started. Last status was [$sbLastStatus] - seconds elapsed - [$($i + 1)] of [$sbTimeout]"
}
}
Write-Host "$sbLogLead : start service [$sbServiceName] failed due to a TIMEOUT FAILURE"
throw "$sbLogLead : Service [$sbServiceName] could not be started within the timeout period. Please investigate. Last status was [$sbLastStatus]"
}
$icwrSplat = @{
ScriptBlock = $scriptblockStartService
Arguments = @($ServiceName, $icwrArgs)
Exponential = $true
}
Invoke-CommandWithRetry @icwrSplat @icwrTiming
}