From 1476d08ea348342e54251954d02fed6f9cb5f811 Mon Sep 17 00:00:00 2001 From: jchas2 Date: Sun, 19 Jul 2026 09:49:32 +1000 Subject: [PATCH] Remove dep on ServiceController --- src/Task.Monitor.Interop.Win32/WinService.cs | 118 ++++++++++++------ .../PInvokeErrorHelpers.cs | 11 +- .../Process/ProcessService.Windows.cs | 10 +- .../Process/ServiceInfo.cs | 7 ++ .../Process/ServiceUtils.Windows.cs | 111 ++++++++++++++-- .../SystemTerminal.Windows.cs | 6 +- .../Task.Monitor.System.csproj | 3 - .../Process/ServiceUtilsTests.Windows.cs | 16 ++- 8 files changed, 220 insertions(+), 62 deletions(-) create mode 100644 src/Task.Monitor.System/Process/ServiceInfo.cs diff --git a/src/Task.Monitor.Interop.Win32/WinService.cs b/src/Task.Monitor.Interop.Win32/WinService.cs index acd4345..b016ce5 100644 --- a/src/Task.Monitor.Interop.Win32/WinService.cs +++ b/src/Task.Monitor.Interop.Win32/WinService.cs @@ -6,8 +6,13 @@ public static class WinService { public const int SC_MANAGER_CONNECT = 0x0001; public const uint SC_MANAGER_ALL_ACCESS = 0xF003F; + public const int SC_MANAGER_ENUMERATE_SERVICE = 0x0004; + + public const uint SERVICE_TYPE_ALL = 0x00000030; + public const uint SERVICE_STATE_ALL = 0x00000003; public const int SC_ENUM_PROCESS_INFO = 0; + public const uint INFO_LEVEL_STANDARD = 0; public const uint SERVICE_ERROR_NORMAL = 0x00000001; public const uint SERVICE_AUTO_START = 0x00000002; @@ -16,32 +21,56 @@ public static class WinService public const uint SERVICE_STOP = 0x00000020; public const uint SERVICE_WIN32 = 0x00000030; public const uint SERVICE_ALL_ACCESS = 0xF01FF; - - public const uint DELETE = 0x00010000; - [DllImport(Libraries.Advapi32, SetLastError = true)] - public static extern IntPtr OpenSCManager( - string lpMachineName, - string lpDatabaseName, - int dwDesiredAccess); + public enum ServiceCurrentState : uint + { + SERVICE_STOPPED = 0x00000001, + SERVICE_START_PENDING = 0x00000002, + SERVICE_STOP_PENDING = 0x00000003, + SERVICE_RUNNING = 0x00000004, + SERVICE_CONTINUE_PENDING = 0x00000005, + SERVICE_PAUSE_PENDING = 0x00000006, + SERVICE_PAUSED = 0x00000007 + } + + public const uint DELETE = 0x00010000; - [DllImport(Libraries.Advapi32, SetLastError = true)] - public static extern IntPtr OpenService( - IntPtr hSCManager, - string lpServiceName, - int dwDesiredAccess); + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public struct ENUM_SERVICE_STATUS_PROCESS + { + public string lpServiceName; + public string lpDisplayName; + public int dwServiceType; + public int dwCurrentState; + public int dwControlsAccepted; + public int dwWin32ExitCode; + public int dwServiceSpecificExitCode; + public int dwCheckPoint; + public int dwWaitHint; + public int dwProcessId; + public int dwServiceFlags; + } + + [StructLayout(LayoutKind.Sequential)] + public struct SERVICE_STATUS_PROCESS + { + public int dwServiceType; + public int dwCurrentState; + public int dwControlsAccepted; + public int dwWin32ExitCode; + public int dwServiceSpecificExitCode; + public int dwCheckPoint; + public int dwWaitHint; + public int dwProcessId; + public int dwServiceFlags; + } [DllImport(Libraries.Advapi32, SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] - public static extern bool QueryServiceStatusEx( - IntPtr hService, - int InfoLevel, - IntPtr lpBuffer, - uint cbBufSize, - out uint pcbBytesNeeded); - + public static extern bool CloseServiceHandle(IntPtr hSCObject); + [DllImport(Libraries.Advapi32, SetLastError = true, CharSet = CharSet.Auto)] - private static extern IntPtr CreateService( + public static extern IntPtr CreateService( IntPtr hSCManager, string lpServiceName, string lpDisplayName, @@ -58,23 +87,40 @@ private static extern IntPtr CreateService( [DllImport(Libraries.Advapi32, SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] - private static extern bool DeleteService(IntPtr hService); + public static extern bool DeleteService(IntPtr hService); - [DllImport(Libraries.Advapi32, SetLastError = true)] + [DllImport(Libraries.Advapi32, EntryPoint = "EnumServicesStatusExW", SetLastError = true, CharSet = CharSet.Unicode)] [return: MarshalAs(UnmanagedType.Bool)] - public static extern bool CloseServiceHandle(IntPtr hSCObject); + public static extern bool EnumServicesStatusEx( + IntPtr hSCManager, + uint InfoLevel, + uint dwServiceType, + uint dwServiceState, + IntPtr lpServices, + uint cbBufSize, + out uint pcbBytesNeeded, + out uint lpServicesReturned, + ref uint lpResumeHandle, + string pszGroupName); + + [DllImport(Libraries.Advapi32, SetLastError = true)] + public static extern IntPtr OpenSCManager( + string lpMachineName, + string lpDatabaseName, + int dwDesiredAccess); - [StructLayout(LayoutKind.Sequential)] - public struct SERVICE_STATUS_PROCESS - { - public int dwServiceType; - public int dwCurrentState; - public int dwControlsAccepted; - public int dwWin32ExitCode; - public int dwServiceSpecificExitCode; - public int dwCheckPoint; - public int dwWaitHint; - public int dwProcessId; - public int dwServiceFlags; - } + [DllImport(Libraries.Advapi32, SetLastError = true)] + public static extern IntPtr OpenService( + IntPtr hSCManager, + string lpServiceName, + int dwDesiredAccess); + + [DllImport(Libraries.Advapi32, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool QueryServiceStatusEx( + IntPtr hService, + int InfoLevel, + IntPtr lpBuffer, + uint cbBufSize, + out uint pcbBytesNeeded); } \ No newline at end of file diff --git a/src/Task.Monitor.System/PInvokeErrorHelpers.cs b/src/Task.Monitor.System/PInvokeErrorHelpers.cs index c92b84f..e7fdebb 100644 --- a/src/Task.Monitor.System/PInvokeErrorHelpers.cs +++ b/src/Task.Monitor.System/PInvokeErrorHelpers.cs @@ -9,7 +9,14 @@ public static void AssertOnLastError(string methodName) { #if DEBUG int error = Marshal.GetLastPInvokeError(); - Debug.Assert(error == 0, $"Failed {methodName}: {Marshal.GetPInvokeErrorMessage(error)}"); + Debug.Assert(error == 0, $"Failed with {GetFormattedErrorMesage()}"); #endif } -} \ No newline at end of file + + public static string GetFormattedErrorMesage() + { + int error = Marshal.GetLastPInvokeError(); + string message = Marshal.GetPInvokeErrorMessage(error); + return $"PInvoke error ({error}): {message}"; + } +} diff --git a/src/Task.Monitor.System/Process/ProcessService.Windows.cs b/src/Task.Monitor.System/Process/ProcessService.Windows.cs index 00fc3d9..4141398 100644 --- a/src/Task.Monitor.System/Process/ProcessService.Windows.cs +++ b/src/Task.Monitor.System/Process/ProcessService.Windows.cs @@ -46,7 +46,7 @@ public sealed partial class ProcessService processInfo.ProcessName); processInfo.ModuleName = Path.GetFileName(entry.szExeFile); - processInfo.IsDaemon = ServiceUtils.GetService((int)entry.th32ProcessID, out ServiceController? _); + processInfo.IsDaemon = ServiceUtils.GetService((int)entry.th32ProcessID, out ServiceInfo? _); processInfo.IsLowPriority = entry.pcPriClassBase < 8; processInfo.UserName = GetProcessUserName(processHandle); processInfo.CmdLine = GetProcessCommandLine((int)entry.th32ProcessID, processInfo.FileName); @@ -84,8 +84,8 @@ public sealed partial class ProcessService private static string GetProcessCommandLine(int pid, in string defaultValue) { try { - if (ServiceUtils.GetService(pid, out ServiceController? serviceController)) { - string imagePath = ServiceUtils.GetServiceImagePath(serviceController!.ServiceName) ?? defaultValue; + if (ServiceUtils.GetService(pid, out ServiceInfo? serviceInfo)) { + string imagePath = ServiceUtils.GetServiceImagePath(serviceInfo!.ServiceName) ?? defaultValue; return Environment.ExpandEnvironmentVariables(imagePath); } @@ -240,8 +240,8 @@ private static string GetProcessProductName( string processPath, string defaultValue) { - if (ServiceUtils.GetService((int)pid, out ServiceController? serviceController)) { - return serviceController?.DisplayName ?? defaultValue; + if (ServiceUtils.GetService((int)pid, out ServiceInfo? serviceInfo)) { + return serviceInfo?.DisplayName ?? defaultValue; } if (string.IsNullOrWhiteSpace(processPath)) { diff --git a/src/Task.Monitor.System/Process/ServiceInfo.cs b/src/Task.Monitor.System/Process/ServiceInfo.cs new file mode 100644 index 0000000..fe35177 --- /dev/null +++ b/src/Task.Monitor.System/Process/ServiceInfo.cs @@ -0,0 +1,7 @@ +namespace Task.Monitor.System.Process; + +public sealed class ServiceInfo +{ + public string ServiceName { get; set; } = string.Empty; + public string DisplayName { get; set; } = string.Empty; +} diff --git a/src/Task.Monitor.System/Process/ServiceUtils.Windows.cs b/src/Task.Monitor.System/Process/ServiceUtils.Windows.cs index be8ce21..4cc5ae8 100644 --- a/src/Task.Monitor.System/Process/ServiceUtils.Windows.cs +++ b/src/Task.Monitor.System/Process/ServiceUtils.Windows.cs @@ -1,4 +1,6 @@ -using System.Runtime.InteropServices; +using System.Diagnostics; +using System.Reflection.Metadata.Ecma335; +using System.Runtime.InteropServices; using System.ServiceProcess; using Microsoft.Win32; using Task.Monitor.Interop.Win32; @@ -10,7 +12,7 @@ namespace Task.Monitor.System.Process; public static partial class ServiceUtils { #if __WIN32__ - private static readonly Dictionary serviceMap = new(); + private static readonly Dictionary serviceMap = new(); private static readonly Lock criticalSection = new(); private static void BuildServiceMap() @@ -20,13 +22,14 @@ private static void BuildServiceMap() IntPtr hSCM = WinService.OpenSCManager(null!, null!, WinService.SC_MANAGER_CONNECT); if (hSCM == IntPtr.Zero) { + Trace.WriteLine($"Failed to open SCM Manager: {PInvokeErrorHelpers.GetFormattedErrorMesage()}"); return; } - ServiceController[] services = ServiceController.GetServices(); + ServiceInfo[] services = GetServices(); for (int i = 0; i < services.Length; i++) { - ServiceController service = services[i]; + ServiceInfo service = services[i]; IntPtr hService = WinService.OpenService( hSCM, @@ -49,7 +52,7 @@ private static void BuildServiceMap() WinService.CloseServiceHandle(hSCM); } - public static bool GetService(int pid, out ServiceController? service) + public static bool GetService(int pid, out ServiceInfo? service) { lock (criticalSection) { if (serviceMap.Count == 0) { @@ -83,17 +86,18 @@ private static int GetServiceProcessId(IntPtr hService) pss, (uint)Marshal.SizeOf(), out _)) { - + + Trace.WriteLine($"GetServiceProcessId QueryServiceStatusEx: {PInvokeErrorHelpers.GetFormattedErrorMesage()}"); return 0; } var ssp = Marshal.PtrToStructure(pss); - if (ssp.dwCurrentState == (int)ServiceControllerStatus.Running || - ssp.dwCurrentState == (int)ServiceControllerStatus.PausePending || - ssp.dwCurrentState == (int)ServiceControllerStatus.Paused || - ssp.dwCurrentState == (int)ServiceControllerStatus.StartPending || - ssp.dwCurrentState == (int)ServiceControllerStatus.StopPending) { + if (ssp.dwCurrentState == (int)WinService.ServiceCurrentState.SERVICE_RUNNING || + ssp.dwCurrentState == (int)WinService.ServiceCurrentState.SERVICE_PAUSE_PENDING || + ssp.dwCurrentState == (int)WinService.ServiceCurrentState.SERVICE_PAUSED || + ssp.dwCurrentState == (int)WinService.ServiceCurrentState.SERVICE_START_PENDING || + ssp.dwCurrentState == (int)WinService.ServiceCurrentState.SERVICE_STOP_PENDING) { pid = ssp.dwProcessId; } @@ -101,8 +105,91 @@ private static int GetServiceProcessId(IntPtr hService) Marshal.FreeHGlobal(pss); return pid; } + + internal static ServiceInfo[] GetServices() + { + ServiceInfo[] services = []; + IntPtr hSCM = WinService.OpenSCManager(null!, null!, WinService.SC_MANAGER_ENUMERATE_SERVICE); + + if (hSCM == IntPtr.Zero) { + Trace.WriteLine($"Failed to open SCM Manager: {PInvokeErrorHelpers.GetFormattedErrorMesage()}"); + return services; + } + + IntPtr buffer = IntPtr.Zero; + + uint bytesNeeded = 0; + uint servicesReturned = 0; + uint resumeHandle = 0; + + WinService.EnumServicesStatusEx( + hSCM, + WinService.INFO_LEVEL_STANDARD, + WinService.SERVICE_TYPE_ALL, + WinService.SERVICE_STATE_ALL, + IntPtr.Zero, + 0, + out bytesNeeded, + out servicesReturned, + ref resumeHandle, + null!); + + if (bytesNeeded == 0) { + Trace.WriteLine($"EnumServicesStatusEx bytesNeeded returned 0: {PInvokeErrorHelpers.GetFormattedErrorMesage()}"); + WinService.CloseServiceHandle(hSCM); + return services; + } + + buffer = Marshal.AllocHGlobal((IntPtr)bytesNeeded); + resumeHandle = 0; + + bool result = WinService.EnumServicesStatusEx( + hSCM, + WinService.INFO_LEVEL_STANDARD, + WinService.SERVICE_TYPE_ALL, + WinService.SERVICE_STATE_ALL, + buffer, + bytesNeeded, + out bytesNeeded, + out servicesReturned, + ref resumeHandle, + null!); + + if (!result) { + Trace.WriteLine($"Failed to EnumServicesStatusEx: {PInvokeErrorHelpers.GetFormattedErrorMesage()}"); + + if (buffer != IntPtr.Zero) { + Marshal.FreeHGlobal(buffer); + } + + WinService.CloseServiceHandle(hSCM); + return services; + } + + IntPtr currentPtr = buffer; + int structSize = Marshal.SizeOf(); + services = new ServiceInfo[servicesReturned]; + + for (int i = 0; i < servicesReturned; i++) { + var status = Marshal.PtrToStructure(currentPtr); + + services[i] = new ServiceInfo() { + ServiceName = status.lpServiceName, + DisplayName = status.lpDisplayName + }; + + currentPtr = IntPtr.Add(currentPtr, structSize); + } + + if (buffer != IntPtr.Zero) { + Marshal.FreeHGlobal(buffer); + } + + WinService.CloseServiceHandle(hSCM); + return services; + } - public static bool IsService(int pid) => ServiceUtils.GetService(pid, out ServiceController? _); + public static bool IsService(int pid) => ServiceUtils.GetService(pid, out ServiceInfo? _); #endif } diff --git a/src/Task.Monitor.System/SystemTerminal.Windows.cs b/src/Task.Monitor.System/SystemTerminal.Windows.cs index 91344c9..ce2647a 100644 --- a/src/Task.Monitor.System/SystemTerminal.Windows.cs +++ b/src/Task.Monitor.System/SystemTerminal.Windows.cs @@ -25,18 +25,19 @@ private bool CursorVisibleInternal private void EnableAnsiTerminalCodesInternal() { - // Not all terminals on Windows platforms (cmd.exe as an example) support ANSI escape codes. + // Not all terminals on Windows platforms (conhost.exe as an example) support ANSI escape codes. // We attempt to enable here to support older terminals. - IntPtr consoleHandle = ProcessEnv.GetStdHandle(ProcessEnv.STD_OUTPUT_HANDLE); if (consoleHandle == IntPtr.Zero || consoleHandle == new IntPtr(-1)) { PInvokeErrorHelpers.AssertOnLastError(nameof(ProcessEnv.GetStdHandle)); + Trace.WriteLine($"EnableAnsiTerminalCodesInternal GetStdHandle: {PInvokeErrorHelpers.GetFormattedErrorMesage()}"); return; } if (!ConsoleApi.GetConsoleMode(consoleHandle, out uint originalMode)) { PInvokeErrorHelpers.AssertOnLastError(nameof(ConsoleApi.GetConsoleMode)); + Trace.WriteLine($"EnableAnsiTerminalCodesInternal GetConsoleMode: {PInvokeErrorHelpers.GetFormattedErrorMesage()}"); return; } @@ -44,6 +45,7 @@ private void EnableAnsiTerminalCodesInternal() if (!ConsoleApi.SetConsoleMode(consoleHandle, newMode)) { PInvokeErrorHelpers.AssertOnLastError(nameof(ConsoleApi.SetConsoleMode)); + Trace.WriteLine($"EnableAnsiTerminalCodesInternal SetConsoleMode: {PInvokeErrorHelpers.GetFormattedErrorMesage()}"); } } #endif diff --git a/src/Task.Monitor.System/Task.Monitor.System.csproj b/src/Task.Monitor.System/Task.Monitor.System.csproj index b7c4cd6..6680cf6 100644 --- a/src/Task.Monitor.System/Task.Monitor.System.csproj +++ b/src/Task.Monitor.System/Task.Monitor.System.csproj @@ -6,9 +6,6 @@ - - - diff --git a/tests/Task.Monitor.System.Tests/Process/ServiceUtilsTests.Windows.cs b/tests/Task.Monitor.System.Tests/Process/ServiceUtilsTests.Windows.cs index 03bbb49..5af81af 100644 --- a/tests/Task.Monitor.System.Tests/Process/ServiceUtilsTests.Windows.cs +++ b/tests/Task.Monitor.System.Tests/Process/ServiceUtilsTests.Windows.cs @@ -1,5 +1,6 @@ using System.Runtime.Versioning; using System.ServiceProcess; +using Task.Monitor.Interop.Win32; using Task.Monitor.System.Process; using SysDiag = System.Diagnostics; @@ -8,6 +9,17 @@ namespace Task.Monitor.System.Tests.Process; public class ServiceUtilsTests { #if __WIN32__ + [SkippableFact] + [SupportedOSPlatform("windows")] + public void Should_Get_Services() + { + ProcessService processService = new(); + + ServiceInfo[] serviceInfos = ServiceUtils.GetServices(); + Assert.NotNull(serviceInfos); + Assert.NotEmpty(serviceInfos); + } + [SkippableFact] [SupportedOSPlatform("windows")] public void Should_Get_Services_With_Pid() @@ -29,8 +41,8 @@ public void Should_Get_Services_With_ImagePath() bool foundAnyService = processService.GetProcesses() .Select(p => { - if (ServiceUtils.GetService(p.Pid, out ServiceController? sc)) { - return sc; + if (ServiceUtils.GetService(p.Pid, out ServiceInfo? si)) { + return si; } return null; })