diff --git a/src/WinSW.Core/CommandException.cs b/src/WinSW.Core/CommandException.cs new file mode 100644 index 0000000..5bb9514 --- /dev/null +++ b/src/WinSW.Core/CommandException.cs @@ -0,0 +1,22 @@ +using System; + +namespace WinSW +{ + internal sealed class CommandException : Exception + { + internal CommandException(Exception inner) + : base(inner.Message, inner) + { + } + + internal CommandException(string message) + : base(message) + { + } + + internal CommandException(string message, Exception inner) + : base(message, inner) + { + } + } +} diff --git a/src/WinSW.Core/Configuration/DefaultSettings.cs b/src/WinSW.Core/Configuration/DefaultSettings.cs index a55d21e..3c116dc 100644 --- a/src/WinSW.Core/Configuration/DefaultSettings.cs +++ b/src/WinSW.Core/Configuration/DefaultSettings.cs @@ -2,8 +2,8 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; +using System.ServiceProcess; using System.Xml; -using WMI; namespace WinSW.Configuration { @@ -14,9 +14,9 @@ namespace WinSW.Configuration { public static LogDefaults DefaultLogSettings { get; } = new LogDefaults(); - public string Id => throw new InvalidOperationException(nameof(this.Id) + " must be specified."); + public string Name => throw new InvalidOperationException(nameof(this.Name) + " must be specified."); - public string Caption => throw new InvalidOperationException(nameof(this.Caption) + " must be specified."); + public string DisplayName => throw new InvalidOperationException(nameof(this.DisplayName) + " must be specified."); public string Description => throw new InvalidOperationException(nameof(this.Description) + " must be specified."); @@ -49,7 +49,7 @@ namespace WinSW.Configuration public bool StopParentProcessFirst => true; // Service management - public StartMode StartMode => StartMode.Automatic; + public ServiceStartMode StartMode => ServiceStartMode.Automatic; public bool DelayedAutoStart => false; diff --git a/src/WinSW.Core/Configuration/IWinSWConfiguration.cs b/src/WinSW.Core/Configuration/IWinSWConfiguration.cs index 7c6080f..b064097 100644 --- a/src/WinSW.Core/Configuration/IWinSWConfiguration.cs +++ b/src/WinSW.Core/Configuration/IWinSWConfiguration.cs @@ -1,17 +1,17 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.ServiceProcess; using System.Xml; -using WMI; namespace WinSW.Configuration { public interface IWinSWConfiguration { // TODO: Document the parameters && refactor - string Id { get; } + string Name { get; } - string Caption { get; } + string DisplayName { get; } string Description { get; } @@ -44,7 +44,7 @@ namespace WinSW.Configuration bool StopParentProcessFirst { get; } // Service management - StartMode StartMode { get; } + ServiceStartMode StartMode { get; } string[] ServiceDependencies { get; } diff --git a/src/WinSW.Core/Configuration/YamlConfiguration.cs b/src/WinSW.Core/Configuration/YamlConfiguration.cs index 3226bce..009c614 100644 --- a/src/WinSW.Core/Configuration/YamlConfiguration.cs +++ b/src/WinSW.Core/Configuration/YamlConfiguration.cs @@ -2,10 +2,10 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; +using System.ServiceProcess; using System.Xml; using WinSW.Native; using WinSW.Util; -using WMI; using YamlDotNet.Serialization; using static WinSW.Download; @@ -458,7 +458,7 @@ namespace WinSW.Configuration return Environment.ExpandEnvironmentVariables(str); } - public string Id => this.IdYaml is null ? this.Defaults.Id : ExpandEnv(this.IdYaml); + public string Name => this.IdYaml is null ? this.Defaults.Name : ExpandEnv(this.IdYaml); public string Description => this.DescriptionYaml is null ? this.Defaults.Description : ExpandEnv(this.DescriptionYaml); @@ -468,7 +468,7 @@ namespace WinSW.Configuration this.Defaults.ExecutablePath : ExpandEnv(this.ExecutablePathYaml); - public string Caption => this.NameYaml is null ? this.Defaults.Caption : ExpandEnv(this.NameYaml); + public string DisplayName => this.NameYaml is null ? this.Defaults.DisplayName : ExpandEnv(this.NameYaml); public bool HideWindow => this.HideWindowYaml is null ? this.Defaults.HideWindow : (bool)this.HideWindowYaml; @@ -482,7 +482,7 @@ namespace WinSW.Configuration } } - public StartMode StartMode + public ServiceStartMode StartMode { get { @@ -495,12 +495,12 @@ namespace WinSW.Configuration try { - return (StartMode)Enum.Parse(typeof(StartMode), p, true); + return (ServiceStartMode)Enum.Parse(typeof(ServiceStartMode), p, true); } catch { Console.WriteLine("Start mode in YAML must be one of the following:"); - foreach (string sm in Enum.GetNames(typeof(StartMode))) + foreach (string sm in Enum.GetNames(typeof(ServiceStartMode))) { Console.WriteLine(sm); } diff --git a/src/WinSW.Core/DynamicProxy.cs b/src/WinSW.Core/DynamicProxy.cs deleted file mode 100644 index 9d7ec5a..0000000 --- a/src/WinSW.Core/DynamicProxy.cs +++ /dev/null @@ -1,205 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Reflection; -using System.Reflection.Emit; - -namespace DynamicProxy -{ - /// - /// Interface that a user defined proxy handler needs to implement. This interface - /// defines one method that gets invoked by the generated proxy. - /// - public interface IProxyInvocationHandler - { - /// The instance of the proxy - /// The method info that can be used to invoke the actual method on the object implementation - /// Parameters to pass to the method - /// Object - object? Invoke(object proxy, MethodInfo method, object[] parameters); - } - - /// - /// - public static class ProxyFactory - { - private const string ProxySuffix = "Proxy"; - private const string AssemblyName = "ProxyAssembly"; - private const string ModuleName = "ProxyModule"; - private const string HandlerName = "handler"; - - private static readonly Dictionary TypeCache = new(); - - private static readonly AssemblyBuilder AssemblyBuilder = -#if VNEXT - AssemblyBuilder.DefineDynamicAssembly( -#else - AppDomain.CurrentDomain.DefineDynamicAssembly( -#endif - new AssemblyName(AssemblyName), AssemblyBuilderAccess.Run); - - private static readonly ModuleBuilder ModuleBuilder = AssemblyBuilder.DefineDynamicModule(ModuleName); - - public static object Create(IProxyInvocationHandler handler, Type objType, bool isObjInterface = false) - { - string typeName = objType.FullName + ProxySuffix; - Type? type = null; - lock (TypeCache) - { - if (!TypeCache.TryGetValue(typeName, out type)) - { - type = CreateType(typeName, isObjInterface ? new Type[] { objType } : objType.GetInterfaces()); - TypeCache.Add(typeName, type); - } - } - - return Activator.CreateInstance(type, new object[] { handler })!; - } - - private static Type CreateType(string dynamicTypeName, Type[] interfaces) - { - var objType = typeof(object); - var handlerType = typeof(IProxyInvocationHandler); - - var typeAttributes = TypeAttributes.Public | TypeAttributes.Sealed; - - // Gather up the proxy information and create a new type builder. One that - // inherits from Object and implements the interface passed in - var typeBuilder = ModuleBuilder.DefineType( - dynamicTypeName, typeAttributes, objType, interfaces); - - // Define a member variable to hold the delegate - var handlerField = typeBuilder.DefineField( - HandlerName, handlerType, FieldAttributes.Private | FieldAttributes.InitOnly); - - // build a constructor that takes the delegate object as the only argument - var baseConstructor = objType.GetConstructor(Type.EmptyTypes)!; - var delegateConstructor = typeBuilder.DefineConstructor( - MethodAttributes.Public, CallingConventions.Standard, new Type[] { handlerType }); - - var constructorIL = delegateConstructor.GetILGenerator(); - - // Load "this" - constructorIL.Emit(OpCodes.Ldarg_0); - - // Load first constructor parameter - constructorIL.Emit(OpCodes.Ldarg_1); - - // Set the first parameter into the handler field - constructorIL.Emit(OpCodes.Stfld, handlerField); - - // Load "this" - constructorIL.Emit(OpCodes.Ldarg_0); - - // Call the super constructor - constructorIL.Emit(OpCodes.Call, baseConstructor); - - // Constructor return - constructorIL.Emit(OpCodes.Ret); - - // for every method that the interfaces define, build a corresponding - // method in the dynamic type that calls the handlers invoke method. - foreach (var interfaceType in interfaces) - { - GenerateMethod(interfaceType, handlerField, typeBuilder); - } - - return typeBuilder.CreateType()!; - } - - /// - /// . - /// - private static readonly MethodInfo InvokeMethod = typeof(IProxyInvocationHandler).GetMethod(nameof(IProxyInvocationHandler.Invoke))!; - - /// - /// . - /// - private static readonly MethodInfo GetMethodFromHandleMethod = typeof(MethodBase).GetMethod(nameof(MethodBase.GetMethodFromHandle), new[] { typeof(RuntimeMethodHandle) })!; - - private static void GenerateMethod(Type interfaceType, FieldBuilder handlerField, TypeBuilder typeBuilder) - { - var interfaceMethods = interfaceType.GetMethods(); - - for (int i = 0; i < interfaceMethods.Length; i++) - { - var methodInfo = interfaceMethods[i]; - - // Get the method parameters since we need to create an array - // of parameter types - var methodParams = methodInfo.GetParameters(); - int numOfParams = methodParams.Length; - var methodParameters = new Type[numOfParams]; - - // convert the ParameterInfo objects into Type - for (int j = 0; j < numOfParams; j++) - { - methodParameters[j] = methodParams[j].ParameterType; - } - - // create a new builder for the method in the interface - var methodBuilder = typeBuilder.DefineMethod( - methodInfo.Name, - /*MethodAttributes.Public | MethodAttributes.Virtual | */ methodInfo.Attributes & ~MethodAttributes.Abstract, - CallingConventions.Standard, - methodInfo.ReturnType, - methodParameters); - - var methodIL = methodBuilder.GetILGenerator(); - - // invoke target: IProxyInvocationHandler - methodIL.Emit(OpCodes.Ldarg_0); - methodIL.Emit(OpCodes.Ldfld, handlerField); - - // 1st parameter: object proxy - methodIL.Emit(OpCodes.Ldarg_0); - - // 2nd parameter: MethodInfo method - methodIL.Emit(OpCodes.Ldtoken, methodInfo); - methodIL.Emit(OpCodes.Call, GetMethodFromHandleMethod); - methodIL.Emit(OpCodes.Castclass, typeof(MethodInfo)); - - // 3rd parameter: object[] parameters - methodIL.Emit(OpCodes.Ldc_I4, numOfParams); - methodIL.Emit(OpCodes.Newarr, typeof(object)); - - // if we have any parameters, then iterate through and set the values - // of each element to the corresponding arguments - for (int j = 0; j < numOfParams; j++) - { - methodIL.Emit(OpCodes.Dup); // copy the array - methodIL.Emit(OpCodes.Ldc_I4, j); - methodIL.Emit(OpCodes.Ldarg, j + 1); // +1 for "this" - if (methodParameters[j].IsValueType) - { - methodIL.Emit(OpCodes.Box, methodParameters[j]); - } - - methodIL.Emit(OpCodes.Stelem_Ref); - } - - // call the Invoke method - methodIL.Emit(OpCodes.Callvirt, InvokeMethod); - - if (methodInfo.ReturnType != typeof(void)) - { - methodIL.Emit(OpCodes.Unbox_Any, methodInfo.ReturnType); - } - else - { - // pop the return value that Invoke returned from the stack since - // the method's return type is void. - methodIL.Emit(OpCodes.Pop); - } - - // Return - methodIL.Emit(OpCodes.Ret); - } - - // Iterate through the parent interfaces and recursively call this method - foreach (var parentType in interfaceType.GetInterfaces()) - { - GenerateMethod(parentType, handlerField, typeBuilder); - } - } - } -} diff --git a/src/WinSW.Core/Extensions/WinSWExtensionManager.cs b/src/WinSW.Core/Extensions/WinSWExtensionManager.cs index a81f168..18fdebe 100644 --- a/src/WinSW.Core/Extensions/WinSWExtensionManager.cs +++ b/src/WinSW.Core/Extensions/WinSWExtensionManager.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.IO; using System.Xml; using log4net; using WinSW.Configuration; diff --git a/src/WinSW.Core/Native/Errors.cs b/src/WinSW.Core/Native/Errors.cs index 490f289..a45f76f 100644 --- a/src/WinSW.Core/Native/Errors.cs +++ b/src/WinSW.Core/Native/Errors.cs @@ -7,6 +7,11 @@ namespace WinSW.Native internal const int ERROR_ACCESS_DENIED = 5; internal const int ERROR_INVALID_HANDLE = 6; internal const int ERROR_INVALID_PARAMETER = 7; + internal const int ERROR_SERVICE_ALREADY_RUNNING = 1056; + internal const int ERROR_SERVICE_DOES_NOT_EXIST = 1060; + internal const int ERROR_SERVICE_NOT_ACTIVE = 1062; + internal const int ERROR_SERVICE_MARKED_FOR_DELETE = 1072; + internal const int ERROR_SERVICE_EXISTS = 1073; internal const int ERROR_CANCELLED = 1223; } } diff --git a/src/WinSW.Core/Native/Security.cs b/src/WinSW.Core/Native/Security.cs index 92f51a8..3f30055 100644 --- a/src/WinSW.Core/Native/Security.cs +++ b/src/WinSW.Core/Native/Security.cs @@ -41,7 +41,7 @@ namespace WinSW.Native { if (!LookupAccountName(null, accountName, sid, ref sidSize, domainName, ref domainNameLength, out _)) { - Throw.Win32Exception("Failed to find the account."); + Throw.Command.Win32Exception("Failed to find the account."); } return sid; diff --git a/src/WinSW.Core/Native/Service.cs b/src/WinSW.Core/Native/Service.cs index cdc8692..d941498 100644 --- a/src/WinSW.Core/Native/Service.cs +++ b/src/WinSW.Core/Native/Service.cs @@ -1,5 +1,8 @@ using System; +using System.Runtime.InteropServices; using System.Security.AccessControl; +using System.ServiceProcess; +using System.Text; using static WinSW.Native.ServiceApis; namespace WinSW.Native @@ -52,28 +55,72 @@ namespace WinSW.Native private ServiceManager(IntPtr handle) => this.handle = handle; - internal static ServiceManager Open() + internal static ServiceManager Open(ServiceManagerAccess access = ServiceManagerAccess.All) { - var handle = OpenSCManager(null, null, ServiceManagerAccess.ALL_ACCESS); + var handle = OpenSCManager(null, null, access); if (handle == IntPtr.Zero) { - Throw.Win32Exception("Failed to open the service control manager database."); + Throw.Command.Win32Exception("Failed to open the service control manager database."); } return new ServiceManager(handle); } - internal Service OpenService(string serviceName) + internal Service CreateService( + string serviceName, + string displayName, + bool interactive, + ServiceStartMode startMode, + string executablePath, + string[] dependencies, + string? username, + string? password) { - var serviceHandle = ServiceApis.OpenService(this.handle, serviceName, ServiceAccess.ALL_ACCESS); + var handle = ServiceApis.CreateService( + this.handle, + serviceName, + displayName, + ServiceAccess.All, + ServiceType.Win32OwnProcess | (interactive ? ServiceType.InteractiveProcess : default), + startMode, + ServiceErrorControl.Normal, + executablePath, + default, + default, + Service.GetNativeDependencies(dependencies), + username, + password); + if (handle == IntPtr.Zero) + { + Throw.Command.Win32Exception("Failed to create service."); + } + + return new Service(handle); + } + + internal Service OpenService(string serviceName, ServiceAccess access = ServiceAccess.All) + { + var serviceHandle = ServiceApis.OpenService(this.handle, serviceName, access); if (serviceHandle == IntPtr.Zero) { - Throw.Win32Exception("Failed to open the service."); + Throw.Command.Win32Exception("Failed to open the service."); } return new Service(serviceHandle); } + internal bool ServiceExists(string serviceName) + { + var serviceHandle = ServiceApis.OpenService(this.handle, serviceName, ServiceAccess.All); + if (serviceHandle == IntPtr.Zero) + { + return false; + } + + _ = CloseServiceHandle(this.handle); + return true; + } + public void Dispose() { if (this.handle != IntPtr.Zero) @@ -91,6 +138,67 @@ namespace WinSW.Native internal Service(IntPtr handle) => this.handle = handle; + internal ServiceControllerStatus Status + { + get + { + if (!QueryServiceStatus(this.handle, out var status)) + { + Throw.Command.Win32Exception("Failed to query service status."); + } + + return status.CurrentState; + } + } + + internal static StringBuilder? GetNativeDependencies(string[] dependencies) + { + int arrayLength = 1; + for (int i = 0; i < dependencies.Length; i++) + { + arrayLength += dependencies[i].Length + 1; + } + + StringBuilder? array = null; + if (dependencies.Length != 0) + { + array = new StringBuilder(arrayLength); + for (int i = 0; i < dependencies.Length; i++) + { + _ = array.Append(dependencies[i]).Append('\0'); + } + + _ = array.Append('\0'); + } + + return array; + } + + internal void SetStatus(IntPtr statusHandle, ServiceControllerStatus state) + { + if (!QueryServiceStatus(this.handle, out var status)) + { + Throw.Command.Win32Exception("Failed to query service status."); + } + + status.CheckPoint = 0; + status.WaitHint = 0; + status.CurrentState = state; + + if (!SetServiceStatus(statusHandle, status)) + { + Throw.Command.Win32Exception("Failed to set service status."); + } + } + + internal void Delete() + { + if (!DeleteService(this.handle)) + { + Throw.Command.Win32Exception("Failed to delete service."); + } + } + internal void SetDescription(string description) { if (!ChangeServiceConfig2( @@ -98,7 +206,7 @@ namespace WinSW.Native ServiceConfigInfoLevels.DESCRIPTION, new SERVICE_DESCRIPTION { Description = description })) { - Throw.Win32Exception("Failed to configure the description."); + Throw.Command.Win32Exception("Failed to configure the description."); } } @@ -118,7 +226,7 @@ namespace WinSW.Native Actions = actionsPtr, })) { - Throw.Win32Exception("Failed to configure the failure actions."); + Throw.Command.Win32Exception("Failed to configure the failure actions."); } } } @@ -130,7 +238,7 @@ namespace WinSW.Native ServiceConfigInfoLevels.DELAYED_AUTO_START_INFO, new SERVICE_DELAYED_AUTO_START_INFO { DelayedAutostart = enabled })) { - Throw.Win32Exception("Failed to configure the delayed auto-start setting."); + Throw.Command.Win32Exception("Failed to configure the delayed auto-start setting."); } } @@ -140,7 +248,7 @@ namespace WinSW.Native securityDescriptor.GetBinaryForm(securityDescriptorBytes, 0); if (!SetServiceObjectSecurity(this.handle, SecurityInfos.DiscretionaryAcl, securityDescriptorBytes)) { - Throw.Win32Exception("Failed to configure the security descriptor."); + Throw.Command.Win32Exception("Failed to configure the security descriptor."); } } diff --git a/src/WinSW.Core/Native/ServiceApis.cs b/src/WinSW.Core/Native/ServiceApis.cs index 42d0e7b..9575609 100644 --- a/src/WinSW.Core/Native/ServiceApis.cs +++ b/src/WinSW.Core/Native/ServiceApis.cs @@ -1,6 +1,8 @@ using System; using System.Runtime.InteropServices; using System.Security.AccessControl; +using System.ServiceProcess; +using System.Text; namespace WinSW.Native { @@ -18,16 +20,38 @@ namespace WinSW.Native [DllImport(Libraries.Advapi32)] internal static extern bool CloseServiceHandle(IntPtr objectHandle); + [DllImport(Libraries.Advapi32, SetLastError = true, CharSet = CharSet.Unicode, EntryPoint = "CreateServiceW")] + internal static extern IntPtr CreateService( + IntPtr databaseHandle, + string serviceName, + string displayName, + ServiceAccess desiredAccess, + ServiceType serviceType, + ServiceStartMode startType, + ServiceErrorControl errorControl, + string binaryPath, + string? loadOrderGroup, + IntPtr tagId, + StringBuilder? dependencies, // TODO + string? serviceStartName, + string? password); + + [DllImport(Libraries.Advapi32, SetLastError = true)] + internal static extern bool DeleteService(IntPtr serviceHandle); + [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 extern IntPtr OpenService(IntPtr databaseHandle, string serviceName, ServiceAccess desiredAccess); + [DllImport(Libraries.Advapi32, SetLastError = true)] + internal static extern bool QueryServiceStatus(IntPtr serviceHandle, out SERVICE_STATUS serviceStatus); + [DllImport(Libraries.Advapi32, SetLastError = true)] internal static extern bool SetServiceObjectSecurity(IntPtr serviceHandle, SecurityInfos securityInformation, byte[] securityDescriptor); - [DllImport(Libraries.Advapi32)] + [DllImport(Libraries.Advapi32, SetLastError = true)] internal static extern bool SetServiceStatus(IntPtr serviceStatusHandle, in SERVICE_STATUS serviceStatus); // SERVICE_ @@ -35,27 +59,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_ @@ -73,17 +97,13 @@ namespace WinSW.Native PREFERRED_NODE = 9, } - // SERVICE_ - // https://docs.microsoft.com/windows/win32/api/winsvc/ns-winsvc-service_status - internal enum ServiceState : uint + // SERVICE_ERROR_ + internal enum ServiceErrorControl : uint { - STOPPED = 0x00000001, - START_PENDING = 0x00000002, - STOP_PENDING = 0x00000003, - RUNNING = 0x00000004, - CONTINUE_PENDING = 0x00000005, - PAUSE_PENDING = 0x00000006, - PAUSED = 0x00000007, + Ignore = 0x00000000, + Normal = 0x00000001, + Severe = 0x00000002, + Critical = 0x00000003, } // SC_MANAGER_ @@ -91,21 +111,42 @@ 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, + } + + 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 @@ -121,19 +162,19 @@ namespace WinSW.Native // https://docs.microsoft.com/windows/win32/api/winsvc/ns-winsvc-service_failure_actionsw [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] - internal unsafe struct SERVICE_FAILURE_ACTIONS + internal struct SERVICE_FAILURE_ACTIONS { public int ResetPeriod; public string RebootMessage; public string Command; public int ActionsCount; - public SC_ACTION* Actions; + public unsafe SC_ACTION* Actions; } internal struct SERVICE_STATUS { - public int ServiceType; - public ServiceState CurrentState; + public ServiceType ServiceType; + public ServiceControllerStatus CurrentState; public int ControlsAccepted; public int Win32ExitCode; public int ServiceSpecificExitCode; diff --git a/src/WinSW.Core/Native/Throw.cs b/src/WinSW.Core/Native/Throw.cs index 7fe07d4..1674096 100644 --- a/src/WinSW.Core/Native/Throw.cs +++ b/src/WinSW.Core/Native/Throw.cs @@ -1,16 +1,74 @@ -using System.ComponentModel; +using System; +using System.ComponentModel; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; namespace WinSW.Native { internal static class Throw { - internal static void Win32Exception(string message) + internal static class Command { - var inner = new Win32Exception(); - Debug.Assert(inner.NativeErrorCode != 0); - Debug.Assert(message.EndsWith(".")); - throw new Win32Exception(inner.NativeErrorCode, message + ' ' + inner.Message); + [DoesNotReturn] + [MethodImpl(MethodImplOptions.NoInlining)] + internal static void Exception(Exception inner) + { + throw new CommandException(inner); + } + + [DoesNotReturn] + [MethodImpl(MethodImplOptions.NoInlining)] + internal static void Exception(string message) + { + Debug.Assert(message.EndsWith(".")); + throw new CommandException(message); + } + + [DoesNotReturn] + [MethodImpl(MethodImplOptions.NoInlining)] + internal static void Exception(string message, Exception inner) + { + Debug.Assert(message.EndsWith(".")); + throw new CommandException(message + ' ' + inner.Message, inner); + } + + [DoesNotReturn] + [MethodImpl(MethodImplOptions.NoInlining)] + internal static void Win32Exception(int error) + { + Debug.Assert(error != 0); + throw new CommandException(new Win32Exception(error)); + } + + [DoesNotReturn] + [MethodImpl(MethodImplOptions.NoInlining)] + internal static void Win32Exception(int error, string message) + { + Debug.Assert(error != 0); + var inner = new Win32Exception(error); + Debug.Assert(message.EndsWith(".")); + throw new CommandException(message + ' ' + inner.Message, inner); + } + + [DoesNotReturn] + [MethodImpl(MethodImplOptions.NoInlining)] + internal static void Win32Exception() + { + var inner = new Win32Exception(); + Debug.Assert(inner.NativeErrorCode != 0); + throw new CommandException(inner); + } + + [DoesNotReturn] + [MethodImpl(MethodImplOptions.NoInlining)] + internal static void Win32Exception(string message) + { + var inner = new Win32Exception(); + Debug.Assert(inner.NativeErrorCode != 0); + Debug.Assert(message.EndsWith(".")); + throw new CommandException(message + ' ' + inner.Message, inner); + } } } } diff --git a/src/WinSW.Core/NullableAttributes.cs b/src/WinSW.Core/NullableAttributes.cs index a2a05fe..d4a0ad3 100644 --- a/src/WinSW.Core/NullableAttributes.cs +++ b/src/WinSW.Core/NullableAttributes.cs @@ -1,13 +1,25 @@ -#if !NETCOREAPP -#pragma warning disable SA1502 // Element should not be on a single line +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if !NET namespace System.Diagnostics.CodeAnalysis { /// Specifies that null is allowed as an input even if the corresponding type disallows it. [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)] - internal sealed class AllowNullAttribute : Attribute { } + internal sealed class AllowNullAttribute : Attribute + { + } /// Specifies that an output may be null even if the corresponding type disallows it. [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)] - internal sealed class MaybeNullAttribute : Attribute { } + internal sealed class MaybeNullAttribute : Attribute + { + } + + /// Applied to a method that will never return under any circumstance. + [AttributeUsage(AttributeTargets.Method, Inherited = false)] + internal sealed class DoesNotReturnAttribute : Attribute + { + } } #endif diff --git a/src/WinSW.Core/ServiceDescriptor.cs b/src/WinSW.Core/ServiceDescriptor.cs index 3216760..109be12 100644 --- a/src/WinSW.Core/ServiceDescriptor.cs +++ b/src/WinSW.Core/ServiceDescriptor.cs @@ -2,12 +2,12 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; +using System.ServiceProcess; using System.Text; using System.Xml; using WinSW.Configuration; using WinSW.Native; using WinSW.Util; -using WMI; namespace WinSW { @@ -56,13 +56,13 @@ namespace WinSW Environment.SetEnvironmentVariable("BASE", d.FullName); // ditto for ID - Environment.SetEnvironmentVariable("SERVICE_ID", this.Id); + Environment.SetEnvironmentVariable("SERVICE_ID", this.Name); // New name Environment.SetEnvironmentVariable(WinSWSystem.EnvVarNameExecutablePath, this.ExecutablePath); // Also inject system environment variables - Environment.SetEnvironmentVariable(WinSWSystem.EnvVarNameServiceId, this.Id); + Environment.SetEnvironmentVariable(WinSWSystem.EnvVarNameServiceId, this.Name); this.environmentVariables = this.LoadEnvironmentVariables(); } @@ -455,16 +455,16 @@ namespace WinSW } } - public string Id => this.SingleElement("id"); + public string Name => this.SingleElement("id"); - public string Caption => this.SingleElement("name"); + public string DisplayName => this.SingleElement("name"); public string Description => this.SingleElement("description"); /// /// Start mode of the Service /// - public StartMode StartMode + public ServiceStartMode StartMode { get { @@ -476,12 +476,12 @@ namespace WinSW try { - return (StartMode)Enum.Parse(typeof(StartMode), p, true); + return (ServiceStartMode)Enum.Parse(typeof(ServiceStartMode), p, true); } catch { Console.WriteLine("Start mode in XML must be one of the following:"); - foreach (string sm in Enum.GetNames(typeof(StartMode))) + foreach (string sm in Enum.GetNames(typeof(ServiceStartMode))) { Console.WriteLine(sm); } @@ -607,7 +607,7 @@ namespace WinSW return false; } - public ServiceAccount ServiceAccount + public Configuration.ServiceAccount ServiceAccount { get { diff --git a/src/WinSW.Core/ServiceDescriptorYaml.cs b/src/WinSW.Core/ServiceDescriptorYaml.cs index e982979..5305e72 100644 --- a/src/WinSW.Core/ServiceDescriptorYaml.cs +++ b/src/WinSW.Core/ServiceDescriptorYaml.cs @@ -26,13 +26,13 @@ namespace WinSW Environment.SetEnvironmentVariable("BASE", d.FullName); // ditto for ID - Environment.SetEnvironmentVariable("SERVICE_ID", this.Configurations.Id); + Environment.SetEnvironmentVariable("SERVICE_ID", this.Configurations.Name); // New name Environment.SetEnvironmentVariable(WinSWSystem.EnvVarNameExecutablePath, Defaults.ExecutablePath); // Also inject system environment variables - Environment.SetEnvironmentVariable(WinSWSystem.EnvVarNameServiceId, this.Configurations.Id); + Environment.SetEnvironmentVariable(WinSWSystem.EnvVarNameServiceId, this.Configurations.Name); this.Configurations.LoadEnvironmentVariables(); } diff --git a/src/WinSW.Core/WinSW.Core.csproj b/src/WinSW.Core/WinSW.Core.csproj index 7ca8c79..d64883e 100644 --- a/src/WinSW.Core/WinSW.Core.csproj +++ b/src/WinSW.Core/WinSW.Core.csproj @@ -19,9 +19,8 @@ - - - + + @@ -31,12 +30,12 @@ + - - + diff --git a/src/WinSW.Core/Wmi.cs b/src/WinSW.Core/Wmi.cs deleted file mode 100644 index ed68947..0000000 --- a/src/WinSW.Core/Wmi.cs +++ /dev/null @@ -1,227 +0,0 @@ -using System; -using System.Management; -using System.Reflection; -using System.Text; -using DynamicProxy; - -namespace WMI -{ - // https://docs.microsoft.com/windows/win32/cimwin32prov/create-method-in-class-win32-service - public enum ReturnValue : uint - { - Success = 0, - NotSupported = 1, - AccessDenied = 2, - DependentServicesRunning = 3, - InvalidServiceControl = 4, - ServiceCannotAcceptControl = 5, - ServiceNotActive = 6, - ServiceRequestTimeout = 7, - UnknownFailure = 8, - PathNotFound = 9, - ServiceAlreadyRunning = 10, - ServiceDatabaseLocked = 11, - ServiceDependencyDeleted = 12, - ServiceDependencyFailure = 13, - ServiceDisabled = 14, - ServiceLogonFailure = 15, - ServiceMarkedForDeletion = 16, - ServiceNoThread = 17, - StatusCircularDependency = 18, - StatusDuplicateName = 19, - StatusInvalidName = 20, - StatusInvalidParameter = 21, - StatusInvalidServiceAccount = 22, - StatusServiceExists = 23, - ServiceAlreadyPaused = 24, - - NoSuchService = 200 - } - - /// - /// Signals a problem in WMI related operations - /// - public class WmiException : Exception - { - public readonly ReturnValue ErrorCode; - - public WmiException(string message, ReturnValue code) - : base(message) - { - this.ErrorCode = code; - } - - public WmiException(ReturnValue code) - : this(code.ToString(), code) - { - } - } - - /// - /// Associated a WMI class name to the proxy interface (which should extend from IWmiCollection) - /// - public class WmiClassName : Attribute - { - public readonly string Name; - - public WmiClassName(string name) => this.Name = name; - } - - /// - /// Marker interface to denote a collection in WMI. - /// - public interface IWmiCollection - { - } - - /// - /// Marker interface to denote an individual managed object - /// - public interface IWmiObject - { - } - - public sealed class WmiRoot - { - private readonly ManagementScope wmiScope; - - public WmiRoot() - { - var options = new ConnectionOptions - { - EnablePrivileges = true, - Impersonation = ImpersonationLevel.Impersonate, - Authentication = AuthenticationLevel.PacketPrivacy, - }; - - this.wmiScope = new ManagementScope(@"\\.\root\cimv2", options); - this.wmiScope.Connect(); - } - - private static string Capitalize(string s) - { - return char.ToUpper(s[0]) + s.Substring(1); - } - - private abstract class BaseHandler : IProxyInvocationHandler - { - public abstract object? Invoke(object proxy, MethodInfo method, object[] arguments); - - protected void CheckError(ManagementBaseObject result) - { - uint code = (uint)result["returnValue"]; - if (code != 0) - { - throw new WmiException((ReturnValue)code); - } - } - - protected ManagementBaseObject GetMethodParameters(ManagementObject wmiObject, string methodName, ParameterInfo[] methodParameters, object[] arguments) - { - var wmiParameters = wmiObject.GetMethodParameters(methodName); - for (int i = 0; i < arguments.Length; i++) - { - string capitalizedName = Capitalize(methodParameters[i].Name!); - wmiParameters[capitalizedName] = arguments[i]; - } - - return wmiParameters; - } - } - - private class InstanceHandler : BaseHandler, IWmiObject - { - private readonly ManagementObject wmiObject; - - public InstanceHandler(ManagementObject wmiObject) => this.wmiObject = wmiObject; - - public override object? Invoke(object proxy, MethodInfo method, object[] arguments) - { - if (method.DeclaringType == typeof(IWmiObject)) - { - return method.Invoke(this, arguments); - } - - // TODO: proper property support - if (method.Name.StartsWith("set_")) - { - this.wmiObject[method.Name.Substring(4)] = arguments[0]; - return null; - } - - if (method.Name.StartsWith("get_")) - { - return this.wmiObject[method.Name.Substring(4)]; - } - - string methodName = method.Name; - using var wmiParameters = arguments.Length == 0 ? null : - this.GetMethodParameters(this.wmiObject, methodName, method.GetParameters(), arguments); - using var result = this.wmiObject.InvokeMethod(methodName, wmiParameters, null); - this.CheckError(result); - return null; - } - } - - private class ClassHandler : BaseHandler - { - private readonly ManagementClass wmiClass; - private readonly string className; - - public ClassHandler(ManagementScope wmiScope, string className) - { - this.wmiClass = new ManagementClass(wmiScope, new ManagementPath(className), null); - this.className = className; - } - - public override object? Invoke(object proxy, MethodInfo method, object[] arguments) - { - var methodParameters = method.GetParameters(); - - if (method.Name == nameof(IWin32Services.Select)) - { - // select method to find instances - var query = new StringBuilder("SELECT * FROM ").Append(this.className).Append(" WHERE "); - for (int i = 0; i < arguments.Length; i++) - { - if (i != 0) - { - query.Append(" AND "); - } - - query.Append(' ').Append(Capitalize(methodParameters[i].Name!)).Append(" = '").Append(arguments[i]).Append('\''); - } - - using var searcher = new ManagementObjectSearcher(this.wmiClass.Scope, new ObjectQuery(query.ToString())); - using var results = searcher.Get(); - - // TODO: support collections - foreach (ManagementObject wmiObject in results) - { - return ProxyFactory.Create(new InstanceHandler(wmiObject), method.ReturnType, true); - } - - return null; - } - - string methodName = method.Name; - using var wmiParameters = arguments.Length == 0 ? null : - this.GetMethodParameters(this.wmiClass, methodName, methodParameters, arguments); - using var result = this.wmiClass.InvokeMethod(methodName, wmiParameters, null); - this.CheckError(result); - return null; - } - } - - /// - /// Obtains an object that corresponds to a table in WMI, which is a collection of a managed object. - /// - public T GetCollection() - where T : IWmiCollection - { - var className = (WmiClassName)typeof(T).GetCustomAttributes(typeof(WmiClassName), false)[0]; - - return (T)ProxyFactory.Create(new ClassHandler(this.wmiScope, className.Name), typeof(T), true); - } - } -} diff --git a/src/WinSW.Core/WmiSchema.cs b/src/WinSW.Core/WmiSchema.cs deleted file mode 100644 index a9e962f..0000000 --- a/src/WinSW.Core/WmiSchema.cs +++ /dev/null @@ -1,72 +0,0 @@ -namespace WMI -{ - public enum ServiceType - { - KernalDriver = 1, - FileSystemDriver = 2, - Adapter = 4, - RecognizerDriver = 8, - OwnProcess = 16, - ShareProcess = 32, - InteractiveProcess = 256, - } - - public enum ErrorControl - { - UserNotNotified = 0, - UserNotified = 1, - SystemRestartedWithLastKnownGoodConfiguration = 2, - SystemAttemptsToStartWithAGoodConfiguration = 3 - } - - public enum StartMode - { - /// - /// Device driver started by the operating system loader. This value is valid only for driver services. - /// - Boot, - - /// - /// Device driver started by the operating system initialization process. This value is valid only for driver services. - /// - System, - - /// - /// Service to be started automatically by the Service Control Manager during system startup. - /// - Automatic, - - /// - /// Service to be started by the Service Control Manager when a process calls the StartService method. - /// - Manual, - - /// - /// Service that can no longer be started. - /// - Disabled, - } - - [WmiClassName("Win32_Service")] - public interface IWin32Services : IWmiCollection - { - // ReturnValue Create(bool desktopInteract, string displayName, int errorControl, string loadOrderGroup, string loadOrderGroupDependencies, string name, string pathName, string serviceDependencies, string serviceType, string startMode, string startName, string startPassword); - void Create(string name, string displayName, string pathName, ServiceType serviceType, ErrorControl errorControl, string startMode, bool desktopInteract, string? startName, string? startPassword, string[] serviceDependencies); - - void Create(string name, string displayName, string pathName, ServiceType serviceType, ErrorControl errorControl, string startMode, bool desktopInteract, string[] serviceDependencies); - - IWin32Service? Select(string name); - } - - // https://docs.microsoft.com/windows/win32/cimwin32prov/win32-service - public interface IWin32Service : IWmiObject - { - bool Started { get; } - - void Delete(); - - void StartService(); - - void StopService(); - } -} diff --git a/src/WinSW.Plugins/RunawayProcessKillerExtension.cs b/src/WinSW.Plugins/RunawayProcessKillerExtension.cs index 648648d..85e1434 100644 --- a/src/WinSW.Plugins/RunawayProcessKillerExtension.cs +++ b/src/WinSW.Plugins/RunawayProcessKillerExtension.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Collections.Specialized; using System.Diagnostics; using System.IO; using System.Text; @@ -188,7 +186,7 @@ namespace WinSW.Plugins this.Pidfile = XmlHelper.SingleElement(node, "pidfile", false)!; this.StopTimeout = TimeSpan.FromMilliseconds(int.Parse(XmlHelper.SingleElement(node, "stopTimeout", false)!)); this.StopParentProcessFirst = bool.Parse(XmlHelper.SingleElement(node, "stopParentFirst", false)!); - this.ServiceId = descriptor.Id; + this.ServiceId = descriptor.Name; // TODO: Consider making it documented string? checkWinSWEnvironmentVariable = XmlHelper.SingleElement(node, "checkWinSWEnvironmentVariable", true); this.CheckWinSWEnvironmentVariable = checkWinSWEnvironmentVariable is null ? true : bool.Parse(checkWinSWEnvironmentVariable); diff --git a/src/WinSW.Tests/Configuration/ExamplesTest.cs b/src/WinSW.Tests/Configuration/ExamplesTest.cs index 29ee0f2..f495f23 100644 --- a/src/WinSW.Tests/Configuration/ExamplesTest.cs +++ b/src/WinSW.Tests/Configuration/ExamplesTest.cs @@ -19,8 +19,8 @@ namespace winswTests.Configuration { var desc = Load("allOptions"); - Assert.That(desc.Id, Is.EqualTo("myapp")); - Assert.That(desc.Caption, Is.EqualTo("MyApp Service (powered by WinSW)")); + Assert.That(desc.Name, Is.EqualTo("myapp")); + Assert.That(desc.DisplayName, Is.EqualTo("MyApp Service (powered by WinSW)")); Assert.That(desc.Description, Is.EqualTo("This service is a service created from a sample configuration")); Assert.That(desc.Executable, Is.EqualTo("%BASE%\\myExecutable.exe")); @@ -32,8 +32,8 @@ namespace winswTests.Configuration { var desc = Load("minimal"); - Assert.That(desc.Id, Is.EqualTo("myapp")); - Assert.That(desc.Caption, Is.EqualTo("MyApp Service (powered by WinSW)")); + Assert.That(desc.Name, Is.EqualTo("myapp")); + Assert.That(desc.DisplayName, Is.EqualTo("MyApp Service (powered by WinSW)")); Assert.That(desc.Description, Is.EqualTo("This service is a service created from a minimal configuration")); Assert.That(desc.Executable, Is.EqualTo("%BASE%\\myExecutable.exe")); diff --git a/src/WinSW.Tests/Extensions/RunawayProcessKillerTest.cs b/src/WinSW.Tests/Extensions/RunawayProcessKillerTest.cs index f534f47..3b3b119 100644 --- a/src/WinSW.Tests/Extensions/RunawayProcessKillerTest.cs +++ b/src/WinSW.Tests/Extensions/RunawayProcessKillerTest.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Diagnostics; using System.IO; using NUnit.Framework; diff --git a/src/WinSW.Tests/ServiceDescriptorTests.cs b/src/WinSW.Tests/ServiceDescriptorTests.cs index e3cb2a7..c64cf68 100644 --- a/src/WinSW.Tests/ServiceDescriptorTests.cs +++ b/src/WinSW.Tests/ServiceDescriptorTests.cs @@ -1,9 +1,9 @@ using System; using System.Diagnostics; +using System.ServiceProcess; using NUnit.Framework; using WinSW; using winswTests.Util; -using WMI; namespace winswTests { @@ -44,7 +44,7 @@ $@" [Test] public void DefaultStartMode() { - Assert.That(this._extendedServiceDescriptor.StartMode, Is.EqualTo(StartMode.Automatic)); + Assert.That(this._extendedServiceDescriptor.StartMode, Is.EqualTo(ServiceStartMode.Automatic)); } [Test] @@ -96,7 +96,7 @@ $@" "; this._extendedServiceDescriptor = ServiceDescriptor.FromXML(seedXml); - Assert.That(this._extendedServiceDescriptor.StartMode, Is.EqualTo(StartMode.Manual)); + Assert.That(this._extendedServiceDescriptor.StartMode, Is.EqualTo(ServiceStartMode.Manual)); } [Test] diff --git a/src/WinSW.Tests/ServiceDescriptorYamlTest.cs b/src/WinSW.Tests/ServiceDescriptorYamlTest.cs index 2b65dcc..44b7235 100644 --- a/src/WinSW.Tests/ServiceDescriptorYamlTest.cs +++ b/src/WinSW.Tests/ServiceDescriptorYamlTest.cs @@ -1,9 +1,9 @@ using System; using System.Diagnostics; +using System.ServiceProcess; using NUnit.Framework; using WinSW; using WinSW.Configuration; -using WMI; namespace winswTests { @@ -43,7 +43,7 @@ workingdirectory: {ExpectedWorkingDirectory}"; [Test] public void DefaultStartMode() { - Assert.That(this._extendedServiceDescriptor.StartMode, Is.EqualTo(StartMode.Automatic)); + Assert.That(this._extendedServiceDescriptor.StartMode, Is.EqualTo(ServiceStartMode.Automatic)); } [Test] @@ -73,7 +73,7 @@ arguments: My Arguments startMode: manual"; this._extendedServiceDescriptor = ServiceDescriptorYaml.FromYaml(yaml).Configurations; - Assert.That(this._extendedServiceDescriptor.StartMode, Is.EqualTo(StartMode.Manual)); + Assert.That(this._extendedServiceDescriptor.StartMode, Is.EqualTo(ServiceStartMode.Manual)); } [Test] @@ -470,7 +470,7 @@ description: This is test winsw"; Assert.That(() => { - _ = ServiceDescriptorYaml.FromYaml(yml).Configurations.Id; + _ = ServiceDescriptorYaml.FromYaml(yml).Configurations.Name; }, Throws.TypeOf()); } diff --git a/src/WinSW.Tests/Util/ServiceDescriptorAssert.cs b/src/WinSW.Tests/Util/ServiceDescriptorAssert.cs index 14e9254..e75704b 100644 --- a/src/WinSW.Tests/Util/ServiceDescriptorAssert.cs +++ b/src/WinSW.Tests/Util/ServiceDescriptorAssert.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Reflection; using NUnit.Framework; using WinSW; using WinSW.Configuration; @@ -55,14 +54,14 @@ namespace winswTests.Util get { var properties = AllProperties; - properties.Remove("Id"); - properties.Remove("Caption"); - properties.Remove("Description"); - properties.Remove("Executable"); - properties.Remove("BaseName"); - properties.Remove("BasePath"); - properties.Remove("Log"); - properties.Remove("ServiceAccount"); + properties.Remove(nameof(IWinSWConfiguration.Name)); + properties.Remove(nameof(IWinSWConfiguration.DisplayName)); + properties.Remove(nameof(IWinSWConfiguration.Description)); + properties.Remove(nameof(IWinSWConfiguration.Executable)); + properties.Remove(nameof(IWinSWConfiguration.BaseName)); + properties.Remove(nameof(IWinSWConfiguration.BasePath)); + properties.Remove(nameof(IWinSWConfiguration.Log)); + properties.Remove(nameof(IWinSWConfiguration.ServiceAccount)); return properties; } } diff --git a/src/WinSW/FormatExtensions.cs b/src/WinSW/FormatExtensions.cs new file mode 100644 index 0000000..8fb3319 --- /dev/null +++ b/src/WinSW/FormatExtensions.cs @@ -0,0 +1,20 @@ +using System.ServiceProcess; +using WinSW.Configuration; + +namespace WinSW +{ + internal static class FormatExtensions + { + internal static string Format(IWinSWConfiguration config) + { + string name = config.Name; + string displayName = config.DisplayName; + return $"{(string.IsNullOrEmpty(displayName) ? name : displayName)} ({name})"; + } + + internal static string Format(ServiceController controller) + { + return $"{controller.DisplayName} ({controller.ServiceName})"; + } + } +} diff --git a/src/WinSW/Program.cs b/src/WinSW/Program.cs index ab7beb6..a9eecad 100644 --- a/src/WinSW/Program.cs +++ b/src/WinSW/Program.cs @@ -2,12 +2,10 @@ using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using System.IO; #if NET using System.Reflection; #endif -using System.Runtime.InteropServices; using System.Security.AccessControl; using System.Security.Principal; using System.ServiceProcess; @@ -21,8 +19,10 @@ using log4net.Layout; using WinSW.Configuration; using WinSW.Logging; using WinSW.Native; -using WMI; -using ServiceType = WMI.ServiceType; +using static WinSW.FormatExtensions; +using static WinSW.ServiceControllerExtension; +using static WinSW.Native.ServiceApis; +using TimeoutException = System.ServiceProcess.TimeoutException; namespace WinSW { @@ -35,7 +35,6 @@ namespace WinSW try { Run(args); - Log.Debug("Completed. Exit code is 0"); return 0; } catch (InvalidDataException e) @@ -45,16 +44,27 @@ namespace WinSW Console.Error.WriteLine(message); return -1; } - catch (WmiException e) + catch (CommandException e) { - Log.Fatal("WMI Operation failure: " + e.ErrorCode, e); - Console.Error.WriteLine(e); - return (int)e.ErrorCode; + string message = e.Message; + Log.Fatal(message); + return e.InnerException is Win32Exception inner ? inner.NativeErrorCode : -1; + } + catch (InvalidOperationException e) when (e.InnerException is Win32Exception inner) + { + string message = e.Message; + Log.Fatal(message); + return inner.NativeErrorCode; + } + catch (Win32Exception e) + { + string message = e.Message; + Log.Fatal(message, e); + return e.NativeErrorCode; } catch (Exception e) { Log.Fatal("Unhandled exception", e); - Console.Error.WriteLine(e); return -1; } } @@ -94,10 +104,6 @@ namespace WinSW return; } - // Get service info for the future use - var svcs = new WmiRoot().GetCollection(); - var svc = svcs.Select(descriptor.Id); - var args = new List(Array.AsReadOnly(argsArray)); if (args[0] == "/redirect") { @@ -148,11 +154,11 @@ namespace WinSW return; case "stop": - Stop(); + Stop(true); return; case "stopwait": - StopWait(); + Stop(false); return; case "restart": @@ -201,14 +207,15 @@ namespace WinSW return; } - Log.Info("Installing the service with id '" + descriptor.Id + "'"); + Log.Info($"Installing service '{Format(descriptor)}'..."); - // Check if the service exists - if (svc != null) + using var scm = ServiceManager.Open(ServiceManagerAccess.CreateService); + + if (scm.ServiceExists(descriptor.Name)) { - Console.WriteLine("Service with id '" + descriptor.Id + "' already exists"); - Console.WriteLine("To install the service, delete the existing one or change service Id in the configuration file"); - throw new Exception("Installation failure: Service with id '" + descriptor.Id + "' already exists"); + + Log.Error($"A service with ID '{descriptor.Name}' already exists."); + Throw.Command.Win32Exception(Errors.ERROR_SERVICE_EXISTS, "Failed to install the service."); } string? username = null; @@ -244,22 +251,21 @@ namespace WinSW Security.AddServiceLogonRight(descriptor.ServiceAccount.ServiceAccountDomain!, descriptor.ServiceAccount.ServiceAccountName!); } - svcs.Create( - descriptor.Id, - descriptor.Caption, - "\"" + descriptor.ExecutablePath + "\"", - ServiceType.OwnProcess, - ErrorControl.UserNotified, - descriptor.StartMode.ToString(), + using var sc = scm.CreateService( + descriptor.Name, + descriptor.DisplayName, descriptor.Interactive, + descriptor.StartMode, + $"\"{descriptor.ExecutablePath}\"", + descriptor.ServiceDependencies, username, - password, - descriptor.ServiceDependencies); + password); - using var scm = ServiceManager.Open(); - using var sc = scm.OpenService(descriptor.Id); - - sc.SetDescription(descriptor.Description); + string description = descriptor.Description; + if (description.Length != 0) + { + sc.SetDescription(description); + } var actions = descriptor.FailureActions; if (actions.Length > 0) @@ -267,7 +273,7 @@ namespace WinSW sc.SetFailureActions(descriptor.ResetFailureAfter, actions); } - bool isDelayedAutoStart = descriptor.StartMode == StartMode.Automatic && descriptor.DelayedAutoStart; + bool isDelayedAutoStart = descriptor.StartMode == ServiceStartMode.Automatic && descriptor.DelayedAutoStart; if (isDelayedAutoStart) { sc.SetDelayedAutoStart(true); @@ -280,11 +286,13 @@ namespace WinSW sc.SetSecurityDescriptor(new RawSecurityDescriptor(securityDescriptor)); } - string eventLogSource = descriptor.Id; + string eventLogSource = descriptor.Name; if (!EventLog.SourceExists(eventLogSource)) { EventLog.CreateEventSource(eventLogSource, "Application"); } + + Log.Info($"Service '{Format(descriptor)}' was installed successfully."); } void Uninstall() @@ -295,40 +303,42 @@ namespace WinSW return; } - Log.Info("Uninstalling the service with id '" + descriptor.Id + "'"); - if (svc is null) - { - Log.Warn("The service with id '" + descriptor.Id + "' does not exist. Nothing to uninstall"); - return; // there's no such service, so consider it already uninstalled - } - - if (svc.Started) - { - // We could fail the opeartion here, but it would be an incompatible change. - // So it is just a warning - Log.Warn("The service with id '" + descriptor.Id + "' is running. It may be impossible to uninstall it"); - } + Log.Info($"Uninstalling service '{Format(descriptor)}'..."); + using var scm = ServiceManager.Open(ServiceManagerAccess.Connect); try { - svc.Delete(); + using var sc = scm.OpenService(descriptor.Name); + + if (sc.Status != ServiceControllerStatus.Stopped) + { + // We could fail the opeartion here, but it would be an incompatible change. + // So it is just a warning + Log.Warn($"Service '{Format(descriptor)}' is started. It may be impossible to uninstall it."); + } + + sc.Delete(); + + Log.Info($"Service '{Format(descriptor)}' was uninstalled successfully."); } - catch (WmiException e) + catch (CommandException e) when (e.InnerException is Win32Exception inner) { - if (e.ErrorCode == ReturnValue.ServiceMarkedForDeletion) + switch (inner.NativeErrorCode) { - Log.Error("Failed to uninstall the service with id '" + descriptor.Id + "'" - + ". It has been marked for deletion."); + case Errors.ERROR_SERVICE_DOES_NOT_EXIST: + Log.Warn($"Service '{Format(descriptor)}' does not exist."); + break; // there's no such service, so consider it already uninstalled - // TODO: change the default behavior to Error? - return; // it's already uninstalled, so consider it a success - } - else - { - Log.Fatal("Failed to uninstall the service with id '" + descriptor.Id + "'. WMI Error code is '" + e.ErrorCode + "'"); - } + case Errors.ERROR_SERVICE_MARKED_FOR_DELETE: + Log.Error(e.Message); - throw; + // TODO: change the default behavior to Error? + break; // it's already uninstalled, so consider it a success + + default: + Throw.Command.Exception("Failed to uninstall the service.", inner); + break; + } } } @@ -340,30 +350,28 @@ namespace WinSW return; } - Log.Info("Starting the service with id '" + descriptor.Id + "'"); - if (svc is null) - { - ThrowNoSuchService(); - } + using var svc = new ServiceController(descriptor.Name); try { - svc.StartService(); + Log.Info($"Starting service '{Format(svc)}'..."); + svc.Start(); + + Log.Info($"Service '{Format(svc)}' started successfully."); } - catch (WmiException e) + catch (InvalidOperationException e) + when (e.InnerException is Win32Exception inner && inner.NativeErrorCode == Errors.ERROR_SERVICE_DOES_NOT_EXIST) { - if (e.ErrorCode == ReturnValue.ServiceAlreadyRunning) - { - Log.Info($"The service with ID '{descriptor.Id}' has already been started"); - } - else - { - throw; - } + Throw.Command.Exception(inner); + } + catch (InvalidOperationException e) + when (e.InnerException is Win32Exception inner && inner.NativeErrorCode == Errors.ERROR_SERVICE_ALREADY_RUNNING) + { + Log.Info($"Service '{Format(svc)}' has already started."); } } - void Stop() + void Stop(bool noWait) { if (!elevated) { @@ -371,56 +379,37 @@ namespace WinSW return; } - Log.Info("Stopping the service with id '" + descriptor.Id + "'"); - if (svc is null) - { - ThrowNoSuchService(); - } + using var svc = new ServiceController(descriptor.Name); try { - svc.StopService(); - } - catch (WmiException e) - { - if (e.ErrorCode == ReturnValue.ServiceCannotAcceptControl) + Log.Info($"Stopping service '{Format(svc)}'..."); + svc.Stop(); + + if (!noWait) { - Log.Info($"The service with ID '{descriptor.Id}' is not running"); + try + { + WaitForStatus(svc, ServiceControllerStatus.Stopped, ServiceControllerStatus.StopPending); + } + catch (TimeoutException) + { + Throw.Command.Exception("Failed to stop the service."); + } } - else - { - throw; - } - } - } - void StopWait() - { - if (!elevated) + Log.Info($"Service '{Format(svc)}' stopped successfully."); + } + catch (InvalidOperationException e) + when (e.InnerException is Win32Exception inner && inner.NativeErrorCode == Errors.ERROR_SERVICE_DOES_NOT_EXIST) { - Elevate(); - return; + Throw.Command.Exception(inner); } - - Log.Info("Stopping the service with id '" + descriptor.Id + "'"); - if (svc is null) + catch (InvalidOperationException e) + when (e.InnerException is Win32Exception inner && inner.NativeErrorCode == Errors.ERROR_SERVICE_NOT_ACTIVE) { - ThrowNoSuchService(); + Log.Info($"Service '{Format(svc)}' has already stopped."); } - - if (svc.Started) - { - svc.StopService(); - } - - while (svc != null && svc.Started) - { - Log.Info("Waiting the service to stop..."); - Thread.Sleep(1000); - svc = svcs.Select(descriptor.Id); - } - - Log.Info("The service stopped."); } void Restart() @@ -431,47 +420,115 @@ namespace WinSW return; } - Log.Info("Restarting the service with id '" + descriptor.Id + "'"); - if (svc is null) + + using var svc = new ServiceController(descriptor.Name); + + List? startedDependentServices = null; + + try + { + if (HasAnyStartedDependentService(svc)) + { + startedDependentServices = new(); + foreach (var service in svc.DependentServices) + { + if (service.Status != ServiceControllerStatus.Stopped) + { + startedDependentServices.Add(service); + } + } + } + + Log.Info($"Stopping service '{Format(svc)}'..."); + svc.Stop(); + + try + { + WaitForStatus(svc, ServiceControllerStatus.Stopped, ServiceControllerStatus.StopPending); + } + catch (TimeoutException) + { + Throw.Command.Exception("Failed to stop the service."); + } + } + catch (InvalidOperationException e) + when (e.InnerException is Win32Exception inner && inner.NativeErrorCode == Errors.ERROR_SERVICE_DOES_NOT_EXIST) + { + Throw.Command.Exception(inner); + } + catch (InvalidOperationException e) + when (e.InnerException is Win32Exception inner && inner.NativeErrorCode == Errors.ERROR_SERVICE_NOT_ACTIVE) { - ThrowNoSuchService(); } - if (svc.Started) + Log.Info($"Starting service '{Format(svc)}'..."); + svc.Start(); + + try { - svc.StopService(); + WaitForStatus(svc, ServiceControllerStatus.Running, ServiceControllerStatus.StartPending); + } + catch (TimeoutException) + { + Throw.Command.Exception("Failed to start the service."); } - while (svc.Started) + if (startedDependentServices != null) { - Thread.Sleep(1000); - svc = svcs.Select(descriptor.Id)!; + foreach (var service in startedDependentServices) + { + if (service.Status == ServiceControllerStatus.Stopped) + { + Log.Info($"Starting service '{Format(service)}'..."); + service.Start(); + } + } } - svc.StartService(); + Log.Info($"Service '{Format(svc)}' restarted successfully."); } void RestartSelf() { if (!elevated) { - throw new UnauthorizedAccessException("Access is denied."); + Throw.Command.Win32Exception(Errors.ERROR_ACCESS_DENIED); } - Log.Info("Restarting the service with id '" + descriptor.Id + "'"); + Log.Info("Restarting the service with id '" + descriptor.Name + "'"); // run restart from another process group. see README.md for why this is useful. - bool result = ProcessApis.CreateProcess(null, descriptor.ExecutablePath + " restart", IntPtr.Zero, IntPtr.Zero, false, ProcessApis.CREATE_NEW_PROCESS_GROUP, IntPtr.Zero, null, default, out _); - if (!result) + if (!ProcessApis.CreateProcess( + null, + descriptor.ExecutablePath + " restart", + IntPtr.Zero, + IntPtr.Zero, + false, + ProcessApis.CREATE_NEW_PROCESS_GROUP, + IntPtr.Zero, + null, + default, + out var processInfo)) { - throw new Exception("Failed to invoke restart: " + Marshal.GetLastWin32Error()); + Throw.Command.Win32Exception("Failed to invoke restart."); } + + _ = HandleApis.CloseHandle(processInfo.ProcessHandle); + _ = HandleApis.CloseHandle(processInfo.ThreadHandle); } void Status() { - Log.Debug("User requested the status of the process with id '" + descriptor.Id + "'"); - Console.WriteLine(svc is null ? "NonExistent" : svc.Started ? "Started" : "Stopped"); + using var svc = new ServiceController(descriptor.Name); + try + { + Console.WriteLine(svc.Status != ServiceControllerStatus.Stopped ? "Started" : "Stopped"); + } + catch (InvalidOperationException e) + when (e.InnerException is Win32Exception inner && inner.NativeErrorCode == Errors.ERROR_SERVICE_DOES_NOT_EXIST) + { + Console.WriteLine("NonExistent"); + } } void Test() @@ -538,9 +595,6 @@ namespace WinSW } } - [DoesNotReturn] - private static void ThrowNoSuchService() => throw new WmiException(ReturnValue.NoSuchService); - private static void InitLoggers(IWinSWConfiguration descriptor, bool enableConsoleLogging) { // TODO: Make logging levels configurable diff --git a/src/WinSW/ServiceControllerExtension.cs b/src/WinSW/ServiceControllerExtension.cs new file mode 100644 index 0000000..dd76b06 --- /dev/null +++ b/src/WinSW/ServiceControllerExtension.cs @@ -0,0 +1,32 @@ +using System; +using System.ServiceProcess; +using TimeoutException = System.ServiceProcess.TimeoutException; + +namespace WinSW +{ + internal static class ServiceControllerExtension + { + /// + internal static void WaitForStatus(ServiceController serviceController, ServiceControllerStatus desiredStatus, ServiceControllerStatus pendingStatus) + { + var timeout = new TimeSpan(TimeSpan.TicksPerSecond); + for (; ; ) + { + try + { + serviceController.WaitForStatus(desiredStatus, timeout); + break; + } + catch (TimeoutException) + when (serviceController.Status == desiredStatus || serviceController.Status == pendingStatus) + { + } + } + } + + internal static bool HasAnyStartedDependentService(ServiceController serviceController) + { + return Array.Exists(serviceController.DependentServices, service => service.Status != ServiceControllerStatus.Stopped); + } + } +} diff --git a/src/WinSW/WinSW.csproj b/src/WinSW/WinSW.csproj index 3688b2c..7d6e909 100644 --- a/src/WinSW/WinSW.csproj +++ b/src/WinSW/WinSW.csproj @@ -25,13 +25,8 @@ 3.0.41 - - - - - diff --git a/src/WinSW/WrapperService.cs b/src/WinSW/WrapperService.cs index afe9938..fa51f20 100644 --- a/src/WinSW/WrapperService.cs +++ b/src/WinSW/WrapperService.cs @@ -20,8 +20,6 @@ namespace WinSW { public class WrapperService : ServiceBase, IEventLogger { - private ServiceApis.SERVICE_STATUS wrapperServiceStatus; - private readonly Process process = new(); private readonly IWinSWConfiguration descriptor; @@ -61,7 +59,7 @@ namespace WinSW public WrapperService(IWinSWConfiguration descriptor) { this.descriptor = descriptor; - this.ServiceName = this.descriptor.Id; + this.ServiceName = this.descriptor.Name; this.ExtensionManager = new WinSWExtensionManager(this.descriptor); this.CanShutdown = true; this.CanStop = true; @@ -340,8 +338,8 @@ namespace WinSW private void DoStop() { string? stopArguments = this.descriptor.StopArguments; - this.LogEvent("Stopping " + this.descriptor.Id); - Log.Info("Stopping " + this.descriptor.Id); + this.LogEvent("Stopping " + this.descriptor.Name); + Log.Info("Stopping " + this.descriptor.Name); this.orderlyShutdown = true; if (stopArguments is null) @@ -359,7 +357,7 @@ namespace WinSW } else { - this.SignalShutdownPending(); + this.SignalPending(); stopArguments += " " + this.descriptor.Arguments; @@ -384,12 +382,12 @@ namespace WinSW Console.Beep(); } - Log.Info("Finished " + this.descriptor.Id); + Log.Info("Finished " + this.descriptor.Name); } private void WaitForProcessToExit(Process processoWait) { - this.SignalShutdownPending(); + this.SignalPending(); int effectiveProcessWaitSleepTime; if (this.descriptor.SleepTime.TotalMilliseconds > int.MaxValue) @@ -409,7 +407,7 @@ namespace WinSW while (!processoWait.WaitForExit(effectiveProcessWaitSleepTime)) { - this.SignalShutdownPending(); + this.SignalPending(); // WriteEvent("WaitForProcessToExit [repeat]"); } } @@ -421,7 +419,7 @@ namespace WinSW // WriteEvent("WaitForProcessToExit [finished]"); } - private void SignalShutdownPending() + private void SignalPending() { int effectiveWaitHint; if (this.descriptor.WaitHint.TotalMilliseconds > int.MaxValue) @@ -438,13 +436,12 @@ namespace WinSW this.RequestAdditionalTime(effectiveWaitHint); } - private void SignalShutdownComplete() + private void SignalStopped() { - var handle = this.ServiceHandle; - this.wrapperServiceStatus.CheckPoint++; - // WriteEvent("SignalShutdownComplete " + wrapperServiceStatus.checkPoint + ":" + wrapperServiceStatus.waitHint); - this.wrapperServiceStatus.CurrentState = ServiceApis.ServiceState.STOPPED; - ServiceApis.SetServiceStatus(handle, this.wrapperServiceStatus); + using var scm = ServiceManager.Open(); + using var sc = scm.OpenService(this.ServiceName, ServiceApis.ServiceAccess.QueryStatus); + + sc.SetStatus(this.ServiceHandle, ServiceControllerStatus.Stopped); } private void StartProcess(Process processToStart, string arguments, string executable, LogHandler? logHandler) @@ -468,7 +465,7 @@ namespace WinSW // restart the service automatically if (proc.ExitCode == 0) { - this.SignalShutdownComplete(); + this.SignalStopped(); } Environment.Exit(proc.ExitCode);