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 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;

View File

@ -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;

View File

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

View File

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

View File

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

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.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,

View File

@ -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
{
/// <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)]
internal static void Win32Exception(string message)
{

View File

@ -15,5 +15,11 @@ namespace System.Diagnostics.CodeAnalysis
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

View File

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

View File

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

View File

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

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.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<Exception, InvocationContext>? 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<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)
{
@ -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<string?, bool>(Start),
Handler = CommandHandler.Create<string?, bool, bool, CancellationToken>(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<string?, bool, bool, bool>(Stop),
Handler = CommandHandler.Create<string?, bool, bool, bool, CancellationToken>(Stop),
};
stop.Add(config);
@ -167,7 +164,7 @@ namespace WinSW
{
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);
@ -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<string?, bool>(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<ServiceController>? 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
}
}
/// <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)
{
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<IAppender> appenders = new List<IAppender>();
// .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);

View File

@ -1,15 +1,17 @@
using System;
using System.ServiceProcess;
using System.Threading;
using TimeoutException = System.ServiceProcess.TimeoutException;
namespace WinSW
{
internal static class ServiceControllerExtension
{
/// <exception cref="OperationCanceledException" />
/// <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 (; ; )
{
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();
}
}
}

View File

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