Decouple CLI and service

pull/553/head
NextTurn 2020-01-18 00:00:00 -08:00 committed by Next Turn
parent 80f109a6a5
commit 1b8cbccd8a
5 changed files with 1180 additions and 1163 deletions

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,694 @@
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.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, ServiceDescriptor? descriptor = null)
{
bool inConsoleMode = argsArray.Length > 0;
// If descriptor is not specified, initialize the new one (and load configs from there)
descriptor ??= new ServiceDescriptor();
// 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
Win32Services svcs = new WmiRoot().GetCollection<Win32Services>();
Win32Service? 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();
throw new Exception("Unknown command: " + args[0]);
}
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");
throw new Exception("Installation failure: Service with id '" + descriptor.Id + "' already exists");
}
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.HasServiceAccount())
{
username = descriptor.ServiceAccountUser;
password = descriptor.ServiceAccountPassword;
allowServiceLogonRight = descriptor.AllowServiceAcountLogonRight;
}
}
if (allowServiceLogonRight)
{
Security.AddServiceLogonRight(descriptor.ServiceAccountDomain!, descriptor.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 e;
}
}
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)
{
throw new Exception("Failed to invoke restart: " + Marshal.GetLastWin32Error());
}
}
void Status()
{
Log.Debug("User requested the status of the process with id '" + descriptor.Id + "'");
Console.WriteLine(svc is null ? "NonExistent" : svc.Started ? "Started" : "Stopped");
}
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(ServiceDescriptor 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 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);
}
}
}

View File

