winsw/src/WinSW/Program.cs

1180 lines
44 KiB
C#

using System;
using System.Collections.Generic;
using System.CommandLine;
using System.CommandLine.Builder;
using System.CommandLine.Invocation;
using System.CommandLine.Parsing;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.IO.Pipes;
using System.Linq;
using System.Reflection;
using System.Security.AccessControl;
using System.Security.Principal;
using System.ServiceProcess;
using System.Threading;
using log4net;
using log4net.Appender;
using log4net.Config;
using log4net.Core;
using log4net.Layout;
using Microsoft.Win32;
using WinSW.Logging;
using WinSW.Native;
using WinSW.Util;
using static WinSW.Native.ServiceApis;
using Process = System.Diagnostics.Process;
using TimeoutException = System.ServiceProcess.TimeoutException;
namespace WinSW
{
// NOTE: Keep description strings in sync with docs.
public static class Program
{
private const string NoPipe = "-";
private static readonly ILog Log = LogManager.GetLogger(typeof(Program));
internal static Action<Exception, InvocationContext>? TestExceptionHandler;
internal static XmlServiceConfig? TestConfig;
internal static string? TestExecutablePath;
private static string ExecutablePath
{
get
{
if (TestExecutablePath != null)
{
return TestExecutablePath;
}
using var current = Process.GetCurrentProcess();
return current.MainModule!.FileName!;
}
}
internal static int Main(string[] args)
{
bool elevated;
if (args.Length > 0 && args[0] == "--elevated")
{
elevated = true;
_ = ConsoleApis.FreeConsole();
_ = ConsoleApis.AttachConsole(ConsoleApis.ATTACH_PARENT_PROCESS);
string stdinName = args[1];
if (stdinName != NoPipe)
{
var stdin = new NamedPipeClientStream(".", stdinName, PipeDirection.In, PipeOptions.Asynchronous);
stdin.Connect();
Console.SetIn(new StreamReader(stdin));
}
string stdoutName = args[2];
if (stdoutName != NoPipe)
{
var stdout = new NamedPipeClientStream(".", stdoutName, PipeDirection.Out, PipeOptions.Asynchronous);
stdout.Connect();
Console.SetOut(new StreamWriter(stdout) { AutoFlush = true });
}
string stderrName = args[3];
if (stderrName != NoPipe)
{
var stderr = new NamedPipeClientStream(".", stderrName, PipeDirection.Out, PipeOptions.Asynchronous);
stderr.Connect();
Console.SetError(new StreamWriter(stderr) { AutoFlush = true });
}
#if NET
args = args[4..];
#else
string[] oldArgs = args;
int newLength = oldArgs.Length - 4;
args = new string[newLength];
Array.Copy(oldArgs, 4, args, 0, newLength);
#endif
}
else if (Environment.OSVersion.Version.Major == 5)
{
// Windows XP
elevated = true;
}
else
{
elevated = IsProcessElevated();
}
var root = new RootCommand("A wrapper binary that can be used to host executables as Windows services. https://github.com/winsw/winsw")
{
Handler = CommandHandler.Create((string? pathToConfig) =>
{
XmlServiceConfig config = null!;
try
{
config = LoadConfigAndInitLoggers(pathToConfig, false);
}
catch (FileNotFoundException)
{
Throw.Command.Exception("The specified command or file was not found.");
}
Log.Debug("Starting WinSW in service mode.");
AutoRefresh(config);
using var service = new WrapperService(config);
try
{
ServiceBase.Run(service);
}
catch
{
// handled in OnStart
}
}),
};
using (var identity = WindowsIdentity.GetCurrent())
{
var principal = new WindowsPrincipal(identity);
if (principal.IsInRole(new SecurityIdentifier(WellKnownSidType.ServiceSid, null)) ||
principal.IsInRole(new SecurityIdentifier(WellKnownSidType.LocalSystemSid, null)) ||
principal.IsInRole(new SecurityIdentifier(WellKnownSidType.LocalServiceSid, null)) ||
principal.IsInRole(new SecurityIdentifier(WellKnownSidType.NetworkServiceSid, null)))
{
root.Add(new Argument<string?>("path-to-config")
{
Arity = ArgumentArity.ZeroOrOne,
IsHidden = true,
});
}
}
var config = new Argument<string?>("path-to-config", "The path to the configuration file.")
{
Arity = ArgumentArity.ZeroOrOne,
};
var noElevate = new Option("--no-elevate", "Doesn't automatically trigger a UAC prompt.");
{
var install = new Command("install", "Installs the service.")
{
Handler = CommandHandler.Create<string?, bool, string?, string?>(Install),
};
install.Add(config);
install.Add(noElevate);
install.Add(new Option<string?>(new[] { "--username", "--user" }, "Specifies the user name of the service account."));
install.Add(new Option<string?>(new[] { "--password", "--pass" }, "Specifies the password of the service account."));
root.Add(install);
}
{
var uninstall = new Command("uninstall", "Uninstalls the service.")
{
Handler = CommandHandler.Create<string?, bool>(Uninstall),
};
uninstall.Add(config);
uninstall.Add(noElevate);
root.Add(uninstall);
}
{
var start = new Command("start", "Starts the service.")
{
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);
}
{
var stop = new Command("stop", "Stops the service.")
{
Handler = CommandHandler.Create<string?, bool, bool, bool, CancellationToken>(Stop),
};
stop.Add(config);
stop.Add(noElevate);
stop.Add(new Option("--no-wait", "Doesn't wait for the service to actually stop."));
stop.Add(new Option("--force", "Stops the service even if it has started dependent services."));
root.Add(stop);
}
{
var restart = new Command("restart", "Stops and then starts the service.")
{
Handler = CommandHandler.Create<string?, bool, bool, CancellationToken>(Restart),
};
restart.Add(config);
restart.Add(noElevate);
restart.Add(new Option("--force", "Restarts the service even if it has started dependent services."));
root.Add(restart);
}
{
var restartSelf = new Command("restart!", "self-restart (can be called from child processes)")
{
Handler = CommandHandler.Create<string?>(RestartSelf),
};
restartSelf.Add(config);
root.Add(restartSelf);
}
{
var status = new Command("status", "Checks the status of the service.")
{
Handler = CommandHandler.Create<string?>(Status),
};
status.Add(config);
root.Add(status);
}
{
var refresh = new Command("refresh", "Refreshes the service properties without reinstallation.")
{
Handler = CommandHandler.Create<string?, bool>(Refresh),
};
refresh.Add(config);
refresh.Add(noElevate);
root.Add(refresh);
}
{
var customize = new Command("customize", "Customizes the wrapper executable.")
{
Handler = CommandHandler.Create<string, string>(Customize),
};
customize.Add(new Option<string>(new[] { "--output", "-o" })
{
Required = true,
});
var manufacturer = new Option<string>("--manufacturer")
{
Required = true,
};
manufacturer.Argument.AddValidator(argument =>
{
const int minLength = 12;
const int maxLength = 15;
string token = argument.Tokens.Single().Value;
int length = token.Length;
return
length < minLength ? $"The length of argument '{token}' must be greater than or equal to {minLength}." :
length > maxLength ? $"The length of argument '{token}' must be less than or equal to {maxLength}." :
null;
});
customize.Add(manufacturer);
root.Add(customize);
}
{
var dev = new Command("dev", "Experimental commands.");
root.Add(dev);
{
var ps = new Command("ps", "Draws the process tree associated with the service.")
{
Handler = CommandHandler.Create<string?, bool>(DevPs),
};
ps.Add(config);
ps.Add(new Option(new[] { "--all", "-a" }));
dev.Add(ps);
}
{
var kill = new Command("kill", "Terminates the service if it has stopped responding.")
{
Handler = CommandHandler.Create<string?, bool>(DevKill),
};
kill.Add(config);
kill.Add(noElevate);
dev.Add(kill);
}
{
var list = new Command("list", "Lists services managed by the current executable.")
{
Handler = CommandHandler.Create(DevList),
};
dev.Add(list);
}
}
return new CommandLineBuilder(root)
.UseVersionOption()
.UseHelp()
.RegisterWithDotnetSuggest()
.UseTypoCorrections()
.UseParseErrorReporting()
.UseExceptionHandler(TestExceptionHandler ?? OnException)
.CancelOnProcessTermination()
.Build()
.Invoke(args);
static void OnException(Exception exception, InvocationContext context)
{
Debug.Assert(exception is TargetInvocationException);
Debug.Assert(exception.InnerException != null);
exception = exception.InnerException!;
switch (exception)
{
case InvalidDataException e:
{
string message = "The configuration file could not be loaded. " + e.Message;
Log.Fatal(message, e);
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);
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);
context.ResultCode = inner.NativeErrorCode;
break;
}
case Win32Exception e:
{
string message = e.Message;
Log.Fatal(message, e);
context.ResultCode = e.NativeErrorCode;
break;
}
default:
{
Log.Fatal("Unhandled exception", exception);
context.ResultCode = -1;
break;
}
}
}
void Install(string? pathToConfig, bool noElevate, string? username, string? password)
{
var config = LoadConfigAndInitLoggers(pathToConfig, true);
if (!elevated)
{
Elevate(noElevate);
return;
}
Log.Info($"Installing service '{config.Format()}'...");
using var scm = ServiceManager.Open(ServiceManagerAccess.CreateService);
if (scm.ServiceExists(config.Name))
{
Log.Error($"A service with ID '{config.Name}' already exists.");
Throw.Command.Win32Exception(Errors.ERROR_SERVICE_EXISTS, "Failed to install the service.");
}
bool saveCredential = false;
if (config.HasServiceAccount())
{
username = config.ServiceAccountUserName ?? username;
password = config.ServiceAccountPassword ?? password;
if (username is null || password is null && !Security.IsSpecialAccount(username))
{
switch (config.ServiceAccountPrompt)
{
case "dialog":
if (!Credentials.Load($"WinSW:{config.Name}", out username, out password))
{
Credentials.PromptForCredentialsDialog(
ref username,
ref password,
"Windows Service Wrapper",
"Enter the service account credentials",
ref saveCredential);
}
break;
case "console":
Credentials.PromptForCredentialsConsole(ref username, ref password);
break;
}
}
}
if (username != null && !Security.IsSpecialAccount(username))
{
Security.AddServiceLogonRight(ref username);
}
using var sc = scm.CreateService(
config.Name,
config.DisplayName,
config.StartMode,
$"\"{config.ExecutablePath}\"" + (pathToConfig is null ? null : $" \"{Path.GetFullPath(pathToConfig)}\""),
config.ServiceDependencies,
username,
password);
if (saveCredential)
{
Credentials.Save($"WinSW:{config.Name}", username, password);
}
string description = config.Description;
if (description.Length != 0)
{
sc.SetDescription(description);
}
var actions = config.FailureActions;
if (actions.Length > 0)
{
sc.SetFailureActions(config.ResetFailureAfter, actions);
}
bool isDelayedAutoStart = config.StartMode == ServiceStartMode.Automatic && config.DelayedAutoStart;
if (isDelayedAutoStart)
{
sc.SetDelayedAutoStart(true);
}
if (config.PreshutdownTimeout is TimeSpan preshutdownTimeout)
{
sc.SetPreshutdownTimeout(preshutdownTimeout);
}
string? securityDescriptor = config.SecurityDescriptor;
if (securityDescriptor != null)
{
// throws ArgumentException
sc.SetSecurityDescriptor(new RawSecurityDescriptor(securityDescriptor));
}
string eventLogSource = config.Name;
if (!EventLog.SourceExists(eventLogSource))
{
EventLog.CreateEventSource(eventLogSource, "Application");
}
Log.Info($"Service '{config.Format()}' was installed successfully.");
}
void Uninstall(string? pathToConfig, bool noElevate)
{
var config = LoadConfigAndInitLoggers(pathToConfig, true);
if (!elevated)
{
Elevate(noElevate);
return;
}
Log.Info($"Uninstalling service '{config.Format()}'...");
using var scm = ServiceManager.Open(ServiceManagerAccess.Connect);
try
{
using var sc = scm.OpenService(config.Name);
if (sc.Status != ServiceControllerStatus.Stopped)
{
// We could fail the opeartion here, but it would be an incompatible change.
// So it is just a warning
Log.Warn($"Service '{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($"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(e.Message);
// TODO: change the default behavior to Error?
break; // it's already uninstalled, so consider it a success
default:
Throw.Command.Exception("Failed to uninstall the service.", inner);
break;
}
}
}
void Start(string? pathToConfig, bool noElevate, bool noWait, CancellationToken ct)
{
var config = LoadConfigAndInitLoggers(pathToConfig, true);
if (!elevated)
{
Elevate(noElevate);
return;
}
AutoRefresh(config);
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)
{
Throw.Command.Exception(inner);
}
catch (InvalidOperationException e)
when (e.InnerException is Win32Exception inner && inner.NativeErrorCode == Errors.ERROR_SERVICE_ALREADY_RUNNING)
{
Log.Info($"Service '{svc.Format()}' has already started.");
}
}
void Stop(string? pathToConfig, bool noElevate, bool noWait, bool force, CancellationToken ct)
{
var config = LoadConfigAndInitLoggers(pathToConfig, true);
if (!elevated)
{
Elevate(noElevate);
return;
}
AutoRefresh(config);
using var svc = new ServiceController(config.Name);
try
{
if (!force)
{
if (svc.HasAnyStartedDependentService())
{
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)
{
try
{
svc.WaitForStatus(ServiceControllerStatus.Stopped, ServiceControllerStatus.StopPending, ct);
}
catch (TimeoutException)
{
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)
{
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.");
}
}
void Restart(string? pathToConfig, bool noElevate, bool force, CancellationToken ct)
{
var config = LoadConfigAndInitLoggers(pathToConfig, true);
if (!elevated)
{
Elevate(noElevate);
return;
}
AutoRefresh(config);
using var svc = new ServiceController(config.Name);
List<ServiceController>? startedDependentServices = null;
try
{
if (svc.HasAnyStartedDependentService())
{
if (!force)
{
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();
try
{
svc.WaitForStatus(ServiceControllerStatus.Stopped, ServiceControllerStatus.StopPending, ct);
}
catch (TimeoutException)
{
Throw.Command.Exception("Failed to stop the service.");
}
}
catch (InvalidOperationException e)
when (e.InnerException is Win32Exception inner && inner.NativeErrorCode == Errors.ERROR_SERVICE_DOES_NOT_EXIST)
{
Throw.Command.Exception(inner);
}
catch (InvalidOperationException e)
when (e.InnerException is Win32Exception inner && inner.NativeErrorCode == Errors.ERROR_SERVICE_NOT_ACTIVE)
{
}
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 (var 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)
{
var config = LoadConfigAndInitLoggers(pathToConfig, true);
if (!elevated)
{
Throw.Command.Win32Exception(Errors.ERROR_ACCESS_DENIED);
}
AutoRefresh(config);
// run restart from another process group. see README.md for why this is useful.
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 var processInfo))
{
Throw.Command.Win32Exception("Failed to invoke restart.");
}
_ = HandleApis.CloseHandle(processInfo.ProcessHandle);
_ = HandleApis.CloseHandle(processInfo.ThreadHandle);
}
static int Status(string? pathToConfig)
{
var config = LoadConfigAndInitLoggers(pathToConfig, true);
using var svc = new ServiceController(config.Name);
try
{
Console.WriteLine(svc.Status switch
{
ServiceControllerStatus.StartPending => "Active (starting)",
ServiceControllerStatus.StopPending => "Active (stopping)",
ServiceControllerStatus.Running => "Active (running)",
ServiceControllerStatus.ContinuePending => "Active (continuing)",
ServiceControllerStatus.PausePending => "Active (pausing)",
ServiceControllerStatus.Paused => "Active (paused)",
_ => "Inactive (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;
}
}
void Refresh(string? pathToConfig, bool noElevate)
{
var config = LoadConfigAndInitLoggers(pathToConfig, true);
if (!elevated)
{
Elevate(noElevate);
return;
}
DoRefresh(config);
}
static unsafe void DevPs(string? pathToConfig, bool all)
{
if (all)
{
using var scm = ServiceManager.Open(ServiceManagerAccess.EnumerateService);
int prevProcessId = -1;
foreach (var status in scm.EnumerateServices())
{
using var sc = scm.OpenService(status->ServiceName, ServiceAccess.QueryConfig | ServiceAccess.QueryStatus);
if (sc.ExecutablePath.StartsWith($"\"{ExecutablePath}\""))
{
int processId = sc.ProcessId;
if (processId >= 0)
{
if (prevProcessId >= 0)
{
using var process = Process.GetProcessById(prevProcessId);
Draw(process, string.Empty, false);
}
}
prevProcessId = processId;
}
}
if (prevProcessId >= 0)
{
using var process = Process.GetProcessById(prevProcessId);
Draw(process, string.Empty, true);
}
}
else
{
var config = LoadConfigAndInitLoggers(pathToConfig, true);
using var scm = ServiceManager.Open(ServiceManagerAccess.Connect);
using var sc = scm.OpenService(config.Name, ServiceAccess.QueryStatus);
int processId = sc.ProcessId;
if (processId >= 0)
{
using var process = Process.GetProcessById(processId);
Draw(process, string.Empty, true);
}
}
static void Draw(Process process, string indentation, bool isLastChild)
{
const string Vertical = " \u2502 ";
const string Corner = " \u2514\u2500";
const string Cross = " \u251c\u2500";
const string Space = " ";
Console.Write(indentation);
if (isLastChild)
{
Console.Write(Corner);
indentation += Space;
}
else
{
Console.Write(Cross);
indentation += Vertical;
}
Console.WriteLine(process.Format());
var children = process.GetChildren();
int count = children.Count;
for (int i = 0; i < count; i++)
{
var child = children[i];
using (child.Process)
using (child.Handle)
{
Draw(child.Process, indentation, i == count - 1);
}
}
}
}
void DevKill(string? pathToConfig, bool noElevate)
{
var config = LoadConfigAndInitLoggers(pathToConfig, true);
if (!elevated)
{
Elevate(noElevate);
return;
}
using var scm = ServiceManager.Open();
using var sc = scm.OpenService(config.Name);
int processId = sc.ProcessId;
if (processId >= 0)
{
using var process = Process.GetProcessById(processId);
process.StopDescendants(config.StopTimeoutInMs);
}
}
static unsafe void DevList()
{
using var scm = ServiceManager.Open(ServiceManagerAccess.EnumerateService);
foreach (var status in scm.EnumerateServices())
{
using var sc = scm.OpenService(status->ServiceName, ServiceAccess.QueryConfig);
if (sc.ExecutablePath.StartsWith($"\"{ExecutablePath}\""))
{
Console.WriteLine(status->ToString());
}
}
}
static void Customize(string output, string manufacturer)
{
if (Resources.UpdateCompanyName(ExecutablePath, output, manufacturer))
{
Console.WriteLine("The operation succeeded.");
}
else
{
Console.Error.WriteLine("The operation failed.");
}
}
// [DoesNotReturn]
static void Elevate(bool noElevate)
{
if (noElevate)
{
Throw.Command.Win32Exception(Errors.ERROR_ACCESS_DENIED);
}
string? stdinName = Console.IsInputRedirected ? Guid.NewGuid().ToString() : null;
string? stdoutName = Console.IsOutputRedirected ? Guid.NewGuid().ToString() : null;
string? stderrName = Console.IsErrorRedirected ? Guid.NewGuid().ToString() : null;
string exe = Environment.GetCommandLineArgs()[0];
string commandLine = Environment.CommandLine;
string arguments = "--elevated" +
" " + (stdinName ?? NoPipe) +
" " + (stdoutName ?? NoPipe) +
" " + (stderrName ?? NoPipe) +
commandLine.Remove(commandLine.IndexOf(exe), exe.Length).TrimStart('"');
var startInfo = new ProcessStartInfo
{
UseShellExecute = true,
Verb = "runas",
FileName = ExecutablePath,
Arguments = arguments,
WindowStyle = ProcessWindowStyle.Hidden,
};
try
{
using var elevated = Process.Start(startInfo)!;
if (stdinName is not null)
{
var stdin = new NamedPipeServerStream(stdinName, PipeDirection.Out, 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous);
stdin.WaitForConnectionAsync().ContinueWith(_ => Console.OpenStandardInput().CopyToAsync(stdin));
}
if (stdoutName is not null)
{
var stdout = new NamedPipeServerStream(stdoutName, PipeDirection.In, 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous);
stdout.WaitForConnectionAsync().ContinueWith(_ => stdout.CopyToAsync(Console.OpenStandardOutput()));
}
if (stderrName is not null)
{
var stderr = new NamedPipeServerStream(stderrName, PipeDirection.In, 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous);
stderr.WaitForConnectionAsync().ContinueWith(_ => stderr.CopyToAsync(Console.OpenStandardError()));
}
elevated.WaitForExit();
Environment.Exit(elevated.ExitCode);
}
catch (Win32Exception e) when (e.NativeErrorCode == Errors.ERROR_CANCELLED)
{
Log.Fatal(e.Message);
Environment.Exit(e.ErrorCode);
}
}
static void AutoRefresh(XmlServiceConfig config)
{
if (!config.AutoRefresh)
{
return;
}
var fileLastWriteTime = File.GetLastWriteTime(config.FullPath);
using var registryKey = Registry.LocalMachine
.OpenSubKey("SYSTEM")?
.OpenSubKey("CurrentControlSet")?
.OpenSubKey("Services")?
.OpenSubKey(config.Name);
if (registryKey is null)
{
return;
}
var registryLastWriteTime = registryKey.GetLastWriteTime();
if (fileLastWriteTime > registryLastWriteTime)
{
DoRefresh(config);
}
}
static void DoRefresh(XmlServiceConfig config)
{
using var scm = ServiceManager.Open(ServiceManagerAccess.Connect);
try
{
using var sc = scm.OpenService(config.Name);
sc.ChangeConfig(config.DisplayName, config.StartMode, config.ServiceDependencies);
sc.SetDescription(config.Description);
var actions = config.FailureActions;
if (actions.Length > 0)
{
sc.SetFailureActions(config.ResetFailureAfter, actions);
}
bool isDelayedAutoStart = config.StartMode == ServiceStartMode.Automatic && config.DelayedAutoStart;
if (isDelayedAutoStart)
{
sc.SetDelayedAutoStart(true);
}
if (config.PreshutdownTimeout is TimeSpan preshutdownTimeout)
{
sc.SetPreshutdownTimeout(preshutdownTimeout);
}
string? securityDescriptor = config.SecurityDescriptor;
if (securityDescriptor != null)
{
// throws ArgumentException
sc.SetSecurityDescriptor(new RawSecurityDescriptor(securityDescriptor));
}
}
catch (CommandException e)
when (e.InnerException is Win32Exception inner && inner.NativeErrorCode == Errors.ERROR_SERVICE_DOES_NOT_EXIST)
{
Throw.Command.Exception(inner);
}
Log.Info($"Service '{config.Format()}' was refreshed successfully.");
}
}
/// <exception cref="FileNotFoundException" />
private static XmlServiceConfig LoadConfigAndInitLoggers(string? path, bool inConsoleMode)
{
if (TestConfig != null)
{
return TestConfig;
}
// TODO: Make logging levels configurable
var fileLogLevel = Level.Debug;
// TODO: Debug should not be printed to console by default. Otherwise commands like 'status' will be pollutted
// This is a workaround till there is a better command line parsing, which will allow determining
var consoleLogLevel = Level.Info;
var eventLogLevel = Level.Warn;
var repository = LogManager.GetRepository(Assembly.GetExecutingAssembly());
if (inConsoleMode)
{
var consoleAppender = new WinSWConsoleAppender
{
Name = "Wrapper console log",
Threshold = consoleLogLevel,
Layout = new PatternLayout("%date{ABSOLUTE} - %message%newline"),
};
consoleAppender.ActivateOptions();
BasicConfigurator.Configure(repository, consoleAppender);
}
else
{
var eventLogAppender = new ServiceEventLogAppender(WrapperService.EventLogProvider)
{
Name = "Wrapper event log",
Threshold = eventLogLevel,
};
eventLogAppender.ActivateOptions();
BasicConfigurator.Configure(repository, eventLogAppender);
}
XmlServiceConfig config;
if (path != null)
{
config = new XmlServiceConfig(path);
}
else
{
path = Path.ChangeExtension(ExecutablePath, ".xml");
if (!File.Exists(path))
{
throw new FileNotFoundException("Unable to locate " + Path.GetFileNameWithoutExtension(path) + ".xml file within executable directory.");
}
config = new XmlServiceConfig(path);
}
// .wrapper.log
string wrapperLogPath = Path.Combine(config.LogDirectory, config.BaseName + ".wrapper.log");
var fileAppender = new FileAppender
{
AppendToFile = true,
File = wrapperLogPath,
ImmediateFlush = true,
Name = "Wrapper file log",
Threshold = fileLogLevel,
LockingModel = new FileAppender.MinimalLock(),
Layout = new PatternLayout("%date %-5level - %message%newline"),
};
fileAppender.ActivateOptions();
BasicConfigurator.Configure(repository, fileAppender);
return config;
}
/// <exception cref="CommandException" />
internal static bool IsProcessElevated()
{
var process = ProcessApis.GetCurrentProcess();
if (!ProcessApis.OpenProcessToken(process, TokenAccessLevels.Read, out var token))
{
Throw.Command.Win32Exception("Failed to open process token.");
}
using (token)
{
unsafe
{
if (!SecurityApis.GetTokenInformation(
token,
SecurityApis.TOKEN_INFORMATION_CLASS.TokenElevation,
out var elevation,
sizeof(SecurityApis.TOKEN_ELEVATION),
out _))
{
Throw.Command.Win32Exception("Failed to get token information.");
}
return elevation.TokenIsElevated != 0;
}
}
}
}
}