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 = GetConfigType(); // 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(); IWin32Service? svc = svcs.Select(descriptor.Id); var args = new List(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 appenders = new List(); // .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 GetConfigType() { var executablePath = new DefaultWinSWSettings().ExecutablePath; var baseName = Path.GetFileNameWithoutExtension(executablePath); if (baseName.EndsWith(".vshost")) { baseName = baseName.Substring(0, baseName.Length - 7); } var d = new DirectoryInfo(Path.GetDirectoryName(executablePath)); while (true) { 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; } if (d.Parent is null) { throw new FileNotFoundException($"Unable to locate { baseName }.[xml|yml] file within executable directory or any parents"); } d = d.Parent; } } 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 []"); 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); } } }