Last active
February 28, 2024 03:27
-
-
Save jborean93/889288b56087a2c5def7fa49b6a8a0ad to your computer and use it in GitHub Desktop.
Get and Set the Windows service recovery options
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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) | |
} | |
} | |
} |
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
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 theFailureActionsOnNonCrashFailures
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