mirror of https://github.com/winsw/winsw
913 lines
33 KiB
C#
913 lines
33 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using System.IO;
|
|
using System.Reflection;
|
|
using System.Runtime.InteropServices;
|
|
using System.ServiceProcess;
|
|
using System.Text;
|
|
using System.Threading;
|
|
using log4net;
|
|
using log4net.Appender;
|
|
using log4net.Config;
|
|
using log4net.Core;
|
|
using log4net.Layout;
|
|
using Microsoft.Win32;
|
|
using winsw.Extensions;
|
|
using winsw.Logging;
|
|
using winsw.Native;
|
|
using winsw.Util;
|
|
using WMI;
|
|
using ServiceType = WMI.ServiceType;
|
|
|
|
namespace winsw
|
|
{
|
|
public class WrapperService : ServiceBase, EventLogger
|
|
{
|
|
private 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");
|
|
private 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;
|
|
}
|
|
|
|
CopyFile(tokens[0], tokens[1]);
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
File.Delete(file);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// File replacement.
|
|
/// </summary>
|
|
private void CopyFile(string sourceFileName, string destFileName)
|
|
{
|
|
try
|
|
{
|
|
File.Delete(destFileName);
|
|
File.Move(sourceFileName, destFileName);
|
|
}
|
|
catch (IOException e)
|
|
{
|
|
LogEvent("Failed to copy :" + 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
|
|
foreach (Download d in _descriptor.Downloads)
|
|
{
|
|
string downloadMsg = "Downloading: " + d.From + " to " + d.To + ". failOnError=" + d.FailOnError;
|
|
LogEvent(downloadMsg);
|
|
Log.Info(downloadMsg);
|
|
try
|
|
{
|
|
d.Perform();
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
string errorMessage = "Failed to download " + d.From + " to " + d.To;
|
|
LogEvent(errorMessage + ". " + e.Message);
|
|
Log.Error(errorMessage, e);
|
|
|
|
// TODO: move this code into the download logic
|
|
if (d.FailOnError)
|
|
{
|
|
throw new IOException(errorMessage, e);
|
|
}
|
|
|
|
// Else just keep going
|
|
}
|
|
}
|
|
|
|
string? startarguments = _descriptor.Startarguments;
|
|
|
|
if (startarguments == null)
|
|
{
|
|
startarguments = _descriptor.Arguments;
|
|
}
|
|
else
|
|
{
|
|
startarguments += " " + _descriptor.Arguments;
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <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 == 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;
|
|
|
|
if (executable == null)
|
|
{
|
|
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);
|
|
SignalShutdownComplete();
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|
|
IntPtr handle = ServiceHandle;
|
|
_wrapperServiceStatus.checkPoint++;
|
|
_wrapperServiceStatus.waitHint = effectiveWaitHint;
|
|
// WriteEvent("SignalShutdownPending " + wrapperServiceStatus.checkPoint + ":" + wrapperServiceStatus.waitHint);
|
|
_wrapperServiceStatus.currentState = (int)State.SERVICE_STOP_PENDING;
|
|
Advapi32.SetServiceStatus(handle, ref _wrapperServiceStatus);
|
|
}
|
|
|
|
private void SignalShutdownComplete()
|
|
{
|
|
IntPtr handle = ServiceHandle;
|
|
_wrapperServiceStatus.checkPoint++;
|
|
// WriteEvent("SignalShutdownComplete " + wrapperServiceStatus.checkPoint + ":" + wrapperServiceStatus.waitHint);
|
|
_wrapperServiceStatus.currentState = (int)State.SERVICE_STOPPED;
|
|
Advapi32.SetServiceStatus(handle, ref _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);
|
|
}
|
|
|
|
public static int Main(string[] args)
|
|
{
|
|
// Run app
|
|
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;
|
|
}
|
|
}
|
|
|
|
[DoesNotReturn]
|
|
private static void ThrowNoSuchService()
|
|
{
|
|
throw new WmiException(ReturnValue.NoSuchService);
|
|
}
|
|
|
|
// ReSharper disable once InconsistentNaming
|
|
/// <summary>
|
|
/// Runs the wrapper.
|
|
/// </summary>
|
|
/// <param name="_args">Arguments.</param>
|
|
/// <param name="descriptor">Service descriptor. If null, it will be initialized within the method.
|
|
/// In such case configs will be loaded from the XML Configuration File.</param>
|
|
/// <exception cref="Exception">Any unhandled exception</exception>
|
|
public static void Run(string[] _args, ServiceDescriptor? descriptor = null)
|
|
{
|
|
bool inCliMode = Console.OpenStandardInput() != Stream.Null;
|
|
|
|
// If descriptor is not specified, initialize the new one (and load configs from there)
|
|
descriptor ??= new ServiceDescriptor();
|
|
|
|
// Configure the wrapper-internal logging.
|
|
// STDIN and STDOUT of the child process will be handled independently.
|
|
InitLoggers(descriptor, inCliMode);
|
|
|
|
if (!inCliMode)
|
|
{
|
|
Log.Debug("Starting WinSW in the service mode");
|
|
Run(new WrapperService(descriptor));
|
|
return;
|
|
}
|
|
|
|
Log.Debug("Starting WinSW in the CLI mode");
|
|
|
|
if (_args.Length == 0)
|
|
{
|
|
printHelp();
|
|
return;
|
|
}
|
|
|
|
// Get service info for the future use
|
|
Win32Services svc = new WmiRoot().GetCollection<Win32Services>();
|
|
Win32Service s = svc.Select(descriptor.Id);
|
|
|
|
var args = new List<string>(Array.AsReadOnly(_args));
|
|
if (args[0] == "/redirect")
|
|
{
|
|
// Redirect output
|
|
// One might ask why we support this when the caller
|
|
// can redirect the output easily. The answer is for supporting UAC.
|
|
// On UAC-enabled Windows such as Vista, SCM operation requires
|
|
// elevated privileges, thus winsw.exe needs to be launched
|
|
// accordingly. This in turn limits what the caller can do,
|
|
// and among other things it makes it difficult for the caller
|
|
// to read stdout/stderr. Thus redirection becomes handy.
|
|
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);
|
|
}
|
|
|
|
args[0] = args[0].ToLower();
|
|
if (args[0] == "install")
|
|
{
|
|
Log.Info("Installing the service with id '" + descriptor.Id + "'");
|
|
|
|
// Check if the service exists
|
|
if (s != 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 setallowlogonasaserviceright = false; // This variable is very readable.
|
|
if (args.Count > 1 && args[1] == "/p")
|
|
{
|
|
// we expected username/password on stdin
|
|
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)
|
|
{
|
|
setallowlogonasaserviceright = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (descriptor.HasServiceAccount())
|
|
{
|
|
username = descriptor.ServiceAccountUser;
|
|
password = descriptor.ServiceAccountPassword;
|
|
setallowlogonasaserviceright = descriptor.AllowServiceAcountLogonRight;
|
|
}
|
|
}
|
|
|
|
if (setallowlogonasaserviceright)
|
|
{
|
|
LogonAsAService.AddLogonAsAServiceRight(username!);
|
|
}
|
|
|
|
svc.Create(
|
|
descriptor.Id,
|
|
descriptor.Caption,
|
|
"\"" + descriptor.ExecutablePath + "\"",
|
|
ServiceType.OwnProcess,
|
|
ErrorControl.UserNotified,
|
|
descriptor.StartMode,
|
|
descriptor.Interactive,
|
|
username,
|
|
password,
|
|
descriptor.ServiceDependencies);
|
|
|
|
// update the description
|
|
/* Somehow this doesn't work, even though it doesn't report an error
|
|
Win32Service s = svc.Select(d.Id);
|
|
s.Description = d.Description;
|
|
s.Commit();
|
|
*/
|
|
|
|
// so using a classic method to set the description. Ugly.
|
|
Registry.LocalMachine
|
|
.OpenSubKey("System")
|
|
.OpenSubKey("CurrentControlSet")
|
|
.OpenSubKey("Services")
|
|
.OpenSubKey(descriptor.Id, true)
|
|
.SetValue("Description", descriptor.Description);
|
|
|
|
var actions = descriptor.FailureActions;
|
|
var isDelayedAutoStart = descriptor.StartMode == StartMode.Automatic && descriptor.DelayedAutoStart;
|
|
if (actions.Count > 0 || isDelayedAutoStart)
|
|
{
|
|
using ServiceManager scm = new ServiceManager();
|
|
using Service sc = scm.Open(descriptor.Id);
|
|
|
|
// Delayed auto start
|
|
if (isDelayedAutoStart)
|
|
{
|
|
sc.SetDelayedAutoStart(true);
|
|
}
|
|
|
|
// Set the failure actions
|
|
if (actions.Count > 0)
|
|
{
|
|
sc.ChangeConfig(descriptor.ResetFailureAfter, actions);
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (args[0] == "uninstall")
|
|
{
|
|
Log.Info("Uninstalling the service with id '" + descriptor.Id + "'");
|
|
if (s == 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 (s.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
|
|
{
|
|
s.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;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (args[0] == "start")
|
|
{
|
|
Log.Info("Starting the service with id '" + descriptor.Id + "'");
|
|
if (s == null)
|
|
ThrowNoSuchService();
|
|
|
|
s.StartService();
|
|
return;
|
|
}
|
|
|
|
if (args[0] == "stop")
|
|
{
|
|
Log.Info("Stopping the service with id '" + descriptor.Id + "'");
|
|
if (s == null)
|
|
ThrowNoSuchService();
|
|
|
|
s.StopService();
|
|
return;
|
|
}
|
|
|
|
if (args[0] == "restart")
|
|
{
|
|
Log.Info("Restarting the service with id '" + descriptor.Id + "'");
|
|
if (s == null)
|
|
ThrowNoSuchService();
|
|
|
|
if (s.Started)
|
|
s.StopService();
|
|
|
|
while (s.Started)
|
|
{
|
|
Thread.Sleep(1000);
|
|
s = svc.Select(descriptor.Id);
|
|
}
|
|
|
|
s.StartService();
|
|
return;
|
|
}
|
|
|
|
if (args[0] == "restart!")
|
|
{
|
|
Log.Info("Restarting the service with id '" + descriptor.Id + "'");
|
|
|
|
// run restart from another process group. see README.md for why this is useful.
|
|
|
|
STARTUPINFO si = default;
|
|
bool result = Kernel32.CreateProcess(null, descriptor.ExecutablePath + " restart", IntPtr.Zero, IntPtr.Zero, false, 0x200/*CREATE_NEW_PROCESS_GROUP*/, IntPtr.Zero, null, ref si, out _);
|
|
if (!result)
|
|
{
|
|
throw new Exception("Failed to invoke restart: " + Marshal.GetLastWin32Error());
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (args[0] == "status")
|
|
{
|
|
Log.Debug("User requested the status of the process with id '" + descriptor.Id + "'");
|
|
if (s == null)
|
|
Console.WriteLine("NonExistent");
|
|
else if (s.Started)
|
|
Console.WriteLine("Started");
|
|
else
|
|
Console.WriteLine("Stopped");
|
|
|
|
return;
|
|
}
|
|
|
|
if (args[0] == "test")
|
|
{
|
|
WrapperService wsvc = new WrapperService(descriptor);
|
|
wsvc.OnStart(args.ToArray());
|
|
Thread.Sleep(1000);
|
|
wsvc.OnStop();
|
|
return;
|
|
}
|
|
|
|
if (args[0] == "testwait")
|
|
{
|
|
WrapperService wsvc = new WrapperService(descriptor);
|
|
wsvc.OnStart(args.ToArray());
|
|
Console.WriteLine("Press any key to stop the service...");
|
|
Console.Read();
|
|
wsvc.OnStop();
|
|
return;
|
|
}
|
|
|
|
if (args[0] == "help" || args[0] == "--help" || args[0] == "-h"
|
|
|| args[0] == "-?" || args[0] == "/?")
|
|
{
|
|
printHelp();
|
|
return;
|
|
}
|
|
|
|
if (args[0] == "version")
|
|
{
|
|
printVersion();
|
|
return;
|
|
}
|
|
|
|
Console.WriteLine("Unknown command: " + args[0]);
|
|
printAvailableCommandsInfo();
|
|
throw new Exception("Unknown command: " + args[0]);
|
|
}
|
|
|
|
private static void InitLoggers(ServiceDescriptor d, bool enableCLILogging)
|
|
{
|
|
// TODO: Make logging levels configurable
|
|
Level logLevel = 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 pl = new PatternLayout { ConversionPattern = "%d %-5p - %m%n" };
|
|
pl.ActivateOptions();
|
|
|
|
List<IAppender> appenders = new List<IAppender>();
|
|
|
|
// wrapper.log
|
|
string wrapperLogPath = Path.Combine(d.LogDirectory, d.BaseName + ".wrapper.log");
|
|
var wrapperLog = new FileAppender
|
|
{
|
|
AppendToFile = true,
|
|
File = wrapperLogPath,
|
|
ImmediateFlush = true,
|
|
Name = "Wrapper file log",
|
|
Threshold = logLevel,
|
|
LockingModel = new FileAppender.MinimalLock(),
|
|
Layout = pl
|
|
};
|
|
wrapperLog.ActivateOptions();
|
|
appenders.Add(wrapperLog);
|
|
|
|
// Also display logs in CLI if required
|
|
if (enableCLILogging)
|
|
{
|
|
var consoleAppender = new ConsoleAppender
|
|
{
|
|
Name = "Wrapper console log",
|
|
Threshold = consoleLogLevel,
|
|
Layout = pl,
|
|
};
|
|
consoleAppender.ActivateOptions();
|
|
appenders.Add(consoleAppender);
|
|
}
|
|
|
|
// System log
|
|
var systemEventLogger = new ServiceEventLogAppender
|
|
{
|
|
Name = "System event log",
|
|
Threshold = eventLogLevel,
|
|
provider = eventLogProvider
|
|
};
|
|
systemEventLogger.ActivateOptions();
|
|
appenders.Add(systemEventLogger);
|
|
|
|
BasicConfigurator.Configure(
|
|
#if NETCOREAPP
|
|
LogManager.GetRepository(Assembly.GetExecutingAssembly()),
|
|
#endif
|
|
appenders.ToArray());
|
|
}
|
|
|
|
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 [/redirect file] <command> [<args>]");
|
|
Console.WriteLine(" Missing arguments trigger the service mode");
|
|
Console.WriteLine();
|
|
printAvailableCommandsInfo();
|
|
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/kohsuke/winsw");
|
|
Console.WriteLine("Bug tracker: https://github.com/kohsuke/winsw/issues");
|
|
}
|
|
|
|
// TODO: Rework to enum in winsw-2.0
|
|
private static void printAvailableCommandsInfo()
|
|
{
|
|
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
|
|
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 " + Version);
|
|
}
|
|
}
|
|
}
|