Search code examples
c#.netwindows-services

How to change windows service recovery option using c#


I have WCF service that is hosted on windows service. I installed this service using Windows installer. Sometimes, when i stop service using C# code, it stucks on stopping. So i thought, why not kill service if service is not stopping within 2 minutes. My code is below to stop service:

var service = ServiceController.GetServices()
                .FirstOrDefault(s => s.ServiceName == serviceName);
            try
            {
                if (service == null || service.Status != ServiceControllerStatus.Running) return;
                if(service.CanStop)
                {
                    session.LogInfo($"Stopping '{serviceName}'.");
                    TimeSpan timeout = TimeSpan.FromMilliseconds(ServiceStopTime);
                    service.Stop();
                    service.WaitForStatus(ServiceControllerStatus.Stopped, timeout);
                    session.LogInfo($"'{serviceName}' stopped successfully.");
                }

It is working as expected. I want to kill my process if service does not stop. Here is my code to kill process.

var processName = GetProcessNameByWindowsService(serviceName);
            if (processName == null) return;
            Process[] procs = Process.GetProcessesByName(processName);
            if (procs.Length > 0)
            {
                foreach (Process proc in procs)
                {
                    session.LogInfo($"Killing Process'{processName}'.");
                    proc.Kill();
                    session.LogInfo($"'{processName}' killed successfully.");
                }
            }

It is working as expected too but the problem is when i kill the process, the service does not stop. It assigns new process to service and service keep runs. After googled and investing some time i found the cause that is the window service recovery option which is restart the service if it fails. I want to change/set the recovery option for service in case of first failure, second failure and subsequent failure to take no action using C# code. I googled but did not find anything. So i want to know how i can change the recovery option of installed window service using C#?


Solution

  • After investing time finally i have found the solution with the help of this link. I have written two helper classes to set/update recovery option of windows service. First of all i wrote a static helper class which is below:

    using System;
    using System.Runtime.InteropServices;
    
    namespace HRTC.CustomActions.Helpers
    {
        public static class ServiceRecoveryOptionHelper
        {
            //Action Enum
            public enum RecoverAction
            {
                None = 0, Restart = 1, Reboot = 2, RunCommand = 3
            }
    
            [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
    
            public struct ServiceFailureActions
            {
                public int dwResetPeriod;
                [MarshalAs(UnmanagedType.LPWStr)]
    
                public string lpRebootMsg;
                [MarshalAs(UnmanagedType.LPWStr)]
    
                public string lpCommand;
                public int cActions;
                public IntPtr lpsaActions;
            }
    
            [StructLayout(LayoutKind.Sequential)]
            public class ScAction
            {
                public int type;
                public uint dwDelay;
            }
    
            // Win32 function to open the service control manager
            [DllImport("advapi32.dll")]
            public static extern IntPtr OpenSCManager(string lpMachineName, string lpDatabaseName, int dwDesiredAccess);
    
            // Win32 function to open a service instance
            [DllImport("advapi32.dll")]
            public static extern IntPtr OpenService(IntPtr hScManager, string lpServiceName, int dwDesiredAccess);
    
            // Win32 function to change the service config for the failure actions.
            [DllImport("advapi32.dll", EntryPoint = "ChangeServiceConfig2")]
    
            public static extern bool ChangeServiceFailureActions(IntPtr hService, int dwInfoLevel,
                [MarshalAs(UnmanagedType.Struct)]
                ref ServiceFailureActions lpInfo);
    
            [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true, EntryPoint = "QueryServiceConfig2W")]
            public static extern Boolean QueryServiceConfig2(IntPtr hService, UInt32 dwInfoLevel, IntPtr buffer, UInt32 cbBufSize, out UInt32 pcbBytesNeeded);
    
            [DllImport("kernel32.dll")]
            public static extern int GetLastError();
        }
        public class FailureAction
        {
            // Default constructor
            public FailureAction() { }
    
            // Constructor
            public FailureAction(ServiceRecoveryOptionHelper.RecoverAction actionType, int actionDelay)
            {
                Type = actionType;
                Delay = actionDelay;
            }
    
            // Property to set recover action type
            public ServiceRecoveryOptionHelper.RecoverAction Type { get; set; } = ServiceRecoveryOptionHelper.RecoverAction.None;
    
            // Property to set recover action delay
            public int Delay { get; set; }
        }
    }
    

    Then i already have static class for windows services that have different methods like to start windows service, stop windows service and install service etc. I added new static method in this class to change recovery option of windows service which receive 4 parameters. First one is the service name, and other three are the recovery options of first,second and subsequent recovery options respectively. Below is it's implementation.

    using System;
    using System.Collections;
    using System.Runtime.InteropServices;
    
    namespace HRTC.CustomActions.Helpers
    {
        public class LocalServiceHelper
        {
            //Change service recovery option settings
            private const int ServiceAllAccess = 0xF01FF;
            private const int ScManagerAllAccess = 0xF003F;
            private const int ServiceConfigFailureActions = 0x2;
            private const int ErrorAccessDenied = 5;
    
    
            public static void ChangeRevoveryOption(string serviceName, ServiceRecoveryOptionHelper.RecoverAction firstFailureAction,
                ServiceRecoveryOptionHelper.RecoverAction secondFailureAction, ServiceRecoveryOptionHelper.RecoverAction thirdFailureAction)
            {
                try
                {
                    // Open the service control manager
                    var scmHndl = ServiceRecoveryOptionHelper.OpenSCManager(null, null, ScManagerAllAccess);
                    if (scmHndl.ToInt32() <= 0)
                        return;
    
                    // Open the service
                    var svcHndl = ServiceRecoveryOptionHelper.OpenService(scmHndl, serviceName, ServiceAllAccess);
    
                    if (svcHndl.ToInt32() <= 0)
                        return;
    
                    var failureActions = new ArrayList
                    {
                        // First Failure Actions and Delay (msec)
                        new FailureAction(firstFailureAction, 0),
                        // Second Failure Actions and Delay (msec)
                        new FailureAction(secondFailureAction, 0),
                        // Subsequent Failures Actions and Delay (msec)
                        new FailureAction(thirdFailureAction, 0)
                    };
    
                    var numActions = failureActions.Count;
                    var myActions = new int[numActions * 2];
                    var currInd = 0;
    
                    foreach (FailureAction fa in failureActions)
                    {
                        myActions[currInd] = (int) fa.Type;
                        myActions[++currInd] = fa.Delay;
                        currInd++;
                    }
    
                    // Need to pack 8 bytes per struct
                    var tmpBuf = Marshal.AllocHGlobal(numActions * 8);
    
                    // Move array into marshallable pointer
                    Marshal.Copy(myActions, 0, tmpBuf, numActions * 2);
    
                    // Set the SERVICE_FAILURE_ACTIONS struct
                    var config =
                        new ServiceRecoveryOptionHelper.ServiceFailureActions
                        {
                            cActions = 3,
                            dwResetPeriod = 0,
                            lpCommand = null,
                            lpRebootMsg = null,
                            lpsaActions = new IntPtr(tmpBuf.ToInt32())
                        };
    
                    // Call the ChangeServiceFailureActions() abstraction of ChangeServiceConfig2()
                    var result =
                        ServiceRecoveryOptionHelper.ChangeServiceFailureActions(svcHndl, ServiceConfigFailureActions,
                            ref config);
    
                    //Check the return
                    if (!result)
                    {
                        var err = ServiceRecoveryOptionHelper.GetLastError();
                        if (err == ErrorAccessDenied)
                        {
                            throw new Exception("Access Denied while setting Failure Actions");
    
                        }
    
                        // Free the memory
                        Marshal.FreeHGlobal(tmpBuf);
                    }
                }
                catch (Exception)
                {
                    throw new Exception("Unable to set service recovery options");
                }
            }
        }
    
    }
    

    That's it. You just only need to call the method to change recovery option of windows service. For example:

    LocalServiceHelper.ChangeRevoveryOption("ServiceName",
                        ServiceRecoveryOptionHelper.RecoverAction.Restart,
                        ServiceRecoveryOptionHelper.RecoverAction.Restart,
                        ServiceRecoveryOptionHelper.RecoverAction.None);
    

    It will update the recovery option of windows service as you will mention when calling the method. Hope this help. Happy codding! :)