From 6cefa93c69c731004bb748b4f66a37123a3edd88 Mon Sep 17 00:00:00 2001 From: NextTurn <45985406+NextTurn@users.noreply.github.com> Date: Mon, 3 Aug 2020 00:00:00 +0800 Subject: [PATCH] CLI updates --- src/WinSW.Core/Configuration/ServiceConfig.cs | 4 +- .../Configuration/XmlServiceConfig.cs | 12 +- src/WinSW.Core/FormatExtensions.cs | 14 + src/WinSW.Core/Native/Errors.cs | 1 + src/WinSW.Core/Native/Service.cs | 30 +- src/WinSW.Core/Native/ServiceApis.cs | 6 +- src/WinSW.Core/Native/Throw.cs | 51 ++- src/WinSW.Core/NullableAttributes.cs | 6 + src/WinSW.Tests/Configuration/ExamplesTest.cs | 4 +- src/WinSW.Tests/Util/CommandLineTestHelper.cs | 4 +- src/WinSW.Tests/Util/ServiceConfigAssert.cs | 2 +- src/WinSW/Logging/WinSWConsoleAppender.cs | 29 ++ src/WinSW/NullableAttributes.cs | 13 - src/WinSW/Program.cs | 340 ++++++++++-------- src/WinSW/ServiceControllerExtension.cs | 10 +- src/WinSW/WrapperService.cs | 6 +- 16 files changed, 328 insertions(+), 204 deletions(-) create mode 100644 src/WinSW/Logging/WinSWConsoleAppender.cs delete mode 100644 src/WinSW/NullableAttributes.cs diff --git a/src/WinSW.Core/Configuration/ServiceConfig.cs b/src/WinSW.Core/Configuration/ServiceConfig.cs index 397aee9..9462859 100644 --- a/src/WinSW.Core/Configuration/ServiceConfig.cs +++ b/src/WinSW.Core/Configuration/ServiceConfig.cs @@ -14,9 +14,9 @@ namespace WinSW.Configuration { public abstract string FullPath { get; } - public abstract string Id { get; } + public abstract string Name { get; } - public virtual string Caption => string.Empty; + public virtual string DisplayName => string.Empty; public virtual string Description => string.Empty; diff --git a/src/WinSW.Core/Configuration/XmlServiceConfig.cs b/src/WinSW.Core/Configuration/XmlServiceConfig.cs index 0e7a9ff..ebb3dbc 100644 --- a/src/WinSW.Core/Configuration/XmlServiceConfig.cs +++ b/src/WinSW.Core/Configuration/XmlServiceConfig.cs @@ -67,13 +67,13 @@ namespace WinSW Environment.SetEnvironmentVariable("BASE", baseDir); // 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(); } @@ -105,13 +105,13 @@ namespace WinSW Environment.SetEnvironmentVariable("BASE", baseDir); // 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(); } @@ -529,9 +529,9 @@ namespace WinSW } } - public override string Id => this.SingleElement("id"); + public override string Name => this.SingleElement("id"); - public override string Caption => this.SingleElement("name", true) ?? base.Caption; + public override string DisplayName => this.SingleElement("name", true) ?? base.DisplayName; public override string Description => this.SingleElement("description", true) ?? base.Description; diff --git a/src/WinSW.Core/FormatExtensions.cs b/src/WinSW.Core/FormatExtensions.cs index 76ed0cb..fb70c41 100644 --- a/src/WinSW.Core/FormatExtensions.cs +++ b/src/WinSW.Core/FormatExtensions.cs @@ -1,5 +1,7 @@ using System; using System.Diagnostics; +using System.ServiceProcess; +using WinSW.Configuration; namespace WinSW { @@ -16,5 +18,17 @@ namespace WinSW return $"({process.Id})"; } } + + internal static string Format(this ServiceConfig config) + { + string name = config.Name; + string displayName = config.DisplayName; + return $"{(string.IsNullOrEmpty(displayName) ? name : displayName)} ({name})"; + } + + internal static string Format(this ServiceController controller) + { + return $"{controller.DisplayName} ({controller.ServiceName})"; + } } } diff --git a/src/WinSW.Core/Native/Errors.cs b/src/WinSW.Core/Native/Errors.cs index 3d9b2c5..e026089 100644 --- a/src/WinSW.Core/Native/Errors.cs +++ b/src/WinSW.Core/Native/Errors.cs @@ -12,6 +12,7 @@ namespace WinSW.Native 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/Service.cs b/src/WinSW.Core/Native/Service.cs index e889c10..6a390a9 100644 --- a/src/WinSW.Core/Native/Service.cs +++ b/src/WinSW.Core/Native/Service.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using System.Security.AccessControl; using System.ServiceProcess; using System.Text; @@ -218,20 +219,23 @@ namespace WinSW.Native ServiceStartMode startMode, string[] dependencies) { - if (!ChangeServiceConfig( - this.handle, - default, - startMode, - default, - null, - null, - IntPtr.Zero, - GetNativeDependencies(dependencies), - null, - null, - displayName)) + unchecked { - Throw.Command.Win32Exception("Failed to change service config."); + if (!ChangeServiceConfig( + this.handle, + (ServiceType)SERVICE_NO_CHANGE, + startMode, + (ServiceErrorControl)SERVICE_NO_CHANGE, + null, + null, + IntPtr.Zero, + GetNativeDependencies(dependencies), + null, + null, + displayName)) + { + Throw.Command.Win32Exception("Failed to change service config."); + } } } diff --git a/src/WinSW.Core/Native/ServiceApis.cs b/src/WinSW.Core/Native/ServiceApis.cs index ae59139..e500fcb 100644 --- a/src/WinSW.Core/Native/ServiceApis.cs +++ b/src/WinSW.Core/Native/ServiceApis.cs @@ -1,4 +1,6 @@ -using System; +#pragma warning disable SA1310 // Field names should not contain underscore + +using System; using System.Runtime.InteropServices; using System.Security.AccessControl; using System.ServiceProcess; @@ -8,6 +10,8 @@ namespace WinSW.Native { internal static class ServiceApis { + internal const uint SERVICE_NO_CHANGE = 0xffffffff; + [DllImport(Libraries.Advapi32, SetLastError = true, CharSet = CharSet.Unicode, EntryPoint = "ChangeServiceConfigW")] internal static extern bool ChangeServiceConfig( IntPtr serviceHandle, diff --git a/src/WinSW.Core/Native/Throw.cs b/src/WinSW.Core/Native/Throw.cs index b6125fa..bb935fd 100644 --- a/src/WinSW.Core/Native/Throw.cs +++ b/src/WinSW.Core/Native/Throw.cs @@ -1,5 +1,7 @@ -using System.ComponentModel; +using System; +using System.ComponentModel; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; namespace WinSW.Native @@ -9,6 +11,53 @@ namespace WinSW.Native internal static class Command { /// + [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); + } + + /// + [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); + Win32Exception inner = new Win32Exception(error); + Debug.Assert(message.EndsWith(".")); + throw new CommandException(message + ' ' + inner.Message, inner); + } + + /// + [DoesNotReturn] [MethodImpl(MethodImplOptions.NoInlining)] internal static void Win32Exception(string message) { diff --git a/src/WinSW.Core/NullableAttributes.cs b/src/WinSW.Core/NullableAttributes.cs index f4c0ad8..d0e2804 100644 --- a/src/WinSW.Core/NullableAttributes.cs +++ b/src/WinSW.Core/NullableAttributes.cs @@ -15,5 +15,11 @@ namespace System.Diagnostics.CodeAnalysis 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.Tests/Configuration/ExamplesTest.cs b/src/WinSW.Tests/Configuration/ExamplesTest.cs index 4e868c0..c10da65 100644 --- a/src/WinSW.Tests/Configuration/ExamplesTest.cs +++ b/src/WinSW.Tests/Configuration/ExamplesTest.cs @@ -17,7 +17,7 @@ namespace WinSW.Tests.Configuration { XmlServiceConfig config = Load("complete"); - Assert.Equal("myapp", config.Id); + Assert.Equal("myapp", config.Name); Assert.Equal("%BASE%\\myExecutable.exe", config.Executable); ServiceConfigAssert.AssertAllOptionalPropertiesAreDefault(config); @@ -28,7 +28,7 @@ namespace WinSW.Tests.Configuration { XmlServiceConfig config = Load("minimal"); - Assert.Equal("myapp", config.Id); + Assert.Equal("myapp", config.Name); Assert.Equal("%BASE%\\myExecutable.exe", config.Executable); ServiceConfigAssert.AssertAllOptionalPropertiesAreDefault(config); diff --git a/src/WinSW.Tests/Util/CommandLineTestHelper.cs b/src/WinSW.Tests/Util/CommandLineTestHelper.cs index 91feed7..280cf62 100644 --- a/src/WinSW.Tests/Util/CommandLineTestHelper.cs +++ b/src/WinSW.Tests/Util/CommandLineTestHelper.cs @@ -46,7 +46,7 @@ $@" XmlServiceConfig.TestConfig = config ?? DefaultServiceConfig; try { - _ = Program.Run(arguments); + _ = Program.Main(arguments); } finally { @@ -81,7 +81,7 @@ $@" Program.TestExceptionHandler = (e, _) => exception = e; try { - _ = Program.Run(arguments); + _ = Program.Main(arguments); } catch (Exception e) { diff --git a/src/WinSW.Tests/Util/ServiceConfigAssert.cs b/src/WinSW.Tests/Util/ServiceConfigAssert.cs index dec9553..87be0f3 100644 --- a/src/WinSW.Tests/Util/ServiceConfigAssert.cs +++ b/src/WinSW.Tests/Util/ServiceConfigAssert.cs @@ -26,7 +26,7 @@ namespace WinSW.Tests.Util public override string FullPath => this.config.FullPath; - public override string Id => this.config.Id; + public override string Name => this.config.Name; public override string Executable => this.config.Executable; } diff --git a/src/WinSW/Logging/WinSWConsoleAppender.cs b/src/WinSW/Logging/WinSWConsoleAppender.cs new file mode 100644 index 0000000..aea6020 --- /dev/null +++ b/src/WinSW/Logging/WinSWConsoleAppender.cs @@ -0,0 +1,29 @@ +using System; +using log4net.Appender; +using log4net.Core; + +namespace WinSW.Logging +{ + internal sealed class WinSWConsoleAppender : AppenderSkeleton + { + protected override void Append(LoggingEvent loggingEvent) + { + Console.ResetColor(); + + Level level = loggingEvent.Level; + Console.ForegroundColor = + level >= Level.Error ? ConsoleColor.Red : + level >= Level.Warn ? ConsoleColor.Yellow : + level >= Level.Info ? ConsoleColor.Gray : + ConsoleColor.DarkGray; + try + { + this.Layout.Format(Console.Out, loggingEvent); + } + finally + { + Console.ResetColor(); + } + } + } +} diff --git a/src/WinSW/NullableAttributes.cs b/src/WinSW/NullableAttributes.cs deleted file mode 100644 index 7b33c84..0000000 --- a/src/WinSW/NullableAttributes.cs +++ /dev/null @@ -1,13 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -#if !NETCOREAPP -namespace System.Diagnostics.CodeAnalysis -{ - /// 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/Program.cs b/src/WinSW/Program.cs index fd13eb7..31f8489 100644 --- a/src/WinSW/Program.cs +++ b/src/WinSW/Program.cs @@ -3,16 +3,12 @@ using System.Collections.Generic; using System.CommandLine; using System.CommandLine.Builder; using System.CommandLine.Invocation; -using System.CommandLine.IO; using System.CommandLine.Parsing; using System.ComponentModel; using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; using System.Security.AccessControl; using System.Security.Principal; using System.ServiceProcess; @@ -39,24 +35,24 @@ namespace WinSW internal static Action? TestExceptionHandler; - private static int Main(string[] args) - { - int exitCode = Run(args); - Log.Debug("Completed. Exit code is " + exitCode); - return exitCode; - } - - internal static int Run(string[] args) + internal static int Main(string[] args) { bool elevated; - if (args[0] == "--elevated") + if (args.Length > 0 && args[0] == "--elevated") { elevated = true; _ = ConsoleApis.FreeConsole(); _ = ConsoleApis.AttachConsole(ConsoleApis.ATTACH_PARENT_PROCESS); - args = new List(args).GetRange(1, args.Length - 1).ToArray(); +#if NETCOREAPP + args = args[1..]; +#else + string[] oldArgs = args; + int newLength = oldArgs.Length - 1; + args = new string[newLength]; + Array.Copy(oldArgs, 1, args, 0, newLength); +#endif } else if (Environment.OSVersion.Version.Major == 5) { @@ -72,19 +68,19 @@ namespace WinSW { Handler = CommandHandler.Create((string? pathToConfig) => { - XmlServiceConfig config; + XmlServiceConfig config = null!; try { config = XmlServiceConfig.Create(pathToConfig); } catch (FileNotFoundException) { - throw new CommandException("The specified command or file was not found."); + Throw.Command.Exception("The specified command or file was not found."); } InitLoggers(config, enableConsoleLogging: false); - Log.Debug("Starting WinSW in service mode"); + Log.Debug("Starting WinSW in service mode."); ServiceBase.Run(new WrapperService(config)); }), }; @@ -141,11 +137,12 @@ namespace WinSW { var start = new Command("start", "Starts the service.") { - Handler = CommandHandler.Create(Start), + Handler = CommandHandler.Create(Start), }; start.Add(config); start.Add(noElevate); + start.Add(new Option("--no-wait", "Doesn't wait for the service to actually start.")); root.Add(start); } @@ -153,7 +150,7 @@ namespace WinSW { var stop = new Command("stop", "Stops the service.") { - Handler = CommandHandler.Create(Stop), + Handler = CommandHandler.Create(Stop), }; stop.Add(config); @@ -167,7 +164,7 @@ namespace WinSW { var restart = new Command("restart", "Stops and then starts the service.") { - Handler = CommandHandler.Create(Restart), + Handler = CommandHandler.Create(Restart), }; restart.Add(config); @@ -240,14 +237,15 @@ namespace WinSW } { - var dev = new Command("dev"); - - dev.Add(config); - dev.Add(noElevate); + var dev = new Command("dev", "Experimental commands.") + { + config, + noElevate, + }; root.Add(dev); - var ps = new Command("ps") + var ps = new Command("ps", "Draws the process tree associated with the service.") { Handler = CommandHandler.Create(DevPs), }; @@ -256,14 +254,8 @@ namespace WinSW } return new CommandLineBuilder(root) - - // see UseDefaults .UseVersionOption() .UseHelp() - /* .UseEnvironmentVariableDirective() */ - .UseParseDirective() - .UseDebugDirective() - .UseSuggestDirective() .RegisterWithDotnetSuggest() .UseTypoCorrections() .UseParseErrorReporting() @@ -274,64 +266,57 @@ namespace WinSW static void OnException(Exception exception, InvocationContext context) { - Console.ForegroundColor = ConsoleColor.Red; - try + Debug.Assert(exception is TargetInvocationException); + Debug.Assert(exception.InnerException != null); + exception = exception.InnerException!; + switch (exception) { - IStandardStreamWriter error = context.Console.Error; + case InvalidDataException e: + { + string message = "The configuration file could not be loaded. " + e.Message; + Log.Fatal(message, e); + context.ResultCode = -1; + break; + } - Debug.Assert(exception is TargetInvocationException); - Debug.Assert(exception.InnerException != null); - exception = exception.InnerException!; - switch (exception) - { - case InvalidDataException e: - { - string message = "The configuration file cound not be loaded. " + e.Message; - Log.Fatal(message, e); - error.WriteLine(message); - context.ResultCode = -1; - break; - } + case OperationCanceledException e: + { + Debug.Assert(e.CancellationToken == context.GetCancellationToken()); + Log.Fatal(e.Message); + context.ResultCode = -1; + break; + } - case CommandException e: - { - string message = e.Message; - Log.Fatal(message); - error.WriteLine(message); - context.ResultCode = e.InnerException is Win32Exception inner ? inner.NativeErrorCode : -1; - break; - } + case CommandException e: + { + string message = e.Message; + Log.Fatal(message); + context.ResultCode = e.InnerException is Win32Exception inner ? inner.NativeErrorCode : -1; + break; + } - case InvalidOperationException e when e.InnerException is Win32Exception inner: - { - string message = e.Message; - Log.Fatal(message, e); - error.WriteLine(message); - context.ResultCode = inner.NativeErrorCode; - break; - } + case InvalidOperationException e when e.InnerException is Win32Exception inner: + { + string message = e.Message; + Log.Fatal(message); + context.ResultCode = inner.NativeErrorCode; + break; + } - case Win32Exception e: - { - string message = e.Message; - Log.Fatal(message, e); - error.WriteLine(message); - context.ResultCode = e.NativeErrorCode; - break; - } + case Win32Exception e: + { + string message = e.Message; + Log.Fatal(message, e); + context.ResultCode = e.NativeErrorCode; + break; + } - default: - { - Log.Fatal("Unhandled exception", exception); - error.WriteLine(exception.ToString()); - context.ResultCode = -1; - break; - } - } - } - finally - { - Console.ResetColor(); + default: + { + Log.Fatal("Unhandled exception", exception); + context.ResultCode = -1; + break; + } } } @@ -346,15 +331,14 @@ namespace WinSW return; } - Log.Info("Installing the service with id '" + config.Id + "'"); + Log.Info($"Installing service '{config.Format()}'..."); using ServiceManager scm = ServiceManager.Open(); - if (scm.ServiceExists(config.Id)) + if (scm.ServiceExists(config.Name)) { - Console.WriteLine("Service with id '" + config.Id + "' already exists"); - Console.WriteLine("To install the service, delete the existing one or change service Id in the configuration file"); - throw new CommandException("Installation failure: Service with id '" + config.Id + "' already exists"); + Log.Error($"A service with ID '{config.Name}' already exists."); + Throw.Command.Win32Exception(Errors.ERROR_SERVICE_EXISTS, "Failed to install the service."); } if (config.HasServiceAccount()) @@ -371,7 +355,7 @@ namespace WinSW ref username, ref password, "Windows Service Wrapper", - "service account credentials"); // TODO + "Enter the service account credentials"); break; case "console": @@ -387,10 +371,10 @@ namespace WinSW } using Service sc = scm.CreateService( - config.Id, - config.Caption, + config.Name, + config.DisplayName, config.StartMode, - "\"" + config.ExecutablePath + "\"" + (pathToConfig != null ? " \"" + Path.GetFullPath(pathToConfig) + "\"" : null), + $"\"{config.ExecutablePath}\"" + (pathToConfig is null ? null : $" \"{Path.GetFullPath(pathToConfig)}\""), config.ServiceDependencies, username, password); @@ -425,12 +409,14 @@ namespace WinSW sc.SetSecurityDescriptor(new RawSecurityDescriptor(securityDescriptor)); } - string eventLogSource = config.Id; + string eventLogSource = config.Name; if (!EventLog.SourceExists(eventLogSource)) { EventLog.CreateEventSource(eventLogSource, "Application"); } + Log.Info($"Service '{config.Format()}' was installed successfully."); + void PromptForCredentialsConsole() { if (username is null) @@ -470,45 +456,46 @@ namespace WinSW return; } - Log.Info("Uninstalling the service with id '" + config.Id + "'"); + Log.Info($"Uninstalling service '{config.Format()}'..."); using ServiceManager scm = ServiceManager.Open(); try { - using Service sc = scm.OpenService(config.Id); + using Service sc = scm.OpenService(config.Name); - if (sc.Status == ServiceControllerStatus.Running) + 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("The service with id '" + config.Id + "' is running. It may be impossible to uninstall it"); + Log.Warn($"Service '{config.Format()}' is started. It may be impossible to uninstall it."); } sc.Delete(); + + Log.Info($"Service '{config.Format()}' was uninstalled successfully."); } catch (CommandException e) when (e.InnerException is Win32Exception inner) { switch (inner.NativeErrorCode) { case Errors.ERROR_SERVICE_DOES_NOT_EXIST: - Log.Warn("The service with id '" + config.Id + "' does not exist. Nothing to uninstall"); + Log.Warn($"Service '{config.Format()}' does not exist."); break; // there's no such service, so consider it already uninstalled case Errors.ERROR_SERVICE_MARKED_FOR_DELETE: - Log.Error("Failed to uninstall the service with id '" + config.Id + "'" - + ". It has been marked for deletion."); + Log.Error(e.Message); // TODO: change the default behavior to Error? break; // it's already uninstalled, so consider it a success default: - Log.Fatal("Failed to uninstall the service with id '" + config.Id + "'. Error code is '" + inner.NativeErrorCode + "'"); - throw; + Throw.Command.Exception("Failed to uninstall the service.", inner); + break; } } } - void Start(string? pathToConfig, bool noElevate) + void Start(string? pathToConfig, bool noElevate, bool noWait, CancellationToken ct) { XmlServiceConfig config = XmlServiceConfig.Create(pathToConfig); InitLoggers(config, enableConsoleLogging: true); @@ -519,27 +506,40 @@ namespace WinSW return; } - Log.Info("Starting the service with id '" + config.Id + "'"); - - using var svc = new ServiceController(config.Id); + using var svc = new ServiceController(config.Name); try { + Log.Info($"Starting service '{svc.Format()}'..."); svc.Start(); + + if (!noWait) + { + try + { + svc.WaitForStatus(ServiceControllerStatus.Running, ServiceControllerStatus.StartPending, ct); + } + catch (TimeoutException) + { + Throw.Command.Exception("Failed to start the service."); + } + } + + Log.Info($"Service '{svc.Format()}' started successfully."); } catch (InvalidOperationException e) when (e.InnerException is Win32Exception inner && inner.NativeErrorCode == Errors.ERROR_SERVICE_DOES_NOT_EXIST) { - ThrowNoSuchService(inner); + Throw.Command.Exception(inner); } catch (InvalidOperationException e) when (e.InnerException is Win32Exception inner && inner.NativeErrorCode == Errors.ERROR_SERVICE_ALREADY_RUNNING) { - Log.Info($"The service with ID '{config.Id}' has already been started"); + Log.Info($"Service '{svc.Format()}' has already started."); } } - void Stop(string? pathToConfig, bool noElevate, bool noWait, bool force) + void Stop(string? pathToConfig, bool noElevate, bool noWait, bool force, CancellationToken ct) { XmlServiceConfig config = XmlServiceConfig.Create(pathToConfig); InitLoggers(config, enableConsoleLogging: true); @@ -550,9 +550,7 @@ namespace WinSW return; } - Log.Info("Stopping the service with id '" + config.Id + "'"); - - using var svc = new ServiceController(config.Id); + using var svc = new ServiceController(config.Name); try { @@ -560,39 +558,40 @@ namespace WinSW { if (svc.HasAnyStartedDependentService()) { - throw new CommandException("Failed to stop the service because it has started dependent services. Specify '--force' to proceed."); + Throw.Command.Exception("Failed to stop the service because it has started dependent services. Specify '--force' to proceed."); } } + Log.Info($"Stopping service '{svc.Format()}'..."); svc.Stop(); if (!noWait) { - Log.Info("Waiting for the service to stop..."); try { - svc.WaitForStatus(ServiceControllerStatus.Stopped, ServiceControllerStatus.StopPending); + svc.WaitForStatus(ServiceControllerStatus.Stopped, ServiceControllerStatus.StopPending, ct); } - catch (TimeoutException e) + catch (TimeoutException) { - throw new CommandException("Failed to stop the service.", e); + Throw.Command.Exception("Failed to stop the service."); } } + + Log.Info($"Service '{svc.Format()}' stopped successfully."); } catch (InvalidOperationException e) when (e.InnerException is Win32Exception inner && inner.NativeErrorCode == Errors.ERROR_SERVICE_DOES_NOT_EXIST) { - ThrowNoSuchService(inner); + Throw.Command.Exception(inner); } catch (InvalidOperationException e) when (e.InnerException is Win32Exception inner && inner.NativeErrorCode == Errors.ERROR_SERVICE_NOT_ACTIVE) { + Log.Info($"Service '{svc.Format()}' has already stopped."); } - - Log.Info("The service stopped."); } - void Restart(string? pathToConfig, bool noElevate, bool force) + void Restart(string? pathToConfig, bool noElevate, bool force, CancellationToken ct) { XmlServiceConfig config = XmlServiceConfig.Create(pathToConfig); InitLoggers(config, enableConsoleLogging: true); @@ -603,9 +602,7 @@ namespace WinSW return; } - Log.Info("Restarting the service with id '" + config.Id + "'"); - - using var svc = new ServiceController(config.Id); + using var svc = new ServiceController(config.Name); List? startedDependentServices = null; @@ -615,46 +612,59 @@ namespace WinSW { if (!force) { - throw new CommandException("Failed to restart the service because it has started dependent services. Specify '--force' to proceed."); + Throw.Command.Exception("Failed to restart the service because it has started dependent services. Specify '--force' to proceed."); } startedDependentServices = svc.DependentServices.Where(service => service.Status != ServiceControllerStatus.Stopped).ToList(); } + Log.Info($"Stopping service '{svc.Format()}'..."); svc.Stop(); - Log.Info("Waiting for the service to stop..."); try { - svc.WaitForStatus(ServiceControllerStatus.Stopped, ServiceControllerStatus.StopPending); + svc.WaitForStatus(ServiceControllerStatus.Stopped, ServiceControllerStatus.StopPending, ct); } - catch (TimeoutException e) + catch (TimeoutException) { - throw new CommandException("Failed to stop the service.", e); + 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) { - ThrowNoSuchService(inner); + Throw.Command.Exception(inner); } catch (InvalidOperationException e) when (e.InnerException is Win32Exception inner && inner.NativeErrorCode == Errors.ERROR_SERVICE_NOT_ACTIVE) { } + Log.Info($"Starting service '{svc.Format()}'..."); svc.Start(); + try + { + svc.WaitForStatus(ServiceControllerStatus.Running, ServiceControllerStatus.StartPending, ct); + } + catch (TimeoutException) + { + Throw.Command.Exception("Failed to start the service."); + } + if (startedDependentServices != null) { foreach (ServiceController service in startedDependentServices) { if (service.Status == ServiceControllerStatus.Stopped) { + Log.Info($"Starting service '{service.Format()}'..."); service.Start(); } } } + + Log.Info($"Service '{svc.Format()}' restarted successfully."); } void RestartSelf(string? pathToConfig) @@ -664,34 +674,56 @@ namespace WinSW if (!elevated) { - throw new CommandException(new Win32Exception(Errors.ERROR_ACCESS_DENIED)); + Throw.Command.Win32Exception(Errors.ERROR_ACCESS_DENIED); } - Log.Info("Restarting the service with id '" + config.Id + "'"); - // run restart from another process group. see README.md for why this is useful. - - if (!ProcessApis.CreateProcess(null, config.ExecutablePath + " restart", IntPtr.Zero, IntPtr.Zero, false, ProcessApis.CREATE_NEW_PROCESS_GROUP, IntPtr.Zero, null, default, out _)) + if (!ProcessApis.CreateProcess( + null, + $"\"{config.ExecutablePath}\" restart" + (pathToConfig is null ? null : $" \"{pathToConfig}\""), + IntPtr.Zero, + IntPtr.Zero, + false, + ProcessApis.CREATE_NEW_PROCESS_GROUP, + IntPtr.Zero, + null, + default, + out _)) { - throw new CommandException("Failed to invoke restart: " + Marshal.GetLastWin32Error()); + Throw.Command.Win32Exception("Failed to invoke restart."); } } - static void Status(string? pathToConfig) + static int Status(string? pathToConfig) { XmlServiceConfig config = XmlServiceConfig.Create(pathToConfig); InitLoggers(config, enableConsoleLogging: true); - Log.Debug("User requested the status of the process with id '" + config.Id + "'"); - using var svc = new ServiceController(config.Id); + using var svc = new ServiceController(config.Name); try { - Console.WriteLine(svc.Status == ServiceControllerStatus.Running ? "Started" : "Stopped"); + Console.WriteLine(svc.Status switch + { + ServiceControllerStatus.StartPending => "Starting", + ServiceControllerStatus.StopPending => "Stopping", + ServiceControllerStatus.Running => "Running", + ServiceControllerStatus.ContinuePending => "Continuing", + ServiceControllerStatus.PausePending => "Pausing", + ServiceControllerStatus.Paused => "Paused", + _ => "Stopped" + }); + + return svc.Status switch + { + ServiceControllerStatus.Stopped => 0, + _ => 1 + }; } catch (InvalidOperationException e) when (e.InnerException is Win32Exception inner && inner.NativeErrorCode == Errors.ERROR_SERVICE_DOES_NOT_EXIST) { Console.WriteLine("NonExistent"); + return Errors.ERROR_SERVICE_DOES_NOT_EXIST; } } @@ -730,6 +762,7 @@ namespace WinSW void CancelKeyPress(object sender, ConsoleCancelEventArgs e) { + e.Cancel = true; evt.Set(); } } @@ -754,9 +787,9 @@ namespace WinSW using ServiceManager scm = ServiceManager.Open(); try { - using Service sc = scm.OpenService(config.Id); + using Service sc = scm.OpenService(config.Name); - sc.ChangeConfig(config.Caption, config.StartMode, config.ServiceDependencies); + sc.ChangeConfig(config.DisplayName, config.StartMode, config.ServiceDependencies); sc.SetDescription(config.Description); @@ -787,8 +820,10 @@ namespace WinSW catch (CommandException e) when (e.InnerException is Win32Exception inner && inner.NativeErrorCode == Errors.ERROR_SERVICE_DOES_NOT_EXIST) { - ThrowNoSuchService(inner); + Throw.Command.Exception(inner); } + + Log.Info($"Service '{config.Format()}' was refreshed successfully."); } void DevPs(string? pathToConfig, bool noElevate) @@ -802,7 +837,7 @@ namespace WinSW } using ServiceManager scm = ServiceManager.Open(); - using Service sc = scm.OpenService(config.Id); + using Service sc = scm.OpenService(config.Name); int processId = sc.ProcessId; if (processId >= 0) @@ -848,7 +883,7 @@ namespace WinSW { if (noElevate) { - throw new CommandException(new Win32Exception(Errors.ERROR_ACCESS_DENIED)); + Throw.Command.Win32Exception(Errors.ERROR_ACCESS_DENIED); } using Process current = Process.GetCurrentProcess(); @@ -881,11 +916,6 @@ namespace WinSW } } - /// - [DoesNotReturn] - [MethodImpl(MethodImplOptions.NoInlining)] - private static void ThrowNoSuchService(Win32Exception inner) => throw new CommandException(inner); - private static void InitLoggers(XmlServiceConfig config, bool enableConsoleLogging) { if (XmlServiceConfig.TestConfig != null) @@ -900,10 +930,6 @@ namespace WinSW Level consoleLogLevel = Level.Info; Level eventLogLevel = Level.Warn; - // Legacy format from winsw-1.x: (DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + " - " + message); - PatternLayout layout = new PatternLayout { ConversionPattern = "%d %-5p - %m%n" }; - layout.ActivateOptions(); - List appenders = new List(); // .wrapper.log @@ -916,7 +942,7 @@ namespace WinSW Name = "Wrapper file log", Threshold = fileLogLevel, LockingModel = new FileAppender.MinimalLock(), - Layout = layout, + Layout = new PatternLayout("%date %-5level - %message%newline"), }; wrapperLog.ActivateOptions(); appenders.Add(wrapperLog); @@ -924,11 +950,11 @@ namespace WinSW // console log if (enableConsoleLogging) { - var consoleAppender = new ConsoleAppender + var consoleAppender = new WinSWConsoleAppender { Name = "Wrapper console log", Threshold = consoleLogLevel, - Layout = layout, + Layout = new PatternLayout("%date{ABSOLUTE} - %message%newline"), }; consoleAppender.ActivateOptions(); appenders.Add(consoleAppender); diff --git a/src/WinSW/ServiceControllerExtension.cs b/src/WinSW/ServiceControllerExtension.cs index 499424a..fdae0c5 100644 --- a/src/WinSW/ServiceControllerExtension.cs +++ b/src/WinSW/ServiceControllerExtension.cs @@ -1,15 +1,17 @@ using System; using System.ServiceProcess; +using System.Threading; using TimeoutException = System.ServiceProcess.TimeoutException; namespace WinSW { internal static class ServiceControllerExtension { + /// /// - internal static void WaitForStatus(this ServiceController serviceController, ServiceControllerStatus desiredStatus, ServiceControllerStatus pendingStatus) + internal static void WaitForStatus(this ServiceController serviceController, ServiceControllerStatus desiredStatus, ServiceControllerStatus pendingStatus, CancellationToken ct) { - TimeSpan timeout = TimeSpan.FromSeconds(1); + TimeSpan timeout = new TimeSpan(TimeSpan.TicksPerSecond); for (; ; ) { try @@ -17,8 +19,10 @@ namespace WinSW serviceController.WaitForStatus(desiredStatus, timeout); break; } - catch (TimeoutException) when (serviceController.Status == desiredStatus || serviceController.Status == pendingStatus) + catch (TimeoutException) + when (serviceController.Status == desiredStatus || serviceController.Status == pendingStatus) { + ct.ThrowIfCancellationRequested(); } } } diff --git a/src/WinSW/WrapperService.cs b/src/WinSW/WrapperService.cs index 0984a78..c9d0a71 100644 --- a/src/WinSW/WrapperService.cs +++ b/src/WinSW/WrapperService.cs @@ -46,7 +46,7 @@ namespace WinSW public WrapperService(XmlServiceConfig config) { - this.ServiceName = config.Id; + this.ServiceName = config.Name; this.CanStop = true; this.AutoLog = false; @@ -371,7 +371,7 @@ namespace WinSW } } - Log.Info("Stopping " + this.config.Id); + Log.Info("Stopping " + this.config.Name); this.process.EnableRaisingEvents = false; string? stopExecutable = this.config.StopExecutable; @@ -441,7 +441,7 @@ namespace WinSW Console.Beep(); } - Log.Info("Finished " + this.config.Id); + Log.Info("Finished " + this.config.Name); Process StartProcessLocked(string executable, string? arguments) {