Meh, not too hard to parse:
function New-ServiceRecoveryAction {
[CmdletBinding()]
[OutputType('ServiceRecoveryAction')]
Param (
[Parameter(Position = 0)]
[ValidateSet('Run', 'Restart', 'Reboot', 'None')]
[string] $Action = 'None',
[Parameter(Position = 1)]
[TimeSpan] $Delay = [TimeSpan]::Zero
)
End {
[PSCustomObject]@{
PSTypeName = 'ServiceRecoveryAction'
Action = $Action
Delay = $Delay
}
}
}
function New-ServiceRecovery {
[CmdletBinding()]
[OutputType('ServiceRecovery')]
Param (
[Parameter(Position = 0)]
[ValidateCount(0, 3)]
[PSTypeName('ServiceRecoveryAction')]
[PSCustomObject[]] $RecoveryAction = @(),
[Parameter(Position = 1)]
[TimeSpan] $ResetFailureCountAfter = [TimeSpan]::Zero,
[string] $RebootMessage,
[string] $RunCommandOnFailure = '',
[string[]] $CommandArguments = @(),
[switch] $AppendFailureCountToCommandArguments,
[switch] $RecoverOnNonCrashFailure
)
End {
$RecoveryAction = [PSCustomObject]@{
PSTypeName = 'ServiceRecovery'
RecoveryActions = if ($PSBoundParameters.ContainsKey('RecoveryAction')) { $RecoveryAction } else { @() }
ResetFailureCountAfter = $ResetFailureCountAfter
RebootMessage = $RebootMessage
CommandLine = "`"$RunCommandOnFailure`"$(if ($CommandArguments.Count -gt 0) { "$CommandArguments" })$(if ($AppendFailureCountToCommandArguments) { ' /fail=%1%' })"
RecoverOnNonCrashFailure = [switch]$RecoverOnNonCrashFailures
}
$RecoveryAction | Add-Member -MemberType ScriptProperty -Name FailureRecoveryArguments -Value {
@(
# NOTE
#
# 49711 days, converted to seconds, results in a value larger than can be held in a UInt32, which is the
# size of the field which holds the FailureResetCounter in the ServiceController object. When using, e.g.,
# `sc.exe failure <service> actions= restart/120000 reset= INIFINITE`, INFINITE gets translated to
# [UInt32]::MaxValue (approx. 49710 days, as shown in the UI). When attempting to set a value larger than what
# can fit in a unsigned 32-bit integer, the value becomes the "overflow" amount--i.e., (<value>.TotalSeconds % [UInt32]::MaxValue) - 1
# To prevent this (potentially) unexpected behavior, we're going to simply convert the value to INFINITE
# (i.e., [UInt32]::Maxvalue
#
# One other interesting piece of trivia: the UI for setting recovery actions only allows you to enter the number of
# days to reset the failed service counter. But, through the CLI, the granularity used is the number of seconds.
#
# Due to this "limitation", I'm going to assume that the field holding the milliseconds to delay before taking
# a recovery action is also a [Uint32] field.
'actions='
if ($this.RecoveryActions.Count -eq 0) { '///' } else {
$Actions = @()
foreach ($i in 1..3) {
if ($this.RecoveryActions.Count -ge $i) {
$RecoveryAction = $this.RecoveryActions[($i - 1)]
$DelayMilliseconds = if ($RecoveryAction.Delay.TotalMilliseconds -gt [UInt32]::MaxValue) { [UInt32]::MaxValue } else { [Convert]::ToUint32($RecoveryAction.Delay.TotalMilliseconds) }
$Actions += '{0}/{1}' -f $RecoveryAction.Action.ToLowerInvariant(), $DelayMilliseconds
} else {
$Actions += '/'
}
}
$Actions -join '/'
}
'reset='
if ($this.RecoveryActions.Count -eq 0) {
'0'
} elseif ($this.ResetFailureCountAfter.TotalSeconds -gt [UInt32]::MaxValue) {
[UInt32]::MaxValue
} else {
[Convert]::ToUInt32($this.ResetFailureCounterAfter.TotalSeconds)
}
if ($this.RecoveryActions | Where-Object { $_.Action -ieq 'Run' }) {
'command='
$this.CommandLine
}
if ($this.RecoveryActions | where-Object { $_.ACtion -ieq 'Reboot' }) {
'reboot='
$this.RebootMessage
}
)
} -PassThru
}
}
function Get-ServiceRecovery {
[CmdletBinding(DefaultParameterSetName = 'Default')]
[OutputType('ServiceRecovery')]
Param (
[Parameter(Mandatory, Position = 0, ValueFromPipeline, ParameterSetName = 'Default')]
[SupportsWildcards()]
[string[]] $Name,
[Parameter(Mandatory, Position = 0, ValueFromPipeline, ParameterSetName = 'InputObject')]
[System.ServiceProcess.ServiceController[]] $InputObject
)
Process {
if ($PSCmdlet.ParameterSetName -ieq 'Default') {
$Services = $Name | Get-Service
} else {
$Services = $InputObject
}
foreach ($Service in $Services) {
$ServiceName = $Service.Name
$(sc.exe qfailure "$ServiceName") -split '\r?\n' |
Select-Object -Skip 3 |
ForEach-Object -Begin {
$RecoveryParams = [hashtable]@{
RecoveryAction = [PSCustomObject[]]@()
RecoverOnNonCrashFailure = $(sc.exe qfailureflag "$Name") -split '\r?\n' | ForEach-Object -Begin {
$RetVal = $False
} -Process {
$RetVal = $RetVal -or $_ -imatch 'FAILURE_ACTIONS_ON_NONCRASH_FAILURES:\s*TRUE$'
} -End {
$RetVal
}
}
} -Process {
Write-Debug "Processing output string: $_"
if ($_ -imatch "^\s*RESET_PERIOD\s+\(in seconds\)\s+:\s+(?<TimeoutSeconds>\d+|INFINITE)\s*$") {
$Seconds = $matches.TimeoutSeconds
if ($Seconds -ieq 'INFINITE') {
$RecoveryParams.Add('ResetFailureCountAfter', ([TimeSpan]::FromSeconds([UInt32]::MaxValue)))
} else {
$RecoveryParams.Add('ResetFailureCountAfter', ([TimeSpan]::FromSeconds($Seconds)))
}
} elseif ($_ -imatch '^\s*REBOOT_MESSAGE\s+:\s+(?<RebootMessage>[^\s].*)$') {
$RecoveryParams.Add('RebootMessage', $matches.RebootMessage)
} elseif ($_ -imatch '^\s*COMMAND_LINE\s+:\s+(?<CommandLine>.*)$' -and $matches.CommandLine.Trim() -ne '""') {
$RecoveryParams.Add('RunCommandOnFailure', $matches.CommandLine.Trim())
} elseif ($_ -imatch '^\s*(?:FAILURE_ACTIONS\s+:\s+)?(?<RecoveryAction>RESTART|RUN PROCESS|REBOOT) -- Delay = (?<RecoveryDelay>\d+) milliseconds.$') {
switch ($matches.RecoveryAction) {
'RESTART' { $RecoveryParams.RecoveryAction += New-ServiceRecoveryAction 'Restart' ([TimeSpan]::FromMilliseconds($matches.RecoveryDelay)); break }
'RUN PROCESS' { $RecoveryParams.RecoveryAction += New-ServiceRecoveryAction 'Run' ([TimeSpan]::FromMilliseconds($matches.RecoveryDelay)); break }
'REBOOT' { $RecoveryParams.RecoveryAction += New-ServiceRecoveryAction 'Reboot' ([TimeSpan]::FromMilliseconds=($matches.RecoveryDelay)); break }
default { Write-Error "Unknown service failure recovery action: '$_'"; exit 1 }
}
}
} -End {
New-ServiceRecovery @RecoveryParams
}
}
}
}
function Set-ServiceRecovery {
[CmdletBinding(DefaultParameterSetName = 'Default', SupportsShouldProcess)]
[OutputType([System.ServiceProcess.ServiceController])]
Param (
[Parameter(Mandatory, Position = 1, ValueFromPipeline, ParameterSetName = 'Default')]
[SupportsWildcards()]
[string[]] $Name,
[Parameter(Mandatory, Position = 1, ValueFromPipeline, ParameterSetName = 'InputObject')]
[System.ServiceProcess.ServiceController[]] $InputObject,
[Parameter(Mandatory, Position = 0, ValueFromPipelineByPropertyName)]
[PSTypeName('ServiceRecovery')]
[PSCustomObject] $ServiceRecovery,
[switch] $Force
)
Process {
if ($Force -and !($PSBoundParameters.ContainsKey('Confirm') -and $Confirm)) {
$ConfirmPreference = 'None'
}
$Services = if ($PSCmdlet.ParameterSetName -ieq 'Default') {
$Name | Get-Service
} else {
$InputObject
}
foreach ($Service in $Services) {
$ServiceName = $Service.Name
if ($PSCmdlet.ShouldProcess($ServiceName)) {
$Output = sc.exe failure $ServiceName $ServiceRecovery.FailureRecoveryArguments
Write-Verbose $Output
$RetVal = $LASTEXITCODE
if ($RetVal -ne 0) {
$exn = New-Object -TypeName Exception -ArgumentList "Failed to set service recovery actions for $ServiceName."
$exn.Data.Add('Command', "sc.exe failure $ServiceName $($ServiceRecovery.FailureRecoveryArguments)")
$exn.Data.Add('Output', $Output)
throw (New-Object System.Management.Automation.ErrorRecord $exn, '', "$([System.Management.Automation.ErrorCategory]::InvalidOperation)" , $null)
}
$FailureFlag = if ($ServiceRecovery.RecoverOnNonCrashFailure) { 1 } else { 0 }
$Output = sc.exe failureflag $ServiceName $FailureFlag
Write-Verbose $Output
$RetVal = $LASTEXITCODE
if ($RetVal -ne 0) {
$exn = New-Object -TypeName Exception -ArgumentList "Failed to set service recovery to occur on non-crash failures for $($Service.Name)."
$exn.Data.Add('Command', "sc.exe failureflag $ServiceName $FailureFlag")
$exn.Data.Add('Output', $Output)
throw (New-Object System.Management.Automation.ErrorRecord $exn, '', "$([System.Management.Automation.ErrorCategory]::InvalidOperation)", $null)
}
}
$Service
}
}
}
So, to use this:
$ServiceRecoveryConfig = 'MSMQ' | Get-ServiceRecoveryConfig
You'll notice that I added a property to the object called FailureRecoveryArguments
. The idea behind this property is two-fold:
You can easily compare the service recovery configuration for two different services or compare it to a new/proposed service recovery configuration:
$RestartService = New-RecoveryAction 'Restart' ([TimeSpan]::FromMinutes(2))
$NewServiceRecovery = New-ServiceRecovery $RestartService ([TimeSpan]::FromMinutes(5))
$CurrentServiceRecovery = 'MSMQ' | Get-ServiceRecovery
if ("$($CurrentServiceRecovery.FailureRecoveryArguments)" -ine "$($NewServiceRecovery.FailureRecoveryArguments)") {
sc.exe MSMQ $NewServiceRecovery.FailureRecoveryArguments
}
You can "splat" the arguments to an invocation of sc.exe
, as show above when we found the desired service recovery configuration to not match the current service recovery configuration.
Of course, those two use-cases are moot when I have also provided Set-ServiceRecovery
as well: 'MSMQ' | Set-ServiceRecovery $NewServiceRecovery
If you get an error when running Set-ServiceRecovery
, check the Data
property on the exception of the ErrorRecord
object which will show the command that was run to set the service recovery, as well as the output from executing the command:
try {
'MSMQ' | Set-ServiceRecovery $NewServiceRecovery
} catch {
Write-Host "Aww shucks, I couldn't set the service recovery."
$Err = $Error[0]
Write-Host "When running '$($Err.Exception.Data['Command'])', received the output:`n$($Err.Exception.Data['Output'])"
}
Hope that helps!
NOTE that to set the service recovery configuration, you will need to run the function in an elevated context, i.e., run PowerShell as Administrator.