// Copyright Epic Games, Inc. All Rights Reserved.
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.ServiceProcess;
namespace HordeAgent.Utility;
///
/// Utility class for Windows services using Win32 API via P/Invoke
///
public static class NativeWindowsServiceUtils
{
#region P/Invoke
#pragma warning disable IDE1006 // Naming rule violation
private const uint SC_MANAGER_CONNECT = 0x0001;
private const uint SC_MANAGER_ENUMERATE_SERVICE = 0x0004;
private const uint SERVICE_WIN32 = 0x00000030; // SERVICE_WIN32_OWN_PROCESS and SERVICE_WIN32_SHARE_PROCESS combined
private const uint SERVICE_STATE_ALL = 0x00000003; // SERVICE_ACTIVE and SERVICE_INACTIVE states combined
private const int ERROR_MORE_DATA = 234;
private const int SC_ENUM_PROCESS_INFO = 0;
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private 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;
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private struct ENUM_SERVICE_STATUS_PROCESS
{
public string lpServiceName;
public string lpDisplayName;
public SERVICE_STATUS_PROCESS ServiceStatusProcess;
}
[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
private static extern IntPtr OpenSCManager(
string? machineName,
string? databaseName,
uint dwDesiredAccess);
[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Auto)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool EnumServicesStatusEx(
IntPtr hSCManager,
int InfoLevel, // SC_ENUM_PROCESS_INFO
uint dwServiceType,
uint dwServiceState,
IntPtr lpServices,
uint cbBufSize,
out uint pcbBytesNeeded,
out uint lpServicesReturned,
IntPtr lpResumeHandle, // Should be 0
string? pszGroupName); // Should be null
[DllImport("advapi32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool CloseServiceHandle(IntPtr hSCObject);
#pragma warning restore IDE1006 // Naming Styles
#endregion
///
/// Try to find the service controller for the given process ID using Win32 API.
///
/// The process ID to search for
/// The service controller corresponding to this process, or null if not found
[SupportedOSPlatform("windows")]
public static ServiceController? GetServiceForProcess(int processId)
{
if (processId <= 0)
{
return null;
}
IntPtr scmHandle = IntPtr.Zero;
IntPtr buffer = IntPtr.Zero;
uint bytesNeeded;
uint servicesReturned;
try
{
scmHandle = OpenSCManager(null, null, SC_MANAGER_CONNECT | SC_MANAGER_ENUMERATE_SERVICE);
if (scmHandle == IntPtr.Zero)
{
throw new InvalidOperationException($"Failed to open service control manager. Win32 Error: {Marshal.GetLastWin32Error()}");
}
// First call to get the required buffer size
EnumServicesStatusEx(
scmHandle,
SC_ENUM_PROCESS_INFO,
SERVICE_WIN32,
SERVICE_STATE_ALL,
IntPtr.Zero,
0,
out bytesNeeded,
out servicesReturned, // Unused in this call
IntPtr.Zero,
null);
int lastError = Marshal.GetLastWin32Error();
if (lastError != ERROR_MORE_DATA && !(lastError == 0 && bytesNeeded == 0))
{
throw new InvalidOperationException($"Failed to query buffer size for services. Win32 Error: {lastError}");
}
if (bytesNeeded == 0)
{
// Zero buffer size needed means nothing to process, no services to be found
return null;
}
// Allocate memory for the services information
buffer = Marshal.AllocHGlobal((int)bytesNeeded);
if (buffer == IntPtr.Zero)
{
throw new InvalidOperationException("Failed to allocate memory for service list.");
}
// Get the actual data
if (!EnumServicesStatusEx(scmHandle,
SC_ENUM_PROCESS_INFO,
SERVICE_WIN32,
SERVICE_STATE_ALL,
buffer,
bytesNeeded,
out _,
out servicesReturned,
IntPtr.Zero,
null))
{
throw new InvalidOperationException($"Failed to enumerate services. Win32 Error: {Marshal.GetLastWin32Error()}");
}
IntPtr currentPtr = buffer;
int structSize = Marshal.SizeOf(typeof(ENUM_SERVICE_STATUS_PROCESS));
for (int i = 0; i < servicesReturned; i++)
{
ENUM_SERVICE_STATUS_PROCESS serviceInfo = (ENUM_SERVICE_STATUS_PROCESS)Marshal.PtrToStructure(currentPtr, typeof(ENUM_SERVICE_STATUS_PROCESS))!;
uint servicePid = serviceInfo.ServiceStatusProcess.dwProcessId;
if (servicePid != 0 && servicePid == processId)
{
return new ServiceController(serviceInfo.lpServiceName);
}
currentPtr = IntPtr.Add(currentPtr, structSize);
}
}
finally
{
if (buffer != IntPtr.Zero)
{
Marshal.FreeHGlobal(buffer);
}
if (scmHandle != IntPtr.Zero)
{
CloseServiceHandle(scmHandle);
}
}
return null;
}
}