// Copyright 2026 sjackson0109 — Apache License 2.0
using System.Runtime.InteropServices;
namespace RDPWrap.Common;
///
/// Wrappers around the Windows Service Control Manager API, mirroring the
/// SvcGetStart / SvcConfigStart / SvcStart / CheckTermsrvProcess helpers
/// in RDPWInst.dpr and GetTermSrvState in RDPConf MainUnit.pas.
///
public static class ServiceHelper
{
// ── Start-type query / change ─────────────────────────────────────────────
///
/// Returns the configured start type of
/// (e.g. SERVICE_AUTO_START) or -1 on failure.
///
public static int GetStartType(string serviceName)
{
var hSC = NativeMethods.OpenSCManager(null, null, NativeMethods.SC_MANAGER_CONNECT);
if (hSC == IntPtr.Zero) return -1;
try
{
var hSvc = NativeMethods.OpenService(hSC, serviceName,
NativeMethods.SERVICE_QUERY_CONFIG);
if (hSvc == IntPtr.Zero) return -1;
try
{
NativeMethods.QueryServiceConfig(hSvc, IntPtr.Zero, 0, out uint needed);
var buf = Marshal.AllocHGlobal((int)needed);
try
{
if (!NativeMethods.QueryServiceConfig(hSvc, buf, needed, out _))
return -1;
// dwStartType is the second DWORD in QUERY_SERVICE_CONFIG
return Marshal.ReadInt32(buf, 4);
}
finally { Marshal.FreeHGlobal(buf); }
}
finally { NativeMethods.CloseServiceHandle(hSvc); }
}
finally { NativeMethods.CloseServiceHandle(hSC); }
}
///
/// Changes the start type of to
/// (one of the SERVICE_*_START constants).
///
public static bool SetStartType(string serviceName, uint startType)
{
var hSC = NativeMethods.OpenSCManager(null, null, NativeMethods.SC_MANAGER_CONNECT);
if (hSC == IntPtr.Zero) return false;
try
{
var hSvc = NativeMethods.OpenService(hSC, serviceName,
NativeMethods.SERVICE_CHANGE_CONFIG);
if (hSvc == IntPtr.Zero) return false;
try
{
return NativeMethods.ChangeServiceConfig(hSvc,
NativeMethods.SERVICE_NO_CHANGE, startType,
NativeMethods.SERVICE_NO_CHANGE,
null, null, IntPtr.Zero, null, null, null, null);
}
finally { NativeMethods.CloseServiceHandle(hSvc); }
}
finally { NativeMethods.CloseServiceHandle(hSC); }
}
// ── Start a service ───────────────────────────────────────────────────────
///
/// Starts . If the service is already
/// running (error 1056) it waits 2 s and retries once, matching the
/// original Delphi SvcStart behaviour.
/// Returns true on success.
///
public static bool StartService(string serviceName)
{
var hSC = NativeMethods.OpenSCManager(null, null, NativeMethods.SC_MANAGER_CONNECT);
if (hSC == IntPtr.Zero) return false;
try
{
var hSvc = NativeMethods.OpenService(hSC, serviceName,
NativeMethods.SERVICE_START);
if (hSvc == IntPtr.Zero) return false;
try
{
if (NativeMethods.StartService(hSvc, 0, null)) return true;
var err = Marshal.GetLastWin32Error();
if (err == (int)NativeMethods.ERROR_SERVICE_ALREADY_RUNNING)
{
Thread.Sleep(2000);
return NativeMethods.StartService(hSvc, 0, null);
}
return false;
}
finally { NativeMethods.CloseServiceHandle(hSvc); }
}
finally { NativeMethods.CloseServiceHandle(hSC); }
}
// ── Status query ──────────────────────────────────────────────────────────
///
/// Returns the dwCurrentState for
/// (e.g. SERVICE_RUNNING = 4) or -1 on failure.
///
public static int GetCurrentState(string serviceName)
{
var hSC = NativeMethods.OpenSCManager(null, null, NativeMethods.SC_MANAGER_CONNECT);
if (hSC == IntPtr.Zero) return -1;
try
{
var hSvc = NativeMethods.OpenService(hSC, serviceName,
NativeMethods.SERVICE_QUERY_STATUS);
if (hSvc == IntPtr.Zero) return -1;
try
{
NativeMethods.QueryServiceStatusEx(hSvc,
NativeMethods.SC_STATUS_PROCESS_INFO,
IntPtr.Zero, 0, out uint needed);
var buf = Marshal.AllocHGlobal((int)needed);
try
{
if (!NativeMethods.QueryServiceStatusEx(hSvc,
NativeMethods.SC_STATUS_PROCESS_INFO,
buf, needed, out _)) return -1;
// dwCurrentState is the second DWORD in SERVICE_STATUS_PROCESS
return Marshal.ReadInt32(buf, 4);
}
finally { Marshal.FreeHGlobal(buf); }
}
finally { NativeMethods.CloseServiceHandle(hSvc); }
}
finally { NativeMethods.CloseServiceHandle(hSC); }
}
// ── Process-info enumeration ──────────────────────────────────────────────
///
/// Represents the name and PID of a service process returned by
/// .
///
public record ServiceProcessInfo(string ServiceName, string DisplayName, uint ProcessId, uint CurrentState);
///
/// Enumerates all Win32 services (all states) and returns their
/// process information. Mirrors the EnumServicesStatusEx loop in
/// CheckTermsrvProcess.
///
public static IReadOnlyList EnumServiceProcesses()
{
var result = new List();
var hSC = NativeMethods.OpenSCManager(null, null,
NativeMethods.SC_MANAGER_CONNECT | NativeMethods.SC_MANAGER_ENUMERATE_SERVICE);
if (hSC == IntPtr.Zero) return result;
try
{
uint resumeHandle = 0;
// Ask for the required buffer size
NativeMethods.EnumServicesStatusEx(hSC,
NativeMethods.SC_ENUM_PROCESS_INFO,
NativeMethods.SERVICE_WIN32,
NativeMethods.SERVICE_STATE_ALL,
IntPtr.Zero, 0,
out uint needed, out _, ref resumeHandle, null);
if (needed == 0) return result;
var buf = Marshal.AllocHGlobal((int)needed);
try
{
resumeHandle = 0;
if (!NativeMethods.EnumServicesStatusEx(hSC,
NativeMethods.SC_ENUM_PROCESS_INFO,
NativeMethods.SERVICE_WIN32,
NativeMethods.SERVICE_STATE_ALL,
buf, needed,
out _, out uint returned, ref resumeHandle, null))
return result;
// ENUM_SERVICE_STATUS_PROCESS layout (Unicode, packed):
// IntPtr lpServiceName (pointer to string)
// IntPtr lpDisplayName (pointer to string)
// SERVICE_STATUS_PROCESS (9 × DWORD = 36 bytes)
int ptrSize = IntPtr.Size;
int entrySize = ptrSize * 2 + 36; // 2 string pointers + status struct
for (int i = 0; i < (int)returned; i++)
{
var entryPtr = buf + i * entrySize;
var namePtr = Marshal.ReadIntPtr(entryPtr);
var dispPtr = Marshal.ReadIntPtr(entryPtr + ptrSize);
var svcName = Marshal.PtrToStringUni(namePtr) ?? string.Empty;
var dispName = Marshal.PtrToStringUni(dispPtr) ?? string.Empty;
// dwCurrentState at offset 4, dwProcessId at offset 28
var statusBase = entryPtr + ptrSize * 2;
uint currentState = (uint)Marshal.ReadInt32(statusBase + 4);
uint pid = (uint)Marshal.ReadInt32(statusBase + 28);
result.Add(new ServiceProcessInfo(svcName, dispName, pid, currentState));
}
}
finally { Marshal.FreeHGlobal(buf); }
}
finally { NativeMethods.CloseServiceHandle(hSC); }
return result;
}
}