From 7725ac0ee73d96b8fb3e6f1d3be6365fc734e031 Mon Sep 17 00:00:00 2001 From: bill Date: Thu, 26 Mar 2020 19:25:41 +0000 Subject: [PATCH 01/13] First pass implementation of Get,Set,Test This commit is the first pass at Get,Set,Test, with comment based help for the functions. No attempt is yet made at fixing any existing tests that are broken, or adding new tests. --- .../DSC_xServiceResource.psm1 | 611 +++++++++++++++++- .../en-US/DSC_xServiceResource.strings.psd1 | 1 + 2 files changed, 601 insertions(+), 11 deletions(-) diff --git a/source/DSCResources/DSC_xServiceResource/DSC_xServiceResource.psm1 b/source/DSCResources/DSC_xServiceResource/DSC_xServiceResource.psm1 index 5a29d0612..000fa333b 100644 --- a/source/DSCResources/DSC_xServiceResource/DSC_xServiceResource.psm1 +++ b/source/DSCResources/DSC_xServiceResource/DSC_xServiceResource.psm1 @@ -16,6 +16,14 @@ Import-Module -Name (Join-Path -Path $modulePath ` # Import Localization Strings $script:localizedData = Get-LocalizedData -ResourceName 'DSC_xServiceResource' +# This type is documented here: https://docs.microsoft.com/en-us/windows/win32/api/winsvc/ns-winsvc-sc_action +enum ACTION_TYPE { + NONE = 0 + RESTART = 1 + REBOOT = 2 + RUN_COMMAND = 3 +} + <# .SYNOPSIS Retrieves the current status of the service resource with the given name. @@ -78,17 +86,24 @@ function Get-TargetResource default { $serviceCimInstance.StartName } } + $serviceFailureActions = Get-ServiceFailureActions -Service $service.Name + $serviceResource = @{ - Name = $Name - Ensure = 'Present' - Path = $serviceCimInstance.PathName - StartupType = $startupType - BuiltInAccount = $builtInAccount - State = $service.Status.ToString() - DisplayName = $service.DisplayName - Description = $serviceCimInstance.Description - DesktopInteract = $serviceCimInstance.DesktopInteract - Dependencies = $dependencies + Name = $Name + Ensure = 'Present' + Path = $serviceCimInstance.PathName + StartupType = $startupType + BuiltInAccount = $builtInAccount + State = $service.Status.ToString() + DisplayName = $service.DisplayName + Description = $serviceCimInstance.Description + DesktopInteract = $serviceCimInstance.DesktopInteract + Dependencies = $dependencies + ResetPeriodSeconds = $serviceFailureActions.resetPeriodSeconds + FailureCommand = $serviceFailureActions.failureCommand + RebootMessage = $serviceFailureActions.rebootMessage + FailureActionsCollection = $serviceFailureActions.ActionsCollection + FailureActionsOnNonCrashFailures = $serviceFailureActions.FailureActionsOnNonCrashFailures } } else @@ -176,6 +191,32 @@ function Get-TargetResource The time to wait for the service to stop in milliseconds. The default value is 30000 (30 seconds). + .PARAMETER ResetPeriodSeconds + The time after which to reset the failure count to zero if there are no failures, in seconds. + Specify INFINITE to indicate that this value should never be reset. + + .PARAMETER RebootMessage + The message to be broadcast to server users before rebooting in response to the SC_ACTION_REBOOT service controller action. + If this value is NULL, the reboot message is unchanged. If the value is an empty string (""), the reboot message is deleted + and no message is broadcast. + + .PARAMETER FailureCommand + The command line of the process for the CreateProcess function to execute in response to the SC_ACTION_RUN_COMMAND service controller action. + This process runs under the same account as the service. If this value is NULL, the command is unchanged. + If the value is an empty string (""), the command is deleted and no program is run when the service fails. + + .PARAMETER FailureActionsCollection + An array of hash tables representing the failure actions to take. Each hash table should have + two keys: type and delaySeconds. The value for the type key should be one of RESTART, RUN_COMMAND, REBOOT, or NONE. + The value for the delaySeconds key is an integer value of seconds to wait before applying the requested action. + + .PARAMETER FailureActionsOnNonCrashFailures + By default, failure actions are only queued if the service terminates without reporting + a status of SERVICE_STOPPED. Setting this to true will queue actions in that case, as well + as if the SERVICE_STOPPED state is reported but the exit code is non-zero. This property + correspones to the 'Enable Actions For Stops With Errors' check box in the 'Recovery' tab + of the service properties GUI. + .PARAMETER Credential The credential of the user account the service should start under. @@ -261,6 +302,26 @@ function Set-TargetResource [System.UInt32] $TerminateTimeout = 30000, + [Parameter()] + [System.UInt32] + $ResetPeriodSeconds, + + [Parameter()] + [System.String] + $RebootMessage, + + [Parameter()] + [System.String] + $FailureCommand, + + [Parameter()] + [System.Object[]] + $FailureActionsCollection, + + [Parameter()] + [System.Boolean] + $FailureActionsOnNonCrashFailures, + [Parameter()] [ValidateNotNull()] [System.Management.Automation.PSCredential] @@ -327,7 +388,10 @@ function Set-TargetResource # Update the properties of the service if needed $setServicePropertyParameters = @{} - $servicePropertyParameterNames = @( 'StartupType', 'BuiltInAccount', 'Credential', 'GroupManagedServiceAccount', 'DesktopInteract', 'DisplayName', 'Description', 'Dependencies' ) + $servicePropertyParameterNames = @( 'StartupType', 'BuiltInAccount', 'Credential', 'GroupManagedServiceAccount', + 'DesktopInteract', 'DisplayName', 'Description', 'Dependencies', + 'ResetPeriodSeconds', 'RebootMessage', 'FailureCommand', 'FailureActionsCollection', + 'FailureActionsOnNonCrashFailures') foreach ($servicePropertyParameterName in $servicePropertyParameterNames) { @@ -425,6 +489,32 @@ function Set-TargetResource .PARAMETER TerminateTimeout Not used in Test-TargetResource. + .PARAMETER ResetPeriodSeconds + The time after which to reset the failure count to zero if there are no failures, in seconds. + Specify INFINITE to indicate that this value should never be reset. + + .PARAMETER RebootMessage + The message to be broadcast to server users before rebooting in response to the SC_ACTION_REBOOT service controller action. + If this value is NULL, the reboot message is unchanged. If the value is an empty string (""), the reboot message is deleted + and no message is broadcast. + + .PARAMETER FailureCommand + The command line of the process for the CreateProcess function to execute in response to the SC_ACTION_RUN_COMMAND service controller action. + This process runs under the same account as the service. If this value is NULL, the command is unchanged. + If the value is an empty string (""), the command is deleted and no program is run when the service fails. + + .PARAMETER FailureActionsCollection + An array of hash tables representing the failure actions to take. Each hash table should have + two keys: type and delaySeconds. The value for the type key should be one of RESTART, RUN_COMMAND, REBOOT, or NONE. + The value for the delaySeconds key is an integer value of seconds to wait before applying the requested action. + + .PARAMETER FailureActionsOnNonCrashFailures + By default, failure actions are only queued if the service terminates without reporting + a status of SERVICE_STOPPED. Setting this to true will queue actions in that case, as well + as if the SERVICE_STOPPED state is reported but the exit code is non-zero. This property + correspones to the 'Enable Actions For Stops With Errors' check box in the 'Recovery' tab + of the service properties GUI. + .PARAMETER Credential The credential the service should be running under. @@ -497,6 +587,26 @@ function Test-TargetResource [System.UInt32] $TerminateTimeout = 30000, + [Parameter()] + [System.UInt32] + $ResetPeriodSeconds, + + [Parameter()] + [System.String] + $RebootMessage, + + [Parameter()] + [System.String] + $FailureCommand, + + [Parameter()] + [System.Object[]] + $FailureActionsCollection, + + [Parameter()] + [System.Boolean] + $FailureActionsOnNonCrashFailures, + [Parameter()] [ValidateNotNull()] [System.Management.Automation.PSCredential] @@ -518,6 +628,14 @@ function Test-TargetResource New-InvalidArgumentException -ArgumentName 'BuiltInAccount / Credential / GroupManagedServiceAccount' -Message $errorMessage } + if(($PSBoundParameters.ContainsKey('Failure3Action') -and (-not $PSBoundParameters.ContainsKey('Failure2Action'))) -or + ($PSBoundParameters.ContainsKey('Failure2Action') -and (-not $PSBoundParameters.ContainsKey('Failure1Action'))) + ) + { + $errorMessage = $script:localizedData.FailureActionsMustBeSpecifiedInOrder + New-InvalidArgumentException -ArgumentName 'Failure2Action / Failure3Action' -Message $errorMessage + } + $serviceResource = Get-TargetResource -Name $Name if ($serviceResource.Ensure -eq 'Absent') @@ -638,6 +756,62 @@ function Test-TargetResource Write-Verbose -Message ($script:localizedData.ServicePropertyDoesNotMatch -f 'State', $Name, $State, $serviceResource.State) return $false } + + # Check the reset period + if($PSBoundParameters.ContainsKey('ResetPeriodSeconds') -and $ResetPeriodSeconds -ine $serviceResource.ResetPeriodSeconds) + { + Write-Verbose -Message ($script:localizedData.ServicePropertyDoesNotMatch -f 'ResetPeriodSeconds', $Name, $ResetPeriodSeconds, $serviceResource.ResetPeriodSeconds) + return $false + } + + # Check the failure command + if($PSBoundParameters.ContainsKey('FailureCommand') -and $FailureCommand -ine $serviceResource.failureCommand) + { + Write-Verbose -Message ($script:localizedData.ServicePropertyDoesNotMatch -f 'FailureCommand', $Name, $FailureCommand, $serviceResource.failureCommand) + return $false + } + + # Check the reboot message + if($PSBoundParameters.ContainsKey('RebootMessage') -and $RebootMessage -ine $serviceResource.rebootMessage) + { + Write-Verbose -Message ($script:localizedData.ServicePropertyDoesNotMatch -f 'RebootMessage', $Name, $RebootMessage, $serviceResource.rebootMessage) + return $false + } + + # Check the failure actions collection + if($PSBoundParameters.ContainsKey('FailureActionsCollection')) { + $inSync = $true + if($FailureActionsCollection.count -ne $serviceResource.FailureActionsCollection.count) { + Write-Verbose -Message ($script:localizedData.ServicePropertyDoesNotMatch -f 'FailureActionsCollection.count', $Name, $FailureActionsCollection.count, $serviceResource.FailureActionsCollection.count) + return $false + } + + foreach ($actionIndex in (0..($FailureActionsCollection.count - 1))) { + $parameterAction = $FailureActionsCollection[$actionIndex] + $serviceResourceAction = $serviceResource.FailureActionsCollection[$actionIndex] + + if($parameterAction.type -ne $serviceResourceAction.type) { + Write-Verbose -Message ($script:localizedData.ServicePropertyDoesNotMatch -f "FailureActionsCollection Action $actionIndex type", $Name, $parameterAction.type, $serviceResourceAction.type) + $inSync = $false + } + + if($parameterAction.delaySeconds -ne $serviceResourceAction.delaySeconds) { + Write-Verbose -Message ($script:localizedData.ServicePropertyDoesNotMatch -f "FailureActionsCollection Action $actionIndex delaySeconds", $Name, $parameterAction.delaySeconds, $serviceResourceAction.delaySeconds) + $inSync = $false + } + } + + if(-not $inSync) { + return $inSync + } + } + + # Check for service actions on non crash failures + if($PSBoundParameters.ContainsKey('FailureActionsOnNonCrashFailures') -and $FailureActionsOnNonCrashFailures -ne $serviceResource.failureActionsOnNonCrashFailures) + { + Write-Verbose -Message ($script:localizedData.ServicePropertyDoesNotMatch -f 'FailureActionsOnNonCrashFailures', $Name, $FailureActionsOnNonCrashFailures, $serviceResource.failureActionsOnNonCrashFailures) + return $false + } } return $true @@ -1573,6 +1747,32 @@ function Set-ServiceStartupType .PARAMETER StartupType The startup type the service should have. + .PARAMETER ResetPeriodSeconds + The time after which to reset the failure count to zero if there are no failures, in seconds. + Specify INFINITE to indicate that this value should never be reset. + + .PARAMETER RebootMessage + The message to be broadcast to server users before rebooting in response to the SC_ACTION_REBOOT service controller action. + If this value is NULL, the reboot message is unchanged. If the value is an empty string (""), the reboot message is deleted + and no message is broadcast. + + .PARAMETER FailureCommand + The command line of the process for the CreateProcess function to execute in response to the SC_ACTION_RUN_COMMAND service controller action. + This process runs under the same account as the service. If this value is NULL, the command is unchanged. + If the value is an empty string (""), the command is deleted and no program is run when the service fails. + + .PARAMETER FailureActionsCollection + An array of hash tables representing the failure actions to take. Each hash table should have + two keys: type and delaySeconds. The value for the type key should be one of RESTART, RUN_COMMAND, REBOOT, or NONE. + The value for the delaySeconds key is an integer value of seconds to wait before applying the requested action. + + .PARAMETER FailureActionsOnNonCrashFailures + By default, failure actions are only queued if the service terminates without reporting + a status of SERVICE_STOPPED. Setting this to true will queue actions in that case, as well + as if the SERVICE_STOPPED state is reported but the exit code is non-zero. This property + correspones to the 'Enable Actions For Stops With Errors' check box in the 'Recovery' tab + of the service properties GUI. + .NOTES SupportsShouldProcess is enabled because Invoke-CimMethod calls ShouldProcess. Here are the paths through which Set-ServiceProperty calls Invoke-CimMethod: @@ -1624,6 +1824,26 @@ function Set-ServiceProperty [AllowEmptyCollection()] $Dependencies, + [Parameter()] + [System.UInt32] + $ResetPeriodSeconds, + + [Parameter()] + [System.String] + $RebootMessage, + + [Parameter()] + [System.String] + $FailureCommand, + + [Parameter()] + [System.Object[]] + $FailureActionsCollection, + + [Parameter()] + [System.Boolean] + $FailureActionsOnNonCrashFailures, + [Parameter()] [ValidateNotNull()] [System.Management.Automation.PSCredential] @@ -1683,6 +1903,34 @@ function Set-ServiceProperty Set-ServiceAccountProperty -ServiceName $ServiceName @setServiceAccountPropertyParameters } + # Update service failure actions properties if needed + $setServiceFailureActionsPropertyParameters = @{} + + if ($PSBoundParameters.ContainsKey('ResetPeriodSeconds')) + { + $setServiceFailureActionsPropertyParameters['ResetPeriodSeconds'] = $ResetPeriodSeconds + } + + if ($PSBoundParameters.ContainsKey('RebootMessage')) + { + $setServiceFailureActionsPropertyParameters['RebootMessage'] = $RebootMessage + } + + if ($PSBoundParameters.ContainsKey('FailureCommand')) + { + $setServiceFailureActionsPropertyParameters['FailureCommand'] = $FailureCommand + } + + if ($PSBoundParameters.ContainsKey('FailureActionsCollection')) + { + $setServiceFailureActionsPropertyParameters['FailureActionsCollection'] = $FailureActionsCollection + } + + if($setServiceFailureActionsPropertyParameters.count -gt 0) + { + Set-ServiceFailureActionProperty -ServiceName $ServiceName @setServiceFailureActionsPropertyParameters + } + # Update startup type if ($PSBoundParameters.ContainsKey('StartupType')) { @@ -1868,3 +2116,344 @@ function Stop-ServiceWithTimeout $waitTimeSpan = New-Object -TypeName 'TimeSpan' -ArgumentList (0, 0, 0, 0, $TerminateTimeout) Wait-ServiceStateWithTimeout -ServiceName $ServiceName -State 'Stopped' -WaitTimeSpan $waitTimeSpan } + +<# + .SYNOPSIS + Get the Failure Actions properties for a service from the registry + .DESCRIPTION + For the named service, read the registry and find its Failure Actions settings. + + Most of the data this function needs to retrieve is from the FailureActions registry key. + This key is a binary field that encodes the contents of a SERVICE_FAILURE_ACTIONSA C++ struct. + The struct and how to read its contents are documented at the link below. + https://docs.microsoft.com/en-us/windows/win32/api/winsvc/ns-winsvc-service_failure_actionsa + + It will also read the 'FailureCommand' and 'RebootMessage' property to get the string values for + those settings, and the 'FailureActionsOnNonCrashFailures' property to get the + flag value that stores the current state of the 'Enable Actions For Stops With Errors' checkbox. + .EXAMPLE + PS C:\> $serviceFailureActions = Get-ServiceFailureActions -Service $service + Reads the failure actions from the binary data in the registry + .PARAMETER Service + The name of the service to retrieve properties for. +#> +function Get-ServiceFailureActions { + [CmdletBinding()] + param ( + [Parameter(Mandatory)] + [String] + $Service + ) + process { + $registryData = Get-Item -Path HKLM:\SYSTEM\CurrentControlSet\Services\$service + + $failureActions = [PSCustomObject]@{ + resetPeriodSeconds = $null + hasRebootMessage = $null + hasFailureCommand = $null + failureActionCount = $null + failureCommand = $null + rebootMessage = $null + actionsCollection = $null + $FailureActionsOnNonCrashFailures = $false + } + + if($registryData.GetvalueNames() -match 'FailureCommand') { + $failureActions.failureCommand = $registryData.GetValue('FailureCommand') + } + + if($registryData.GetValueNames() -match 'RebootMessage') { + $failureActions.rebootMessage = $registryData.GetValue('RebootMessage') + } + + if($registryData.GetvalueNames() -match 'FailureActionsOnNonCrashFailures') { + $failureActions.FailureActionsOnNonCrashFailures = [System.Boolean]$registryData.GetValue('FailureActionsOnNonCrashFailures') + } + + if($registryData.GetValueNames() -match 'FailureActions') + { + $failureActionsBinaryData = $registryData.GetValue('FailureActions') + + # The first four bytes represent the Reset Period. + $failureActions.resetPeriodSeconds = Get-FailureActionsProperty -PropertyName ResetPeriodSeconds -Bytes $failureActionsBinaryData + + # Next four bytes indicate the presence of a reboot message in case one of the chosen failure actions is + # SC_ACTION_REBOOT. The actual value of the message is stored in the 'RebootMessage' property + $failureActions.hasRebootMessage = Get-FailureActionsProperty -PropertyName HasRebootMsg -Bytes $failureActionsBinaryData + + # The next four bytes indicate whether a failure action run command exists. This command + # would be run in the case one of the failure actions chosen is SC_ACTION_RUN_COMMAND + # If this value is true then the actual command string is stored in the 'FailureCommand' property. + $failureActions.hasFailureCommand = Get-FailureActionsProperty -PropertyName HasFailureCommand -Bytes $failureActionsBinaryData + + # These four bytes give the count of how many reboot failure actions have been defined. + $failureActions.failureActionCount = Get-FailureActionsProperty -PropertyName FailureActionCount -Bytes $failureActionsBinaryData + + if($failureActions.failureActionCount -gt 0) + { + $failureActions.ActionsCollection = Get-FailureActionCollection -Bytes $failureActionsBinaryData -ActionsCount $failureActions.failureActionCount + } + } + + $failureActions + } +} + +<# + .SYNOPSIS + Translate the binary data from the registry into a property value + + .DESCRIPTION + The binary data in the 'FailureActions' registry property encodes a _SERVICE_FAILURE_ACTIONSA struct. + This function translates human readable property names into the byte range encoded by the binary data. + Those bytes are then read and cast to the integer values they store. + + .PARAMETER PropertyName + The name of the property to retrieve from the struct. + + .PARAMETER Bytes + The raw binary data read out from the registry. +#> +function Get-FailureActionsProperty +{ + [CmdletBinding()] + param ( + [Parameter(Mandatory)] + [ValidateSet('ResetPeriodSeconds','HasRebootMsg','HasFailureCommand','FailureActionCount')] + [System.String] + $PropertyName, + [Parameter(Mandatory)] + [System.Byte[]] + $Bytes + ) + + process { + $byteRange = switch ($PropertyName) { + ResetPeriodSeconds { 0..3 } + HasRebootMsg { 4..7 } + HasFailureCommand { 8..11 } + FailureActionCount { 12..15 } + Default {} + } + + [System.BitConverter]::ToInt32($Bytes[$byteRange],0) + } +} + +<# + .SYNOPSIS + Retrieve and decode the array of service failure actions + + .DESCRIPTION + The array of SC_ACTION structures that define each failure action is stored + in the same binary blob property as the struct that defines the rest of the + service failure actions settings. This function takes the binary data and a + count of how many actions to expect, and does the offset math to read each + action and decode it, and return the actions as an array of custom objects. + + .PARAMETER Bytes + The binary data that stores the actions array. + + .PARAMETER ActionsCount + The number of actions encoded in the data. +#> +function Get-FailureActionCollection +{ + [CmdletBinding()] + param ( + [Parameter(Mandatory)] + [System.Byte[]] + $Bytes, + [Parameter(Mandatory)] + [System.UInt32] + $ActionsCount + ) + + process { + $actionsCollection = New-Object System.Collections.Generic.List[PSObject] + + foreach($action in 0..($ActionsCount - 1)) { + + # The structure of the _SERVICE_FAILURE_ACTIONS struct encoded in this registry key + # dictates that the array of _SC_ACTION structs will always start at byte 20. + # The 8 is because each _SC_ACTION is a 8 byte struct. + $actionTypeByteRange = (20 + (8 * $action))..(20 + (8 * $action) + 3) + $actionDelayByteRange = (20 + (8 * $action) + 4)..(20 + (8 * $action) + 7) + + $currentAction = [PSCustomObject]@{ + type = [ACTION_TYPE]([System.BitConverter]::ToInt32($Bytes[$actionTypeByteRange],0)) + delaySeconds = [System.BitConverter]::ToInt32($Bytes[$actionDelayByteRange],0) + } + + $actionsCollection.Add($currentAction) | Out-Null + } + + $actionsCollection + } +} + +<# + .SYNOPSIS + Set a failure actions settings registry value. + + .DESCRIPTION + Set all of the settings that are stored in the 'FailureActions' binary registry property + as well as some of the settings stored in other properties like 'RebootMessage', + 'FailureCommand', and 'FailureActionsOnNonCrashFailures'. + + .PARAMETER ServiceName + The name of the service to set + + .PARAMETER ResetPeriodSeconds + The time after which to reset the failure count to zero if there are no failures, in seconds. + Specify INFINITE to indicate that this value should never be reset. + + .PARAMETER RebootMessage + The message to be broadcast to server users before rebooting in response to the SC_ACTION_REBOOT service controller action. + If this value is NULL, the reboot message is unchanged. If the value is an empty string (""), the reboot message is deleted + and no message is broadcast. + + .PARAMETER FailureCommand + The command line of the process for the CreateProcess function to execute in response to the SC_ACTION_RUN_COMMAND service controller action. + This process runs under the same account as the service. If this value is NULL, the command is unchanged. + If the value is an empty string (""), the command is deleted and no program is run when the service fails. + + .PARAMETER FailureActionsCollection + An array of hash tables representing the failure actions to take. Each hash table should have + two keys: type and delaySeconds. The value for the type key should be one of RESTART, RUN_COMMAND, REBOOT, or NONE. + The value for the delaySeconds key is an integer value of seconds to wait before applying the requested action. + + .PARAMETER FailureActionsOnNonCrashFailures + By default, failure actions are only queued if the service terminates without reporting + a status of SERVICE_STOPPED. Setting this to true will queue actions in that case, as well + as if the SERVICE_STOPPED state is reported but the exit code is non-zero. This property + correspones to the 'Enable Actions For Stops With Errors' check box in the 'Recovery' tab + of the service properties GUI. +#> +function Set-ServiceFailureActionProperty { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [System.String] + $ServiceName, + + [Parameter()] + [System.UInt32] + $ResetPeriodSeconds, + + [Parameter()] + [System.String] + $RebootMessage, + + [Parameter()] + [System.String] + $FailureCommand, + + [Parameter()] + [System.Object[]] + $FailureActionsCollection, + + [Parameter()] + [System.Boolean] + $FailureActionsOnNonCrashFailures + ) + + process { + $failureActions = Get-ServiceFailureActions -Service $ServiceName + + <# + This integer array will store the cumulative value that should be stored + in the 'FailureActions' registry property. This property must always be set as + one large byte array, so the way this works is as follows: + 1. Get the settings as they exist now. + 2. Check each of the available values to see if something has changed. + If it has, modify the value in the $failureActions object. + 3. Once that check as been run add the value from the $failureActions object to the + $integerData array. + 4. Once all values have been checked, the integer array can be turned into a + byte array of hex values and then that is written to the registry. + #> + $integerData = New-Object System.Collections.Generic.List[int32] + + # Check to see if we need to modify the existing value. + if ($PSBoundParameters.ContainsKey('ResetPeriodSeconds')) + { + $failureActions.resetPeriodSeconds = $ResetPeriodSeconds + } + + # Add the value to the integer array. + $integerData.add($failureActions.resetPeriodSeconds) | Out-Null + + if ($PSBoundParameters.ContainsKey('RebootMessage')) + { + # This setting is a combination a flag in the _SERVICE_FAILURE_ACTIONSA struct, + # and the actual string value for the message stored in the 'RebootMessage' registry property. + # If hasRebootMessage is already true, then we know the key exists and we can just set it. + if ($failureActions.hasRebootMessage) + { + Set-ItemProperty -Path HKLM:\SYSTEM\CurrentControlSet\Services\$serviceName -Name 'RebootMessage' -Value $RebootMessage | Out-Null + } + else + { + # If hasRebootMessage was false, we both have to create the key with the string value, and set the flag to true in the struct. + New-ItemProperty -Path HKLM:\SYSTEM\CurrentControlSet\Services\$serviceName -Name 'RebootMessage' -Value $RebootMessage | Out-Null + $failureActions.hasRebootMessage = 1 + } + } + + # Add the current value of the flag to the array. + $integerData.add($failureActions.hasRebootMessage) | Out-Null + + # This is the same as the RebootMessage property above. It's a combination of a flag in the struct, and an external property that stores the value. + if ($PSBoundParameters.ContainsKey('FailureCommand')) + { + if ($failureActions.hasFailureCommand) + { + Set-ItemProperty -Path HKLM:\SYSTEM\CurrentControlSet\Services\$serviceName -Name 'FailureCommand' -Value $FailureCommand | Out-Null + } + else + { + New-ItemProperty -Path HKLM:\SYSTEM\CurrentControlSet\Services\$serviceName -Name 'FailureCommand' -Value $FailureCommand | Out-Null + $failureActions.hasFailureCommand = 1 + } + } + + # Add the current value of the flag to the array. + $integerData.Add($failureActions.hasFailureCommand) | Out-Null + + if ($PSBoundParameters.ContainsKey('FailureActionsCollection')) + { + $failureActions.ActionsCollection = $FailureActionsCollection + } + + # Even if we didn't change anything about the failure actions, we still have to build the array + $integerData.add($failureActions.ActionsCollection.count) + + # This element of the array is a pointer to the array of actions. It's always going to be 0x14 or 20 in decimal. + $integerData.Add(20) + + # Iterate over the actions and their properties to add the integers that they encode to the array. + foreach ($action in $failureActions.ActionsCollection) { + $integerData.add($action.type) | Out-Null + $integerData.add($action.delaySeconds) | Out-Null + } + + # Now that we finally have all of the data we need, we can convert it to a byte array. + $bytes = $integerData | Format-Hex -raw | Select-Object -ExpandProperty bytes + + Set-ItemProperty -Path HKLM:\SYSTEM\CurrentControlSet\Services\$serviceName -Name 'FailureActions' -Value $bytes | Out-Null + + # This flag is stored in it's own registry property, so we don't have to add a flag value to the struct, but we do have to create the key + # if it doesn't already exist. + if ($PSBoundParameters.ContainsKey('FailureActionsOnNonCrashFailures')) + { + if ($failureActions.FailureActionsOnNonCrashFailures) { + Set-ItemProperty -Path HKLM:\SYSTEM\CurrentControlSet\Services\$serviceName -Name 'FailureActionsOnNonCrashFailures' -Value $FailureActionsOnNonCrashFailures | Out-Null + } + else + { + New-ItemProperty -Path HKLM:\SYSTEM\CurrentControlSet\Services\$serviceName -Name 'FailureActionsOnNonCrashFailures' -Value $FailureActionsOnNonCrashFailures | Out-Null + } + } + } +} diff --git a/source/DSCResources/DSC_xServiceResource/en-US/DSC_xServiceResource.strings.psd1 b/source/DSCResources/DSC_xServiceResource/en-US/DSC_xServiceResource.strings.psd1 index 6bfb21e89..c3ca06e89 100644 --- a/source/DSCResources/DSC_xServiceResource/en-US/DSC_xServiceResource.strings.psd1 +++ b/source/DSCResources/DSC_xServiceResource/en-US/DSC_xServiceResource.strings.psd1 @@ -37,4 +37,5 @@ ConvertFrom-StringData @' CannotGetAccountAccessErrorMessage = Failed to get user policy rights. CannotSetAccountAccessErrorMessage = Failed to set user policy rights. CorruptDependency = Service '{0}' has a corrupt dependency. For more information, inspect the registry value at HKLM:\\SYSTEM\\CurrentControlSet\\Services\\{0}\\DependOnService. + FailureActionsMustBeSpecifiedInOrder = Failure actions must be specified in order from 1 to 3. '@ From 84fd6af38802e5d36e2ba55ec0587fa24859a4c3 Mon Sep 17 00:00:00 2001 From: Bill Hurt Date: Mon, 6 Apr 2020 21:16:48 -0700 Subject: [PATCH 02/13] Existing unit tests are now green. Still need to start adding new unit tests. --- .../DSC_xServiceResource.psm1 | 81 +++---- tests/Unit/DSC_xServiceResource.Tests.ps1 | 198 ++++++++++++++---- 2 files changed, 199 insertions(+), 80 deletions(-) diff --git a/source/DSCResources/DSC_xServiceResource/DSC_xServiceResource.psm1 b/source/DSCResources/DSC_xServiceResource/DSC_xServiceResource.psm1 index 000fa333b..a4beb4c9b 100644 --- a/source/DSCResources/DSC_xServiceResource/DSC_xServiceResource.psm1 +++ b/source/DSCResources/DSC_xServiceResource/DSC_xServiceResource.psm1 @@ -2145,57 +2145,58 @@ function Get-ServiceFailureActions { $Service ) process { - $registryData = Get-Item -Path HKLM:\SYSTEM\CurrentControlSet\Services\$service - - $failureActions = [PSCustomObject]@{ - resetPeriodSeconds = $null - hasRebootMessage = $null - hasFailureCommand = $null - failureActionCount = $null - failureCommand = $null - rebootMessage = $null - actionsCollection = $null - $FailureActionsOnNonCrashFailures = $false - } + if($registryData = Get-Item -Path HKLM:\SYSTEM\CurrentControlSet\Services\$service -ErrorAction SilentlyContinue) + { + $failureActions = [PSCustomObject]@{ + resetPeriodSeconds = $null + hasRebootMessage = $null + hasFailureCommand = $null + failureActionCount = $null + failureCommand = $null + rebootMessage = $null + actionsCollection = $null + FailureActionsOnNonCrashFailures = $false + } - if($registryData.GetvalueNames() -match 'FailureCommand') { - $failureActions.failureCommand = $registryData.GetValue('FailureCommand') - } + if($registryData.GetvalueNames() -match 'FailureCommand') { + $failureActions.failureCommand = $registryData.GetValue('FailureCommand') + } - if($registryData.GetValueNames() -match 'RebootMessage') { - $failureActions.rebootMessage = $registryData.GetValue('RebootMessage') - } + if($registryData.GetValueNames() -match 'RebootMessage') { + $failureActions.rebootMessage = $registryData.GetValue('RebootMessage') + } - if($registryData.GetvalueNames() -match 'FailureActionsOnNonCrashFailures') { - $failureActions.FailureActionsOnNonCrashFailures = [System.Boolean]$registryData.GetValue('FailureActionsOnNonCrashFailures') - } + if($registryData.GetvalueNames() -match 'FailureActionsOnNonCrashFailures') { + $failureActions.FailureActionsOnNonCrashFailures = [System.Boolean]$registryData.GetValue('FailureActionsOnNonCrashFailures') + } - if($registryData.GetValueNames() -match 'FailureActions') - { - $failureActionsBinaryData = $registryData.GetValue('FailureActions') + if($registryData.GetValueNames() -match 'FailureActions') + { + $failureActionsBinaryData = $registryData.GetValue('FailureActions') - # The first four bytes represent the Reset Period. - $failureActions.resetPeriodSeconds = Get-FailureActionsProperty -PropertyName ResetPeriodSeconds -Bytes $failureActionsBinaryData + # The first four bytes represent the Reset Period. + $failureActions.resetPeriodSeconds = Get-FailureActionsProperty -PropertyName ResetPeriodSeconds -Bytes $failureActionsBinaryData - # Next four bytes indicate the presence of a reboot message in case one of the chosen failure actions is - # SC_ACTION_REBOOT. The actual value of the message is stored in the 'RebootMessage' property - $failureActions.hasRebootMessage = Get-FailureActionsProperty -PropertyName HasRebootMsg -Bytes $failureActionsBinaryData + # Next four bytes indicate the presence of a reboot message in case one of the chosen failure actions is + # SC_ACTION_REBOOT. The actual value of the message is stored in the 'RebootMessage' property + $failureActions.hasRebootMessage = Get-FailureActionsProperty -PropertyName HasRebootMsg -Bytes $failureActionsBinaryData - # The next four bytes indicate whether a failure action run command exists. This command - # would be run in the case one of the failure actions chosen is SC_ACTION_RUN_COMMAND - # If this value is true then the actual command string is stored in the 'FailureCommand' property. - $failureActions.hasFailureCommand = Get-FailureActionsProperty -PropertyName HasFailureCommand -Bytes $failureActionsBinaryData + # The next four bytes indicate whether a failure action run command exists. This command + # would be run in the case one of the failure actions chosen is SC_ACTION_RUN_COMMAND + # If this value is true then the actual command string is stored in the 'FailureCommand' property. + $failureActions.hasFailureCommand = Get-FailureActionsProperty -PropertyName HasFailureCommand -Bytes $failureActionsBinaryData - # These four bytes give the count of how many reboot failure actions have been defined. - $failureActions.failureActionCount = Get-FailureActionsProperty -PropertyName FailureActionCount -Bytes $failureActionsBinaryData + # These four bytes give the count of how many reboot failure actions have been defined. + $failureActions.failureActionCount = Get-FailureActionsProperty -PropertyName FailureActionCount -Bytes $failureActionsBinaryData - if($failureActions.failureActionCount -gt 0) - { - $failureActions.ActionsCollection = Get-FailureActionCollection -Bytes $failureActionsBinaryData -ActionsCount $failureActions.failureActionCount + if($failureActions.failureActionCount -gt 0) + { + $failureActions.ActionsCollection = Get-FailureActionCollection -Bytes $failureActionsBinaryData -ActionsCount $failureActions.failureActionCount + } } - } - $failureActions + $failureActions + } } } diff --git a/tests/Unit/DSC_xServiceResource.Tests.ps1 b/tests/Unit/DSC_xServiceResource.Tests.ps1 index da6a5642d..1569a6d56 100644 --- a/tests/Unit/DSC_xServiceResource.Tests.ps1 +++ b/tests/Unit/DSC_xServiceResource.Tests.ps1 @@ -217,6 +217,44 @@ try $getTargetResourceResult.Dependencies | Should -Be $ExpectedValues.Dependencies } } + + if ($ExpectedValues.ContainsKey('ResetPeriodSeconds')) + { + it 'Should return the reset period in seconds' { + $getTargetResourceResult.resetPeriodSeconds | Should -Be $ExpectedValues.resetPeriodSeconds + } + } + + if ($ExpectedValues.ContainsKey('FailureCommand')) + { + it 'Should return the failure command' { + $getTargetResourceResult.failureCommand | Should -Be $ExpectedValues.FailureCommand + } + } + + if ($ExpectedValues.ContainsKey('RebootMessage')) + { + it 'Should return the reboot message' { + $getTargetResourceResult.rebootMessage | Should -Be $ExpectedValues.RebootMessage + } + } + + if ($ExpectedValues.ContainsKey('FailureActionsCollection')) + { + foreach ($index in 0..($ExpectedValues.failureActionsCollection.count - 1)) { + it "Should return the correct failure action for index $index" { + $getTargetResourceResult.failureActionsCollection[$index].type | Should -Be $ExpectedValues.FailureActionsCollection[$index].type + $getTargetResourceResult.failureActionsCollection[$index].delaySeconds | Should -Be $ExpectedValues.FailureActionsCollection[$index].delaySeconds + } + } + } + + if ($ExpectedValues.ContainsKey('FailureActionsOnNonCrashFailures')) + { + it 'Should return the failure actions on non crash failures flag' { + $getTargetResourceResult.failureActionsOnNonCrashFailures | Should -Be $ExpectedValues.failureActionsOnNonCrashFailures + } + } } Mock -CommandName 'Get-Service' -MockWith { } @@ -265,23 +303,43 @@ try DesktopInteract = $true } + $testServiceFailureActions = @{ + resetPeriodSeconds = 86400 + hasRebootMessage = 0 + hasFailureCommand = 0 + failureActionCount = 1 + failureCommand = $null + rebootMessage = $null + actionsCollection = @([PSCustomObject]@{ + type = 'RESTART' + delaySeconds = 30000 + }) + failureActionsOnNonCrashFailures = $true + } + Mock -CommandName 'Get-Service' -MockWith { return $testService } Mock -CommandName 'Get-ServiceCimInstance' -MockWith { return $testServiceCimInstance } Mock -CommandName 'ConvertTo-StartupTypeString' -MockWith { return $convertToStartupTypeStringResult } + Mock -CommandName 'Get-ServiceFailureActions' -MockWith { return $testServiceFailureActions } Test-GetTargetResourceDoesntThrow -GetTargetResourceParameters $getTargetResourceParameters -TestServiceCimInstance $testServiceCimInstance $expectedValues = @{ - Name = $getTargetResourceParameters.Name - Ensure = 'Present' - Path = $testServiceCimInstance.PathName - StartupType = $convertToStartupTypeStringResult - BuiltInAccount = $testServiceCimInstance.StartName - State = $testService.Status - DisplayName = $testService.DisplayName - Description = $testServiceCimInstance.Description - DesktopInteract = $testServiceCimInstance.DesktopInteract - Dependencies = [System.Object[]] $testService.ServicesDependedOn.Name + Name = $getTargetResourceParameters.Name + Ensure = 'Present' + Path = $testServiceCimInstance.PathName + StartupType = $convertToStartupTypeStringResult + BuiltInAccount = $testServiceCimInstance.StartName + State = $testService.Status + DisplayName = $testService.DisplayName + Description = $testServiceCimInstance.Description + DesktopInteract = $testServiceCimInstance.DesktopInteract + Dependencies = [System.Object[]] $testService.ServicesDependedOn.Name + ResetPeriodSeconds = $testServiceFailureActions.resetPeriodSeconds + FailureCommand = $testServiceFailureActions.failureCommand + RebootMessage = $testServiceFailureActions.rebootMessage + FailureActionsCollection = $testServiceFailureActions.actionsCollection + failureActionsOnNonCrashFailures = $testServiceFailureActions.failureActionsOnNonCrashFailures } Test-GetTargetResourceResult -GetTargetResourceParameters $getTargetResourceParameters -ExpectedValues $expectedValues @@ -307,23 +365,43 @@ try DesktopInteract = $false } + $testServiceFailureActions = @{ + resetPeriodSeconds = 86400 + hasRebootMessage = 0 + hasFailureCommand = 0 + failureActionCount = 1 + failureCommand = $null + rebootMessage = $null + actionsCollection = @([PSCustomObject]@{ + type = 'RESTART' + delaySeconds = 30000 + }) + failureActionsOnNonCrashFailures = $true + } + Mock -CommandName 'Get-Service' -MockWith { return $testService } Mock -CommandName 'Get-ServiceCimInstance' -MockWith { return $testServiceCimInstance } Mock -CommandName 'ConvertTo-StartupTypeString' -MockWith { return $convertToStartupTypeStringResult } + Mock -CommandName 'Get-ServiceFailureActions' -MockWith { return $testServiceFailureActions } Test-GetTargetResourceDoesntThrow -GetTargetResourceParameters $getTargetResourceParameters -TestServiceCimInstance $testServiceCimInstance $expectedValues = @{ - Name = $getTargetResourceParameters.Name - Ensure = 'Present' - Path = $testServiceCimInstance.PathName - StartupType = $convertToStartupTypeStringResult - BuiltInAccount = $expectedBuiltInAccountValue - State = $testService.Status - DisplayName = $testService.DisplayName - Description = $testServiceCimInstance.Description - DesktopInteract = $testServiceCimInstance.DesktopInteract - Dependencies = $null + Name = $getTargetResourceParameters.Name + Ensure = 'Present' + Path = $testServiceCimInstance.PathName + StartupType = $convertToStartupTypeStringResult + BuiltInAccount = $expectedBuiltInAccountValue + State = $testService.Status + DisplayName = $testService.DisplayName + Description = $testServiceCimInstance.Description + DesktopInteract = $testServiceCimInstance.DesktopInteract + Dependencies = $null + ResetPeriodSeconds = $testServiceFailureActions.resetPeriodSeconds + FailureCommand = $testServiceFailureActions.failureCommand + RebootMessage = $testServiceFailureActions.rebootMessage + FailureActionsCollection = $testServiceFailureActions.actionsCollection + failureActionsOnNonCrashFailures = $testServiceFailureActions.failureActionsOnNonCrashFailures } Test-GetTargetResourceResult -GetTargetResourceParameters $getTargetResourceParameters -ExpectedValues $expectedValues @@ -356,23 +434,43 @@ try DesktopInteract = $false } + $testServiceFailureActions = @{ + resetPeriodSeconds = 86400 + hasRebootMessage = 0 + hasFailureCommand = 0 + failureActionCount = 1 + failureCommand = $null + rebootMessage = $null + actionsCollection = @([PSCustomObject]@{ + type = 'RESTART' + delaySeconds = 30000 + }) + failureActionsOnNonCrashFailures = $true + } + Mock -CommandName 'Get-Service' -MockWith { return $testService } Mock -CommandName 'Get-ServiceCimInstance' -MockWith { return $testServiceCimInstance } Mock -CommandName 'ConvertTo-StartupTypeString' -MockWith { return $convertToStartupTypeStringResult } + Mock -CommandName 'Get-ServiceFailureActions' -MockWith { return $testServiceFailureActions } Test-GetTargetResourceDoesntThrow -GetTargetResourceParameters $getTargetResourceParameters -TestServiceCimInstance $testServiceCimInstance $expectedValues = @{ - Name = $getTargetResourceParameters.Name - Ensure = 'Present' - Path = $testServiceCimInstance.PathName - StartupType = $convertToStartupTypeStringResult - BuiltInAccount = $expectedBuiltInAccountValue - State = $testService.Status - DisplayName = $null - Description = $null - DesktopInteract = $testServiceCimInstance.DesktopInteract - Dependencies = [System.Object[]] $testService.ServicesDependedOn.Name + Name = $getTargetResourceParameters.Name + Ensure = 'Present' + Path = $testServiceCimInstance.PathName + StartupType = $convertToStartupTypeStringResult + BuiltInAccount = $expectedBuiltInAccountValue + State = $testService.Status + DisplayName = $null + Description = $null + DesktopInteract = $testServiceCimInstance.DesktopInteract + Dependencies = [System.Object[]] $testService.ServicesDependedOn.Name + ResetPeriodSeconds = $testServiceFailureActions.resetPeriodSeconds + FailureCommand = $testServiceFailureActions.failureCommand + RebootMessage = $testServiceFailureActions.rebootMessage + FailureActionsCollection = $testServiceFailureActions.actionsCollection + failureActionsOnNonCrashFailures = $testServiceFailureActions.failureActionsOnNonCrashFailures } Test-GetTargetResourceResult -GetTargetResourceParameters $getTargetResourceParameters -ExpectedValues $expectedValues @@ -442,10 +540,25 @@ try DesktopInteract = $false } + $testServiceFailureActions = @{ + resetPeriodSeconds = 86400 + hasRebootMessage = 0 + hasFailureCommand = 0 + failureActionCount = 1 + failureCommand = $null + rebootMessage = $null + actionsCollection = @([PSCustomObject]@{ + type = 'RESTART' + delaySeconds = 30000 + }) + failureActionsOnNonCrashFailures = $true + } + Mock -CommandName 'Get-Service' -MockWith { return $testService } Mock -CommandName 'Get-ServiceCimInstance' -MockWith { return $testServiceCimInstance } Mock -CommandName 'ConvertTo-StartupTypeString' -MockWith { return $convertToStartupTypeStringResult } Mock -CommandName 'Write-Warning' + Mock -CommandName 'Get-ServiceFailureActions' -MockWith { return $testServiceFailureActions } Test-GetTargetResourceDoesntThrow -GetTargetResourceParameters $getTargetResourceParameters -TestServiceCimInstance $testServiceCimInstance @@ -454,16 +567,21 @@ try } $expectedValues = @{ - Name = $getTargetResourceParameters.Name - Ensure = 'Present' - Path = $testServiceCimInstance.PathName - StartupType = $convertToStartupTypeStringResult - BuiltInAccount = $testServiceCimInstance.StartName - State = $testService.Status - DisplayName = $testService.DisplayName - Description = $testServiceCimInstance.Description - DesktopInteract = $testServiceCimInstance.DesktopInteract - Dependencies = [System.Object[]] ($testService.ServicesDependedOn | Where-Object -FilterScript {![System.String]::IsNullOrEmpty($_.Name)}).Name + Name = $getTargetResourceParameters.Name + Ensure = 'Present' + Path = $testServiceCimInstance.PathName + StartupType = $convertToStartupTypeStringResult + BuiltInAccount = $testServiceCimInstance.StartName + State = $testService.Status + DisplayName = $testService.DisplayName + Description = $testServiceCimInstance.Description + DesktopInteract = $testServiceCimInstance.DesktopInteract + Dependencies = [System.Object[]] ($testService.ServicesDependedOn | Where-Object -FilterScript {![System.String]::IsNullOrEmpty($_.Name)}).Name + ResetPeriodSeconds = $testServiceFailureActions.resetPeriodSeconds + FailureCommand = $testServiceFailureActions.failureCommand + RebootMessage = $testServiceFailureActions.rebootMessage + FailureActionsCollection = $testServiceFailureActions.actionsCollection + failureActionsOnNonCrashFailures = $testServiceFailureActions.failureActionsOnNonCrashFailures } Test-GetTargetResourceResult -GetTargetResourceParameters $getTargetResourceParameters -ExpectedValues $expectedValues From 4649c2738442061215b0c77ef5e2cc3b998bebef Mon Sep 17 00:00:00 2001 From: Bill Hurt Date: Fri, 17 Apr 2020 07:58:15 -0700 Subject: [PATCH 03/13] Do a better job of mocking the registry --- tests/Unit/DSC_xServiceResource.Tests.ps1 | 86 +++++++++++++++++++---- 1 file changed, 72 insertions(+), 14 deletions(-) diff --git a/tests/Unit/DSC_xServiceResource.Tests.ps1 b/tests/Unit/DSC_xServiceResource.Tests.ps1 index 1569a6d56..f55f5adb1 100644 --- a/tests/Unit/DSC_xServiceResource.Tests.ps1 +++ b/tests/Unit/DSC_xServiceResource.Tests.ps1 @@ -257,6 +257,76 @@ try } } + function Get-MockFailureActionsData + { + [CmdletBinding()] + param ( + [Parameter()] + [System.String] + $DataSetName + ) + + switch ($DataSetName) { + '1ActionNoCommandOrMessage' { + $data = @{ + resetPeriodSeconds = 86400 + hasRebootMessage = 0 + hasFailureCommand = 0 + failureActionCount = 1 + failureCommand = $null + rebootMessage = $null + actionsCollection = @([PSCustomObject]@{ + type = 'RESTART' + delaySeconds = 30000 + }) + failureActionsOnNonCrashFailures = $true + } + } + Default {} + } + + $keyData = New-Object 'System.Collections.Generic.List[int]' + + $keyData.add($data.resetPeriodSeconds) | Out-Null + $keyData.add($data.hasRebootMessage) | Out-Null + $keyData.add($data.hasFailureCommand) | Out-Null + $keyData.add($data.failureActionCount) | Out-Null + $keyData.add(20) | Out-Null + + foreach ($action in $data.ActionsCollection) { + $keyData.add([ACTION_TYPE]$action.type) | Out-Null + $keyData.add($action.delaySeconds) | Out-Null + } + + $key = @{ + FailureActions = $keyData | Format-Hex -raw | Select-Object -ExpandProperty bytes + } + + if ($data.failureActionsOnNonCrashFailures) + { + $key.failureActionsOnNonCrashFailures = 1 + } + + if ($data.hasRebootMessage) + { + $key.RebootMessage = 'Mocked Reboot Message' + } + + if ($data.hasFailureCommand) + { + $key.FailureCommand = 'C:\new\command.exe' + } + + $GetValueNames = {$this.keys} + $GetValue = {param($key) $this[$key]} + + Add-Member -InputObject $key -MemberType ScriptMethod -Name GetValueNames -Value $GetValueNames + Add-Member -InputObject $key -MemberType ScriptMethod -Name GetValue -Value $GetValue + + @([PSCustomObject]$data, $key) + } + + Mock -CommandName 'Get-Service' -MockWith { } Mock -CommandName 'Get-ServiceCimInstance' -MockWith { } Mock -CommandName 'ConvertTo-StartupTypeString' -MockWith { } @@ -303,24 +373,12 @@ try DesktopInteract = $true } - $testServiceFailureActions = @{ - resetPeriodSeconds = 86400 - hasRebootMessage = 0 - hasFailureCommand = 0 - failureActionCount = 1 - failureCommand = $null - rebootMessage = $null - actionsCollection = @([PSCustomObject]@{ - type = 'RESTART' - delaySeconds = 30000 - }) - failureActionsOnNonCrashFailures = $true - } + $testServiceFailureActions, $failureRegistryKey = Get-MockFailureActionsData -DataSetName '1ActionNoCommandOrMessage' Mock -CommandName 'Get-Service' -MockWith { return $testService } Mock -CommandName 'Get-ServiceCimInstance' -MockWith { return $testServiceCimInstance } Mock -CommandName 'ConvertTo-StartupTypeString' -MockWith { return $convertToStartupTypeStringResult } - Mock -CommandName 'Get-ServiceFailureActions' -MockWith { return $testServiceFailureActions } + Mock -CommandName 'Get-Item' -ParameterFilter { $Path -eq "HKLM:\SYSTEM\CurrentControlSet\Services\$($testService.name)" } -MockWith { return $failureRegistryKey} Test-GetTargetResourceDoesntThrow -GetTargetResourceParameters $getTargetResourceParameters -TestServiceCimInstance $testServiceCimInstance From 37be6ccb750e41f5e906e79abf3b70998d9d434a Mon Sep 17 00:00:00 2001 From: Bill Hurt Date: Fri, 17 Apr 2020 15:18:57 -0700 Subject: [PATCH 04/13] Further Unit Testing Improvements. --- .../DSC_xServiceResource.psm1 | 30 ++- .../en-US/DSC_xServiceResource.strings.psd1 | 2 +- tests/Unit/DSC_xServiceResource.Tests.ps1 | 177 +++++++++++++----- 3 files changed, 155 insertions(+), 54 deletions(-) diff --git a/source/DSCResources/DSC_xServiceResource/DSC_xServiceResource.psm1 b/source/DSCResources/DSC_xServiceResource/DSC_xServiceResource.psm1 index a4beb4c9b..7650039c5 100644 --- a/source/DSCResources/DSC_xServiceResource/DSC_xServiceResource.psm1 +++ b/source/DSCResources/DSC_xServiceResource/DSC_xServiceResource.psm1 @@ -628,12 +628,10 @@ function Test-TargetResource New-InvalidArgumentException -ArgumentName 'BuiltInAccount / Credential / GroupManagedServiceAccount' -Message $errorMessage } - if(($PSBoundParameters.ContainsKey('Failure3Action') -and (-not $PSBoundParameters.ContainsKey('Failure2Action'))) -or - ($PSBoundParameters.ContainsKey('Failure2Action') -and (-not $PSBoundParameters.ContainsKey('Failure1Action'))) - ) + if ($PSBoundParameters.ContainsKey('FailureCommand') -and (-not (Test-HasRestartFailureAction -Collection $FailureActionsCollection))) { - $errorMessage = $script:localizedData.FailureActionsMustBeSpecifiedInOrder - New-InvalidArgumentException -ArgumentName 'Failure2Action / Failure3Action' -Message $errorMessage + $errorMessage = $script:localizedData.MustSpecifyRestartFailureAction + New-InvalidArgumentException -ArgumentName 'FailureCommand' -Message $errorMessage } $serviceResource = Get-TargetResource -Name $Name @@ -2458,3 +2456,25 @@ function Set-ServiceFailureActionProperty { } } } + +function Test-HasRestartFailureAction + { + [CmdletBinding()] + param ( + [Parameter()] + [System.Object[]] + $Collection + ) + + process { + $hasRestartAction = $false + + foreach ($action in $collection) { + if ($action.type -eq 'RUN_COMMAND') { + $hasRestartAction = $true + } + } + + $hasRestartAction + } + } diff --git a/source/DSCResources/DSC_xServiceResource/en-US/DSC_xServiceResource.strings.psd1 b/source/DSCResources/DSC_xServiceResource/en-US/DSC_xServiceResource.strings.psd1 index c3ca06e89..377ff19da 100644 --- a/source/DSCResources/DSC_xServiceResource/en-US/DSC_xServiceResource.strings.psd1 +++ b/source/DSCResources/DSC_xServiceResource/en-US/DSC_xServiceResource.strings.psd1 @@ -37,5 +37,5 @@ ConvertFrom-StringData @' CannotGetAccountAccessErrorMessage = Failed to get user policy rights. CannotSetAccountAccessErrorMessage = Failed to set user policy rights. CorruptDependency = Service '{0}' has a corrupt dependency. For more information, inspect the registry value at HKLM:\\SYSTEM\\CurrentControlSet\\Services\\{0}\\DependOnService. - FailureActionsMustBeSpecifiedInOrder = Failure actions must be specified in order from 1 to 3. + MustSpecifyRestartFailureAction = A failure command can only be specified if one of the failure actions is 'RUN_COMMAND' '@ diff --git a/tests/Unit/DSC_xServiceResource.Tests.ps1 b/tests/Unit/DSC_xServiceResource.Tests.ps1 index f55f5adb1..9f1c2bf4a 100644 --- a/tests/Unit/DSC_xServiceResource.Tests.ps1 +++ b/tests/Unit/DSC_xServiceResource.Tests.ps1 @@ -275,13 +275,34 @@ try failureActionCount = 1 failureCommand = $null rebootMessage = $null - actionsCollection = @([PSCustomObject]@{ + actionsCollection = @(@{ type = 'RESTART' delaySeconds = 30000 }) failureActionsOnNonCrashFailures = $true } } + '3ActionsCommandAndMessage' { + $data = @{ + resetPeriodSeconds = 172800 + hasRebootMessage = 1 + hasFailureCommand = 1 + failureActionCount = 3 + failureCommand = 'C:\new\command.exe' + rebootMessage = 'Mocked Reboot Message' + actionsCollection = @(@{ + type = 'RESTART' + delaySeconds = 30000 + },@{ + type = 'RUN_COMMAND' + delaySeconds = 30000 + },@{ + type = 'REBOOT' + delaySeconds = 30000 + }) + failureActionsOnNonCrashFailures = $true + } + } Default {} } @@ -423,24 +444,12 @@ try DesktopInteract = $false } - $testServiceFailureActions = @{ - resetPeriodSeconds = 86400 - hasRebootMessage = 0 - hasFailureCommand = 0 - failureActionCount = 1 - failureCommand = $null - rebootMessage = $null - actionsCollection = @([PSCustomObject]@{ - type = 'RESTART' - delaySeconds = 30000 - }) - failureActionsOnNonCrashFailures = $true - } + $testServiceFailureActions, $failureRegistryKey = Get-MockFailureActionsData -DataSetName '3ActionsCommandAndMessage' Mock -CommandName 'Get-Service' -MockWith { return $testService } Mock -CommandName 'Get-ServiceCimInstance' -MockWith { return $testServiceCimInstance } Mock -CommandName 'ConvertTo-StartupTypeString' -MockWith { return $convertToStartupTypeStringResult } - Mock -CommandName 'Get-ServiceFailureActions' -MockWith { return $testServiceFailureActions } + Mock -CommandName 'Get-Item' -ParameterFilter { $Path -eq "HKLM:\SYSTEM\CurrentControlSet\Services\$($testService.name)" } -MockWith { return $failureRegistryKey} Test-GetTargetResourceDoesntThrow -GetTargetResourceParameters $getTargetResourceParameters -TestServiceCimInstance $testServiceCimInstance @@ -492,24 +501,12 @@ try DesktopInteract = $false } - $testServiceFailureActions = @{ - resetPeriodSeconds = 86400 - hasRebootMessage = 0 - hasFailureCommand = 0 - failureActionCount = 1 - failureCommand = $null - rebootMessage = $null - actionsCollection = @([PSCustomObject]@{ - type = 'RESTART' - delaySeconds = 30000 - }) - failureActionsOnNonCrashFailures = $true - } + $testServiceFailureActions, $failureRegistryKey = Get-MockFailureActionsData -DataSetName '1ActionNoCommandOrMessage' Mock -CommandName 'Get-Service' -MockWith { return $testService } Mock -CommandName 'Get-ServiceCimInstance' -MockWith { return $testServiceCimInstance } Mock -CommandName 'ConvertTo-StartupTypeString' -MockWith { return $convertToStartupTypeStringResult } - Mock -CommandName 'Get-ServiceFailureActions' -MockWith { return $testServiceFailureActions } + Mock -CommandName 'Get-Item' -ParameterFilter { $Path -eq "HKLM:\SYSTEM\CurrentControlSet\Services\$($testService.name)" } -MockWith { return $failureRegistryKey} Test-GetTargetResourceDoesntThrow -GetTargetResourceParameters $getTargetResourceParameters -TestServiceCimInstance $testServiceCimInstance @@ -598,25 +595,13 @@ try DesktopInteract = $false } - $testServiceFailureActions = @{ - resetPeriodSeconds = 86400 - hasRebootMessage = 0 - hasFailureCommand = 0 - failureActionCount = 1 - failureCommand = $null - rebootMessage = $null - actionsCollection = @([PSCustomObject]@{ - type = 'RESTART' - delaySeconds = 30000 - }) - failureActionsOnNonCrashFailures = $true - } + $testServiceFailureActions, $failureRegistryKey = Get-MockFailureActionsData -DataSetName '1ActionNoCommandOrMessage' Mock -CommandName 'Get-Service' -MockWith { return $testService } Mock -CommandName 'Get-ServiceCimInstance' -MockWith { return $testServiceCimInstance } Mock -CommandName 'ConvertTo-StartupTypeString' -MockWith { return $convertToStartupTypeStringResult } Mock -CommandName 'Write-Warning' - Mock -CommandName 'Get-ServiceFailureActions' -MockWith { return $testServiceFailureActions } + Mock -CommandName 'Get-Item' -ParameterFilter { $Path -eq "HKLM:\SYSTEM\CurrentControlSet\Services\$($testService.name)" } -MockWith { return $failureRegistryKey} Test-GetTargetResourceDoesntThrow -GetTargetResourceParameters $getTargetResourceParameters -TestServiceCimInstance $testServiceCimInstance @@ -805,6 +790,14 @@ try DisplayName = 'TestDisplayName' Description = 'Test device description' Dependencies = @( 'TestServiceDependency1', 'TestServiceDependency2' ) + ResetPeriodSeconds = 86400 + RebootMessage = 'RebootMessage' + FailureCommand = 'C:\Path\To\Command.exe' + FailureActionsCollection = @(@{ + type = 'RESTART' + delaySeconds = 500 + }) + FailureActionsOnNonCrashFailures = $true } It 'Should not throw' { @@ -832,7 +825,20 @@ try } It 'Should change all service properties except Credential' { - Assert-MockCalled -CommandName 'Set-ServiceProperty' -ParameterFilter { $ServiceName -eq $setTargetResourceParameters.Name -and $StartupType -eq $setTargetResourceParameters.StartupType -and $BuiltInAccount -eq $setTargetResourceParameters.BuiltInAccount -and $DesktopInteract -eq $setTargetResourceParameters.DesktopInteract -and $DisplayName -eq $setTargetResourceParameters.DisplayName -and $Description -eq $setTargetResourceParameters.Description -and $null -eq (Compare-Object -ReferenceObject $setTargetResourceParameters.Dependencies -DifferenceObject $Dependencies) } -Times 1 -Scope 'Context' + Assert-MockCalled -CommandName 'Set-ServiceProperty' -ParameterFilter { + $ServiceName -eq $setTargetResourceParameters.Name -and + $StartupType -eq $setTargetResourceParameters.StartupType -and + $BuiltInAccount -eq $setTargetResourceParameters.BuiltInAccount -and + $DesktopInteract -eq $setTargetResourceParameters.DesktopInteract -and + $DisplayName -eq $setTargetResourceParameters.DisplayName -and + $Description -eq $setTargetResourceParameters.Description -and + $null -eq (Compare-Object -ReferenceObject $setTargetResourceParameters.Dependencies -DifferenceObject $Dependencies) -and + $ResetPeriodSeconds -eq $setTargetResourceParameters.resetPeriodSeconds -and + $RebootMessage -eq $setTargetResourceParameters.rebootMessage -and + $FailureCommand -eq $setTargetResourceParameters.failureCommand -and + $null -eq (Compare-Object -ReferenceObject $setTargetResourceParameters.failureActionsCollection -DifferenceObject $FailureActionsCollection) -and + $FailureActionsOnNonCrashFailures -eq $setTargetResourceParameters.failureActionsOnNonCrashFailures + } -Times 1 -Scope 'Context' } It 'Should start the service' { @@ -856,6 +862,14 @@ try DisplayName = 'TestDisplayName' Description = 'Test device description' Dependencies = @( 'TestServiceDependency1', 'TestServiceDependency2' ) + ResetPeriodSeconds = 86400 + RebootMessage = 'RebootMessage' + FailureCommand = 'C:\Path\To\Command.exe' + FailureActionsCollection = @(@{ + type = 'RESTART' + delaySeconds = 500 + }) + FailureActionsOnNonCrashFailures = $true } It 'Should not throw' { @@ -883,7 +897,20 @@ try } It 'Should change all service properties except BuiltInAccount' { - Assert-MockCalled -CommandName 'Set-ServiceProperty' -ParameterFilter { $ServiceName -eq $setTargetResourceParameters.Name -and $StartupType -eq $setTargetResourceParameters.StartupType -and $null -eq (Compare-Object -ReferenceObject $setTargetResourceParameters.Credential -DifferenceObject $Credential) -and $DesktopInteract -eq $setTargetResourceParameters.DesktopInteract -and $DisplayName -eq $setTargetResourceParameters.DisplayName -and $Description -eq $setTargetResourceParameters.Description -and $null -eq (Compare-Object -ReferenceObject $setTargetResourceParameters.Dependencies -DifferenceObject $Dependencies) } -Times 1 -Scope 'Context' + Assert-MockCalled -CommandName 'Set-ServiceProperty' -ParameterFilter { + $ServiceName -eq $setTargetResourceParameters.Name -and + $StartupType -eq $setTargetResourceParameters.StartupType -and + $null -eq (Compare-Object -ReferenceObject $setTargetResourceParameters.Credential -DifferenceObject $Credential) -and + $DesktopInteract -eq $setTargetResourceParameters.DesktopInteract -and + $DisplayName -eq $setTargetResourceParameters.DisplayName -and + $Description -eq $setTargetResourceParameters.Description -and + $null -eq (Compare-Object -ReferenceObject $setTargetResourceParameters.Dependencies -DifferenceObject $Dependencies) + $ResetPeriodSeconds -eq $setTargetResourceParameters.resetPeriodSeconds -and + $RebootMessage -eq $setTargetResourceParameters.rebootMessage -and + $FailureCommand -eq $setTargetResourceParameters.failureCommand -and + $null -eq (Compare-Object -ReferenceObject $setTargetResourceParameters.failureActionsCollection -DifferenceObject $FailureActionsCollection) -and + $FailureActionsOnNonCrashFailures -eq $setTargetResourceParameters.failureActionsOnNonCrashFailures + } -Times 1 -Scope 'Context' } It 'Should not attempt to start the service' { @@ -934,7 +961,9 @@ try } It 'Should set the service to start with the GroupManagedServiceAccount' { - Assert-MockCalled -CommandName 'Set-ServiceProperty' -ParameterFilter { $ServiceName -eq $setTargetResourceParameters.Name -and $StartupType -eq $setTargetResourceParameters.StartupType -and $GroupManagedServiceAccount -eq $setTargetResourceParameters.GroupManagedServiceAccount } -Times 1 -Scope 'Context' + Assert-MockCalled -CommandName 'Set-ServiceProperty' -ParameterFilter { + $ServiceName -eq $setTargetResourceParameters.Name -and$StartupType -eq $setTargetResourceParameters.StartupType -and$GroupManagedServiceAccount -eq $setTargetResourceParameters.GroupManagedServiceAccount + } -Times 1 -Scope 'Context' } It 'Should not attempt to start the service' { @@ -1105,6 +1134,14 @@ try DisplayName = 'TestDisplayName' Description = 'Test device description' Dependencies = @( 'TestServiceDependency1', 'TestServiceDependency2' ) + ResetPeriodSeconds = 86400 + RebootMessage = 'RebootMessage' + FailureCommand = 'C:\Path\To\Command.exe' + FailureActionsCollection = @(@{ + type = 'RESTART' + delaySeconds = 500 + }) + FailureActionsOnNonCrashFailures = $true } It 'Should not throw' { @@ -1132,7 +1169,20 @@ try } It 'Should change all service properties except Credential' { - Assert-MockCalled -CommandName 'Set-ServiceProperty' -ParameterFilter { $ServiceName -eq $setTargetResourceParameters.Name -and $StartupType -eq $setTargetResourceParameters.StartupType -and $BuiltInAccount -eq $setTargetResourceParameters.BuiltInAccount -and $DesktopInteract -eq $setTargetResourceParameters.DesktopInteract -and $DisplayName -eq $setTargetResourceParameters.DisplayName -and $Description -eq $setTargetResourceParameters.Description -and $null -eq (Compare-Object -ReferenceObject $setTargetResourceParameters.Dependencies -DifferenceObject $Dependencies) } -Times 1 -Scope 'Context' + Assert-MockCalled -CommandName 'Set-ServiceProperty' -ParameterFilter { + $ServiceName -eq $setTargetResourceParameters.Name -and + $StartupType -eq $setTargetResourceParameters.StartupType -and + $BuiltInAccount -eq $setTargetResourceParameters.BuiltInAccount -and + $DesktopInteract -eq $setTargetResourceParameters.DesktopInteract -and + $DisplayName -eq $setTargetResourceParameters.DisplayName -and + $Description -eq $setTargetResourceParameters.Description -and + $null -eq (Compare-Object -ReferenceObject $setTargetResourceParameters.Dependencies -DifferenceObject $Dependencies) + $ResetPeriodSeconds -eq $setTargetResourceParameters.resetPeriodSeconds -and + $RebootMessage -eq $setTargetResourceParameters.rebootMessage -and + $FailureCommand -eq $setTargetResourceParameters.failureCommand -and + $null -eq (Compare-Object -ReferenceObject $setTargetResourceParameters.failureActionsCollection -DifferenceObject $FailureActionsCollection) -and + $FailureActionsOnNonCrashFailures -eq $setTargetResourceParameters.failureActionsOnNonCrashFailures + } -Times 1 -Scope 'Context' } It 'Should not attempt to start the service' { @@ -1155,6 +1205,14 @@ try DisplayName = 'TestDisplayName' Description = 'Test device description' Dependencies = @( 'TestServiceDependency1', 'TestServiceDependency2' ) + ResetPeriodSeconds = 86400 + RebootMessage = 'RebootMessage' + FailureCommand = 'C:\Path\To\Command.exe' + FailureActionsCollection = @(@{ + type = 'RESTART' + delaySeconds = 500 + }) + FailureActionsOnNonCrashFailures = $true } It 'Should not throw' { @@ -1182,7 +1240,19 @@ try } It 'Should change all service properties except BuiltInAccount' { - Assert-MockCalled -CommandName 'Set-ServiceProperty' -ParameterFilter { $ServiceName -eq $setTargetResourceParameters.Name -and $StartupType -eq $setTargetResourceParameters.StartupType -and $BuiltInAccount -eq $setTargetResourceParameters.BuiltInAccount -and $DesktopInteract -eq $setTargetResourceParameters.DesktopInteract -and $DisplayName -eq $setTargetResourceParameters.DisplayName -and $Description -eq $setTargetResourceParameters.Description -and $null -eq (Compare-Object -ReferenceObject $setTargetResourceParameters.Dependencies -DifferenceObject $Dependencies) } -Times 1 -Scope 'Context' + Assert-MockCalled -CommandName 'Set-ServiceProperty' -ParameterFilter { $ServiceName -eq $setTargetResourceParameters.Name -and + $StartupType -eq $setTargetResourceParameters.StartupType -and + $BuiltInAccount -eq $setTargetResourceParameters.BuiltInAccount -and + $DesktopInteract -eq $setTargetResourceParameters.DesktopInteract -and + $DisplayName -eq $setTargetResourceParameters.DisplayName -and + $Description -eq $setTargetResourceParameters.Description -and + $null -eq (Compare-Object -ReferenceObject $setTargetResourceParameters.Dependencies -DifferenceObject $Dependencies) + $ResetPeriodSeconds -eq $setTargetResourceParameters.resetPeriodSeconds -and + $RebootMessage -eq $setTargetResourceParameters.rebootMessage -and + $FailureCommand -eq $setTargetResourceParameters.failureCommand -and + $null -eq (Compare-Object -ReferenceObject $setTargetResourceParameters.failureActionsCollection -DifferenceObject $FailureActionsCollection) -and + $FailureActionsOnNonCrashFailures -eq $setTargetResourceParameters.failureActionsOnNonCrashFailures + } -Times 1 -Scope 'Context' } It 'Should not attempt to start the service' { @@ -1329,6 +1399,17 @@ try $expectedErrorMessage = $script:localizedData.CredentialParametersAreMutallyExclusive -f $testTargetResourceParameters.Name { Test-TargetResource @testTargetResourceParameters } | Should -Throw -ExpectedMessage $expectedErrorMessage } + + $testTargetResourceParameters = @{ + Name = $script:testServiceName + FailureCommand = 'C:\Path\To\Command.exe' + FailureActionsCollection = @{Type = 'RESTART'; Delay = 180} + } + + It 'Should throw an error for invalid use of FailureCommand parameter' { + $expectedErrorMessage = $script:localizedData.MustSpecifyRestartFailureAction + { Test-TargetResource @testTargetResourceParameters } | Should -Throw -ExpectedMessage $expectedErrorMessage + } } Context 'When a service does not exist and Ensure set to Absent' { From 45b3637166100a47e7b279c06620a64586b57a21 Mon Sep 17 00:00:00 2001 From: Bill Hurt Date: Sat, 25 Apr 2020 05:49:59 -0700 Subject: [PATCH 05/13] Implement embedded instance strategy --- .../DSC_xServiceResource/DSC_xServiceResource.psm1 | 10 +++++----- .../DSC_xServiceResource.schema.mof | 11 +++++++++++ 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/source/DSCResources/DSC_xServiceResource/DSC_xServiceResource.psm1 b/source/DSCResources/DSC_xServiceResource/DSC_xServiceResource.psm1 index 7650039c5..b20c95c9a 100644 --- a/source/DSCResources/DSC_xServiceResource/DSC_xServiceResource.psm1 +++ b/source/DSCResources/DSC_xServiceResource/DSC_xServiceResource.psm1 @@ -315,7 +315,7 @@ function Set-TargetResource $FailureCommand, [Parameter()] - [System.Object[]] + [Microsoft.Management.Infrastructure.CimInstance[]] $FailureActionsCollection, [Parameter()] @@ -600,7 +600,7 @@ function Test-TargetResource $FailureCommand, [Parameter()] - [System.Object[]] + [Microsoft.Management.Infrastructure.CimInstance[]] $FailureActionsCollection, [Parameter()] @@ -1835,7 +1835,7 @@ function Set-ServiceProperty $FailureCommand, [Parameter()] - [System.Object[]] + [Microsoft.Management.Infrastructure.CimInstance[]] $FailureActionsCollection, [Parameter()] @@ -2349,7 +2349,7 @@ function Set-ServiceFailureActionProperty { $FailureCommand, [Parameter()] - [System.Object[]] + [Microsoft.Management.Infrastructure.CimInstance[]] $FailureActionsCollection, [Parameter()] @@ -2433,7 +2433,7 @@ function Set-ServiceFailureActionProperty { # Iterate over the actions and their properties to add the integers that they encode to the array. foreach ($action in $failureActions.ActionsCollection) { - $integerData.add($action.type) | Out-Null + $integerData.add([ACTION_TYPE]$action.type) | Out-Null $integerData.add($action.delaySeconds) | Out-Null } diff --git a/source/DSCResources/DSC_xServiceResource/DSC_xServiceResource.schema.mof b/source/DSCResources/DSC_xServiceResource/DSC_xServiceResource.schema.mof index 2876dbea2..d409201f2 100644 --- a/source/DSCResources/DSC_xServiceResource/DSC_xServiceResource.schema.mof +++ b/source/DSCResources/DSC_xServiceResource/DSC_xServiceResource.schema.mof @@ -16,4 +16,15 @@ class DSC_xServiceResource : OMI_BaseResource [Write,Description("An array of strings indicating the names of the dependencies of the service.")] String Dependencies[]; [Write,Description("The time to wait for the service to start in milliseconds. Defaults to 30000.")] uint32 StartupTimeout; [Write,Description("The time to wait for the service to stop in milliseconds. Defaults to 30000.")] uint32 TerminateTimeout; + [Write,Description("The time to wait for the Failure count to reset in seconds.")] UInt32 ResetPeriodSeconds; + [Write,Description("The command line to run if a service fails.")] String FailureCommand; + [Write,EmbeddedInstance("DSC_xFailureAction"),Description("The actions to take when a service fails.")] String FailureActionsCollection[]; + [Write,Description("A flag indicating whether failure actions should be invoked on non-crash failures.")] Boolean FailureActionsOnNonCrashFailures; +}; + +[ClassVersion("1.0.0")] +class DSC_xFailureAction +{ + [Write, Description("The action to take on failure"), ValueMap{"NONE", "RESTART", "REBOOT", "RUN_COMMAND"}, Values{"NONE", "RESTART", "REBOOT", "RUN_COMMAND"}] String Type; + [Write, Description("Delay in seconds to wait before taking the specified action")] UInt32 DelaySeconds; }; From 14bb78b5de6118a88deba7ea2d3522c11add687b Mon Sep 17 00:00:00 2001 From: Bill Hurt Date: Sat, 25 Apr 2020 07:32:56 -0700 Subject: [PATCH 06/13] Force actions collection to be array --- .../DSC_xServiceResource/DSC_xServiceResource.psm1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source/DSCResources/DSC_xServiceResource/DSC_xServiceResource.psm1 b/source/DSCResources/DSC_xServiceResource/DSC_xServiceResource.psm1 index b20c95c9a..5cb0e9fb3 100644 --- a/source/DSCResources/DSC_xServiceResource/DSC_xServiceResource.psm1 +++ b/source/DSCResources/DSC_xServiceResource/DSC_xServiceResource.psm1 @@ -2149,7 +2149,7 @@ function Get-ServiceFailureActions { resetPeriodSeconds = $null hasRebootMessage = $null hasFailureCommand = $null - failureActionCount = $null + failureActionCount = @() failureCommand = $null rebootMessage = $null actionsCollection = $null @@ -2287,7 +2287,7 @@ function Get-FailureActionCollection $actionsCollection.Add($currentAction) | Out-Null } - $actionsCollection + @($actionsCollection) } } From e94b7763219bc0562f75715e8d271fa2952df6ae Mon Sep 17 00:00:00 2001 From: Bill Hurt Date: Sat, 25 Apr 2020 07:37:07 -0700 Subject: [PATCH 07/13] add personal todo file. Will not be in final pr. --- todo.txt | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 todo.txt diff --git a/todo.txt b/todo.txt new file mode 100644 index 000000000..0ac8cb3be --- /dev/null +++ b/todo.txt @@ -0,0 +1,10 @@ +1. When the current collection of failure actions has only a single item, and perhaps also if it has zero items, there is a problem somewhere in Test-TargetResource, and the `count` property cannot be found on an object. + Make sure that the collection of actions on both sides is always cast to an array so that the count of actions can be found. + + PowerShell DSC resource DSC_xServiceResource failed to execute Test-TargetResource functionality with error message: The property 'count' cannot be found on this object. + Verify that the property exists. + + CategoryInfo : InvalidOperation: (:) [], CimException + + FullyQualifiedErrorId : ProviderOperationExecutionFailure + + PSComputerName : localhost + +2. Ensure that the unit and acceptance testing suites account for the possibility of the above. From 42fb8c59450228f5d70744cdbc1655e959416b65 Mon Sep 17 00:00:00 2001 From: Bill Hurt Date: Sat, 25 Apr 2020 07:39:36 -0700 Subject: [PATCH 08/13] More attempts to ensure .count exists --- .../DSC_xServiceResource/DSC_xServiceResource.psm1 | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/source/DSCResources/DSC_xServiceResource/DSC_xServiceResource.psm1 b/source/DSCResources/DSC_xServiceResource/DSC_xServiceResource.psm1 index 5cb0e9fb3..5e1660e20 100644 --- a/source/DSCResources/DSC_xServiceResource/DSC_xServiceResource.psm1 +++ b/source/DSCResources/DSC_xServiceResource/DSC_xServiceResource.psm1 @@ -779,14 +779,14 @@ function Test-TargetResource # Check the failure actions collection if($PSBoundParameters.ContainsKey('FailureActionsCollection')) { $inSync = $true - if($FailureActionsCollection.count -ne $serviceResource.FailureActionsCollection.count) { - Write-Verbose -Message ($script:localizedData.ServicePropertyDoesNotMatch -f 'FailureActionsCollection.count', $Name, $FailureActionsCollection.count, $serviceResource.FailureActionsCollection.count) + if($FailureActionsCollection.count -ne @($serviceResource.FailureActionsCollection).count) { + Write-Verbose -Message ($script:localizedData.ServicePropertyDoesNotMatch -f 'FailureActionsCollection.count', $Name, @($FailureActionsCollection).count, @($serviceResource.FailureActionsCollection).count) return $false } - foreach ($actionIndex in (0..($FailureActionsCollection.count - 1))) { - $parameterAction = $FailureActionsCollection[$actionIndex] - $serviceResourceAction = $serviceResource.FailureActionsCollection[$actionIndex] + foreach ($actionIndex in (0..(@($FailureActionsCollection).count - 1))) { + $parameterAction = @($FailureActionsCollection)[$actionIndex] + $serviceResourceAction = @($serviceResource.FailureActionsCollection)[$actionIndex] if($parameterAction.type -ne $serviceResourceAction.type) { Write-Verbose -Message ($script:localizedData.ServicePropertyDoesNotMatch -f "FailureActionsCollection Action $actionIndex type", $Name, $parameterAction.type, $serviceResourceAction.type) @@ -2287,7 +2287,7 @@ function Get-FailureActionCollection $actionsCollection.Add($currentAction) | Out-Null } - @($actionsCollection) + $actionsCollection } } From fe807b16b4ec8b7e5169bee962ca5037e43305fc Mon Sep 17 00:00:00 2001 From: Bill Date: Sun, 26 Apr 2020 14:09:14 +0000 Subject: [PATCH 09/13] Fix FailureActionsOnNonCrashFailures bug --- .../DSC_xServiceResource/DSC_xServiceResource.psm1 | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/source/DSCResources/DSC_xServiceResource/DSC_xServiceResource.psm1 b/source/DSCResources/DSC_xServiceResource/DSC_xServiceResource.psm1 index 5e1660e20..6bf440d28 100644 --- a/source/DSCResources/DSC_xServiceResource/DSC_xServiceResource.psm1 +++ b/source/DSCResources/DSC_xServiceResource/DSC_xServiceResource.psm1 @@ -1924,6 +1924,11 @@ function Set-ServiceProperty $setServiceFailureActionsPropertyParameters['FailureActionsCollection'] = $FailureActionsCollection } + if ($PSBoundParameters.ContainsKey('FailureActionsOnNonCrashFailures')) + { + $setServiceFailureActionsPropertyParameters['FailureActionsOnNonCrashFailures'] = $FailureActionsOnNonCrashFailures + } + if($setServiceFailureActionsPropertyParameters.count -gt 0) { Set-ServiceFailureActionProperty -ServiceName $ServiceName @setServiceFailureActionsPropertyParameters From 4ea89e981dc838bf5ecf3fa1a958c87e339336c3 Mon Sep 17 00:00:00 2001 From: Bill Date: Mon, 27 Apr 2020 14:03:19 +0000 Subject: [PATCH 10/13] Convert delaySeconds to delayMillisepconds --- .../DSC_xServiceResource.psm1 | 24 +++++++++---------- .../DSC_xServiceResource.schema.mof | 2 +- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/source/DSCResources/DSC_xServiceResource/DSC_xServiceResource.psm1 b/source/DSCResources/DSC_xServiceResource/DSC_xServiceResource.psm1 index 6bf440d28..da0942d0d 100644 --- a/source/DSCResources/DSC_xServiceResource/DSC_xServiceResource.psm1 +++ b/source/DSCResources/DSC_xServiceResource/DSC_xServiceResource.psm1 @@ -207,8 +207,8 @@ function Get-TargetResource .PARAMETER FailureActionsCollection An array of hash tables representing the failure actions to take. Each hash table should have - two keys: type and delaySeconds. The value for the type key should be one of RESTART, RUN_COMMAND, REBOOT, or NONE. - The value for the delaySeconds key is an integer value of seconds to wait before applying the requested action. + two keys: type and delayMilliSeconds. The value for the type key should be one of RESTART, RUN_COMMAND, REBOOT, or NONE. + The value for the delayMilliSeconds key is an integer value of seconds to wait before applying the requested action. .PARAMETER FailureActionsOnNonCrashFailures By default, failure actions are only queued if the service terminates without reporting @@ -505,8 +505,8 @@ function Set-TargetResource .PARAMETER FailureActionsCollection An array of hash tables representing the failure actions to take. Each hash table should have - two keys: type and delaySeconds. The value for the type key should be one of RESTART, RUN_COMMAND, REBOOT, or NONE. - The value for the delaySeconds key is an integer value of seconds to wait before applying the requested action. + two keys: type and delayMilliSeconds. The value for the type key should be one of RESTART, RUN_COMMAND, REBOOT, or NONE. + The value for the delayMilliSeconds key is an integer value of seconds to wait before applying the requested action. .PARAMETER FailureActionsOnNonCrashFailures By default, failure actions are only queued if the service terminates without reporting @@ -793,8 +793,8 @@ function Test-TargetResource $inSync = $false } - if($parameterAction.delaySeconds -ne $serviceResourceAction.delaySeconds) { - Write-Verbose -Message ($script:localizedData.ServicePropertyDoesNotMatch -f "FailureActionsCollection Action $actionIndex delaySeconds", $Name, $parameterAction.delaySeconds, $serviceResourceAction.delaySeconds) + if($parameterAction.delayMilliSeconds -ne $serviceResourceAction.delayMilliSeconds) { + Write-Verbose -Message ($script:localizedData.ServicePropertyDoesNotMatch -f "FailureActionsCollection Action $actionIndex delayMilliSeconds", $Name, $parameterAction.delayMilliSeconds, $serviceResourceAction.delayMilliSeconds) $inSync = $false } } @@ -1761,8 +1761,8 @@ function Set-ServiceStartupType .PARAMETER FailureActionsCollection An array of hash tables representing the failure actions to take. Each hash table should have - two keys: type and delaySeconds. The value for the type key should be one of RESTART, RUN_COMMAND, REBOOT, or NONE. - The value for the delaySeconds key is an integer value of seconds to wait before applying the requested action. + two keys: type and delayMilliSeconds. The value for the type key should be one of RESTART, RUN_COMMAND, REBOOT, or NONE. + The value for the delayMilliSeconds key is an integer value of seconds to wait before applying the requested action. .PARAMETER FailureActionsOnNonCrashFailures By default, failure actions are only queued if the service terminates without reporting @@ -2286,7 +2286,7 @@ function Get-FailureActionCollection $currentAction = [PSCustomObject]@{ type = [ACTION_TYPE]([System.BitConverter]::ToInt32($Bytes[$actionTypeByteRange],0)) - delaySeconds = [System.BitConverter]::ToInt32($Bytes[$actionDelayByteRange],0) + delayMilliSeconds = [System.BitConverter]::ToInt32($Bytes[$actionDelayByteRange],0) } $actionsCollection.Add($currentAction) | Out-Null @@ -2324,8 +2324,8 @@ function Get-FailureActionCollection .PARAMETER FailureActionsCollection An array of hash tables representing the failure actions to take. Each hash table should have - two keys: type and delaySeconds. The value for the type key should be one of RESTART, RUN_COMMAND, REBOOT, or NONE. - The value for the delaySeconds key is an integer value of seconds to wait before applying the requested action. + two keys: type and delayMilliSeconds. The value for the type key should be one of RESTART, RUN_COMMAND, REBOOT, or NONE. + The value for the delayMilliSeconds key is an integer value of seconds to wait before applying the requested action. .PARAMETER FailureActionsOnNonCrashFailures By default, failure actions are only queued if the service terminates without reporting @@ -2439,7 +2439,7 @@ function Set-ServiceFailureActionProperty { # Iterate over the actions and their properties to add the integers that they encode to the array. foreach ($action in $failureActions.ActionsCollection) { $integerData.add([ACTION_TYPE]$action.type) | Out-Null - $integerData.add($action.delaySeconds) | Out-Null + $integerData.add($action.delayMilliSeconds) | Out-Null } # Now that we finally have all of the data we need, we can convert it to a byte array. diff --git a/source/DSCResources/DSC_xServiceResource/DSC_xServiceResource.schema.mof b/source/DSCResources/DSC_xServiceResource/DSC_xServiceResource.schema.mof index d409201f2..ff2f9b7f2 100644 --- a/source/DSCResources/DSC_xServiceResource/DSC_xServiceResource.schema.mof +++ b/source/DSCResources/DSC_xServiceResource/DSC_xServiceResource.schema.mof @@ -26,5 +26,5 @@ class DSC_xServiceResource : OMI_BaseResource class DSC_xFailureAction { [Write, Description("The action to take on failure"), ValueMap{"NONE", "RESTART", "REBOOT", "RUN_COMMAND"}, Values{"NONE", "RESTART", "REBOOT", "RUN_COMMAND"}] String Type; - [Write, Description("Delay in seconds to wait before taking the specified action")] UInt32 DelaySeconds; + [Write, Description("Delay in milliseconds to wait before taking the specified action")] UInt32 DelayMilliSeconds; }; From da51d18c9acb576878756c5cd0677b40abc16fc1 Mon Sep 17 00:00:00 2001 From: Bill Date: Mon, 27 Apr 2020 15:33:27 +0000 Subject: [PATCH 11/13] Fix error if key already exists. --- .../DSC_xServiceResource.psm1 | 40 +++++++++---------- .../DSC_xServiceResource.schema.mof | 1 + 2 files changed, 21 insertions(+), 20 deletions(-) diff --git a/source/DSCResources/DSC_xServiceResource/DSC_xServiceResource.psm1 b/source/DSCResources/DSC_xServiceResource/DSC_xServiceResource.psm1 index da0942d0d..c8d22ac83 100644 --- a/source/DSCResources/DSC_xServiceResource/DSC_xServiceResource.psm1 +++ b/source/DSCResources/DSC_xServiceResource/DSC_xServiceResource.psm1 @@ -2393,7 +2393,7 @@ function Set-ServiceFailureActionProperty { # This setting is a combination a flag in the _SERVICE_FAILURE_ACTIONSA struct, # and the actual string value for the message stored in the 'RebootMessage' registry property. # If hasRebootMessage is already true, then we know the key exists and we can just set it. - if ($failureActions.hasRebootMessage) + if (Get-ItemProperty -Path HKLM:\SYSTEM\CurrentControlSet\Services\$serviceName -Name 'RebootMessage' -ErrorAction SilentlyContinue) { Set-ItemProperty -Path HKLM:\SYSTEM\CurrentControlSet\Services\$serviceName -Name 'RebootMessage' -Value $RebootMessage | Out-Null } @@ -2411,7 +2411,7 @@ function Set-ServiceFailureActionProperty { # This is the same as the RebootMessage property above. It's a combination of a flag in the struct, and an external property that stores the value. if ($PSBoundParameters.ContainsKey('FailureCommand')) { - if ($failureActions.hasFailureCommand) + if (Get-ItemProperty -Path HKLM:\SYSTEM\CurrentControlSet\Services\$serviceName -Name 'FailureCommand' -ErrorAction SilentlyContinue) { Set-ItemProperty -Path HKLM:\SYSTEM\CurrentControlSet\Services\$serviceName -Name 'FailureCommand' -Value $FailureCommand | Out-Null } @@ -2451,7 +2451,7 @@ function Set-ServiceFailureActionProperty { # if it doesn't already exist. if ($PSBoundParameters.ContainsKey('FailureActionsOnNonCrashFailures')) { - if ($failureActions.FailureActionsOnNonCrashFailures) { + if (Get-ItemProperty -Path HKLM:\SYSTEM\CurrentControlSet\Services\$serviceName -Name 'FailureActionsOnNonCrashFailures' -ErrorAction SilentlyContinue) { Set-ItemProperty -Path HKLM:\SYSTEM\CurrentControlSet\Services\$serviceName -Name 'FailureActionsOnNonCrashFailures' -Value $FailureActionsOnNonCrashFailures | Out-Null } else @@ -2463,23 +2463,23 @@ function Set-ServiceFailureActionProperty { } function Test-HasRestartFailureAction - { - [CmdletBinding()] - param ( - [Parameter()] - [System.Object[]] - $Collection - ) - - process { - $hasRestartAction = $false - - foreach ($action in $collection) { - if ($action.type -eq 'RUN_COMMAND') { - $hasRestartAction = $true - } - } +{ + [CmdletBinding()] + param ( + [Parameter()] + [System.Object[]] + $Collection + ) + + process { + $hasRestartAction = $false - $hasRestartAction + foreach ($action in $collection) { + if ($action.type -eq 'RUN_COMMAND') { + $hasRestartAction = $true + } } + + $hasRestartAction } +} diff --git a/source/DSCResources/DSC_xServiceResource/DSC_xServiceResource.schema.mof b/source/DSCResources/DSC_xServiceResource/DSC_xServiceResource.schema.mof index ff2f9b7f2..af7cd26ee 100644 --- a/source/DSCResources/DSC_xServiceResource/DSC_xServiceResource.schema.mof +++ b/source/DSCResources/DSC_xServiceResource/DSC_xServiceResource.schema.mof @@ -20,6 +20,7 @@ class DSC_xServiceResource : OMI_BaseResource [Write,Description("The command line to run if a service fails.")] String FailureCommand; [Write,EmbeddedInstance("DSC_xFailureAction"),Description("The actions to take when a service fails.")] String FailureActionsCollection[]; [Write,Description("A flag indicating whether failure actions should be invoked on non-crash failures.")] Boolean FailureActionsOnNonCrashFailures; + [Write,Description("An optional broadcast message to send to logged in users if the machine reboots as a result of a failure action.")] String RebootMessage; }; [ClassVersion("1.0.0")] From 1ad978cf9235152fa99769e1cdffb09a4ab9ade0 Mon Sep 17 00:00:00 2001 From: Bill Hurt Date: Wed, 6 May 2020 06:37:36 -0700 Subject: [PATCH 12/13] start using sc --- .../DSC_xServiceResource.psm1 | 42 +++++++++++++++---- 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/source/DSCResources/DSC_xServiceResource/DSC_xServiceResource.psm1 b/source/DSCResources/DSC_xServiceResource/DSC_xServiceResource.psm1 index c8d22ac83..5bb15558a 100644 --- a/source/DSCResources/DSC_xServiceResource/DSC_xServiceResource.psm1 +++ b/source/DSCResources/DSC_xServiceResource/DSC_xServiceResource.psm1 @@ -2451,13 +2451,7 @@ function Set-ServiceFailureActionProperty { # if it doesn't already exist. if ($PSBoundParameters.ContainsKey('FailureActionsOnNonCrashFailures')) { - if (Get-ItemProperty -Path HKLM:\SYSTEM\CurrentControlSet\Services\$serviceName -Name 'FailureActionsOnNonCrashFailures' -ErrorAction SilentlyContinue) { - Set-ItemProperty -Path HKLM:\SYSTEM\CurrentControlSet\Services\$serviceName -Name 'FailureActionsOnNonCrashFailures' -Value $FailureActionsOnNonCrashFailures | Out-Null - } - else - { - New-ItemProperty -Path HKLM:\SYSTEM\CurrentControlSet\Services\$serviceName -Name 'FailureActionsOnNonCrashFailures' -Value $FailureActionsOnNonCrashFailures | Out-Null - } + Invoke-SCFailureFlag -ServiceName $ServiceName -Flag $FailureActionsOnNonCrashFailures } } } @@ -2483,3 +2477,37 @@ function Test-HasRestartFailureAction $hasRestartAction } } + +<# +.SYNOPSIS + Use sc.exe to set the failure flag +.DESCRIPTION + The FailureActionsOnNonCrashFailures feature of the service controller + cannnot be reliably managed in code via anything but sc.exe. Managing the + registry key value on its own has proved inadequate. This cmdlet gives us + an easily mockable and testable way to invoke sc to manage this flag. +.EXAMPLE + PS C:\> Invoke-SCFailureFlag -ServiceName WSearch -Flag 1 + This will invoke the equivelent of '& sc.exe failureFlag WSearch 1' +#> +function Invoke-SCFailureFlag { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [System.String] + $ServiceName, + [Parameter(Mandatory = $true)] + [ValidateSet(0,1)] + [System.Int32] + $Flag + ) + + process { + & sc.exe @('failureFlag', $ServiceName, $Flag) | Out-Null + # A quick sanity check to make sure the value was set as expected without having to parse the string output of sc. + $failureFlag = (Get-ItemProperty -Path HKLM:\SYSTEM\CurrentControlSet\Services\$serviceName -Name 'FailureActionsOnNonCrashFailures').failureActionsOnNonCrashFailures + if ($failureFlag -ne $Flag) { + throw "sc.exe did not succesfully set the failure flag for $servicename" + } + } +} From d2b27db1c2fb0888c34c653b8e5b7861ba6dd304 Mon Sep 17 00:00:00 2001 From: Bill Hurt Date: Wed, 8 Jul 2020 15:23:49 +0000 Subject: [PATCH 13/13] Add an example file --- .../xService_ChangeServiceRecoveryOptions.ps1 | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 source/Examples/xService_ChangeServiceRecoveryOptions.ps1 diff --git a/source/Examples/xService_ChangeServiceRecoveryOptions.ps1 b/source/Examples/xService_ChangeServiceRecoveryOptions.ps1 new file mode 100644 index 000000000..a158becdc --- /dev/null +++ b/source/Examples/xService_ChangeServiceRecoveryOptions.ps1 @@ -0,0 +1,100 @@ +#Requires -module 'xPSDesiredStateConfiguration' + +<# + .SYNOPSIS + Configuration that changes the recovery options for an existing service. + + .DESCRIPTION + Configuration that changes the recovery options for an existing service. + + .PARAMETER Name + The name of the Windows service. + + .PARAMETER State + The state that the Windows service should have. + + .PARAMETER ResetPeriodSeconds + The time to wait for the Failure count to reset in seconds. + + .PARAMETER FailureCommand + The command line to run if a service fails. + + .PARAMETER RebootMessage + An optional broadcast message to send to logged in users if the machine reboots as a result of a failure action. + + .EXAMPLE + xService_ChangeServiceStateConfig -Name 'spooler' -State 'Stopped' + + Compiles a configuration that make sure the state for the Windows + service 'spooler' is 'Stopped'. If the service is running the + Windows service will be stopped. +#> +Configuration xService_ChangeServiceFailureActionsConfig +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter()] + [ValidateSet('Running', 'Stopped')] + [System.String] + $State = 'Running', + + [Parameter()] + [System.Int32] + $ResetPeriodSeconds = 86400, + + [Parameter()] + [System.String] + $FailureCommand, + + [Parameter()] + [System.String] + $RebootMessage, + + [Parameter()] + [Switch] + $FailureActionsOnNonCrashFailures + ) + + Import-DscResource -ModuleName 'xPSDesiredStateConfiguration' + + Node localhost + { + xService 'ChangeServiceState' + { + Name = $Name + State = $State + Ensure = 'Present' + ResetPeriodSeconds = $ResetPeriodSeconds + RebootMessage = $RebootMessage + FailureCommand = $FailureCommand + FailureActionsOnNonCrashFailures = $FailureActionsOnNonCrashFailures + FailureActionsCollection = @( + DSC_xFailureAction + { + Type = 'RESTART' + DelayMilliSeconds = 60000 + } + DSC_xFailureAction + { + Type = 'RESTART' + DelayMilliSeconds = 120000 + } + DSC_xFailureAction + { + Type = 'Reboot' + DelayMilliSeconds = 240000 + } + DSC_xFailureAction + { + Type = 'RUN_COMMAND' + DelayMilliSeconds = 240000 + } + ) + } + } +}