CLI updates

pull/625/head
NextTurn 2020-08-03 00:00:00 +08:00 committed by Next Turn
parent 274deddfef
commit 6cefa93c69
16 changed files with 328 additions and 204 deletions

View File

@ -14,9 +14,9 @@ namespace WinSW.Configuration
{ {
public abstract string FullPath { get; } 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; public virtual string Description => string.Empty;

View File

@ -67,13 +67,13 @@ namespace WinSW
Environment.SetEnvironmentVariable("BASE", baseDir); Environment.SetEnvironmentVariable("BASE", baseDir);
// ditto for ID // ditto for ID
Environment.SetEnvironmentVariable("SERVICE_ID", this.Id); Environment.SetEnvironmentVariable("SERVICE_ID", this.Name);
// New name // New name
Environment.SetEnvironmentVariable(WinSWSystem.EnvVarNameExecutablePath, this.ExecutablePath); Environment.SetEnvironmentVariable(WinSWSystem.EnvVarNameExecutablePath, this.ExecutablePath);
// Also inject system environment variables // Also inject system environment variables
Environment.SetEnvironmentVariable(WinSWSystem.EnvVarNameServiceId, this.Id); Environment.SetEnvironmentVariable(WinSWSystem.EnvVarNameServiceId, this.Name);
this.environmentVariables = this.LoadEnvironmentVariables(); this.environmentVariables = this.LoadEnvironmentVariables();
} }
@ -105,13 +105,13 @@ namespace WinSW
Environment.SetEnvironmentVariable("BASE", baseDir); Environment.SetEnvironmentVariable("BASE", baseDir);
// ditto for ID // ditto for ID
Environment.SetEnvironmentVariable("SERVICE_ID", this.Id); Environment.SetEnvironmentVariable("SERVICE_ID", this.Name);
// New name // New name
Environment.SetEnvironmentVariable(WinSWSystem.EnvVarNameExecutablePath, this.ExecutablePath); Environment.SetEnvironmentVariable(WinSWSystem.EnvVarNameExecutablePath, this.ExecutablePath);
// Also inject system environment variables // Also inject system environment variables
Environment.SetEnvironmentVariable(WinSWSystem.EnvVarNameServiceId, this.Id); Environment.SetEnvironmentVariable(WinSWSystem.EnvVarNameServiceId, this.Name);
this.environmentVariables = this.LoadEnvironmentVariables(); 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; public override string Description => this.SingleElement("description", true) ?? base.Description;

View File

@ -1,5 +1,7 @@
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.ServiceProcess;
using WinSW.Configuration;
namespace WinSW namespace WinSW
{ {
@ -16,5 +18,17 @@ namespace WinSW
return $"({process.Id})"; 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})";
}
} }
} }

View File

