Skip to content

Instantly share code, notes, and snippets.

@jborean93
Last active February 28, 2024 03:27
Show Gist options
  • Save jborean93/889288b56087a2c5def7fa49b6a8a0ad to your computer and use it in GitHub Desktop.
Save jborean93/889288b56087a2c5def7fa49b6a8a0ad to your computer and use it in GitHub Desktop.
Get and Set the Windows service recovery options
# Copyright: (c) 2018, Jordan Borean (@jborean93) <[email protected]>
# MIT License (see LICENSE or https://opensource.org/licenses/MIT)
Add-Type -TypeDefinition @'
using Microsoft.Win32.SafeHandles;
using System;
using System.Collections.Generic;
using System.Runtime.ConstrainedExecution;
using System.Runtime.InteropServices;
namespace ServiceManager
{
internal class NativeHelpers
{
[Flags]
public enum SCMAccessRights : uint
{
SC_MANAGER_CONNECT = 0x00000001,
SC_MANAGER_CREATE_SERVICE = 0x00000002,
SC_MANAGER_ENUMERATE_SERVICE = 0x00000004,
SC_MANAGER_LOCK = 0x00000008,
SC_MANAGER_QUERY_LOCK_STATUS = 0x00000010,
SC_MANAGER_MODIFY_BOOT_CONFIG = 0x00000020,
SC_MANAGER_ALL_ACCESS = 0x000F003F,
}
public enum SecurityImpersonationLevel : uint
{
Anonymous = 0,
Identification = 1,
Impersonation = 2,
Delegation = 3,
}
[Flags]
public enum ServiceRights : uint
{
SERVICE_QUERY_CONFIG = 0x00000001,
SERVICE_CHANGE_CONFIG = 0x00000002,
SERVICE_QUERY_STATUS = 0x00000004,
SERVICE_ENUMERATE_DEPENDENTS = 0x00000008,
SERVICE_START = 0x00000010,
SERVICE_STOP = 0x00000020,
SERVICE_INTERROGATE = 0x00000080,
SERVICE_PAUSE_CONTINUE = 0x00000040,
SERVICE_USER_DEFINED_CONTROL = 0x00000100,
DELETE = 0x00010000,
READ_CONTROL = 0x00020000,
WRITE_DAC = 0x00040000,
WRITE_OWNER = 0x00080000,
SERVICE_ALL_ACCESS = 0x000F01FF,
ACCESS_SYSTEM_SECURITY = 0x01000000,
}
public enum ServiceInfoLevel : uint
{
SERVICE_CONFIG_DESCRIPTION = 1,
SERVICE_CONFIG_FAILURE_ACTIONS = 2,
SERVICE_CONFIG_DELAYED_AUTO_START_INFO = 3,
SERVICE_CONFIG_FAILURE_ACTIONS_FLAG = 4,
SERVICE_CONFIG_SERVICE_SID_INFO = 5,
SERVICE_CONFIG_REQUIRED_PRIVILEGES_INFO = 6,
SERVICE_CONFIG_PRESHUTDOWN_INFO = 7,
SERVICE_CONFIG_TRIGGER_INFO = 8,
SERVICE_CONFIG_PREFERRED_NODE = 9,
SERVICE_CONFIG_LAUNCH_PROTECTED = 12,
}
[Flags]
public enum TokenPrivilegeAttributes : uint
{
Disabled = 0x00000000,
EnabledByDefault = 0x00000001,
Enabled = 0x00000002,
Removed = 0x00000004,
UsedForAccess = 0x80000000,
}
[StructLayout(LayoutKind.Sequential)]
public struct LUID
{
public UInt32 LowPart;
public Int32 HighPart;
}
[StructLayout(LayoutKind.Sequential)]
public struct LUID_AND_ATTRIBUTES
{
public LUID Luid;
public TokenPrivilegeAttributes Attributes;
}
[StructLayout(LayoutKind.Sequential)]
public struct SC_ACTION
{
public ActionType Type;
public UInt32 Delay;
}
[StructLayout(LayoutKind.Sequential)]
public struct SERVICE_FAILURE_ACTIONSW
{
public UInt32 dwResetPeriod;
[MarshalAs(UnmanagedType.LPWStr)] public string lpRebootMsg;
[MarshalAs(UnmanagedType.LPWStr)] public string lpCommand;
public UInt32 cActions;
public IntPtr lpsaActions;
}
[StructLayout(LayoutKind.Sequential)]
public struct TOKEN_PRIVILEGES
{
public UInt32 PrivilegeCount;
[MarshalAs(UnmanagedType.ByValArray, SizeConst=1)] public LUID_AND_ATTRIBUTES[] Privileges;
}
}
internal class NativeMethods
{
[DllImport("Advapi32.dll", SetLastError = true)]
public static extern bool AdjustTokenPrivileges(
SafeTokenHandle Tokenhandle,
bool DisableAllPrivileges,
NativeHelpers.TOKEN_PRIVILEGES NewState,
UInt32 BufferLength,
IntPtr PreviousState,
IntPtr ReturnLength);
[DllImport("Advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern bool ChangeServiceConfig2W(
SafeServiceHandle hService,
NativeHelpers.ServiceInfoLevel dwInfoLevel,
SafeMemoryBuffer lpInfo);
[DllImport("Kernel32.dll", SetLastError = true)]
public static extern bool CloseHandle(
IntPtr hObject);
[DllImport("Advapi32.dll", SetLastError = true)]
public static extern bool CloseServiceHandle(
IntPtr hSCObject);
[DllImport("Kernel32.dll", SetLastError = true)]
public static extern IntPtr GetCurrentThread();
[DllImport("Advapi32.dll", SetLastError = true)]
public static extern bool ImpersonateSelf(
NativeHelpers.SecurityImpersonationLevel ImpersonationLevel);
[DllImport("Advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern bool LookupPrivilegeValueW(
[MarshalAs(UnmanagedType.LPWStr)] string lpSystemName,
[MarshalAs(UnmanagedType.LPWStr)] string lpName,
ref NativeHelpers.LUID lpLuid);
[DllImport("Advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern SafeServiceHandle OpenSCManagerW(
[MarshalAs(UnmanagedType.LPWStr)] string lpMachineName,
[MarshalAs(UnmanagedType.LPWStr)] string lpDatabaseName,
NativeHelpers.SCMAccessRights dwDesiredAccess);
[DllImport("Advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern SafeServiceHandle OpenServiceW(
SafeServiceHandle hSCManager,
[MarshalAs(UnmanagedType.LPWStr)] string lpServiceName,
NativeHelpers.ServiceRights dwDesiredAccess);
[DllImport("Advapi32.dll", SetLastError = true)]
public static extern bool OpenThreadToken(
IntPtr ThreadHandle,
UInt32 DesiredAccess,
bool OpenAsSelf,
out SafeTokenHandle TokenHandle);
[DllImport("Advapi32.dll", SetLastError = true)]
public static extern bool RevertToSelf();
[DllImport("Advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern bool QueryServiceConfig2W(
SafeServiceHandle hService,
NativeHelpers.ServiceInfoLevel dwInfoLevel,
SafeMemoryBuffer lpBuffer,
UInt32 cbBufSize,
out UInt32 pcbBytesNeeded);
}
internal class SafeMemoryBuffer : SafeHandleZeroOrMinusOneIsInvalid
{
public SafeMemoryBuffer() : base(true) { }
public SafeMemoryBuffer(int cb) : base(true)
{
base.SetHandle(Marshal.AllocHGlobal(cb));
}
public SafeMemoryBuffer(IntPtr handle) : base(true)
{
base.SetHandle(handle);
}
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
protected override bool ReleaseHandle()
{
Marshal.FreeHGlobal(handle);
return true;
}
}
internal class SafeServiceHandle : SafeHandleZeroOrMinusOneIsInvalid
{
public SafeServiceHandle() : base(true) { }
public SafeServiceHandle(IntPtr handle) : base(true) { this.handle = handle; }
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
protected override bool ReleaseHandle()
{
return NativeMethods.CloseServiceHandle(handle);
}
}
internal class SafeTokenHandle : SafeHandleZeroOrMinusOneIsInvalid
{
public SafeTokenHandle() : base(true) { }
public SafeTokenHandle(IntPtr handle) : base(true) { this.handle = handle; }
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
protected override bool ReleaseHandle()
{
return NativeMethods.CloseHandle(handle);
}
}
public class Win32Exception : System.ComponentModel.Win32Exception
{
private string _msg;
public Win32Exception(string message) : this(Marshal.GetLastWin32Error(), message) { }
public Win32Exception(int errorCode, string message) : base(errorCode)
{
_msg = String.Format("{0} ({1}, Win32ErrorCode {2} - 0x{2:X8})", message, base.Message, errorCode);
}
public override string Message { get { return _msg; } }
public static explicit operator Win32Exception(string message) { return new Win32Exception(message); }
}
public enum ActionType : uint
{
None = 0,
Restart = 1,
Reboot = 2,
RunCommand = 3,
}
public class FailureActions
{
public UInt32 ResetPeriodSeconds;
public string Command;
public string RebootMsg;
public List<Action> Actions;
}
public class Action
{
public ActionType ActionType;
public UInt32 DelayMilliseconds;
}
internal class PrivilegeEnabler : IDisposable
{
public PrivilegeEnabler(string privilegeName)
{
ImpersonateSelf(NativeHelpers.SecurityImpersonationLevel.Impersonation);
IntPtr hThread = NativeMethods.GetCurrentThread();
SafeTokenHandle tHandle = null;
// Open with TokenAdjustPrivileges - 0x00000020
if (!NativeMethods.OpenThreadToken(hThread, 0x00000020, true, out tHandle))
throw new Win32Exception("Failed to open current thread's token with AdjustPrivilege access");
using (tHandle)
EnablePrivilege(tHandle, privilegeName);
}
private void EnablePrivilege(SafeTokenHandle tHandle, string privilegeName)
{
if (String.IsNullOrEmpty(privilegeName))
return;
NativeHelpers.LUID privilegeLuid = new NativeHelpers.LUID();
if (!NativeMethods.LookupPrivilegeValueW(null, privilegeName, ref privilegeLuid))
throw new Win32Exception(String.Format("Failed to lookup privilege LUID for '{0}'", privilegeName));
NativeHelpers.LUID_AND_ATTRIBUTES privileges = new NativeHelpers.LUID_AND_ATTRIBUTES();
privileges.Luid = privilegeLuid;
privileges.Attributes = NativeHelpers.TokenPrivilegeAttributes.Enabled;
NativeHelpers.TOKEN_PRIVILEGES tokenPrivileges = new NativeHelpers.TOKEN_PRIVILEGES();
tokenPrivileges.PrivilegeCount = 1;
tokenPrivileges.Privileges = new NativeHelpers.LUID_AND_ATTRIBUTES[1];
tokenPrivileges.Privileges[0] = privileges;
if (!NativeMethods.AdjustTokenPrivileges(tHandle, false, tokenPrivileges, 0, IntPtr.Zero, IntPtr.Zero))
throw new Win32Exception(String.Format("Failed to enable the privilege '{0}' on the current thread", privilegeName));
}
private void ImpersonateSelf(NativeHelpers.SecurityImpersonationLevel level)
{
if (!NativeMethods.ImpersonateSelf(level))
throw new Win32Exception("Failed to impersonate self");
}
private void RevertToSelf()
{
if (!NativeMethods.RevertToSelf())
throw new Win32Exception("Failed to revert thread impersonation");
}
public void Dispose()
{
RevertToSelf();
}
~PrivilegeEnabler() { this.Dispose(); }
}
public class ServiceUtil
{
public static FailureActions GetFailureActions(string computerName, string serviceName)
{
using (SafeServiceHandle hService = OpenService(computerName, serviceName, NativeHelpers.SCMAccessRights.SC_MANAGER_CONNECT,
NativeHelpers.ServiceRights.SERVICE_QUERY_CONFIG))
using (SafeMemoryBuffer buffer = QueryServiceConfig(hService, NativeHelpers.ServiceInfoLevel.SERVICE_CONFIG_FAILURE_ACTIONS))
{
NativeHelpers.SERVICE_FAILURE_ACTIONSW rawFailureActions = (NativeHelpers.SERVICE_FAILURE_ACTIONSW)
Marshal.PtrToStructure(buffer.DangerousGetHandle(), typeof(NativeHelpers.SERVICE_FAILURE_ACTIONSW));
List<Action> actions = new List<Action>();
IntPtr lpActionBuffer = rawFailureActions.lpsaActions;
for (int i = 0; i < rawFailureActions.cActions; i++)
{
NativeHelpers.SC_ACTION scAction = (NativeHelpers.SC_ACTION)
Marshal.PtrToStructure(lpActionBuffer, typeof(NativeHelpers.SC_ACTION));
actions.Add(new Action()
{
ActionType = scAction.Type,
DelayMilliseconds = scAction.Delay,
});
lpActionBuffer = IntPtr.Add(lpActionBuffer, Marshal.SizeOf(scAction));
}
return new FailureActions()
{
ResetPeriodSeconds = rawFailureActions.dwResetPeriod,
Command = rawFailureActions.lpCommand,
RebootMsg = rawFailureActions.lpRebootMsg,
Actions = actions,
};
}
}
public static bool GetFailureActionsFlag(string computerName, string serviceName)
{
using (SafeServiceHandle hService = OpenService(computerName, serviceName, NativeHelpers.SCMAccessRights.SC_MANAGER_CONNECT,
NativeHelpers.ServiceRights.SERVICE_QUERY_CONFIG))
using (SafeMemoryBuffer buffer = QueryServiceConfig(hService, NativeHelpers.ServiceInfoLevel.SERVICE_CONFIG_FAILURE_ACTIONS_FLAG))
{
byte[] boolBytes = new byte[4];
Marshal.Copy(buffer.DangerousGetHandle(), boolBytes, 0, 4);
return BitConverter.ToBoolean(boolBytes, 0);
}
}
public static void SetFailureActions(string computerName, string serviceName, FailureActions actions)
{
NativeHelpers.ServiceRights rights = NativeHelpers.ServiceRights.SERVICE_CHANGE_CONFIG;
int bufferSize = Marshal.SizeOf(typeof(NativeHelpers.SERVICE_FAILURE_ACTIONSW)) +
(Marshal.SizeOf(typeof(NativeHelpers.SC_ACTION)) * actions.Actions.Count);
using (SafeMemoryBuffer buffer = new SafeMemoryBuffer(bufferSize))
{
IntPtr actionsPtr = IntPtr.Add(buffer.DangerousGetHandle(),
Marshal.SizeOf(typeof(NativeHelpers.SERVICE_FAILURE_ACTIONSW)));
NativeHelpers.SERVICE_FAILURE_ACTIONSW rawActions = new NativeHelpers.SERVICE_FAILURE_ACTIONSW()
{
dwResetPeriod = actions.ResetPeriodSeconds,
lpRebootMsg = actions.RebootMsg,
lpCommand = actions.Command,
cActions = (UInt32)actions.Actions.Count,
lpsaActions = actionsPtr,
};
Marshal.StructureToPtr(rawActions, buffer.DangerousGetHandle(), false);
string privilegeName = null;
foreach (Action action in actions.Actions)
{
if (action.ActionType == ActionType.Restart)
// Need to make sure SERVICE_START is also set if we have a restart service action.
rights |= NativeHelpers.ServiceRights.SERVICE_START;
else if (action.ActionType == ActionType.Reboot)
// Need to make sure we have the SeShutdownPrivilege enabled if we set a reboot service action.
privilegeName = "SeShutdownPrivilege";
NativeHelpers.SC_ACTION rawAction = new NativeHelpers.SC_ACTION()
{
Type = action.ActionType,
Delay = action.DelayMilliseconds,
};
Marshal.StructureToPtr(rawAction, actionsPtr, false);
actionsPtr = IntPtr.Add(actionsPtr, Marshal.SizeOf(rawAction));
}
using (new PrivilegeEnabler(privilegeName))
using (SafeServiceHandle hService = OpenService(computerName, serviceName, NativeHelpers.SCMAccessRights.SC_MANAGER_CONNECT, rights))
if (!NativeMethods.ChangeServiceConfig2W(hService, NativeHelpers.ServiceInfoLevel.SERVICE_CONFIG_FAILURE_ACTIONS, buffer))
throw new Win32Exception(String.Format("Failed to set config SERVICE_CONFIG_FAILURE_ACTIONS for '{0}'", serviceName));
}
}
public static void SetFailureActionsFlag(string computerName, string serviceName, bool allowWithStop)
{
using (SafeServiceHandle hService = OpenService(computerName, serviceName, NativeHelpers.SCMAccessRights.SC_MANAGER_CONNECT,
NativeHelpers.ServiceRights.SERVICE_CHANGE_CONFIG))
using (SafeMemoryBuffer buffer = new SafeMemoryBuffer(4))
{
// Make sure the pointer is 0'd out before copying the bool as that is only 1 byte.
byte[] emptyBytes = new byte[4];
Marshal.Copy(emptyBytes, 0, buffer.DangerousGetHandle(), emptyBytes.Length);
byte[] boolBytes = BitConverter.GetBytes(allowWithStop);
Marshal.Copy(boolBytes, 0, buffer.DangerousGetHandle(), boolBytes.Length);
if (!NativeMethods.ChangeServiceConfig2W(hService, NativeHelpers.ServiceInfoLevel.SERVICE_CONFIG_FAILURE_ACTIONS_FLAG, buffer))
throw new Win32Exception(String.Format("Failed to set config SERVICE_CONFIG_FAILURE_ACTIONS_FLAG for '{0}'", serviceName));
}
}
private static SafeServiceHandle OpenService(string computerName, string serviceName, NativeHelpers.SCMAccessRights scmAccess,
NativeHelpers.ServiceRights serviceAccess)
{
using (SafeServiceHandle hSCM = NativeMethods.OpenSCManagerW(computerName, null, scmAccess))
{
if (hSCM.IsInvalid)
throw new Win32Exception("Failed to open SCManager");
SafeServiceHandle hService = NativeMethods.OpenServiceW(hSCM, serviceName, serviceAccess);
if (hService.IsInvalid)
throw new Win32Exception(String.Format("Failed to open service '{0}'", serviceName));
return hService;
}
}
private static SafeMemoryBuffer QueryServiceConfig(SafeServiceHandle hService, NativeHelpers.ServiceInfoLevel infoLevel)
{
UInt32 bytesRequired = 0;
if (!NativeMethods.QueryServiceConfig2W(hService, infoLevel, new SafeMemoryBuffer(0), 0, out bytesRequired))
{
int errorCode = Marshal.GetLastWin32Error();
if (errorCode != 122) // ERROR_INSUFFICIENT_BUFFER
throw new Win32Exception(errorCode, String.Format("Failed to get buffer size for service {0}", infoLevel.ToString()));
}
SafeMemoryBuffer buffer = new SafeMemoryBuffer((int)bytesRequired);
if (!NativeMethods.QueryServiceConfig2W(hService, infoLevel, buffer, bytesRequired, out bytesRequired))
throw new Win32Exception(String.Format("Failed to get service {0}", infoLevel.ToString()));
return buffer;
}
}
}
'@
Function Get-ServiceRecovery {
<#
.SYNOPSIS
Gets the recovery settings for a Windows service.
.DESCRIPTION
Gets the various settings relating to recovery on a Windows service. These are the settings in the 'Recovery' tab
when viewing the service in services.msc.
.PARAMETER Name
The name of the service to get the recovery info for.
.PARAMETER ComputerName
The host to lookup the service on, defaults to localhost if not set or an empty string.
.OUTPUTS
[PSCustomObject] - A PSCustomObject of the service's recovery settings, it contains the following properties:
Name:
The name of the service.
ComputerName:
The host the service was on, "" for localhost.
Actions:
A list of actions and their delay time in milliseconds. Each action can have an ActionType of
None - No action
Reboot - Reboot the computer
Restart - Restart the service
RunCommand - Run the command
The service control manager counts the number of times each service has failed since the system booted.
The count is reset to 0 if the service has not failed for ResetPeriodSeconds. When the service fails for
the Nth time, the service controller performs the Nth action of the Actions list. If N is greater than
the size of the list, the service controller repeats the last action in the list.
ResetPeriodSeconds:
The time, in seconds, after which to reset the failure count to zero if there are no failures. This
corresponds to the 'Reset fail count after:' value except this is in seconds not days.
Command:
The command line of the process to execute in response to a RunCommand recovery action. This process runs
under the same account as the service.
RebootMsg:
The message to be broadcast to server users before rebooting in response to a Reboot recovery action.
RecoverOnStopOnError:
By default failure actions are queued if the service process terminates when it is stopped without
reporting it stopped naturally. When this is set to $true then a failure action is also queued when a
service is stopped but its exit code was not 0 even if it stopped gracefully.
This setting corresponds to the 'Enable actions for stop with errors' checkbox in the recovery tab.
.EXAMPLE
Get-ServiceRecovery -Name Brower
.NOTES
Some of the returned properties don't easily relate to the fields in the Recovery tab. The data structure returned
is designed to match the Win32 API as much as possible to fully expose the service's recovery settings.
#>
[CmdletBinding()]
Param (
[Parameter(Mandatory=$true, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)]
[System.String]
$Name,
[System.String]
$ComputerName = ""
)
Process {
try {
$recoveryInfo = [ServiceManager.ServiceUtil]::GetFailureActions($ComputerName, $Name)
$recoveryFlag = [ServiceManager.ServiceUtil]::GetFailureActionsFlag($ComputerName, $Name)
$actions = foreach ($recoveryAction in $recoveryInfo.Actions) {
[PSCustomObject]@{
PSTypeName = 'ServiceRecoveryAction'
ActionType = $recoveryAction.ActionType
DelayMilliseconds = $recoveryAction.DelayMilliseconds
}
}
[PSCustomObject]@{
PSTypeName = 'ServiceRecoveryInfo'
Name = $Name
ComputerName = $ComputerName
Actions = $actions
ResetPeriodSeconds = $recoveryInfo.ResetPeriodSeconds
Command = $recoveryInfo.Command
RebootMsg = $recoveryInfo.RebootMsg
RecoverOnStopOnError = $recoveryFlag
}
} catch {
$PSCmdlet.ThrowTerminatingError($PSItem)
}
}
}
Function Set-ServiceRecovery {
<#
.SYNOPSIS
Set a service's recovery settings.
.DESCRIPTION
Sets the service's recovery settings on a Windows service. These are the settings in the 'Recovery' tab when
viewing the service in services.msc.
.PARAMETER Name
The name of the service.
.PARAMETER ComputerName
The host the service is on, omit or set to "" for localhost.
.PARAMETER Actions
A list of actions to run on a failure. Each entry can be just the ActionType or a PSCustomObject that contains
the ActionType as well as the DelayMilliseconds property. The following ActionTypes are supported:
None - No action
Reboot - Reboot the computer
Restart - Restart the service
RunCommand - Run the command
See the examples for more info on how to set this.
.PARAMETER ResetPeriodSeconds
The time, in seconds, after which to reset the failure count to zero if there are no failures. This corresponds to
the 'Reset fail count after:' value except this is in seconds not days. Defaults to INIFINITE which means never reset
the failure count. If set to 0 then the action count is always 0 so action 2+ will never fire.
.PARAMETER Command
The command line of the process to execute in response to a RunCommand recovery action. This process runs under the
same account as the service. Do not specify or set to $null to keep the existing command or set to an empty string
to remove the command.
.PARAMETER RebootMsg
The message to be broadcast to server users before rebooting in response to a Reboot recovery action. Do not
specify or set to $null to keep the existing command or set to an empty string to remove the reboot message.
.PARAMETER RecoverOnStopOnError
By default failure actions are queued if the service process terminates when it is stopped without reporting it
stopped naturally. When this is set to $true then a failure action is also queued when a service is stopped but its
exit code was not 0 even if it stopped gracefully.
This setting corresponds to the 'Enable actions for stop with errors' checkbox in the recovery tab.
.EXAMPLE Remove all recovery actions from a service
Set-ServiceRecovery -Name MyService -Actions $null
.EXAMPLE Set a service with recovery actions
$set_params = @{
Name = 'MyService'
Actions = @('RunCommand', 'Restart', 'None')
Command = 'C:\Windows\System32\cmd.exe /c echo hi'
}
Set-ServiceRecovery @set_params
.EXAMPLE Set a service with recovery actions and a delay
$set_params = @{
Name = 'MyService'
Actions = @('RunCommand', 'Restart', 'None')
Command = 'C:\Windows\System32\cmd.exe /c echo hi'
}
Set-ServiceRecovery @set_params
.EXAMPLE Set a service with a recovery action and reboot message
$set_params = @{
Name = 'MyService'
Actions = @(
@{ActionType = 'Restart'; DelayMilliseconds = 1000},
@{ActionType = 'Reboot'; DelayMilliseconds = 60000}
)
RebootMsg = 'Shut down everything!'
}
Set-ServiceRecovery @set_params
.NOTES
The service control manager counts the number of times each service has failed since the system booted. The count
is reset to 0 if the service has not failed for ResetPeriodSeconds. When the service fails for the Nth time, the
service controller performs the Nth action of the Actions list. If N is greater than the size of the list, the
service controller repeats the last action in the list.
#>
[CmdletBinding(SupportsShouldProcess=$true)]
Param (
[Parameter(Mandatory=$true, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)]
[System.String]
$Name,
[Parameter(ValueFromPipelineByPropertyName=$true)]
[System.String]
$ComputerName = "",
[Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true)]
[AllowEmptyCollection()]
[AllowNull()]
[Object[]]
$Actions,
[Parameter(ValueFromPipelineByPropertyName=$true)]
[System.UInt32]
$ResetPeriodSeconds = ([System.UInt32]::MaxValue), # INFINITE
[Parameter(ValueFromPipelineByPropertyName=$true)]
[Object]
$Command = $null,
[Parameter(ValueFromPipelineByPropertyName=$true)]
[Object]
$RebootMsg = $null,
[Parameter(ValueFromPipelineByPropertyName=$true)]
[Boolean]
$RecoverOnStopOnError
)
Process {
try {
$failureActions = New-Object -TypeName ServiceManager.FailureActions
$failureActions.ResetPeriodSeconds = $ResetPeriodSeconds
# Both Command and RebootMsg have logic which is $null == preserve existing and "" == remove existing.
# Due to .NET marshaling we need to set $null to [NullString]::Value to ensure the value is $null when it
# reaches the .NET code.
if ($null -ne $Command) {
$failureActions.Command = [String]$Command
} else {
$failureActions.Command = [NullString]::Value
}
if ($null -ne $RebootMsg) {
$failureActions.RebootMsg = [String]$RebootMsg
} else {
$failureActions.RebootMsg = [NullString]::Value
}
if ($null -ne $Actions -and $Actions.Length -gt 0) {
$failureActions.Actions = @($Actions | ForEach-Object -Process {
$currentAction = New-Object -TypeName ServiceManager.Action
$currentAction.DelayMilliseconds = 0 # Set the default in case it isn't defined by the user.
if ($_ -is [System.Collections.IDictionary]) {
if (-not $_.ContainsKey('ActionType')) {
throw "Actions entry does not contain key 'ActionType'"
}
$currentAction.ActionType = $_.ActionType
if ($_.ContainsKey('DelayMilliseconds')) {
$currentAction.DelayMilliseconds = $_.DelayMilliseconds
}
} elseif ($_ -is [System.Management.Automation.PSCustomObject]) {
$properties = $_.PSObject.Properties.Name
if ('ActionType' -notin $properties) {
throw "Actions entry does not contain key 'ActionType'"
}
$currentAction.ActionType = $_.ActionType
if ('DelayMilliseconds' -in $properties) {
$currentAction.ActionType = $_.DelayMilliseconds
}
} else {
$currentAction.ActionType = $_
}
$currentAction
})
} else {
$failureActions.Actions = @()
}
if ($PSCmdlet.ShouldProcess($Name, "Set recovery actions")) {
[ServiceManager.ServiceUtil]::SetFailureActions($ComputerName, $Name, $failureActions)
}
if ($PSBoundParameters.ContainsKey('RecoverOnStopOnError')) {
if ($PSCmdlet.ShouldProcess($Name, "Set recover on stop")) {
[ServiceManager.ServiceUtil]::SetFailureActionsFlag($ComputerName, $Name, $RecoverOnStopOnError)
}
}
} catch {
$PSCmdlet.ThrowTerminatingError($PSItem)
}
}
}
@RandomNoun7
Copy link

Hello,
I'm working on a PR to teach the xService DSC resource to manage failure actions. I was 90% done with the functional code when I ran into a road block. It seems that simply managing the associated registry key is not enough to manage the Enable actions for stops with errors check box. The associated key being the FailureActionsOnNonCrashFailures registry key value for each service.

The problem is bad enough that it seems sc.exe itself is broken and cannot manage this feature. I have opened a Stack Overflow issue about it here to which no one seems to want to respond.

However, I have tested this code here in your gist and it works perfectly. With very little modification at all it could be adapted to work in a DSC context, and if you don't mind I would either like to do exactly that, or given that you are the author of this code invite you to take over the issue with your own PR to supersede mine.

Thanks,
Bill

@jborean93
Copy link
Author

Hey, by all means copy whatever you need.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment