From 144cff7f19cad69be91133b10fceaf98077fb113 Mon Sep 17 00:00:00 2001 From: NextTurn <45985406+NextTurn@users.noreply.github.com> Date: Sat, 29 Aug 2020 00:00:00 +0800 Subject: [PATCH] Add `dev list` command --- src/WinSW.Core/Native/Security.cs | 28 ++-- src/WinSW.Core/Native/Service.cs | 96 +++++++++++++- src/WinSW.Core/Native/ServiceApis.cs | 121 +++++++++++++----- src/WinSW.Core/WinSW.Core.csproj | 4 +- .../Util/InterProcessCodeCoverageSession.cs | 4 +- src/WinSW/Program.cs | 82 ++++++++---- src/WinSW/WrapperService.cs | 2 +- 7 files changed, 260 insertions(+), 77 deletions(-) diff --git a/src/WinSW.Core/Native/Security.cs b/src/WinSW.Core/Native/Security.cs index 9720ff1..f6a186a 100644 --- a/src/WinSW.Core/Native/Security.cs +++ b/src/WinSW.Core/Native/Security.cs @@ -37,20 +37,28 @@ namespace WinSW.Native _ = LookupAccountName(null, accountName, IntPtr.Zero, ref sidSize, null, ref domainNameLength, out _); IntPtr sid = Marshal.AllocHGlobal(sidSize); - string? domainName = domainNameLength == 0 ? null : new string('\0', domainNameLength - 1); - - if (!LookupAccountName(null, accountName, sid, ref sidSize, domainName, ref domainNameLength, out _)) + try { - Throw.Command.Win32Exception("Failed to find the account."); - } + string? domainName = domainNameLength == 0 ? null : new string('\0', domainNameLength - 1); - // intentionally undocumented - if (!accountName.Contains("\\") && !accountName.Contains("@")) + if (!LookupAccountName(null, accountName, sid, ref sidSize, domainName, ref domainNameLength, out _)) + { + Throw.Command.Win32Exception("Failed to find the account."); + } + + // intentionally undocumented + if (!accountName.Contains("\\") && !accountName.Contains("@")) + { + accountName = domainName + '\\' + accountName; + } + + return sid; + } + catch { - accountName = domainName + '\\' + accountName; + Marshal.FreeHGlobal(sid); + throw; } - - return sid; } /// diff --git a/src/WinSW.Core/Native/Service.cs b/src/WinSW.Core/Native/Service.cs index 6a390a9..54b6765 100644 --- a/src/WinSW.Core/Native/Service.cs +++ b/src/WinSW.Core/Native/Service.cs @@ -1,5 +1,5 @@ using System; -using System.Diagnostics; +using System.Runtime.InteropServices; using System.Security.AccessControl; using System.ServiceProcess; using System.Text; @@ -56,9 +56,9 @@ namespace WinSW.Native private ServiceManager(IntPtr handle) => this.handle = handle; /// - internal static ServiceManager Open() + internal static ServiceManager Open(ServiceManagerAccess access = ServiceManagerAccess.All) { - IntPtr handle = OpenSCManager(null, null, ServiceManagerAccess.ALL_ACCESS); + IntPtr handle = OpenSCManager(null, null, access); if (handle == IntPtr.Zero) { Throw.Command.Win32Exception("Failed to open the service control manager database."); @@ -81,7 +81,7 @@ namespace WinSW.Native this.handle, serviceName, displayName, - ServiceAccess.ALL_ACCESS, + ServiceAccess.All, ServiceType.Win32OwnProcess, startMode, ServiceErrorControl.Normal, @@ -100,7 +100,58 @@ namespace WinSW.Native } /// - internal Service OpenService(string serviceName, ServiceAccess access = ServiceAccess.ALL_ACCESS) + internal unsafe (IntPtr Services, int Count) EnumerateServices() + { + int resume = 0; + _ = EnumServicesStatus( + this.handle, + ServiceType.Win32OwnProcess, + ServiceState.All, + IntPtr.Zero, + 0, + out int bytesNeeded, + out _, + ref resume); + + IntPtr services = Marshal.AllocHGlobal(bytesNeeded); + try + { + if (!EnumServicesStatus( + this.handle, + ServiceType.Win32OwnProcess, + ServiceState.All, + services, + bytesNeeded, + out _, + out int count, + ref resume)) + { + Throw.Command.Win32Exception("Failed to enumerate services."); + } + + return (services, count); + } + catch + { + Marshal.FreeHGlobal(services); + throw; + } + } + + /// + internal unsafe Service OpenService(char* serviceName, ServiceAccess access = ServiceAccess.All) + { + IntPtr serviceHandle = ServiceApis.OpenService(this.handle, serviceName, access); + if (serviceHandle == IntPtr.Zero) + { + Throw.Command.Win32Exception("Failed to open the service."); + } + + return new Service(serviceHandle); + } + + /// + internal Service OpenService(string serviceName, ServiceAccess access = ServiceAccess.All) { IntPtr serviceHandle = ServiceApis.OpenService(this.handle, serviceName, access); if (serviceHandle == IntPtr.Zero) @@ -113,7 +164,7 @@ namespace WinSW.Native internal bool ServiceExists(string serviceName) { - IntPtr serviceHandle = ServiceApis.OpenService(this.handle, serviceName, ServiceAccess.ALL_ACCESS); + IntPtr serviceHandle = ServiceApis.OpenService(this.handle, serviceName, ServiceAccess.All); if (serviceHandle == IntPtr.Zero) { return false; @@ -140,6 +191,39 @@ namespace WinSW.Native internal Service(IntPtr handle) => this.handle = handle; + /// + internal unsafe string ExecutablePath + { + get + { + _ = QueryServiceConfig( + this.handle, + IntPtr.Zero, + 0, + out int bytesNeeded); + + IntPtr config = Marshal.AllocHGlobal(bytesNeeded); + try + { + if (!QueryServiceConfig( + this.handle, + config, + bytesNeeded, + out _)) + { + Throw.Command.Win32Exception("Failed to query service config."); + } + + return Marshal.PtrToStringUni((IntPtr)((QUERY_SERVICE_CONFIG*)config)->BinaryPathName)!; + } + finally + { + Marshal.FreeHGlobal(config); + } + } + } + + /// internal unsafe int ProcessId { get diff --git a/src/WinSW.Core/Native/ServiceApis.cs b/src/WinSW.Core/Native/ServiceApis.cs index e500fcb..ef16a73 100644 --- a/src/WinSW.Core/Native/ServiceApis.cs +++ b/src/WinSW.Core/Native/ServiceApis.cs @@ -60,12 +60,29 @@ namespace WinSW.Native [DllImport(Libraries.Advapi32, SetLastError = true)] internal static extern bool DeleteService(IntPtr serviceHandle); + [DllImport(Libraries.Advapi32, SetLastError = true, CharSet = CharSet.Unicode, EntryPoint = "EnumServicesStatusW")] + internal static extern unsafe bool EnumServicesStatus( + IntPtr databaseHandle, + ServiceType serviceType, + ServiceState serviceState, + IntPtr services, + int bufferSize, + out int bytesNeeded, + out int servicesReturned, + ref int resumeHandle); + [DllImport(Libraries.Advapi32, SetLastError = true, CharSet = CharSet.Unicode, EntryPoint = "OpenSCManagerW")] internal static extern IntPtr OpenSCManager(string? machineName, string? databaseName, ServiceManagerAccess desiredAccess); + [DllImport(Libraries.Advapi32, SetLastError = true, CharSet = CharSet.Unicode, EntryPoint = "OpenServiceW")] + internal static unsafe extern IntPtr OpenService(IntPtr databaseHandle, char* serviceName, ServiceAccess desiredAccess); + [DllImport(Libraries.Advapi32, SetLastError = true, CharSet = CharSet.Unicode, EntryPoint = "OpenServiceW")] internal static extern IntPtr OpenService(IntPtr databaseHandle, string serviceName, ServiceAccess desiredAccess); + [DllImport(Libraries.Advapi32, SetLastError = true, CharSet = CharSet.Unicode, EntryPoint = "QueryServiceConfigW")] + internal static extern bool QueryServiceConfig(IntPtr serviceHandle, IntPtr serviceConfig, int bufferSize, out int bytesNeeded); + [DllImport(Libraries.Advapi32, SetLastError = true)] internal static extern bool QueryServiceStatus(IntPtr serviceHandle, out SERVICE_STATUS serviceStatus); @@ -88,27 +105,27 @@ namespace WinSW.Native [Flags] internal enum ServiceAccess : uint { - QUERY_CONFIG = 0x0001, - CHANGE_CONFIG = 0x0002, - QUERY_STATUS = 0x0004, - ENUMERATE_DEPENDENTS = 0x0008, - START = 0x0010, - STOP = 0x0020, - PAUSE_CONTINUE = 0x0040, - INTERROGATE = 0x0080, - USER_DEFINED_CONTROL = 0x0100, + QueryConfig = 0x0001, + ChangeConfig = 0x0002, + QueryStatus = 0x0004, + EnumerateDependents = 0x0008, + Start = 0x0010, + Stop = 0x0020, + PauseContinue = 0x0040, + Interrogate = 0x0080, + UserDefinedControl = 0x0100, - ALL_ACCESS = + All = SecurityApis.StandardAccess.REQUIRED | - QUERY_CONFIG | - CHANGE_CONFIG | - QUERY_STATUS | - ENUMERATE_DEPENDENTS | - START | - STOP | - PAUSE_CONTINUE | - INTERROGATE | - USER_DEFINED_CONTROL, + QueryConfig | + ChangeConfig | + QueryStatus | + EnumerateDependents | + Start | + Stop | + PauseContinue | + Interrogate | + UserDefinedControl, } // SERVICE_CONFIG_ @@ -140,21 +157,29 @@ namespace WinSW.Native [Flags] internal enum ServiceManagerAccess : uint { - CONNECT = 0x0001, - CREATE_SERVICE = 0x0002, - ENUMERATE_SERVICE = 0x0004, - LOCK = 0x0008, - QUERY_LOCK_STATUS = 0x0010, - MODIFY_BOOT_CONFIG = 0x0020, + Connect = 0x0001, + CreateService = 0x0002, + EnumerateService = 0x0004, + Lock = 0x0008, + QueryLockStatus = 0x0010, + ModifyBootConfig = 0x0020, - ALL_ACCESS = + All = SecurityApis.StandardAccess.REQUIRED | - CONNECT | - CREATE_SERVICE | - ENUMERATE_SERVICE | - LOCK | - QUERY_LOCK_STATUS | - MODIFY_BOOT_CONFIG, + Connect | + CreateService | + EnumerateService | + Lock | + QueryLockStatus | + ModifyBootConfig, + } + + // SERVICE_ + internal enum ServiceState : uint + { + Active = 0x00000001, + Inactive = 0x00000002, + All = 0x00000003, } // SC_STATUS_ @@ -163,6 +188,38 @@ namespace WinSW.Native ProcessInfo = 0, } + internal readonly unsafe struct ENUM_SERVICE_STATUS + { + public readonly char* ServiceName; + public readonly char* DisplayName; + public readonly SERVICE_STATUS ServiceStatus; + + public override string ToString() + { + var serviceName = new ReadOnlySpan(this.ServiceName, new ReadOnlySpan(this.ServiceName, 256).IndexOf('\0')); + var displayName = new ReadOnlySpan(this.DisplayName, new ReadOnlySpan(this.DisplayName, 256).IndexOf('\0')); + +#if NETCOREAPP + return string.Concat(displayName, " (", serviceName, ")"); +#else + return string.Concat(displayName.ToString(), " (", serviceName.ToString(), ")"); +#endif + } + } + + internal unsafe struct QUERY_SERVICE_CONFIG + { + public ServiceType ServiceType; + public ServiceStartMode StartType; + public ServiceErrorControl ErrorControl; + public char* BinaryPathName; + public char* LoadOrderGroup; + public uint TagId; + public char* Dependencies; + public char* ServiceStartName; + public char* DisplayName; + } + internal struct SERVICE_DELAYED_AUTO_START_INFO { public bool DelayedAutostart; diff --git a/src/WinSW.Core/WinSW.Core.csproj b/src/WinSW.Core/WinSW.Core.csproj index 67c7410..cdc228e 100644 --- a/src/WinSW.Core/WinSW.Core.csproj +++ b/src/WinSW.Core/WinSW.Core.csproj @@ -9,7 +9,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -32,6 +32,8 @@ + + diff --git a/src/WinSW.Tests/Util/InterProcessCodeCoverageSession.cs b/src/WinSW.Tests/Util/InterProcessCodeCoverageSession.cs index 5177194..0e01ece 100644 --- a/src/WinSW.Tests/Util/InterProcessCodeCoverageSession.cs +++ b/src/WinSW.Tests/Util/InterProcessCodeCoverageSession.cs @@ -29,8 +29,8 @@ namespace WinSW.Tests.Util var hitsField = this.hitsField = trackerType.GetField("HitsArray", BindingFlags.Public | BindingFlags.Static); Assert.NotNull(hitsField); - using var scm = ServiceManager.Open(); - using var sc = scm.OpenService(serviceName, ServiceApis.ServiceAccess.QUERY_STATUS); + using var scm = ServiceManager.Open(ServiceApis.ServiceManagerAccess.Connect); + using var sc = scm.OpenService(serviceName, ServiceApis.ServiceAccess.QueryStatus); int processId = sc.ProcessId; Assert.True(processId >= 0); diff --git a/src/WinSW/Program.cs b/src/WinSW/Program.cs index 9611167..e3bb0c3 100644 --- a/src/WinSW/Program.cs +++ b/src/WinSW/Program.cs @@ -9,6 +9,7 @@ using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; +using System.Runtime.InteropServices; using System.Security.AccessControl; using System.Security.Principal; using System.ServiceProcess; @@ -22,6 +23,7 @@ using log4net.Layout; using WinSW.Logging; using WinSW.Native; using WinSW.Util; +using static WinSW.Native.ServiceApis; using Process = System.Diagnostics.Process; using TimeoutException = System.ServiceProcess.TimeoutException; @@ -271,27 +273,41 @@ namespace WinSW } { - var dev = new Command("dev", "Experimental commands.") - { - config, - noElevate, - }; + var dev = new Command("dev", "Experimental commands."); root.Add(dev); - var ps = new Command("ps", "Draws the process tree associated with the service.") { - Handler = CommandHandler.Create(DevPs), - }; + var ps = new Command("ps", "Draws the process tree associated with the service.") + { + Handler = CommandHandler.Create(DevPs), + }; - dev.Add(ps); + ps.Add(config); + + dev.Add(ps); + } - var kill = new Command("kill", "Terminates the service if it has stopped responding.") { - Handler = CommandHandler.Create(DevKill), - }; + var kill = new Command("kill", "Terminates the service if it has stopped responding.") + { + Handler = CommandHandler.Create(DevKill), + }; - dev.Add(kill); + kill.Add(config); + kill.Add(noElevate); + + dev.Add(kill); + } + + { + var list = new Command("list", "Lists services managed by the current executable.") + { + Handler = CommandHandler.Create(DevList), + }; + + dev.Add(list); + } } return new CommandLineBuilder(root) @@ -374,7 +390,7 @@ namespace WinSW Log.Info($"Installing service '{config.Format()}'..."); - using ServiceManager scm = ServiceManager.Open(); + using ServiceManager scm = ServiceManager.Open(ServiceManagerAccess.CreateService); if (scm.ServiceExists(config.Name)) { @@ -499,7 +515,7 @@ namespace WinSW Log.Info($"Uninstalling service '{config.Format()}'..."); - using ServiceManager scm = ServiceManager.Open(); + using ServiceManager scm = ServiceManager.Open(ServiceManagerAccess.Connect); try { using Service sc = scm.OpenService(config.Name); @@ -822,7 +838,7 @@ namespace WinSW return; } - using ServiceManager scm = ServiceManager.Open(); + using ServiceManager scm = ServiceManager.Open(ServiceManagerAccess.Connect); try { using Service sc = scm.OpenService(config.Name); @@ -864,18 +880,12 @@ namespace WinSW Log.Info($"Service '{config.Format()}' was refreshed successfully."); } - void DevPs(string? pathToConfig, bool noElevate) + static void DevPs(string? pathToConfig) { XmlServiceConfig config = CreateConfig(pathToConfig); - if (!elevated) - { - Elevate(noElevate); - return; - } - - using ServiceManager scm = ServiceManager.Open(); - using Service sc = scm.OpenService(config.Name); + using ServiceManager scm = ServiceManager.Open(ServiceManagerAccess.Connect); + using Service sc = scm.OpenService(config.Name, ServiceAccess.QueryStatus); int processId = sc.ProcessId; if (processId >= 0) @@ -938,6 +948,28 @@ namespace WinSW } } + static unsafe void DevList() + { + using var scm = ServiceManager.Open(ServiceManagerAccess.EnumerateService); + (IntPtr services, int count) = scm.EnumerateServices(); + try + { + for (int i = 0; i < count; i++) + { + var status = (ServiceApis.ENUM_SERVICE_STATUS*)services + i; + using var sc = scm.OpenService(status->ServiceName, ServiceAccess.QueryConfig); + if (sc.ExecutablePath.StartsWith($"\"{ExecutablePath}\"")) + { + Console.WriteLine(status->ToString()); + } + } + } + finally + { + Marshal.FreeHGlobal(services); + } + } + static void Customize(string output, string manufacturer) { if (Resources.UpdateCompanyName(ExecutablePath, output, manufacturer)) diff --git a/src/WinSW/WrapperService.cs b/src/WinSW/WrapperService.cs index 932013f..0f22ebd 100644 --- a/src/WinSW/WrapperService.cs +++ b/src/WinSW/WrapperService.cs @@ -502,7 +502,7 @@ namespace WinSW private void SignalStopped() { using ServiceManager scm = ServiceManager.Open(); - using Service sc = scm.OpenService(this.ServiceName, ServiceApis.ServiceAccess.QUERY_STATUS); + using Service sc = scm.OpenService(this.ServiceName, ServiceApis.ServiceAccess.QueryStatus); sc.SetStatus(this.ServiceHandle, ServiceControllerStatus.Stopped); }