@ -12,6 +12,7 @@ namespace WinSW.Native
internal const int ERROR_SERVICE_DOES_NOT_EXIST = 1060; internal const int ERROR_SERVICE_DOES_NOT_EXIST = 1060;
internal const int ERROR_SERVICE_NOT_ACTIVE = 1062; internal const int ERROR_SERVICE_NOT_ACTIVE = 1062;
internal const int ERROR_SERVICE_MARKED_FOR_DELETE = 1072; internal const int ERROR_SERVICE_MARKED_FOR_DELETE = 1072;
internal const int ERROR_SERVICE_EXISTS = 1073;
internal const int ERROR_CANCELLED = 1223; internal const int ERROR_CANCELLED = 1223;
} }
} }

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Diagnostics;
using System.Security.AccessControl; using System.Security.AccessControl;
using System.ServiceProcess; using System.ServiceProcess;
using System.Text; using System.Text;
@ -217,12 +218,14 @@ namespace WinSW.Native
string displayName, string displayName,
ServiceStartMode startMode, ServiceStartMode startMode,
string[] dependencies) string[] dependencies)
{
unchecked
{ {
if (!ChangeServiceConfig( if (!ChangeServiceConfig(
this.handle, this.handle,
default, (ServiceType)SERVICE_NO_CHANGE,
startMode, startMode,
default, (ServiceErrorControl)SERVICE_NO_CHANGE,
null, null,
null, null,
IntPtr.Zero, IntPtr.Zero,
@ -234,6 +237,7 @@ namespace WinSW.Native
Throw.Command.Win32Exception("Failed to change service config."); Throw.Command.Win32Exception("Failed to change service config.");
} }
} }
}
/// <exception cref="CommandException" /> /// <exception cref="CommandException" />
internal void Delete() internal void Delete()

View File

@ -1,4 +1,6 @@
using System; #pragma warning disable SA1310 // Field names should not contain underscore
using System;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Security.AccessControl; using System.Security.AccessControl;
using System.ServiceProcess; using System.ServiceProcess;
@ -8,6 +10,8 @@ namespace WinSW.Native
{ {
internal static class ServiceApis internal static class ServiceApis
{ {
internal const uint SERVICE_NO_CHANGE = 0xffffffff;
[DllImport(Libraries.Advapi32, SetLastError = true, CharSet = CharSet.Unicode, EntryPoint = "ChangeServiceConfigW")] [DllImport(Libraries.Advapi32, SetLastError = true, CharSet = CharSet.Unicode, EntryPoint = "ChangeServiceConfigW")]
internal static extern bool ChangeServiceConfig( internal static extern bool ChangeServiceConfig(
IntPtr serviceHandle, IntPtr serviceHandle,

View File

@ -1,5 +1,7 @@
using System.ComponentModel; using System;
using System.ComponentModel;
using System.Diagnostics; using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
namespace WinSW.Native namespace WinSW.Native
@ -9,6 +11,53 @@ namespace WinSW.Native
internal static class Command internal static class Command
{ {
/// <exception cref="CommandException" /> /// <exception cref="CommandException" />
[DoesNotReturn]
[MethodImpl(MethodImplOptions.NoInlining)]
internal static void Exception(Exception inner)
{
throw new CommandException(inner);
}
/// <exception cref="CommandException" />
[DoesNotReturn]
[MethodImpl(MethodImplOptions.NoInlining)]
internal static void Exception(string message)
{
Debug.Assert(message.EndsWith("."));
throw new CommandException(message);
}
/// <exception cref="CommandException" />
[DoesNotReturn]
[MethodImpl(MethodImplOptions.NoInlining)]
internal static void Exception(string message, Exception inner)
{
Debug.Assert(message.EndsWith("."));
throw new CommandException(message, inner);
}
/// <exception cref="CommandException" />
[DoesNotReturn]
[MethodImpl(MethodImplOptions.NoInlining)]
internal static void Win32Exception(int error)
{
Debug.Assert(error != 0);
throw new CommandException(new Win32Exception(error));
}
/// <exception cref="CommandException" />
[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);
}
/// <exception cref="CommandException" />
[DoesNotReturn]
[MethodImpl(MethodImplOptions.NoInlining)] [MethodImpl(MethodImplOptions.NoInlining)]
internal static void Win32Exception(string message) internal static void Win32Exception(string message)
{ {

View File

@ -15,5 +15,11 @@ namespace System.Diagnostics.CodeAnalysis
internal sealed class MaybeNullAttribute : Attribute internal sealed class MaybeNullAttribute : Attribute
{ {
} }
/// <summary>Applied to a method that will never return under any circumstance.</summary>
[AttributeUsage(AttributeTargets.Method, Inherited = false)]
internal sealed class DoesNotReturnAttribute : Attribute
{
}
} }
#endif #endif

View File

@ -17,7 +17,7 @@ namespace WinSW.Tests.Configuration
{ {
XmlServiceConfig config = Load("complete"); XmlServiceConfig config = Load("complete");
Assert.Equal("myapp", config.Id); Assert.Equal("myapp", config.Name);
Assert.Equal("%BASE%\\myExecutable.exe", config.Executable); Assert.Equal("%BASE%\\myExecutable.exe", config.Executable);
ServiceConfigAssert.AssertAllOptionalPropertiesAreDefault(config); ServiceConfigAssert.AssertAllOptionalPropertiesAreDefault(config);
@ -28,7 +28,7 @@ namespace WinSW.Tests.Configuration
{ {
XmlServiceConfig config = Load("minimal"); XmlServiceConfig config = Load("minimal");
Assert.Equal("myapp", config.Id); Assert.Equal("myapp", config.Name);
Assert.Equal("%BASE%\\myExecutable.exe", config.Executable); Assert.Equal("%BASE%\\myExecutable.exe", config.Executable);
ServiceConfigAssert.AssertAllOptionalPropertiesAreDefault(config); ServiceConfigAssert.AssertAllOptionalPropertiesAreDefault(config);

View File

@ -46,7 +46,7 @@ $@"<service>
XmlServiceConfig.TestConfig = config ?? DefaultServiceConfig; XmlServiceConfig.TestConfig = config ?? DefaultServiceConfig;
try try
{ {
_ = Program.Run(arguments); _ = Program.Main(arguments);
} }
finally finally
{ {
@ -81,7 +81,7 @@ $@"<service>
Program.TestExceptionHandler = (e, _) => exception = e; Program.TestExceptionHandler = (e, _) => exception = e;
try try
{ {
_ = Program.Run(arguments); _ = Program.Main(arguments);
} }
catch (Exception e) catch (Exception e)
{ {

View File

@ -26,7 +26,7 @@ namespace WinSW.Tests.Util
public override string FullPath => this.config.FullPath; 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; public override string Executable => this.config.Executable;
} }

View File

@ -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();
}
}
}
}

View File

@ -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
{
/// <summary>Applied to a method that will never return under any circumstance.</summary>
[AttributeUsage(AttributeTargets.Method, Inherited = false)]
internal sealed class DoesNotReturnAttribute : Attribute
{
}
}
#endif

View File

@ -3,16 +3,12 @@ using System.Collections.Generic;
using System.CommandLine; using System.CommandLine;
using System.CommandLine.Builder; using System.CommandLine.Builder;
using System.CommandLine.Invocation; using System.CommandLine.Invocation;
using System.CommandLine.IO;
using System.CommandLine.Parsing; using System.CommandLine.Parsing;
using System.ComponentModel; using System.ComponentModel;
using System.Diagnostics; using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Security.AccessControl; using System.Security.AccessControl;
using System.Security.Principal; using System.Security.Principal;
using System.ServiceProcess; using System.ServiceProcess;
@ -39,24 +35,24 @@ namespace WinSW
internal static Action<Exception, InvocationContext>? TestExceptionHandler; internal static Action<Exception, InvocationContext>? TestExceptionHandler;
private static int Main(string[] args) internal static int Main(string[] args)
{
int exitCode = Run(args);
Log.Debug("Completed. Exit code is " + exitCode);
return exitCode;
}
internal static int Run(string[] args)
{ {
bool elevated; bool elevated;
if (args[0] == "--elevated") if (args.Length > 0 && args[0] == "--elevated")
{ {
elevated = true; elevated = true;
_ = ConsoleApis.FreeConsole(); _ = ConsoleApis.FreeConsole();
_ = ConsoleApis.AttachConsole(ConsoleApis.ATTACH_PARENT_PROCESS); _ = ConsoleApis.AttachConsole(ConsoleApis.ATTACH_PARENT_PROCESS);
args = new List<string>(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) else if (Environment.OSVersion.Version.Major == 5)
{ {
@ -72,19 +68,19 @@ namespace WinSW
{ {
Handler = CommandHandler.Create((string? pathToConfig) => Handler = CommandHandler.Create((string? pathToConfig) =>
{ {
XmlServiceConfig config; XmlServiceConfig config = null!;
try try
{ {
config = XmlServiceConfig.Create(pathToConfig); config = XmlServiceConfig.Create(pathToConfig);
} }
catch (FileNotFoundException) 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); InitLoggers(config, enableConsoleLogging: false);
Log.Debug("Starting WinSW in service mode"); Log.Debug("Starting WinSW in service mode.");
ServiceBase.Run(new WrapperService(config)); ServiceBase.Run(new WrapperService(config));
}), }),
}; };
@ -141,11 +137,12 @@ namespace WinSW
{ {
var start = new Command("start", "Starts the service.") var start = new Command("start", "Starts the service.")
{ {
Handler = CommandHandler.Create<string?, bool>(Start), Handler = CommandHandler.Create<string?, bool, bool, CancellationToken>(Start),
}; };
start.Add(config); start.Add(config);
start.Add(noElevate); start.Add(noElevate);
start.Add(new Option("--no-wait", "Doesn't wait for the service to actually start."));
root.Add(start); root.Add(start);
} }
@ -153,7 +150,7 @@ namespace WinSW
{ {
var stop = new Command("stop", "Stops the service.") var stop = new Command("stop", "Stops the service.")
{ {
Handler = CommandHandler.Create<string?, bool, bool, bool>(Stop), Handler = CommandHandler.Create<string?, bool, bool, bool, CancellationToken>(Stop),
}; };
stop.Add(config); stop.Add(config);
@ -167,7 +164,7 @@ namespace WinSW
{ {
var restart = new Command("restart", "Stops and then starts the service.") var restart = new Command("restart", "Stops and then starts the service.")
{ {
Handler = CommandHandler.Create<string?, bool, bool>(Restart), Handler = CommandHandler.Create<string?, bool, bool, CancellationToken>(Restart),
}; };
restart.Add(config); restart.Add(config);
@ -240,14 +237,15 @@ namespace WinSW
} }
{ {
var dev = new Command("dev"); var dev = new Command("dev", "Experimental commands.")
{
dev.Add(config); config,
dev.Add(noElevate); noElevate,
};
root.Add(dev); root.Add(dev);
var ps = new Command("ps") var ps = new Command("ps", "Draws the process tree associated with the service.")
{ {
Handler = CommandHandler.Create<string?, bool>(DevPs), Handler = CommandHandler.Create<string?, bool>(DevPs),
}; };
@ -256,14 +254,8 @@ namespace WinSW
} }
return new CommandLineBuilder(root) return new CommandLineBuilder(root)
// see UseDefaults
.UseVersionOption() .UseVersionOption()
.UseHelp() .UseHelp()
/* .UseEnvironmentVariableDirective() */
.UseParseDirective()
.UseDebugDirective()
.UseSuggestDirective()
.RegisterWithDotnetSuggest() .RegisterWithDotnetSuggest()
.UseTypoCorrections() .UseTypoCorrections()
.UseParseErrorReporting() .UseParseErrorReporting()
@ -274,11 +266,6 @@ namespace WinSW
static void OnException(Exception exception, InvocationContext context) static void OnException(Exception exception, InvocationContext context)
{ {
Console.ForegroundColor = ConsoleColor.Red;
try
{
IStandardStreamWriter error = context.Console.Error;
Debug.Assert(exception is TargetInvocationException); Debug.Assert(exception is TargetInvocationException);
Debug.Assert(exception.InnerException != null); Debug.Assert(exception.InnerException != null);
exception = exception.InnerException!; exception = exception.InnerException!;
@ -286,9 +273,16 @@ namespace WinSW
{ {
case InvalidDataException e: case InvalidDataException e:
{ {
string message = "The configuration file cound not be loaded. " + e.Message; string message = "The configuration file could not be loaded. " + e.Message;
Log.Fatal(message, e); 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; context.ResultCode = -1;
break; break;
} }
@ -297,7 +291,6 @@ namespace WinSW
{ {
string message = e.Message; string message = e.Message;
Log.Fatal(message); Log.Fatal(message);
error.WriteLine(message);
context.ResultCode = e.InnerException is Win32Exception inner ? inner.NativeErrorCode : -1; context.ResultCode = e.InnerException is Win32Exception inner ? inner.NativeErrorCode : -1;
break; break;
} }
@ -305,8 +298,7 @@ namespace WinSW
case InvalidOperationException e when e.InnerException is Win32Exception inner: case InvalidOperationException e when e.InnerException is Win32Exception inner:
{ {
string message = e.Message; string message = e.Message;
Log.Fatal(message, e); Log.Fatal(message);
error.WriteLine(message);
context.ResultCode = inner.NativeErrorCode; context.ResultCode = inner.NativeErrorCode;
break; break;
} }
@ -315,7 +307,6 @@ namespace WinSW
{ {
string message = e.Message; string message = e.Message;
Log.Fatal(message, e); Log.Fatal(message, e);
error.WriteLine(message);
context.ResultCode = e.NativeErrorCode; context.ResultCode = e.NativeErrorCode;
break; break;
} }
@ -323,17 +314,11 @@ namespace WinSW
default: default:
{ {
Log.Fatal("Unhandled exception", exception); Log.Fatal("Unhandled exception", exception);
error.WriteLine(exception.ToString());
context.ResultCode = -1; context.ResultCode = -1;
break; break;
} }
} }
} }
finally
{
Console.ResetColor();
}
}
void Install(string? pathToConfig, bool noElevate, string? username, string? password) void Install(string? pathToConfig, bool noElevate, string? username, string? password)
{ {
@ -346,15 +331,14 @@ namespace WinSW
return; return;
} }
Log.Info("Installing the service with id '" + config.Id + "'"); Log.Info($"Installing service '{config.Format()}'...");
using ServiceManager scm = ServiceManager.Open(); using ServiceManager scm = ServiceManager.Open();
if (scm.ServiceExists(config.Id)) if (scm.ServiceExists(config.Name))
{ {
Console.WriteLine("Service with id '" + config.Id + "' already exists"); Log.Error($"A service with ID '{config.Name}' already exists.");
Console.WriteLine("To install the service, delete the existing one or change service Id in the configuration file"); Throw.Command.Win32Exception(Errors.ERROR_SERVICE_EXISTS, "Failed to install the service.");
throw new CommandException("Installation failure: Service with id '" + config.Id + "' already exists");
} }
if (config.HasServiceAccount()) if (config.HasServiceAccount())
@ -371,7 +355,7 @@ namespace WinSW
ref username, ref username,
ref password, ref password,
"Windows Service Wrapper", "Windows Service Wrapper",
"service account credentials"); // TODO "Enter the service account credentials");
break; break;
case "console": case "console":
@ -387,10 +371,10 @@ namespace WinSW
} }
using Service sc = scm.CreateService( using Service sc = scm.CreateService(
config.Id, config.Name,
config.Caption, config.DisplayName,
config.StartMode, config.StartMode,
"\"" + config.ExecutablePath + "\"" + (pathToConfig != null ? " \"" + Path.GetFullPath(pathToConfig) + "\"" : null), $"\"{config.ExecutablePath}\"" + (pathToConfig is null ? null : $" \"{Path.GetFullPath(pathToConfig)}\""),
config.ServiceDependencies, config.ServiceDependencies,
username, username,
password); password);
@ -425,12 +409,14 @@ namespace WinSW
sc.SetSecurityDescriptor(new RawSecurityDescriptor(securityDescriptor)); sc.SetSecurityDescriptor(new RawSecurityDescriptor(securityDescriptor));
} }
string eventLogSource = config.Id; string eventLogSource = config.Name;
if (!EventLog.SourceExists(eventLogSource)) if (!EventLog.SourceExists(eventLogSource))
{ {
EventLog.CreateEventSource(eventLogSource, "Application"); EventLog.CreateEventSource(eventLogSource, "Application");
} }
Log.Info($"Service '{config.Format()}' was installed successfully.");
void PromptForCredentialsConsole() void PromptForCredentialsConsole()
{ {
if (username is null) if (username is null)
@ -470,45 +456,46 @@ namespace WinSW
return; return;
} }
Log.Info("Uninstalling the service with id '" + config.Id + "'"); Log.Info($"Uninstalling service '{config.Format()}'...");
using ServiceManager scm = ServiceManager.Open(); using ServiceManager scm = ServiceManager.Open();
try 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. // We could fail the opeartion here, but it would be an incompatible change.
// So it is just a warning // 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(); sc.Delete();
Log.Info($"Service '{config.Format()}' was uninstalled successfully.");
} }
catch (CommandException e) when (e.InnerException is Win32Exception inner) catch (CommandException e) when (e.InnerException is Win32Exception inner)
{ {
switch (inner.NativeErrorCode) switch (inner.NativeErrorCode)
{ {
case Errors.ERROR_SERVICE_DOES_NOT_EXIST: 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 break; // there's no such service, so consider it already uninstalled
case Errors.ERROR_SERVICE_MARKED_FOR_DELETE: case Errors.ERROR_SERVICE_MARKED_FOR_DELETE:
Log.Error("Failed to uninstall the service with id '" + config.Id + "'" Log.Error(e.Message);
+ ". It has been marked for deletion.");
// TODO: change the default behavior to Error? // TODO: change the default behavior to Error?
break; // it's already uninstalled, so consider it a success break; // it's already uninstalled, so consider it a success
default: default:
Log.Fatal("Failed to uninstall the service with id '" + config.Id + "'. Error code is '" + inner.NativeErrorCode + "'"); Throw.Command.Exception("Failed to uninstall the service.", inner);
throw; break;
} }
} }
} }
void Start(string? pathToConfig, bool noElevate) void Start(string? pathToConfig, bool noElevate, bool noWait, CancellationToken ct)
{ {
XmlServiceConfig config = XmlServiceConfig.Create(pathToConfig); XmlServiceConfig config = XmlServiceConfig.Create(pathToConfig);
InitLoggers(config, enableConsoleLogging: true); InitLoggers(config, enableConsoleLogging: true);
@ -519,27 +506,40 @@ namespace WinSW
return; return;
} }
Log.Info("Starting the service with id '" + config.Id + "'"); using var svc = new ServiceController(config.Name);
using var svc = new ServiceController(config.Id);
try try
{ {
Log.Info($"Starting service '{svc.Format()}'...");
svc.Start(); 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) catch (InvalidOperationException e)
when (e.InnerException is Win32Exception inner && inner.NativeErrorCode == Errors.ERROR_SERVICE_DOES_NOT_EXIST) when (e.InnerException is Win32Exception inner && inner.NativeErrorCode == Errors.ERROR_SERVICE_DOES_NOT_EXIST)
{ {
ThrowNoSuchService(inner); Throw.Command.Exception(inner);
} }
catch (InvalidOperationException e) catch (InvalidOperationException e)
when (e.InnerException is Win32Exception inner && inner.NativeErrorCode == Errors.ERROR_SERVICE_ALREADY_RUNNING) 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); XmlServiceConfig config = XmlServiceConfig.Create(pathToConfig);
InitLoggers(config, enableConsoleLogging: true); InitLoggers(config, enableConsoleLogging: true);
@ -550,9 +550,7 @@ namespace WinSW
return; return;
} }
Log.Info("Stopping the service with id '" + config.Id + "'"); using var svc = new ServiceController(config.Name);
using var svc = new ServiceController(config.Id);
try try
{ {
@ -560,39 +558,40 @@ namespace WinSW
{ {
if (svc.HasAnyStartedDependentService()) 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(); svc.Stop();
if (!noWait) if (!noWait)
{ {
Log.Info("Waiting for the service to stop...");
try 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) catch (InvalidOperationException e)
when (e.InnerException is Win32Exception inner && inner.NativeErrorCode == Errors.ERROR_SERVICE_DOES_NOT_EXIST) when (e.InnerException is Win32Exception inner && inner.NativeErrorCode == Errors.ERROR_SERVICE_DOES_NOT_EXIST)
{ {
ThrowNoSuchService(inner); Throw.Command.Exception(inner);
} }
catch (InvalidOperationException e) catch (InvalidOperationException e)
when (e.InnerException is Win32Exception inner && inner.NativeErrorCode == Errors.ERROR_SERVICE_NOT_ACTIVE) 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, CancellationToken ct)
}
void Restart(string? pathToConfig, bool noElevate, bool force)
{ {
XmlServiceConfig config = XmlServiceConfig.Create(pathToConfig); XmlServiceConfig config = XmlServiceConfig.Create(pathToConfig);
InitLoggers(config, enableConsoleLogging: true); InitLoggers(config, enableConsoleLogging: true);
@ -603,9 +602,7 @@ namespace WinSW
return; return;
} }
Log.Info("Restarting the service with id '" + config.Id + "'"); using var svc = new ServiceController(config.Name);
using var svc = new ServiceController(config.Id);
List<ServiceController>? startedDependentServices = null; List<ServiceController>? startedDependentServices = null;
@ -615,46 +612,59 @@ namespace WinSW
{ {
if (!force) 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(); startedDependentServices = svc.DependentServices.Where(service => service.Status != ServiceControllerStatus.Stopped).ToList();
} }
Log.Info($"Stopping service '{svc.Format()}'...");
svc.Stop(); svc.Stop();
Log.Info("Waiting for the service to stop...");
try 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) catch (InvalidOperationException e)
when (e.InnerException is Win32Exception inner && inner.NativeErrorCode == Errors.ERROR_SERVICE_DOES_NOT_EXIST) when (e.InnerException is Win32Exception inner && inner.NativeErrorCode == Errors.ERROR_SERVICE_DOES_NOT_EXIST)
{ {
ThrowNoSuchService(inner); Throw.Command.Exception(inner);
} }
catch (InvalidOperationException e) catch (InvalidOperationException e)
when (e.InnerException is Win32Exception inner && inner.NativeErrorCode == Errors.ERROR_SERVICE_NOT_ACTIVE) when (e.InnerException is Win32Exception inner && inner.NativeErrorCode == Errors.ERROR_SERVICE_NOT_ACTIVE)
{ {
} }
Log.Info($"Starting service '{svc.Format()}'...");
svc.Start(); svc.Start();
try
{
svc.WaitForStatus(ServiceControllerStatus.Running, ServiceControllerStatus.StartPending, ct);
}
catch (TimeoutException)
{
Throw.Command.Exception("Failed to start the service.");
}
if (startedDependentServices != null) if (startedDependentServices != null)
{ {
foreach (ServiceController service in startedDependentServices) foreach (ServiceController service in startedDependentServices)
{ {
if (service.Status == ServiceControllerStatus.Stopped) if (service.Status == ServiceControllerStatus.Stopped)
{ {
Log.Info($"Starting service '{service.Format()}'...");
service.Start(); service.Start();
} }
} }
} }
Log.Info($"Service '{svc.Format()}' restarted successfully.");
} }
void RestartSelf(string? pathToConfig) void RestartSelf(string? pathToConfig)
@ -664,34 +674,56 @@ namespace WinSW
if (!elevated) 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. // run restart from another process group. see README.md for why this is useful.
if (!ProcessApis.CreateProcess(
if (!ProcessApis.CreateProcess(null, config.ExecutablePath + " restart", IntPtr.Zero, IntPtr.Zero, false, ProcessApis.CREATE_NEW_PROCESS_GROUP, IntPtr.Zero, null, default, out _)) 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); XmlServiceConfig config = XmlServiceConfig.Create(pathToConfig);
InitLoggers(config, enableConsoleLogging: true); InitLoggers(config, enableConsoleLogging: true);
Log.Debug("User requested the status of the process with id '" + config.Id + "'"); using var svc = new ServiceController(config.Name);
using var svc = new ServiceController(config.Id);
try 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) catch (InvalidOperationException e)
when (e.InnerException is Win32Exception inner && inner.NativeErrorCode == Errors.ERROR_SERVICE_DOES_NOT_EXIST) when (e.InnerException is Win32Exception inner && inner.NativeErrorCode == Errors.ERROR_SERVICE_DOES_NOT_EXIST)
{ {
Console.WriteLine("NonExistent"); Console.WriteLine("NonExistent");
return Errors.ERROR_SERVICE_DOES_NOT_EXIST;
} }
} }
@ -730,6 +762,7 @@ namespace WinSW
void CancelKeyPress(object sender, ConsoleCancelEventArgs e) void CancelKeyPress(object sender, ConsoleCancelEventArgs e)
{ {
e.Cancel = true;
evt.Set(); evt.Set();
} }
} }
@ -754,9 +787,9 @@ namespace WinSW
using ServiceManager scm = ServiceManager.Open(); using ServiceManager scm = ServiceManager.Open();
try 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); sc.SetDescription(config.Description);
@ -787,8 +820,10 @@ namespace WinSW
catch (CommandException e) catch (CommandException e)
when (e.InnerException is Win32Exception inner && inner.NativeErrorCode == Errors.ERROR_SERVICE_DOES_NOT_EXIST) 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) void DevPs(string? pathToConfig, bool noElevate)
@ -802,7 +837,7 @@ namespace WinSW
} }
using ServiceManager scm = ServiceManager.Open(); using ServiceManager scm = ServiceManager.Open();
using Service sc = scm.OpenService(config.Id); using Service sc = scm.OpenService(config.Name);
int processId = sc.ProcessId; int processId = sc.ProcessId;
if (processId >= 0) if (processId >= 0)
@ -848,7 +883,7 @@ namespace WinSW
{ {
if (noElevate) if (noElevate)
{ {
throw new CommandException(new Win32Exception(Errors.ERROR_ACCESS_DENIED)); Throw.Command.Win32Exception(Errors.ERROR_ACCESS_DENIED);
} }
using Process current = Process.GetCurrentProcess(); using Process current = Process.GetCurrentProcess();
@ -881,11 +916,6 @@ namespace WinSW
} }
} }
/// <exception cref="CommandException" />
[DoesNotReturn]
[MethodImpl(MethodImplOptions.NoInlining)]
private static void ThrowNoSuchService(Win32Exception inner) => throw new CommandException(inner);
private static void InitLoggers(XmlServiceConfig config, bool enableConsoleLogging) private static void InitLoggers(XmlServiceConfig config, bool enableConsoleLogging)
{ {
if (XmlServiceConfig.TestConfig != null) if (XmlServiceConfig.TestConfig != null)
@ -900,10 +930,6 @@ namespace WinSW
Level consoleLogLevel = Level.Info; Level consoleLogLevel = Level.Info;
Level eventLogLevel = Level.Warn; 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<IAppender> appenders = new List<IAppender>(); List<IAppender> appenders = new List<IAppender>();
// .wrapper.log // .wrapper.log
@ -916,7 +942,7 @@ namespace WinSW
Name = "Wrapper file log", Name = "Wrapper file log",
Threshold = fileLogLevel, Threshold = fileLogLevel,
LockingModel = new FileAppender.MinimalLock(), LockingModel = new FileAppender.MinimalLock(),
Layout = layout, Layout = new PatternLayout("%date %-5level - %message%newline"),
}; };
wrapperLog.ActivateOptions(); wrapperLog.ActivateOptions();
appenders.Add(wrapperLog); appenders.Add(wrapperLog);
@ -924,11 +950,11 @@ namespace WinSW
// console log // console log
if (enableConsoleLogging) if (enableConsoleLogging)
{ {
var consoleAppender = new ConsoleAppender var consoleAppender = new WinSWConsoleAppender
{ {
Name = "Wrapper console log", Name = "Wrapper console log",
Threshold = consoleLogLevel, Threshold = consoleLogLevel,
Layout = layout, Layout = new PatternLayout("%date{ABSOLUTE} - %message%newline"),
}; };
consoleAppender.ActivateOptions(); consoleAppender.ActivateOptions();
appenders.Add(consoleAppender); appenders.Add(consoleAppender);

View File

@ -1,15 +1,17 @@
using System; using System;
using System.ServiceProcess; using System.ServiceProcess;
using System.Threading;
using TimeoutException = System.ServiceProcess.TimeoutException; using TimeoutException = System.ServiceProcess.TimeoutException;
namespace WinSW namespace WinSW
{ {
internal static class ServiceControllerExtension internal static class ServiceControllerExtension
{ {
/// <exception cref="OperationCanceledException" />
/// <exception cref="TimeoutException" /> /// <exception cref="TimeoutException" />
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 (; ; ) for (; ; )
{ {
try try
@ -17,8 +19,10 @@ namespace WinSW
serviceController.WaitForStatus(desiredStatus, timeout); serviceController.WaitForStatus(desiredStatus, timeout);
break; break;
} }
catch (TimeoutException) when (serviceController.Status == desiredStatus || serviceController.Status == pendingStatus) catch (TimeoutException)
when (serviceController.Status == desiredStatus || serviceController.Status == pendingStatus)
{ {
ct.ThrowIfCancellationRequested();
} }
} }
} }

View File

@ -46,7 +46,7 @@ namespace WinSW
public WrapperService(XmlServiceConfig config) public WrapperService(XmlServiceConfig config)
{ {
this.ServiceName = config.Id; this.ServiceName = config.Name;
this.CanStop = true; this.CanStop = true;
this.AutoLog = false; 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; this.process.EnableRaisingEvents = false;
string? stopExecutable = this.config.StopExecutable; string? stopExecutable = this.config.StopExecutable;
@ -441,7 +441,7 @@ namespace WinSW
Console.Beep(); Console.Beep();
} }
Log.Info("Finished " + this.config.Id); Log.Info("Finished " + this.config.Name);
Process StartProcessLocked(string executable, string? arguments) Process StartProcessLocked(string executable, string? arguments)
{ {