@ -0,0 +1,483 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.ServiceProcess;
using System.Text;
using System.Text.RegularExpressions;
#if VNEXT
using System.Threading.Tasks;
#endif
using log4net;
using winsw.Extensions;
using winsw.Logging;
using winsw.Native;
using winsw.Util;
namespace winsw
{
public class WrapperService : ServiceBase, EventLogger
{
private ServiceApis.SERVICE_STATUS _wrapperServiceStatus;
private readonly Process _process = new Process();
private readonly ServiceDescriptor _descriptor;
private Dictionary<string, string>? _envs;
internal WinSWExtensionManager ExtensionManager { get; private set; }
private static readonly ILog Log = LogManager.GetLogger(
#if NETCOREAPP
Assembly.GetExecutingAssembly(),
#endif
"WinSW");
internal static readonly WrapperServiceEventLogProvider eventLogProvider = new WrapperServiceEventLogProvider();
/// <summary>
/// Indicates to the watch dog thread that we are going to terminate the process,
/// so don't try to kill us when the child exits.
/// </summary>
private bool _orderlyShutdown;
private bool _systemShuttingdown;
/// <summary>
/// Version of Windows service wrapper
/// </summary>
/// <remarks>
/// The version will be taken from <see cref="AssemblyInfo"/>
/// </remarks>
public static Version Version => Assembly.GetExecutingAssembly().GetName().Version!;
/// <summary>
/// Indicates that the system is shutting down.
/// </summary>
public bool IsShuttingDown => _systemShuttingdown;
public WrapperService(ServiceDescriptor descriptor)
{
_descriptor = descriptor;
ServiceName = _descriptor.Id;
ExtensionManager = new WinSWExtensionManager(_descriptor);
CanShutdown = true;
CanStop = true;
CanPauseAndContinue = false;
AutoLog = true;
_systemShuttingdown = false;
// Register the event log provider
eventLogProvider.service = this;
}
public WrapperService() : this(new ServiceDescriptor())
{
}
/// <summary>
/// Process the file copy instructions, so that we can replace files that are always in use while
/// the service runs.
/// </summary>
private void HandleFileCopies()
{
var file = _descriptor.BasePath + ".copies";
if (!File.Exists(file))
return; // nothing to handle
try
{
using var tr = new StreamReader(file, Encoding.UTF8);
string? line;
while ((line = tr.ReadLine()) != null)
{
LogEvent("Handling copy: " + line);
string[] tokens = line.Split('>');
if (tokens.Length > 2)
{
LogEvent("Too many delimiters in " + line);
continue;
}
MoveFile(tokens[0], tokens[1]);
}
}
finally
{
File.Delete(file);
}
}
/// <summary>
/// File replacement.
/// </summary>
private void MoveFile(string sourceFileName, string destFileName)
{
try
{
FileHelper.MoveOrReplaceFile(sourceFileName, destFileName);
}
catch (IOException e)
{
LogEvent("Failed to move :" + sourceFileName + " to " + destFileName + " because " + e.Message);
}
}
/// <summary>
/// Handle the creation of the logfiles based on the optional logmode setting.
/// </summary>
/// <returns>Log Handler, which should be used for the spawned process</returns>
private LogHandler CreateExecutableLogHandler()
{
string logDirectory = _descriptor.LogDirectory;
if (!Directory.Exists(logDirectory))
{
Directory.CreateDirectory(logDirectory);
}
LogHandler logAppender = _descriptor.LogHandler;
logAppender.EventLogger = this;
return logAppender;
}
public void LogEvent(string message)
{
if (_systemShuttingdown)
{
/* NOP - cannot call EventLog because of shutdown. */
}
else
{
try
{
EventLog.WriteEntry(message);
}
catch (Exception e)
{
Log.Error("Failed to log event in Windows Event Log: " + message + "; Reason: ", e);
}
}
}
public void LogEvent(string message, EventLogEntryType type)
{
if (_systemShuttingdown)
{
/* NOP - cannot call EventLog because of shutdown. */
}
else
{
try
{
EventLog.WriteEntry(message, type);
}
catch (Exception e)
{
Log.Error("Failed to log event in Windows Event Log. Reason: ", e);
}
}
}
protected override void OnStart(string[] args)
{
_envs = _descriptor.EnvironmentVariables;
// TODO: Disabled according to security concerns in https://github.com/kohsuke/winsw/issues/54
// Could be restored, but unlikely it's required in event logs at all
/**
foreach (string key in _envs.Keys)
{
LogEvent("envar " + key + '=' + _envs[key]);
}*/
HandleFileCopies();
// handle downloads
#if VNEXT
List<Download> downloads = _descriptor.Downloads;
Task[] tasks = new Task[downloads.Count];
for (int i = 0; i < downloads.Count; i++)
{
Download download = downloads[i];
string downloadMessage = $"Downloading: {download.From} to {download.To}. failOnError={download.FailOnError.ToString()}";
LogEvent(downloadMessage);
Log.Info(downloadMessage);
tasks[i] = download.PerformAsync();
}
try
{
Task.WaitAll(tasks);
}
catch (AggregateException e)
{
List<Exception> exceptions = new List<Exception>(e.InnerExceptions.Count);
for (int i = 0; i < tasks.Length; i++)
{
if (tasks[i].IsFaulted)
{
Download download = downloads[i];
string errorMessage = $"Failed to download {download.From} to {download.To}";
AggregateException exception = tasks[i].Exception!;
LogEvent($"{errorMessage}. {exception.Message}");
Log.Error(errorMessage, exception);
// TODO: move this code into the download logic
if (download.FailOnError)
{
exceptions.Add(new IOException(errorMessage, exception));
}
}
}
throw new AggregateException(exceptions);
}
#else
foreach (Download download in _descriptor.Downloads)
{
string downloadMessage = $"Downloading: {download.From} to {download.To}. failOnError={download.FailOnError.ToString()}";
LogEvent(downloadMessage);
Log.Info(downloadMessage);
try
{
download.Perform();
}
catch (Exception e)
{
string errorMessage = $"Failed to download {download.From} to {download.To}";
LogEvent($"{errorMessage}. {e.Message}");
Log.Error(errorMessage, e);
// TODO: move this code into the download logic
if (download.FailOnError)
{
throw new IOException(errorMessage, e);
}
// Else just keep going
}
}
#endif
string? startArguments = _descriptor.StartArguments;
if (startArguments is null)
{
startArguments = _descriptor.Arguments;
}
else
{
startArguments += " " + _descriptor.Arguments;
}
// Converting newlines, line returns, tabs into a single
// space. This allows users to provide multi-line arguments
// in the xml for readability.
startArguments = Regex.Replace(startArguments, @"\s*[\n\r]+\s*", " ");
LogEvent("Starting " + _descriptor.Executable + ' ' + startArguments);
Log.Info("Starting " + _descriptor.Executable + ' ' + startArguments);
// Load and start extensions
ExtensionManager.LoadExtensions();
ExtensionManager.FireOnWrapperStarted();
LogHandler executableLogHandler = CreateExecutableLogHandler();
StartProcess(_process, startArguments, _descriptor.Executable, executableLogHandler, true);
ExtensionManager.FireOnProcessStarted(_process);
_process.StandardInput.Close(); // nothing for you to read!
}
protected override void OnShutdown()
{
// WriteEvent("OnShutdown");
try
{
_systemShuttingdown = true;
StopIt();
}
catch (Exception ex)
{
Log.Error("Shutdown exception", ex);
}
}
protected override void OnStop()
{
// WriteEvent("OnStop");
try
{
StopIt();
}
catch (Exception ex)
{
Log.Error("Cannot stop exception", ex);
}
}
internal void RaiseOnStart(string[] args) => this.OnStart(args);
internal void RaiseOnStop() => this.OnStop();
/// <summary>
/// Called when we are told by Windows SCM to exit.
/// </summary>
private void StopIt()
{
string? stopArguments = _descriptor.StopArguments;
LogEvent("Stopping " + _descriptor.Id);
Log.Info("Stopping " + _descriptor.Id);
_orderlyShutdown = true;
if (stopArguments is null)
{
try
{
Log.Debug("ProcessKill " + _process.Id);
ProcessHelper.StopProcessAndChildren(_process.Id, _descriptor.StopTimeout, _descriptor.StopParentProcessFirst);
ExtensionManager.FireOnProcessTerminated(_process);
}
catch (InvalidOperationException)
{
// already terminated
}
}
else
{
SignalShutdownPending();
stopArguments += " " + _descriptor.Arguments;
Process stopProcess = new Process();
string? executable = _descriptor.StopExecutable;
executable ??= _descriptor.Executable;
// TODO: Redirect logging to Log4Net once https://github.com/kohsuke/winsw/pull/213 is integrated
StartProcess(stopProcess, stopArguments, executable, null, false);
Log.Debug("WaitForProcessToExit " + _process.Id + "+" + stopProcess.Id);
WaitForProcessToExit(_process);
WaitForProcessToExit(stopProcess);
}
// Stop extensions
ExtensionManager.FireBeforeWrapperStopped();
if (_systemShuttingdown && _descriptor.BeepOnShutdown)
{
Console.Beep();
}
Log.Info("Finished " + _descriptor.Id);
}
private void WaitForProcessToExit(Process processoWait)
{
SignalShutdownPending();
int effectiveProcessWaitSleepTime;
if (_descriptor.SleepTime.TotalMilliseconds > int.MaxValue)
{
Log.Warn("The requested sleep time " + _descriptor.SleepTime.TotalMilliseconds + "is greater that the max value " +
int.MaxValue + ". The value will be truncated");
effectiveProcessWaitSleepTime = int.MaxValue;
}
else
{
effectiveProcessWaitSleepTime = (int)_descriptor.SleepTime.TotalMilliseconds;
}
try
{
// WriteEvent("WaitForProcessToExit [start]");
while (!processoWait.WaitForExit(effectiveProcessWaitSleepTime))
{
SignalShutdownPending();
// WriteEvent("WaitForProcessToExit [repeat]");
}
}
catch (InvalidOperationException)
{
// already terminated
}
// WriteEvent("WaitForProcessToExit [finished]");
}
private void SignalShutdownPending()
{
int effectiveWaitHint;
if (_descriptor.WaitHint.TotalMilliseconds > int.MaxValue)
{
Log.Warn("The requested WaitHint value (" + _descriptor.WaitHint.TotalMilliseconds + " ms) is greater that the max value " +
int.MaxValue + ". The value will be truncated");
effectiveWaitHint = int.MaxValue;
}
else
{
effectiveWaitHint = (int)_descriptor.WaitHint.TotalMilliseconds;
}
RequestAdditionalTime(effectiveWaitHint);
}
private void SignalShutdownComplete()
{
IntPtr handle = ServiceHandle;
_wrapperServiceStatus.CheckPoint++;
// WriteEvent("SignalShutdownComplete " + wrapperServiceStatus.checkPoint + ":" + wrapperServiceStatus.waitHint);
_wrapperServiceStatus.CurrentState = ServiceApis.ServiceState.STOPPED;
ServiceApis.SetServiceStatus(handle, _wrapperServiceStatus);
}
private void StartProcess(Process processToStart, string arguments, string executable, LogHandler? logHandler, bool redirectStdin)
{
// Define handler of the completed process
void OnProcessCompleted(Process proc)
{
string msg = processToStart.Id + " - " + processToStart.StartInfo.FileName + " " + processToStart.StartInfo.Arguments;
try
{
if (_orderlyShutdown)
{
LogEvent("Child process [" + msg + "] terminated with " + proc.ExitCode, EventLogEntryType.Information);
}
else
{
LogEvent("Child process [" + msg + "] finished with " + proc.ExitCode, EventLogEntryType.Warning);
// if we finished orderly, report that to SCM.
// by not reporting unclean shutdown, we let Windows SCM to decide if it wants to
// restart the service automatically
if (proc.ExitCode == 0)
SignalShutdownComplete();
Environment.Exit(proc.ExitCode);
}
}
catch (InvalidOperationException ioe)
{
LogEvent("WaitForExit " + ioe.Message);
}
finally
{
proc.Dispose();
}
}
// Invoke process and exit
ProcessHelper.StartProcessAndCallbackForExit(
processToStart: processToStart,
executable: executable,
arguments: arguments,
envVars: _envs,
workingDirectory: _descriptor.WorkingDirectory,
priority: _descriptor.Priority,
callback: OnProcessCompleted,
logHandler: logHandler,
redirectStdin: redirectStdin,
hideWindow: _descriptor.HideWindow);
}
}
}

View File

@ -46,7 +46,7 @@ $@"<service>
Console.SetError(swErr);
try
{
WrapperService.Run(arguments, descriptor ?? DefaultServiceDescriptor);
Program.Run(arguments, descriptor ?? DefaultServiceDescriptor);
}
finally
{
@ -78,7 +78,7 @@ $@"<service>
Console.SetError(swErr);
try
{
WrapperService.Run(arguments, descriptor ?? DefaultServiceDescriptor);
Program.Run(arguments, descriptor ?? DefaultServiceDescriptor);
}
catch (Exception ex)
{

View File

@ -7,7 +7,7 @@ namespace winswTests.Util
{
internal static void RequireProcessElevated()
{
if (!WrapperService.IsProcessElevated())
if (!Program.IsProcessElevated())
{
Assert.Ignore();
}