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)
{