using System;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.ServiceProcess;
using System.Text;
using System.Threading.Tasks;
using log4net;
using WinSW.Extensions;
using WinSW.Logging;
using WinSW.Native;
using WinSW.Util;
using Messages = WinSW.ServiceMessages;
namespace WinSW
{
public sealed class WrapperService : ServiceBase, IEventLogger, IServiceEventLog
{
internal static readonly WrapperServiceEventLogProvider EventLogProvider = new();
private static readonly int AdditionalStopTimeout = 1_000;
private static readonly ILog Log = LogManager.GetLogger(LoggerNames.Service);
private readonly XmlServiceConfig config;
private Process process = null!;
private volatile Process? startingProcess;
private volatile Process? stoppingProcess;
internal WinSWExtensionManager ExtensionManager { get; }
private SharedDirectoryMapper? sharedDirectoryMapper;
private bool shuttingdown;
///
/// Version of Windows service wrapper
///
///
/// The version will be taken from
///
public static Version Version => Assembly.GetExecutingAssembly().GetName().Version!;
public WrapperService(XmlServiceConfig config)
{
this.ServiceName = config.Name;
this.CanStop = true;
this.AutoLog = false;
this.config = config;
this.ExtensionManager = new WinSWExtensionManager(config);
// Register the event log provider
EventLogProvider.Service = this;
if (config.Preshutdown)
{
this.AcceptPreshutdown();
}
Environment.CurrentDirectory = config.WorkingDirectory;
}
///
/// Process the file copy instructions, so that we can replace files that are always in use while
/// the service runs.
///
private void HandleFileCopies()
{
string? file = this.config.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)
{
Log.Info("Handling copy: " + line);
string[] tokens = line.Split('>');
if (tokens.Length > 2)
{
Log.Error("Too many delimiters in " + line);
continue;
}
this.MoveFile(tokens[0], tokens[1]);
}
}
finally
{
File.Delete(file);
}
}
///
/// File replacement.
///
private void MoveFile(string sourceFileName, string destFileName)
{
try
{
FileHelper.MoveOrReplaceFile(sourceFileName, destFileName);
}
catch (IOException e)
{
Log.Error("Failed to move :" + sourceFileName + " to " + destFileName + " because " + e.Message);
}
}
///
/// Handle the creation of the logfiles based on the optional logmode setting.
///
/// Log Handler, which should be used for the spawned process
private LogHandler CreateExecutableLogHandler()
{
string logDirectory = this.config.LogDirectory;
if (!Directory.Exists(logDirectory))
{
Directory.CreateDirectory(logDirectory);
}
var logAppender = this.config.LogHandler;
logAppender.EventLogger = this;
return logAppender;
}
public void WriteEntry(string message)
{
if (this.shuttingdown)
{
// The Event Log service exits earlier.
return;
}
try
{
this.EventLog.WriteEntry(message);
}
catch (Exception e)
{
Log.Error("Failed to log event in Windows Event Log: " + message + "; Reason: ", e);
}
}
public void WriteEntry(string message, EventLogEntryType type)
{
if (this.shuttingdown)
{
// The Event Log service exits earlier.
return;
}
try
{
this.EventLog.WriteEntry(message, type);
}
catch (Exception e)
{
Log.Error("Failed to log event in Windows Event Log. Reason: ", e);
}
}
void IServiceEventLog.WriteEntry(string message, EventLogEntryType type)
{
if (this.shuttingdown)
{
// The Event Log service exits earlier.
return;
}
this.EventLog.WriteEntry(message, type);
}
private void LogExited(string message, int exitCode)
{
if (exitCode == 0)
{
Log.Info(message);
}
else
{
Log.Warn(message);
}
}
private void LogMinimal(string message)
{
this.WriteEntry(message);
Log.Info(message);
}
internal void RaiseOnStart(string[] args) => this.OnStart(args);
internal void RaiseOnStop() => this.OnStop();
protected override void OnStart(string[] args)
{
try
{
this.DoStart();
this.LogMinimal(Messages.StartedSuccessfully);
}
catch (Exception e)
{
Log.Error("Failed to start service.", e);
throw;
}
}
protected override void OnStop()
{
try
{
this.DoStop();
this.LogMinimal(Messages.StoppedSuccessfully);
}
catch (Exception e)
{
Log.Error("Failed to stop service.", e);
throw;
}
}
protected override void OnShutdown()
{
try
{
this.shuttingdown = true;
this.DoStop();
this.LogMinimal("Service was shut down successfully.");
}
catch (Exception e)
{
Log.Error("Failed to shut down service.", e);
throw;
}
}
protected override void OnCustomCommand(int command)
{
if (command == 0x0000000F)
{
// SERVICE_CONTROL_PRESHUTDOWN
this.Stop();
}
}
private void DoStart()
{
bool succeeded = ConsoleApis.FreeConsole();
Debug.Assert(succeeded);
succeeded = ConsoleApis.SetConsoleCtrlHandler(null, true);
Debug.Assert(succeeded);
this.HandleFileCopies();
// handle downloads
var downloads = this.config.Downloads;
var tasks = new Task[downloads.Count];
for (int i = 0; i < downloads.Count; i++)
{
var download = downloads[i];
string downloadMessage = $"Downloading: {download.From} to {download.To}. failOnError={download.FailOnError.ToString()}";
Log.Info(downloadMessage);
tasks[i] = download.PerformAsync();
}
Task.WaitAll(tasks);
var sharedDirectories = this.config.SharedDirectories;
if (sharedDirectories.Count > 0)
{
this.sharedDirectoryMapper = new(sharedDirectories);
this.sharedDirectoryMapper.Map();
}
var prestart = this.config.Prestart;
string? prestartExecutable = prestart.Executable;
if (prestartExecutable != null)
{
try
{
using var process = this.StartProcess(prestartExecutable, prestart.Arguments, prestart.CreateLogHandler());
this.WaitForProcessToExit(process);
this.LogExited($"Pre-start process '{process.Format()}' exited with code {process.ExitCode}.", process.ExitCode);
process.StopDescendants(AdditionalStopTimeout);
}
catch (Exception e)
{
Log.Error(e);
}
}
string startArguments = this.config.StartArguments ?? this.config.Arguments;
Log.Info("Starting " + this.config.Executable);
// Load and start extensions
this.ExtensionManager.LoadExtensions();
this.ExtensionManager.FireOnWrapperStarted();
var executableLogHandler = this.CreateExecutableLogHandler();
this.process = this.StartProcess(this.config.Executable, startArguments, executableLogHandler, this.OnMainProcessExited);
this.ExtensionManager.FireOnProcessStarted(this.process);
var poststart = this.config.Poststart;
string? poststartExecutable = poststart.Executable;
if (poststartExecutable != null)
{
try
{
using var process = StartProcessLocked();
this.WaitForProcessToExit(process);
this.LogExited($"Post-start process '{process.Format()}' exited with code {process.ExitCode}.", process.ExitCode);
process.StopDescendants(AdditionalStopTimeout);
this.startingProcess = null;
Process StartProcessLocked()
{
lock (this)
{
return this.startingProcess = this.StartProcess(poststartExecutable, poststart.Arguments, poststart.CreateLogHandler());
}
}
}
catch (Exception e)
{
Log.Error(e);
}
}
}
///
/// Called when we are told by Windows SCM to exit.
///
private void DoStop()
{
var prestop = this.config.Prestop;
string? prestopExecutable = prestop.Executable;
if (prestopExecutable != null)
{
try
{
using var process = StartProcessLocked(prestopExecutable, prestop.Arguments, prestop.CreateLogHandler());
this.WaitForProcessToExit(process);
this.LogExited($"Pre-stop process '{process.Format()}' exited with code {process.ExitCode}.", process.ExitCode);
process.StopDescendants(AdditionalStopTimeout);
this.stoppingProcess = null;
}
catch (Exception e)
{
Log.Error(e);
}
}
Log.Info("Stopping " + this.config.Name);
this.process.EnableRaisingEvents = false;
string? stopExecutable = this.config.StopExecutable;
string? stopArguments = this.config.StopArguments;
if (stopExecutable is null && stopArguments is null)
{
var process = this.process;
Log.Debug("ProcessKill " + process.Id);
bool? result = process.Stop(this.config.StopTimeoutInMs);
this.LogMinimal($"Child process '{process.Format()}' " + result switch
{
true => $"canceled with code {process.ExitCode}.",
false => "terminated.",
null => $"finished with code '{process.ExitCode}'."
});
this.process.StopDescendants(this.config.StopTimeoutInMs);
this.ExtensionManager.FireOnProcessTerminated(process);
}
else
{
this.SignalPending();
stopExecutable ??= this.config.Executable;
try
{
// TODO: Redirect logging to Log4Net once https://github.com/kohsuke/winsw/pull/213 is integrated
using var stopProcess = StartProcessLocked(stopExecutable, stopArguments);
Log.Debug("WaitForProcessToExit " + this.process.Id + "+" + stopProcess.Id);
this.WaitForProcessToExit(stopProcess);
stopProcess.StopDescendants(AdditionalStopTimeout);
this.stoppingProcess = null;
this.WaitForProcessToExit(this.process);
this.process.StopDescendants(this.config.StopTimeoutInMs);
}
catch
{
this.process.StopTree(this.config.StopTimeoutInMs);
throw;
}
}
var poststop = this.config.Poststop;
string? poststopExecutable = poststop.Executable;
if (poststopExecutable != null)
{
try
{
using var process = StartProcessLocked(poststopExecutable, poststop.Arguments, poststop.CreateLogHandler());
this.WaitForProcessToExit(process);
this.LogExited($"Post-stop process '{process.Format()}' exited with code {process.ExitCode}.", process.ExitCode);
process.StopDescendants(AdditionalStopTimeout);
this.stoppingProcess = null;
}
catch (Exception e)
{
Log.Error(e);
}
}
try
{
this.sharedDirectoryMapper?.Unmap();
}
catch (Exception e)
{
Log.Error(e);
}
// Stop extensions
this.ExtensionManager.FireBeforeWrapperStopped();
if (this.shuttingdown && this.config.BeepOnShutdown)
{
Console.Beep();
}
Log.Info("Finished " + this.config.Name);
Process StartProcessLocked(string executable, string? arguments, LogHandler? logHandler = null)
{
lock (this)
{
return this.stoppingProcess = this.StartProcess(executable, arguments, logHandler);
}
}
}
private void WaitForProcessToExit(Process process)
{
this.SignalPending();
// A good interval is one-tenth of the wait hint but not less than 1 second and not more than 10 seconds.
while (!process.WaitForExit(1_500))
{
this.SignalPending();
}
}
///
private void AcceptPreshutdown()
{
const string acceptedCommandsFieldName =
#if NET
"_acceptedCommands";
#else
"acceptedCommands";
#endif
var acceptedCommandsField = typeof(ServiceBase).GetField(acceptedCommandsFieldName, BindingFlags.Instance | BindingFlags.NonPublic);
if (acceptedCommandsField is null)
{
throw new MissingFieldException(nameof(ServiceBase), acceptedCommandsFieldName);
}
int acceptedCommands = (int)acceptedCommandsField.GetValue(this)!;
acceptedCommands |= 0x00000100; // SERVICE_ACCEPT_PRESHUTDOWN
acceptedCommandsField.SetValue(this, acceptedCommands);
}
private void SignalPending()
{
this.RequestAdditionalTime(15_000);
}
private void SignalStopped()
{
using var scm = ServiceManager.Open();
using var sc = scm.OpenService(this.ServiceName, ServiceApis.ServiceAccess.QueryStatus);
sc.SetStatus(this.ServiceHandle, ServiceControllerStatus.Stopped);
}
private void OnMainProcessExited(Process process)
{
lock (this)
{
try
{
Log.Warn($"Child process '{process.Format()}' finished with code {process.ExitCode}.");
process.StopDescendants(this.config.StopTimeoutInMs);
this.startingProcess?.StopTree(AdditionalStopTimeout);
this.stoppingProcess?.StopTree(AdditionalStopTimeout);
// 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 (process.ExitCode == 0)
{
this.SignalStopped();
}
}
finally
{
Environment.Exit(process.ExitCode);
}
}
}
///
/// will not be raised if is .
///
private Process StartProcess(string executable, string? arguments, LogHandler? logHandler = null, Action? onExited = null)
{
var startInfo = new ProcessStartInfo(executable, arguments ?? string.Empty)
{
UseShellExecute = false,
WorkingDirectory = this.config.WorkingDirectory,
CreateNoWindow = this.config.HideWindow,
RedirectStandardOutput = logHandler?.OutFileDisabled == false,
RedirectStandardError = logHandler?.ErrFileDisabled == false,
};
var environment = this.config.EnvironmentVariables;
if (environment.Count > 0)
{
var newEnvironment =
#if NET
startInfo.Environment;
#else
startInfo.EnvironmentVariables;
#endif
foreach (var pair in environment)
{
newEnvironment[pair.Key] = pair.Value;
}
}
bool succeeded = ConsoleApis.AllocConsole(); // inherited
Debug.Assert(succeeded);
succeeded = ConsoleApis.SetConsoleCtrlHandler(null, false); // inherited
Debug.Assert(succeeded);
succeeded = ConsoleApis.SetConsoleOutputCP(ConsoleApis.CP_UTF8);
Debug.Assert(succeeded);
Process process;
try
{
process = Process.Start(startInfo)!;
}
finally
{
succeeded = ConsoleApis.FreeConsole();
Debug.Assert(succeeded);
succeeded = ConsoleApis.SetConsoleCtrlHandler(null, true);
Debug.Assert(succeeded);
}
Log.Info($"Started process {process.Format()}.");
if (this.config.Priority is ProcessPriorityClass priority)
{
try
{
process.PriorityClass = priority;
}
catch (InvalidOperationException)
{
// exited
}
}
if (logHandler != null)
{
logHandler.Log(
startInfo.RedirectStandardOutput ? process.StandardOutput : StreamReader.Null,
startInfo.RedirectStandardError ? process.StandardError : StreamReader.Null);
}
if (onExited != null)
{
process.Exited += (sender, _) =>
{
var process = (Process)sender!;
if (!process.EnableRaisingEvents)
{
return;
}
try
{
onExited(process);
}
catch (Exception e)
{
Log.Error("Unhandled exception in event handler.", e);
}
};
process.EnableRaisingEvents = true;
}
return process;
}
}
}