// Copyright Epic Games, Inc. All Rights Reserved. using System.ComponentModel; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; using Microsoft.Win32.SafeHandles; namespace HordeAgent.Utility { /// /// Native methods for manipulating services on Windows /// static partial class Native { #pragma warning disable CS0649 public class ServiceHandle : SafeHandleZeroOrMinusOneIsInvalid { public ServiceHandle() : base(true) { } public ServiceHandle(IntPtr handle) : base(true) { SetHandle(handle); } protected override bool ReleaseHandle() { return Native.CloseServiceHandle(handle); } } public const uint SC_MANAGER_ALL_ACCESS = 0xF003F; [DllImport("advapi32.dll", EntryPoint = "OpenSCManagerW", ExactSpelling = true, CharSet = CharSet.Unicode, SetLastError = true)] public static extern ServiceHandle OpenSCManager(string? machineName, string? databaseName, uint dwAccess); public const uint SERVICE_ALL_ACCESS = 0xf01ff; public const uint SERVICE_WIN32_OWN_PROCESS = 0x00000010; public const uint SERVICE_AUTO_START = 0x00000002; public const uint SERVICE_DEMAND_START = 0x00000003; public const uint SERVICE_ERROR_NORMAL = 0x00000001; [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Auto)] public static extern ServiceHandle CreateService(ServiceHandle hSCManager, string lpServiceName, string lpDisplayName, uint dwDesiredAccess, uint dwServiceType, uint dwStartType, uint dwErrorControl, string lpBinaryPathName, string? lpLoadOrderGroup, string? lpdwTagId, string? lpDependencies, string? lpServiceStartName, string? lpPassword); [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Auto)] public static extern ServiceHandle OpenService(ServiceHandle hSCManager, string lpServiceName, uint dwDesiredAccess); [DllImport("advapi32", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] public static extern bool StartService(ServiceHandle hService, int dwNumServiceArgs, string[]? lpServiceArgVectors); public const int ERROR_ACCESS_DENIED = 5; public const int ERROR_SERVICE_DOES_NOT_EXIST = 1060; public const int ERROR_SERVICE_NOT_ACTIVE = 1062; [DllImport("advapi32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] public static extern bool CloseServiceHandle(IntPtr hSCObject); [DllImport("advapi32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] public static extern bool DeleteService(ServiceHandle hService); public const int SC_STATUS_PROCESS_INFO = 0; [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "")] public struct SERVICE_STATUS_PROCESS { public uint dwServiceType; public uint dwCurrentState; public uint dwControlsAccepted; public uint dwWin32ExitCode; public uint dwServiceSpecificExitCode; public uint dwCheckPoint; public uint dwWaitHint; public uint dwProcessId; public uint dwServiceFlags; } [DllImport("advapi32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] public static extern bool QueryServiceStatusEx(ServiceHandle hService, int infoLevel, ref SERVICE_STATUS_PROCESS pBuffer, int cbBufSize, out int pcbBytesNeeded); [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "")] public struct SERVICE_STATUS { public uint dwServiceType; public uint dwCurrentState; public uint dwControlsAccepted; public uint dwWin32ExitCode; public uint dwServiceSpecificExitCode; public uint dwCheckPoint; public uint dwWaitHint; } public const int SERVICE_CONTROL_STOP = 1; [DllImport("advapi32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] public static extern bool ControlService(ServiceHandle hService, int dwControl, ref SERVICE_STATUS lpServiceStatus); public const int SERVICE_CONFIG_DESCRIPTION = 1; [StructLayout(LayoutKind.Sequential)] [SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "Native struct")] public struct SERVICE_DESCRIPTION { [MarshalAs(UnmanagedType.LPWStr)] public string lpDescription; } public const int SERVICE_CONFIG_FAILURE_ACTIONS = 0x2; [StructLayout(LayoutKind.Sequential)] [SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "Native struct")] public struct SERVICE_FAILURE_ACTIONS { public int dwResetPeriod; [MarshalAs(UnmanagedType.LPWStr)] public string? lpRebootMsg; [MarshalAs(UnmanagedType.LPWStr)] public string? lpCommand; public int cActions; public IntPtr lpsaActions; } [StructLayout(LayoutKind.Sequential)] [SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "Native struct")] public struct SERVICE_ACTION { public WindowsServiceRecoverAction type; public uint dwDelay; } [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Auto)] [return: MarshalAs(UnmanagedType.Bool)] public static extern bool ChangeServiceConfig2(ServiceHandle hService, int dwInfoLevel, IntPtr lpInfo); #pragma warning restore CS0649 } enum WindowsServiceRecoverAction : int { /// /// No Recover Action /// None = 0, /// /// Restart the service. /// Restart = 1, /// /// Reboot the host. /// Reboot = 2, /// /// Run a command /// RunCommand = 3 } enum WindowsServiceStatus : uint { /// /// The service has stopped. /// Stopped = 1, /// /// The service is starting. /// Starting = 2, /// /// The service is stopping. /// Stopping = 3, /// /// The service is running /// Running = 4, /// /// The service is about to continue. /// Continuing = 5, /// /// The service is pausing /// Pausing = 6, /// /// The service is paused. /// Paused = 7, } /// /// Wrapper around /// class WindowsService : IDisposable { /// /// Handle to the service /// private readonly Native.ServiceHandle _handle; /// /// Constructor /// /// Handle to the service public WindowsService(Native.ServiceHandle handle) { _handle = handle; } /// /// Determines if the service is valid /// public bool IsValid => !_handle.IsInvalid; /// /// Gets the current service status /// /// Status code public WindowsServiceStatus GetStatus() { Native.SERVICE_STATUS_PROCESS status = new Native.SERVICE_STATUS_PROCESS(); if (!Native.QueryServiceStatusEx(_handle, Native.SC_STATUS_PROCESS_INFO, ref status, Marshal.SizeOf(status), out int _)) { throw new Win32Exception(String.Format("Unable to query process status (0x{0:X8)}", Marshal.GetLastWin32Error())); } return (WindowsServiceStatus)status.dwCurrentState; } /// /// Waits for the status to change /// /// Expected status while transitioning /// Maximum time to wait public WindowsServiceStatus WaitForStatusChange(WindowsServiceStatus transitionStatus, TimeSpan maxWaitTime) { Stopwatch timer = Stopwatch.StartNew(); for (; ; ) { WindowsServiceStatus status = GetStatus(); if (status != transitionStatus || timer.Elapsed > maxWaitTime) { return status; } else { Thread.Sleep(1000); } } } /// /// Sets the service description /// /// Description for the service public void SetDescription(string description) { Native.SERVICE_DESCRIPTION descriptionData = new Native.SERVICE_DESCRIPTION(); descriptionData.lpDescription = description; nint descriptionDataPointer = Marshal.AllocHGlobal(Marshal.SizeOf(descriptionData)); Marshal.StructureToPtr(descriptionData, descriptionDataPointer, false); try { Native.ChangeServiceConfig2(_handle, Native.SERVICE_CONFIG_DESCRIPTION, descriptionDataPointer); } finally { Marshal.FreeHGlobal(descriptionDataPointer); } } /// /// Set the recovery actions for the windows service /// /// /// public void ChangeServiceFailureActions(params WindowsServiceRecoverAction[] recoveryActions) { const int ActionsCount = 3; const int ActionsSize = ActionsCount * sizeof(int) * 2; if (recoveryActions == null) { throw new InvalidDataException("No recoveryActions specified, they must be specified."); } if (recoveryActions.Length != ActionsCount) { throw new InvalidDataException($"recoveryActions should contain {ActionsCount} and only contains {recoveryActions.Length} actions."); } // Represents SERVICE_ACTION a collection of ints // Back of restart time for each retry attempt to accommodate for server/network issues int[] serviceActions = { (int)recoveryActions[0], 10, (int)recoveryActions[1], 30, (int)recoveryActions[2], 60, }; nint actionsBuffer = Marshal.AllocHGlobal(ActionsSize); Marshal.Copy(serviceActions, 0, actionsBuffer, serviceActions.Length); Native.SERVICE_FAILURE_ACTIONS serviceFailureActions = new() { cActions = ActionsCount, dwResetPeriod = (int)TimeSpan.FromDays(30).TotalSeconds, lpCommand = null, lpRebootMsg = null, lpsaActions = new IntPtr(actionsBuffer) }; nint serviceActionsPointer = Marshal.AllocHGlobal(Marshal.SizeOf(serviceFailureActions)); Marshal.StructureToPtr(serviceFailureActions, serviceActionsPointer, false); try { bool result = Native.ChangeServiceConfig2(_handle, Native.SERVICE_CONFIG_FAILURE_ACTIONS, serviceActionsPointer); if (!result) { throw new Win32Exception($"Unable to set Service Failure Actions (error 0x{Marshal.GetLastWin32Error():X8})"); } } finally { Marshal.FreeHGlobal(actionsBuffer); Marshal.FreeHGlobal(serviceActionsPointer); } } /// /// Starts the service /// public void Start() { if (!Native.StartService(_handle, 0, null)) { throw new Win32Exception($"Unable to start service (error 0x{Marshal.GetLastWin32Error():X8})"); } } /// /// Stops the service /// public void Stop() { Native.SERVICE_STATUS status = new Native.SERVICE_STATUS(); if (!Native.ControlService(_handle, Native.SERVICE_CONTROL_STOP, ref status)) { int error = Marshal.GetLastWin32Error(); if (error != Native.ERROR_SERVICE_NOT_ACTIVE) { throw new Win32Exception($"Unable to stop service (error 0x{error:X8})"); } } } /// /// Deletes the service /// public void Delete() { if (!Native.DeleteService(_handle)) { throw new Win32Exception($"Unable to delete service (error 0x{Marshal.GetLastWin32Error():X8})"); } } /// /// Dispose of the service handle /// public void Dispose() { _handle.Close(); } } /// /// Helper functionality for manipulating Windows services /// class WindowsServiceManager : IDisposable { /// /// Native handle to the service manager /// private readonly Native.ServiceHandle _serviceManagerHandle; /// /// Constructor. Opens a handle to the service manager. /// public WindowsServiceManager() { _serviceManagerHandle = Native.OpenSCManager(null, null, Native.SC_MANAGER_ALL_ACCESS); if (_serviceManagerHandle.IsInvalid) { int errorCode = Marshal.GetLastWin32Error(); if (errorCode == Native.ERROR_ACCESS_DENIED) { throw new Win32Exception("Unable to open service manager (access denied). Check you're running as administrator."); } else { throw new Win32Exception($"Unable to open service manager (0x{errorCode:X8})."); } } } /// /// Dispose of this object /// public void Dispose() { _serviceManagerHandle.Close(); } /// /// Opens a service with the given name /// /// Name of the service /// New service wrapper public WindowsService Open(string serviceName) { Native.ServiceHandle serviceHandle = Native.OpenService(_serviceManagerHandle, serviceName, Native.SERVICE_ALL_ACCESS); if (serviceHandle.IsInvalid) { int errorCode = Marshal.GetLastWin32Error(); if (errorCode != Native.ERROR_SERVICE_DOES_NOT_EXIST) { throw new Win32Exception("Unable to open handle to service"); } } return new WindowsService(serviceHandle); } /// /// Creates a service with the given settings /// /// Name of the service /// Display name /// Command line to use when starting the service /// Username to run this service as /// Password for the account the service is to run under /// New service instance public WindowsService Create(string name, string displayName, string commandLine, string? userName, string? password) { Native.ServiceHandle newServiceHandle = Native.CreateService(_serviceManagerHandle, name, displayName, Native.SERVICE_ALL_ACCESS, Native.SERVICE_WIN32_OWN_PROCESS, Native.SERVICE_AUTO_START, Native.SERVICE_ERROR_NORMAL, commandLine, null, null, null, userName, password); if (newServiceHandle.IsInvalid) { throw new Win32Exception(String.Format("Unable to create service (0x{0:X8})", Marshal.GetLastWin32Error())); } return new WindowsService(newServiceHandle); } } }