mirror of https://github.com/winsw/winsw
724 lines
24 KiB
C#
724 lines
24 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.ComponentModel;
|
|
using System.Diagnostics;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using System.IO;
|
|
#if NETCOREAPP
|
|
using System.Reflection;
|
|
#endif
|
|
using System.Runtime.InteropServices;
|
|
using System.Security.AccessControl;
|
|
using System.Security.Principal;
|
|
using System.ServiceProcess;
|
|
using System.Text;
|
|
using System.Threading;
|
|
using log4net;
|
|
using log4net.Appender;
|
|
using log4net.Config;
|
|
using log4net.Core;
|
|
using log4net.Layout;
|
|
using WinSW.Configuration;
|
|
using WinSW.Logging;
|
|
using WinSW.Native;
|
|
using WinSW.Util;
|
|
using WMI;
|
|
using ServiceType = WMI.ServiceType;
|
|
|
|
namespace WinSW
|
|
{
|
|
public static class Program
|
|
{
|
|
private static readonly ILog Log = LogManager.GetLogger(typeof(Program));
|
|
|
|
public static int Main(string[] args)
|
|
{
|
|
try
|
|
{
|
|
Run(args);
|
|
Log.Debug("Completed. Exit code is 0");
|
|
return 0;
|
|
}
|
|
catch (InvalidDataException e)
|
|
{
|
|
string message = "The configuration file cound not be loaded. " + e.Message;
|
|
Log.Fatal(message, e);
|
|
Console.Error.WriteLine(message);
|
|
return -1;
|
|
}
|
|
catch (WmiException e)
|
|
{
|
|
Log.Fatal("WMI Operation failure: " + e.ErrorCode, e);
|
|
Console.Error.WriteLine(e);
|
|
return (int)e.ErrorCode;
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Log.Fatal("Unhandled exception", e);
|
|
Console.Error.WriteLine(e);
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
public static void Run(string[] argsArray, IWinSWConfiguration? descriptor = null)
|
|
{
|
|
bool inConsoleMode = argsArray.Length > 0;
|
|
|
|
// If descriptor is not specified, initialize the new one (and load configs from there)
|
|
descriptor ??= GetServiceDescriptor();
|
|
|
|
// Configure the wrapper-internal logging.
|
|
// STDOUT and STDERR of the child process will be handled independently.
|
|
InitLoggers(descriptor, inConsoleMode);
|
|
|
|
if (!inConsoleMode)
|
|
{
|
|
Log.Debug("Starting WinSW in service mode");
|
|
ServiceBase.Run(new WrapperService(descriptor));
|
|
return;
|
|
}
|
|
|
|
Log.Debug("Starting WinSW in console mode");
|
|
|
|
if (argsArray.Length == 0)
|
|
{
|
|
PrintHelp();
|
|
return;
|
|
}
|
|
|
|
// Get service info for the future use
|
|
IWin32Services svcs = new WmiRoot().GetCollection<IWin32Services>();
|
|
IWin32Service? svc = svcs.Select(descriptor.Id);
|
|
|
|
var args = new List<string>(Array.AsReadOnly(argsArray));
|
|
if (args[0] == "/redirect")
|
|
{
|
|
var f = new FileStream(args[1], FileMode.Create);
|
|
var w = new StreamWriter(f) { AutoFlush = true };
|
|
Console.SetOut(w);
|
|
Console.SetError(w);
|
|
|
|
var handle = f.SafeFileHandle;
|
|
_ = Kernel32.SetStdHandle(-11, handle); // set stdout
|
|
_ = Kernel32.SetStdHandle(-12, handle); // set stder
|
|
|
|
args = args.GetRange(2, args.Count - 2);
|
|
}
|
|
|
|
bool elevated;
|
|
if (args[0] == "/elevated")
|
|
{
|
|
elevated = true;
|
|
|
|
_ = ConsoleApis.FreeConsole();
|
|
_ = ConsoleApis.AttachConsole(ConsoleApis.ATTACH_PARENT_PROCESS);
|
|
|
|
args = args.GetRange(1, args.Count - 1);
|
|
}
|
|
else if (Environment.OSVersion.Version.Major == 5)
|
|
{
|
|
// Windows XP
|
|
elevated = true;
|
|
}
|
|
else
|
|
{
|
|
elevated = IsProcessElevated();
|
|
}
|
|
|
|
switch (args[0].ToLower())
|
|
{
|
|
case "install":
|
|
Install();
|
|
return;
|
|
|
|
case "uninstall":
|
|
Uninstall();
|
|
return;
|
|
|
|
case "start":
|
|
Start();
|
|
return;
|
|
|
|
case "stop":
|
|
Stop();
|
|
return;
|
|
|
|
case "stopwait":
|
|
StopWait();
|
|
return;
|
|
|
|
case "restart":
|
|
Restart();
|
|
return;
|
|
|
|
case "restart!":
|
|
RestartSelf();
|
|
return;
|
|
|
|
case "status":
|
|
Status();
|
|
return;
|
|
|
|
case "test":
|
|
Test();
|
|
return;
|
|
|
|
case "testwait":
|
|
TestWait();
|
|
return;
|
|
|
|
case "help":
|
|
case "--help":
|
|
case "-h":
|
|
case "-?":
|
|
case "/?":
|
|
PrintHelp();
|
|
return;
|
|
|
|
case "version":
|
|
PrintVersion();
|
|
return;
|
|
|
|
default:
|
|
Console.WriteLine("Unknown command: " + args[0]);
|
|
PrintAvailableCommands();
|
|
#pragma warning disable S112 // General exceptions should never be thrown
|
|
throw new Exception("Unknown command: " + args[0]);
|
|
#pragma warning restore S112 // General exceptions should never be thrown
|
|
}
|
|
|
|
void Install()
|
|
{
|
|
if (!elevated)
|
|
{
|
|
Elevate();
|
|
return;
|
|
}
|
|
|
|
Log.Info("Installing the service with id '" + descriptor.Id + "'");
|
|
|
|
// Check if the service exists
|
|
if (svc != null)
|
|
{
|
|
Console.WriteLine("Service with id '" + descriptor.Id + "' already exists");
|
|
Console.WriteLine("To install the service, delete the existing one or change service Id in the configuration file");
|
|
#pragma warning disable S112 // General exceptions should never be thrown
|
|
throw new Exception("Installation failure: Service with id '" + descriptor.Id + "' already exists");
|
|
#pragma warning restore S112 // General exceptions should never be thrown
|
|
}
|
|
|
|
string? username = null;
|
|
string? password = null;
|
|
bool allowServiceLogonRight = false;
|
|
if (args.Count > 1 && args[1] == "/p")
|
|
{
|
|
Console.Write("Username: ");
|
|
username = Console.ReadLine();
|
|
Console.Write("Password: ");
|
|
password = ReadPassword();
|
|
Console.WriteLine();
|
|
Console.Write("Set Account rights to allow log on as a service (y/n)?: ");
|
|
var keypressed = Console.ReadKey();
|
|
Console.WriteLine();
|
|
if (keypressed.Key == ConsoleKey.Y)
|
|
{
|
|
allowServiceLogonRight = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (descriptor.ServiceAccount.HasServiceAccount())
|
|
{
|
|
username = descriptor.ServiceAccount.ServiceAccountUser;
|
|
password = descriptor.ServiceAccount.ServiceAccountPassword;
|
|
allowServiceLogonRight = descriptor.ServiceAccount.AllowServiceAcountLogonRight;
|
|
}
|
|
}
|
|
|
|
if (allowServiceLogonRight)
|
|
{
|
|
Security.AddServiceLogonRight(descriptor.ServiceAccount.ServiceAccountDomain!, descriptor.ServiceAccount.ServiceAccountName!);
|
|
}
|
|
|
|
svcs.Create(
|
|
descriptor.Id,
|
|
descriptor.Caption,
|
|
"\"" + descriptor.ExecutablePath + "\"",
|
|
ServiceType.OwnProcess,
|
|
ErrorControl.UserNotified,
|
|
descriptor.StartMode.ToString(),
|
|
descriptor.Interactive,
|
|
username,
|
|
password,
|
|
descriptor.ServiceDependencies);
|
|
|
|
using ServiceManager scm = ServiceManager.Open();
|
|
using Service sc = scm.OpenService(descriptor.Id);
|
|
|
|
sc.SetDescription(descriptor.Description);
|
|
|
|
SC_ACTION[] actions = descriptor.FailureActions;
|
|
if (actions.Length > 0)
|
|
{
|
|
sc.SetFailureActions(descriptor.ResetFailureAfter, actions);
|
|
}
|
|
|
|
bool isDelayedAutoStart = descriptor.StartMode == StartMode.Automatic && descriptor.DelayedAutoStart;
|
|
if (isDelayedAutoStart)
|
|
{
|
|
sc.SetDelayedAutoStart(true);
|
|
}
|
|
|
|
string? securityDescriptor = descriptor.SecurityDescriptor;
|
|
if (securityDescriptor != null)
|
|
{
|
|
// throws ArgumentException
|
|
sc.SetSecurityDescriptor(new RawSecurityDescriptor(securityDescriptor));
|
|
}
|
|
|
|
string eventLogSource = descriptor.Id;
|
|
if (!EventLog.SourceExists(eventLogSource))
|
|
{
|
|
EventLog.CreateEventSource(eventLogSource, "Application");
|
|
}
|
|
}
|
|
|
|
void Uninstall()
|
|
{
|
|
if (!elevated)
|
|
{
|
|
Elevate();
|
|
return;
|
|
}
|
|
|
|
Log.Info("Uninstalling the service with id '" + descriptor.Id + "'");
|
|
if (svc is null)
|
|
{
|
|
Log.Warn("The service with id '" + descriptor.Id + "' does not exist. Nothing to uninstall");
|
|
return; // there's no such service, so consider it already uninstalled
|
|
}
|
|
|
|
if (svc.Started)
|
|
{
|
|
// 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 '" + descriptor.Id + "' is running. It may be impossible to uninstall it");
|
|
}
|
|
|
|
try
|
|
{
|
|
svc.Delete();
|
|
}
|
|
catch (WmiException e)
|
|
{
|
|
if (e.ErrorCode == ReturnValue.ServiceMarkedForDeletion)
|
|
{
|
|
Log.Error("Failed to uninstall the service with id '" + descriptor.Id + "'"
|
|
+ ". It has been marked for deletion.");
|
|
|
|
// TODO: change the default behavior to Error?
|
|
return; // it's already uninstalled, so consider it a success
|
|
}
|
|
else
|
|
{
|
|
Log.Fatal("Failed to uninstall the service with id '" + descriptor.Id + "'. WMI Error code is '" + e.ErrorCode + "'");
|
|
}
|
|
|
|
throw;
|
|
}
|
|
}
|
|
|
|
void Start()
|
|
{
|
|
if (!elevated)
|
|
{
|
|
Elevate();
|
|
return;
|
|
}
|
|
|
|
Log.Info("Starting the service with id '" + descriptor.Id + "'");
|
|
if (svc is null)
|
|
{
|
|
ThrowNoSuchService();
|
|
}
|
|
|
|
try
|
|
{
|
|
svc.StartService();
|
|
}
|
|
catch (WmiException e)
|
|
{
|
|
if (e.ErrorCode == ReturnValue.ServiceAlreadyRunning)
|
|
{
|
|
Log.Info($"The service with ID '{descriptor.Id}' has already been started");
|
|
}
|
|
else
|
|
{
|
|
throw;
|
|
}
|
|
}
|
|
}
|
|
|
|
void Stop()
|
|
{
|
|
if (!elevated)
|
|
{
|
|
Elevate();
|
|
return;
|
|
}
|
|
|
|
Log.Info("Stopping the service with id '" + descriptor.Id + "'");
|
|
if (svc is null)
|
|
{
|
|
ThrowNoSuchService();
|
|
}
|
|
|
|
try
|
|
{
|
|
svc.StopService();
|
|
}
|
|
catch (WmiException e)
|
|
{
|
|
if (e.ErrorCode == ReturnValue.ServiceCannotAcceptControl)
|
|
{
|
|
Log.Info($"The service with ID '{descriptor.Id}' is not running");
|
|
}
|
|
else
|
|
{
|
|
throw;
|
|
}
|
|
}
|
|
}
|
|
|
|
void StopWait()
|
|
{
|
|
if (!elevated)
|
|
{
|
|
Elevate();
|
|
return;
|
|
}
|
|
|
|
Log.Info("Stopping the service with id '" + descriptor.Id + "'");
|
|
if (svc is null)
|
|
{
|
|
ThrowNoSuchService();
|
|
}
|
|
|
|
if (svc.Started)
|
|
{
|
|
svc.StopService();
|
|
}
|
|
|
|
while (svc != null && svc.Started)
|
|
{
|
|
Log.Info("Waiting the service to stop...");
|
|
Thread.Sleep(1000);
|
|
svc = svcs.Select(descriptor.Id);
|
|
}
|
|
|
|
Log.Info("The service stopped.");
|
|
}
|
|
|
|
void Restart()
|
|
{
|
|
if (!elevated)
|
|
{
|
|
Elevate();
|
|
return;
|
|
}
|
|
|
|
Log.Info("Restarting the service with id '" + descriptor.Id + "'");
|
|
if (svc is null)
|
|
{
|
|
ThrowNoSuchService();
|
|
}
|
|
|
|
if (svc.Started)
|
|
{
|
|
svc.StopService();
|
|
}
|
|
|
|
while (svc.Started)
|
|
{
|
|
Thread.Sleep(1000);
|
|
svc = svcs.Select(descriptor.Id)!;
|
|
}
|
|
|
|
svc.StartService();
|
|
}
|
|
|
|
void RestartSelf()
|
|
{
|
|
if (!elevated)
|
|
{
|
|
throw new UnauthorizedAccessException("Access is denied.");
|
|
}
|
|
|
|
Log.Info("Restarting the service with id '" + descriptor.Id + "'");
|
|
|
|
// run restart from another process group. see README.md for why this is useful.
|
|
bool result = ProcessApis.CreateProcess(null, descriptor.ExecutablePath + " restart", IntPtr.Zero, IntPtr.Zero, false, ProcessApis.CREATE_NEW_PROCESS_GROUP, IntPtr.Zero, null, default, out _);
|
|
if (!result)
|
|
{
|
|
#pragma warning disable S112 // General exceptions should never be thrown
|
|
throw new Exception("Failed to invoke restart: " + Marshal.GetLastWin32Error());
|
|
#pragma warning restore S112 // General exceptions should never be thrown
|
|
}
|
|
}
|
|
|
|
void Status()
|
|
{
|
|
Log.Debug("User requested the status of the process with id '" + descriptor.Id + "'");
|
|
#pragma warning disable S3358 // Ternary operators should not be nested
|
|
Console.WriteLine(svc is null ? "NonExistent" : svc.Started ? "Started" : "Stopped");
|
|
#pragma warning restore S3358 // Ternary operators should not be nested
|
|
}
|
|
|
|
void Test()
|
|
{
|
|
if (!elevated)
|
|
{
|
|
Elevate();
|
|
return;
|
|
}
|
|
|
|
WrapperService wsvc = new WrapperService(descriptor);
|
|
wsvc.RaiseOnStart(args.ToArray());
|
|
Thread.Sleep(1000);
|
|
wsvc.RaiseOnStop();
|
|
}
|
|
|
|
void TestWait()
|
|
{
|
|
if (!elevated)
|
|
{
|
|
Elevate();
|
|
return;
|
|
}
|
|
|
|
WrapperService wsvc = new WrapperService(descriptor);
|
|
wsvc.RaiseOnStart(args.ToArray());
|
|
Console.WriteLine("Press any key to stop the service...");
|
|
_ = Console.Read();
|
|
wsvc.RaiseOnStop();
|
|
}
|
|
|
|
// [DoesNotReturn]
|
|
void Elevate()
|
|
{
|
|
using Process current = Process.GetCurrentProcess();
|
|
|
|
ProcessStartInfo startInfo = new ProcessStartInfo
|
|
{
|
|
UseShellExecute = true,
|
|
Verb = "runas",
|
|
FileName = current.MainModule.FileName,
|
|
#if NETCOREAPP
|
|
Arguments = "/elevated " + string.Join(' ', args),
|
|
#elif !NET20
|
|
Arguments = "/elevated " + string.Join(" ", args),
|
|
#else
|
|
Arguments = "/elevated " + string.Join(" ", args.ToArray()),
|
|
#endif
|
|
WindowStyle = ProcessWindowStyle.Hidden,
|
|
};
|
|
|
|
try
|
|
{
|
|
using Process elevated = Process.Start(startInfo);
|
|
|
|
elevated.WaitForExit();
|
|
Environment.Exit(elevated.ExitCode);
|
|
}
|
|
catch (Win32Exception e) when (e.NativeErrorCode == Errors.ERROR_CANCELLED)
|
|
{
|
|
Log.Fatal(e.Message);
|
|
Environment.Exit(e.ErrorCode);
|
|
}
|
|
}
|
|
}
|
|
|
|
[DoesNotReturn]
|
|
private static void ThrowNoSuchService() => throw new WmiException(ReturnValue.NoSuchService);
|
|
|
|
private static void InitLoggers(IWinSWConfiguration descriptor, bool enableConsoleLogging)
|
|
{
|
|
// TODO: Make logging levels configurable
|
|
Level 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
|
|
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
|
|
string wrapperLogPath = Path.Combine(descriptor.LogDirectory, descriptor.BaseName + ".wrapper.log");
|
|
var wrapperLog = new FileAppender
|
|
{
|
|
AppendToFile = true,
|
|
File = wrapperLogPath,
|
|
ImmediateFlush = true,
|
|
Name = "Wrapper file log",
|
|
Threshold = fileLogLevel,
|
|
LockingModel = new FileAppender.MinimalLock(),
|
|
Layout = layout,
|
|
};
|
|
wrapperLog.ActivateOptions();
|
|
appenders.Add(wrapperLog);
|
|
|
|
// console log
|
|
if (enableConsoleLogging)
|
|
{
|
|
var consoleAppender = new ConsoleAppender
|
|
{
|
|
Name = "Wrapper console log",
|
|
Threshold = consoleLogLevel,
|
|
Layout = layout,
|
|
};
|
|
consoleAppender.ActivateOptions();
|
|
appenders.Add(consoleAppender);
|
|
}
|
|
|
|
// event log
|
|
var systemEventLogger = new ServiceEventLogAppender
|
|
{
|
|
Name = "Wrapper event log",
|
|
Threshold = eventLogLevel,
|
|
Provider = WrapperService.eventLogProvider,
|
|
};
|
|
systemEventLogger.ActivateOptions();
|
|
appenders.Add(systemEventLogger);
|
|
|
|
BasicConfigurator.Configure(
|
|
#if NETCOREAPP
|
|
LogManager.GetRepository(Assembly.GetExecutingAssembly()),
|
|
#endif
|
|
appenders.ToArray());
|
|
}
|
|
|
|
internal static unsafe bool IsProcessElevated()
|
|
{
|
|
IntPtr process = ProcessApis.GetCurrentProcess();
|
|
if (!ProcessApis.OpenProcessToken(process, TokenAccessLevels.Read, out IntPtr token))
|
|
{
|
|
ThrowWin32Exception("Failed to open process token.");
|
|
}
|
|
|
|
try
|
|
{
|
|
if (!SecurityApis.GetTokenInformation(
|
|
token,
|
|
SecurityApis.TOKEN_INFORMATION_CLASS.TokenElevation,
|
|
out SecurityApis.TOKEN_ELEVATION elevation,
|
|
sizeof(SecurityApis.TOKEN_ELEVATION),
|
|
out _))
|
|
{
|
|
ThrowWin32Exception("Failed to get token information");
|
|
}
|
|
|
|
return elevation.TokenIsElevated != 0;
|
|
}
|
|
finally
|
|
{
|
|
_ = HandleApis.CloseHandle(token);
|
|
}
|
|
|
|
static void ThrowWin32Exception(string message)
|
|
{
|
|
Win32Exception inner = new Win32Exception();
|
|
throw new Win32Exception(inner.NativeErrorCode, message + ' ' + inner.Message);
|
|
}
|
|
}
|
|
|
|
private static string ReadPassword()
|
|
{
|
|
StringBuilder buf = new StringBuilder();
|
|
while (true)
|
|
{
|
|
ConsoleKeyInfo key = Console.ReadKey(true);
|
|
if (key.Key == ConsoleKey.Enter)
|
|
{
|
|
return buf.ToString();
|
|
}
|
|
else if (key.Key == ConsoleKey.Backspace)
|
|
{
|
|
_ = buf.Remove(buf.Length - 1, 1);
|
|
Console.Write("\b \b");
|
|
}
|
|
else
|
|
{
|
|
Console.Write('*');
|
|
_ = buf.Append(key.KeyChar);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static IWinSWConfiguration GetServiceDescriptor()
|
|
{
|
|
var executablePath = new DefaultWinSWSettings().ExecutablePath;
|
|
var baseName = Path.GetFileNameWithoutExtension(executablePath);
|
|
|
|
var d = new DirectoryInfo(Path.GetDirectoryName(executablePath));
|
|
|
|
if (File.Exists(Path.Combine(d.FullName, baseName + ".xml")))
|
|
{
|
|
return new ServiceDescriptor(baseName, d);
|
|
}
|
|
|
|
if (File.Exists(Path.Combine(d.FullName, baseName + ".yml")))
|
|
{
|
|
return new ServiceDescriptorYaml(baseName, d).Configurations;
|
|
}
|
|
|
|
throw new FileNotFoundException($"Unable to locate { baseName }.[xml|yml] file within executable directory");
|
|
}
|
|
|
|
private static void PrintHelp()
|
|
{
|
|
Console.WriteLine("A wrapper binary that can be used to host executables as Windows services");
|
|
Console.WriteLine();
|
|
Console.WriteLine("Usage: winsw <command> [<args>]");
|
|
Console.WriteLine(" Missing arguments triggers the service mode");
|
|
Console.WriteLine();
|
|
PrintAvailableCommands();
|
|
Console.WriteLine();
|
|
Console.WriteLine("Extra options:");
|
|
Console.WriteLine(" /redirect redirect the wrapper's STDOUT and STDERR to the specified file");
|
|
Console.WriteLine();
|
|
PrintVersion();
|
|
Console.WriteLine("More info: https://github.com/winsw/winsw");
|
|
Console.WriteLine("Bug tracker: https://github.com/winsw/winsw/issues");
|
|
}
|
|
|
|
// TODO: Rework to enum in winsw-2.0
|
|
private static void PrintAvailableCommands()
|
|
{
|
|
Console.WriteLine(
|
|
@"Available commands:
|
|
install install the service to Windows Service Controller
|
|
uninstall uninstall the service
|
|
start start the service (must be installed before)
|
|
stop stop the service
|
|
stopwait stop the service and wait until it's actually stopped
|
|
restart restart the service
|
|
restart! self-restart (can be called from child processes)
|
|
status check the current status of the service
|
|
test check if the service can be started and then stopped
|
|
testwait starts the service and waits until a key is pressed then stops the service
|
|
version print the version info
|
|
help print the help info (aliases: -h,--help,-?,/?)");
|
|
}
|
|
|
|
private static void PrintVersion()
|
|
{
|
|
Console.WriteLine("WinSW " + WrapperService.Version);
|
|
}
|
|
}
|
|
}
|