From 10d3a6113f7a2232ef6b3684fc269b9a038789a8 Mon Sep 17 00:00:00 2001 From: Buddhika Chathuranga Date: Tue, 21 Jul 2020 22:27:27 +0530 Subject: [PATCH] YAML Configurations Support (GSoC - 2020) (#543) * Add ServiceDesciptorYaml.cs * Add YamlConfigurations.cs YmlDotNet library added * Update YamlConfigurations.cs * Add download configuration to YamlConfiguration * Revert "Add download configuration to YamlConfiguration" This reverts commit f150de13b0b252e12d29b53fecff3b88f37e4997. * Add ServiceDescriptrYaml unit test Update YamlConfigurations. * Confid files seperated Download class refactored Unit test updated * Change nullable attributes. * Refactor IWinSWConfiguration and Logging settings propagation * Update YamlConfigurations.cs * Update configuration FailureActions * Update Yaml Confifurations * Update YAML Configuration * Update yaml configurations * Update Yaml Configurations * Yaml Configuration refactored * Refactored YamlConfigurations * Update serviceaccount and log configs * YAML_SC_ACTION method name channged * Refacored Download class. Field names changed to PascalCase readonly * Add seperate download class to YamlConfigurations and create and return List * Created DefaultWinSWSettings singleton * Refactor variable name * Update StopExecutable * Nullable references updated * Null references updated * Add sanity checks for yaml deserializing. * Implement Log Defaults * Call logdefaults in YAMLConfigurations * Update defaults value of ServiceAccout If serviceaccoutn is not specified default ServiceAccount object will be provided from the Defautls. * Merge build.yml with master * Update YamlSeriviceDescriptor Remove invalid Name field from ServiceAccout Add BaseName logics to defults merge build.yml with master * Update IWinSWConfiguration Support Now can use any IWinSWConfiguration type instead of ServiceDescriptor. We can use both ServiceDescriptor or ServiceDescriptorYaml. * Update LogMode unit test * ServiceAccount configurations refactored Get all ServiceAccount configuration into a single ServiceAccout class. * Update default BasePath * Resolve Merge conflicts * Resolve Merge Conflicts * Update YamlDownload configs * Fix null reference issue in arguments * Update ServiceAccount configs in XML ServiceDescriptor * remove BOM header * Update environment variable configurations Co-authored-by: Oleg Nenashev --- src/Core/ServiceWrapper/Program.cs | 26 +- src/Core/ServiceWrapper/WrapperService.cs | 14 +- .../Configuration/DefaultSettings.cs | 97 +- .../Configuration/IWinSWConfiguration.cs | 27 +- src/Core/WinSWCore/Configuration/Log.cs | 107 ++ .../WinSWCore/Configuration/ServiceAccount.cs | 29 + .../Configuration/YamlConfiguration.cs | 514 ++++++ .../Extensions/AbstractWinSWExtension.cs | 3 +- .../WinSWCore/Extensions/IWinSWExtension.cs | 3 +- .../Extensions/WinSWExtensionManager.cs | 5 +- src/Core/WinSWCore/ServiceDescriptor.cs | 1508 +++++++++-------- src/Core/WinSWCore/ServiceDescriptorYaml.cs | 82 + src/Core/WinSWCore/WinSWCore.csproj | 1 + src/NuGet.Config | 7 + .../RunawayProcessKillerExtension.cs | 3 +- .../SharedDirectoryMapper.cs | 4 +- src/Test/winswTests/ServiceDescriptorTests.cs | 890 +++++----- .../winswTests/ServiceDescriptorYamlTest.cs | 119 ++ .../Util/ServiceDescriptorAssert.cs | 4 + 19 files changed, 2213 insertions(+), 1230 deletions(-) create mode 100644 src/Core/WinSWCore/Configuration/Log.cs create mode 100644 src/Core/WinSWCore/Configuration/ServiceAccount.cs create mode 100644 src/Core/WinSWCore/Configuration/YamlConfiguration.cs create mode 100644 src/Core/WinSWCore/ServiceDescriptorYaml.cs create mode 100644 src/NuGet.Config create mode 100644 src/Test/winswTests/ServiceDescriptorYamlTest.cs diff --git a/src/Core/ServiceWrapper/Program.cs b/src/Core/ServiceWrapper/Program.cs index 661a2f5..05152e8 100644 --- a/src/Core/ServiceWrapper/Program.cs +++ b/src/Core/ServiceWrapper/Program.cs @@ -18,9 +18,9 @@ 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; @@ -59,7 +59,7 @@ namespace WinSW } } - public static void Run(string[] argsArray, ServiceDescriptor? descriptor = null) + public static void Run(string[] argsArray, IWinSWConfiguration? descriptor = null) { bool inConsoleMode = argsArray.Length > 0; @@ -181,7 +181,9 @@ namespace WinSW 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() @@ -199,7 +201,9 @@ namespace WinSW { 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; @@ -222,17 +226,17 @@ namespace WinSW } else { - if (descriptor.HasServiceAccount()) + if (descriptor.ServiceAccount.HasServiceAccount()) { - username = descriptor.ServiceAccountUser; - password = descriptor.ServiceAccountPassword; - allowServiceLogonRight = descriptor.AllowServiceAcountLogonRight; + username = descriptor.ServiceAccount.ServiceAccountUser; + password = descriptor.ServiceAccount.ServiceAccountPassword; + allowServiceLogonRight = descriptor.ServiceAccount.AllowServiceAcountLogonRight; } } if (allowServiceLogonRight) { - Security.AddServiceLogonRight(descriptor.ServiceAccountDomain!, descriptor.ServiceAccountName!); + Security.AddServiceLogonRight(descriptor.ServiceAccount.ServiceAccountDomain!, descriptor.ServiceAccount.ServiceAccountName!); } svcs.Create( @@ -319,7 +323,7 @@ namespace WinSW Log.Fatal("Failed to uninstall the service with id '" + descriptor.Id + "'. WMI Error code is '" + e.ErrorCode + "'"); } - throw e; + throw; } } @@ -455,14 +459,18 @@ namespace WinSW 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() @@ -532,7 +540,7 @@ namespace WinSW [DoesNotReturn] private static void ThrowNoSuchService() => throw new WmiException(ReturnValue.NoSuchService); - private static void InitLoggers(ServiceDescriptor descriptor, bool enableConsoleLogging) + private static void InitLoggers(IWinSWConfiguration descriptor, bool enableConsoleLogging) { // TODO: Make logging levels configurable Level fileLogLevel = Level.Debug; diff --git a/src/Core/ServiceWrapper/WrapperService.cs b/src/Core/ServiceWrapper/WrapperService.cs index a8a3ed8..5b53bba 100644 --- a/src/Core/ServiceWrapper/WrapperService.cs +++ b/src/Core/ServiceWrapper/WrapperService.cs @@ -10,6 +10,7 @@ using System.Text.RegularExpressions; using System.Threading.Tasks; #endif using log4net; +using WinSW.Configuration; using WinSW.Extensions; using WinSW.Logging; using WinSW.Native; @@ -20,9 +21,11 @@ namespace WinSW public class WrapperService : ServiceBase, IEventLogger { private ServiceApis.SERVICE_STATUS wrapperServiceStatus; - + private readonly Process process = new Process(); - private readonly ServiceDescriptor descriptor; + + private readonly IWinSWConfiguration descriptor; + private Dictionary? envs; internal WinSWExtensionManager ExtensionManager { get; private set; } @@ -55,7 +58,7 @@ namespace WinSW /// public bool IsShuttingDown => this.systemShuttingdown; - public WrapperService(ServiceDescriptor descriptor) + public WrapperService(IWinSWConfiguration descriptor) { this.descriptor = descriptor; this.ServiceName = this.descriptor.Id; @@ -131,14 +134,15 @@ namespace WinSW /// Log Handler, which should be used for the spawned process private LogHandler CreateExecutableLogHandler() { - string logDirectory = this.descriptor.LogDirectory; + string? logDirectory = this.descriptor.LogDirectory; if (!Directory.Exists(logDirectory)) { Directory.CreateDirectory(logDirectory); } - LogHandler logAppender = this.descriptor.LogHandler; + LogHandler logAppender = this.descriptor.Log.CreateLogHandler(); + logAppender.EventLogger = this; return logAppender; } diff --git a/src/Core/WinSWCore/Configuration/DefaultSettings.cs b/src/Core/WinSWCore/Configuration/DefaultSettings.cs index 845b505..43d0a7c 100644 --- a/src/Core/WinSWCore/Configuration/DefaultSettings.cs +++ b/src/Core/WinSWCore/Configuration/DefaultSettings.cs @@ -12,6 +12,8 @@ namespace WinSW.Configuration /// public sealed class DefaultWinSWSettings : IWinSWConfiguration { + public static LogDefaults DefaultLogSettings { get; } = new LogDefaults(); + public string Id => throw new InvalidOperationException(nameof(this.Id) + " must be specified."); public string Caption => throw new InvalidOperationException(nameof(this.Caption) + " must be specified."); @@ -25,12 +27,6 @@ namespace WinSW.Configuration public string ExecutablePath => Process.GetCurrentProcess().MainModule.FileName; // Installation - public bool AllowServiceAcountLogonRight => false; - - public string? ServiceAccountPassword => null; - - public string? ServiceAccountUser => null; - public Native.SC_ACTION[] FailureActions => new Native.SC_ACTION[0]; public TimeSpan ResetFailureAfter => TimeSpan.FromDays(1); @@ -66,17 +62,67 @@ namespace WinSW.Configuration public bool Interactive => false; // Logging - public string LogDirectory => Path.GetDirectoryName(this.ExecutablePath)!; + public Log Log { get => new LogDefaults(); } - public string LogMode => "append"; + public string LogDirectory => DefaultLogSettings.Directory; - public bool OutFileDisabled => false; + public string LogMode => DefaultLogSettings.Mode; - public bool ErrFileDisabled => false; + public bool OutFileDisabled => this.Log.OutFileDisabled; - public string OutFilePattern => ".out.log"; + public bool ErrFileDisabled => this.Log.ErrFileDisabled; - public string ErrFilePattern => ".err.log"; + public string OutFilePattern => this.Log.OutFilePattern; + + public string ErrFilePattern => this.Log.ErrFilePattern; + + public ServiceAccount ServiceAccount => new ServiceAccount() + { + ServiceAccountName = null, + ServiceAccountDomain = null, + ServiceAccountPassword = null, + AllowServiceAcountLogonRight = false + }; + + public class LogDefaults : Log + { + private readonly DefaultWinSWSettings defaults; + + public LogDefaults() + { + this.defaults = new DefaultWinSWSettings(); + } + + public override string Mode => "append"; + + public override string Name => this.defaults.BaseName; + + public override string Directory => Path.GetDirectoryName(this.defaults.ExecutablePath)!; + + public override int? SizeThreshold => 1024 * 10 * RollingSizeTimeLogAppender.BytesPerKB; + + public override int? KeepFiles => SizeBasedRollingLogAppender.DefaultFilesToKeep; + + public override string Pattern => + throw new InvalidDataException("Time Based rolling policy is specified but no pattern can be found in configuration XML."); + + public override int? Period => 1; + + public override bool OutFileDisabled { get => false; } + + public override bool ErrFileDisabled { get => false; } + + public override string OutFilePattern { get => ".out.log"; } + + public override string ErrFilePattern { get => ".err.log"; } + + public override string? AutoRollAtTime => null; + + public override int? ZipOlderThanNumDays => + throw new InvalidDataException("Roll-Size-Time Based rolling policy is specified but zipOlderThanNumDays does not match the int format found in configuration XML."); + + public override string? ZipDateFormat => null; + } // Environment public List Downloads => new List(0); @@ -88,5 +134,32 @@ namespace WinSW.Configuration // Extensions public XmlNode? ExtensionsConfiguration => null; + + public string BaseName + { + get + { + string baseName = Path.GetFileNameWithoutExtension(this.ExecutablePath); + if (baseName.EndsWith(".vshost")) + { + baseName = baseName.Substring(0, baseName.Length - 7); + } + + return baseName; + } + } + + public string BasePath + { + get + { + var d = new DirectoryInfo(Path.GetDirectoryName(this.ExecutablePath)); + return Path.Combine(d.FullName, this.BaseName); + } + } + + public List ExtensionIds => new List(0); + + public string? SecurityDescriptor => null; } } diff --git a/src/Core/WinSWCore/Configuration/IWinSWConfiguration.cs b/src/Core/WinSWCore/Configuration/IWinSWConfiguration.cs index 0cd85ee..a59bd86 100644 --- a/src/Core/WinSWCore/Configuration/IWinSWConfiguration.cs +++ b/src/Core/WinSWCore/Configuration/IWinSWConfiguration.cs @@ -22,12 +22,6 @@ namespace WinSW.Configuration bool HideWindow { get; } // Installation - bool AllowServiceAcountLogonRight { get; } - - string? ServiceAccountPassword { get; } - - string? ServiceAccountUser { get; } - Native.SC_ACTION[] FailureActions { get; } TimeSpan ResetFailureAfter { get; } @@ -60,12 +54,17 @@ namespace WinSW.Configuration bool Interactive { get; } - // Logging + /// + /// Destination for logging. + /// If undefined, a default one should be used. + /// string LogDirectory { get; } // TODO: replace by enum string LogMode { get; } + Log Log { get; } + // Environment List Downloads { get; } @@ -76,5 +75,19 @@ namespace WinSW.Configuration // Extensions XmlNode? ExtensionsConfiguration { get; } + + // IWinSWConfiguration Support + List ExtensionIds { get; } + + // Service Account + ServiceAccount ServiceAccount { get; } + + string BaseName { get; } + + string BasePath { get; } + + bool DelayedAutoStart { get; } + + string? SecurityDescriptor { get; } } } diff --git a/src/Core/WinSWCore/Configuration/Log.cs b/src/Core/WinSWCore/Configuration/Log.cs new file mode 100644 index 0000000..a73dff6 --- /dev/null +++ b/src/Core/WinSWCore/Configuration/Log.cs @@ -0,0 +1,107 @@ +using System; +using System.IO; + +namespace WinSW.Configuration +{ + public abstract class Log + { + public abstract string? Mode { get; } + + public abstract string Name { get; } + + public abstract string Directory { get; } + + public abstract int? SizeThreshold { get; } + + public abstract int? KeepFiles { get; } + + public abstract string Pattern { get; } + + public abstract int? Period { get; } + + // Filters + public abstract bool OutFileDisabled { get; } + + public abstract bool ErrFileDisabled { get; } + + public abstract string OutFilePattern { get; } + + public abstract string ErrFilePattern { get; } + + // Zip options + public abstract string? AutoRollAtTime { get; } + + public abstract int? ZipOlderThanNumDays { get; } + + public abstract string? ZipDateFormat { get; } + + public LogHandler CreateLogHandler() + { + switch (this.Mode) + { + case "rotate": + return new SizeBasedRollingLogAppender(this.Directory, this.Name, this.OutFileDisabled, this.ErrFileDisabled, this.OutFilePattern, this.ErrFilePattern); + + case "none": + return new IgnoreLogAppender(); + + case "reset": + return new ResetLogAppender(this.Directory, this.Name, this.OutFileDisabled, this.ErrFileDisabled, this.OutFilePattern, this.ErrFilePattern); + + case "roll": + return new RollingLogAppender(this.Directory, this.Name, this.OutFileDisabled, this.ErrFileDisabled, this.OutFilePattern, this.ErrFilePattern); + + case "roll-by-time": + return new TimeBasedRollingLogAppender(this.Directory, this.Name, this.OutFileDisabled, this.ErrFileDisabled, this.OutFilePattern, this.ErrFilePattern, this.Pattern, this.Period.GetValueOrDefault(1)); + + case "roll-by-size": + return new SizeBasedRollingLogAppender( + this.Directory, + this.Name, + this.OutFileDisabled, + this.ErrFileDisabled, + this.OutFilePattern, + this.ErrFilePattern, + this.SizeThreshold.GetValueOrDefault(10 * 1024) * SizeBasedRollingLogAppender.BytesPerKB, + this.KeepFiles.GetValueOrDefault(SizeBasedRollingLogAppender.DefaultFilesToKeep)); + + case "append": + return new DefaultLogAppender(this.Directory, this.Name, this.OutFileDisabled, this.ErrFileDisabled, this.OutFilePattern, this.ErrFilePattern); + + case "roll-by-size-time": + if (this.Pattern is null) + { + throw new InvalidDataException("Roll-Size-Time Based rolling policy is specified but no pattern can be found in configuration"); + } + + TimeSpan? autoRollAtTime = null; + if (this.AutoRollAtTime != null) + { + // validate it + if (!TimeSpan.TryParse(this.AutoRollAtTime, out TimeSpan autoRollAtTimeValue)) + { + throw new InvalidDataException("Roll-Size-Time Based rolling policy is specified but autoRollAtTime does not match the TimeSpan format HH:mm:ss found in configuration XML."); + } + + autoRollAtTime = autoRollAtTimeValue; + } + + return new RollingSizeTimeLogAppender( + this.Directory, + this.Name, + this.OutFileDisabled, + this.ErrFileDisabled, + this.OutFilePattern, + this.ErrFilePattern, + this.SizeThreshold.GetValueOrDefault(10 * 1024) * SizeBasedRollingLogAppender.BytesPerKB, + this.Pattern, + autoRollAtTime, + this.ZipOlderThanNumDays, + this.ZipDateFormat != null ? this.ZipDateFormat : "yyyyMM"); + + default: + throw new InvalidDataException("Undefined logging mode: " + this.Mode); + } + } + } +} \ No newline at end of file diff --git a/src/Core/WinSWCore/Configuration/ServiceAccount.cs b/src/Core/WinSWCore/Configuration/ServiceAccount.cs new file mode 100644 index 0000000..9b638f0 --- /dev/null +++ b/src/Core/WinSWCore/Configuration/ServiceAccount.cs @@ -0,0 +1,29 @@ +using YamlDotNet.Serialization; + +namespace WinSW.Configuration +{ + public class ServiceAccount + { + [YamlMember(Alias = "user")] + public string? ServiceAccountName { get; set; } + + [YamlMember(Alias = "domain")] + public string? ServiceAccountDomain { get; set; } + + [YamlMember(Alias = "Password")] + public string? ServiceAccountPassword { get; set; } + + [YamlMember(Alias = "allowservicelogon")] + public bool AllowServiceAcountLogonRight { get; set; } + + public string? ServiceAccountUser + { + get => this.ServiceAccountName is null ? null : (this.ServiceAccountDomain ?? ".") + "\\" + this.ServiceAccountName; + } + + public bool HasServiceAccount() + { + return !string.IsNullOrEmpty(this.ServiceAccountName); + } + } +} diff --git a/src/Core/WinSWCore/Configuration/YamlConfiguration.cs b/src/Core/WinSWCore/Configuration/YamlConfiguration.cs new file mode 100644 index 0000000..3551a7e --- /dev/null +++ b/src/Core/WinSWCore/Configuration/YamlConfiguration.cs @@ -0,0 +1,514 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Xml; +using WinSW.Native; +using WMI; +using YamlDotNet.Serialization; +using static WinSW.Download; + +namespace WinSW.Configuration +{ + public class YamlConfiguration : IWinSWConfiguration + { + public DefaultWinSWSettings Defaults { get; } = new DefaultWinSWSettings(); + + [YamlMember(Alias = "id")] + public string? IdYaml { get; set; } + + [YamlMember(Alias = "name")] + public string? NameYaml { get; set; } + + [YamlMember(Alias = "description")] + public string? DescriptionYaml { get; set; } + + [YamlMember(Alias = "executable")] + public string? ExecutableYaml { get; set; } + + [YamlMember(Alias = "executablePath")] + public string? ExecutablePathYaml { get; set; } + + [YamlMember(Alias = "caption")] + public string? CaptionYaml { get; set; } + + [YamlMember(Alias = "hideWindow")] + public bool? HideWindowYaml { get; set; } + + [YamlMember(Alias = "workingdirectory")] + public string? WorkingDirectoryYaml { get; set; } + + [YamlMember(Alias = "serviceaccount")] + public ServiceAccount? ServiceAccountYaml { get; set; } + + [YamlMember(Alias = "log")] + public YamlLog? YAMLLog { get; set; } + + [YamlMember(Alias = "download")] + public List? DownloadsYaml { get; set; } + + [YamlMember(Alias = "arguments")] + public string? ArgumentsYaml { get; set; } + + [YamlMember(Alias = "startArguments")] + public string? StartArgumentsYaml { get; set; } + + [YamlMember(Alias = "stopArguments")] + public string? StopArgumentsYaml { get; set; } + + [YamlMember(Alias = "stopExecutable")] + public string? StopExecutableYaml { get; set; } + + [YamlMember(Alias = "stopParentProcessFirst")] + public bool? StopParentProcessFirstYaml { get; set; } + + [YamlMember(Alias = "resetFailureAfter")] + public TimeSpan? ResetFailureAfterYaml { get; set; } + + [YamlMember(Alias = "stopTimeout")] + public TimeSpan? StopTimeoutYaml { get; set; } + + [YamlMember(Alias = "startMode")] + public StartMode? StartModeYaml { get; set; } + + [YamlMember(Alias = "serviceDependencies")] + public string[]? ServiceDependenciesYaml { get; set; } + + [YamlMember(Alias = "waitHint")] + public TimeSpan? WaitHintYaml { get; set; } + + [YamlMember(Alias = "sleepTime")] + public TimeSpan? SleepTimeYaml { get; set; } + + [YamlMember(Alias = "interactive")] + public bool? InteractiveYaml { get; set; } + + [YamlMember(Alias = "priority")] + public ProcessPriorityClass? PriorityYaml { get; set; } + + [YamlMember(Alias = "beepOnShutdown")] + public bool BeepOnShutdown { get; set; } + + [YamlMember(Alias = "env")] + public Dictionary? EnvironmentVariablesYaml { get; set; } + + [YamlMember(Alias = "failureActions")] + public List? YamlFailureActions { get; set; } + + [YamlMember(Alias = "delayedAutoStart")] + public bool DelayedAutoStart { get; set; } + + [YamlMember(Alias = "securityDescriptor")] + public string? SecurityDescriptorYaml { get; set; } + + public class YamlLog : Log + { + private readonly YamlConfiguration configs; + + public YamlLog() + { + this.configs = new YamlConfiguration(); + } + + [YamlMember(Alias = "mode")] + public string? ModeYamlLog { get; set; } + + [YamlMember(Alias = "name")] + public string? NameYamlLog { get; set; } + + [YamlMember(Alias = "sizeThreshold")] + public int? SizeThresholdYamlLog { get; set; } + + [YamlMember(Alias = "keepFiles")] + public int? KeepFilesYamlLog { get; set; } + + [YamlMember(Alias = "pattern")] + public string? PatternYamlLog { get; set; } + + [YamlMember(Alias = "period")] + public int? PeriodYamlLog { get; set; } + + [YamlMember(Alias = "logpath")] + public string? LogPathYamlLog { get; set; } + + // Filters + [YamlMember(Alias = "outFileDisabled")] + public bool? OutFileDisabledYamlLog { get; set; } + + [YamlMember(Alias = "errFileDisabled")] + public bool? ErrFileDisabledYamlLog { get; set; } + + [YamlMember(Alias = "outFilePattern")] + public string? OutFilePatternYamlLog { get; set; } + + [YamlMember(Alias = "errFilePattern")] + public string? ErrFilePatternYamlLog { get; set; } + + // Zip options + [YamlMember(Alias = "autoRollAtTime")] + public string? AutoRollAtTimeYamlLog { get; set; } + + [YamlMember(Alias = "zipOlderThanNumDays")] + public int? ZipOlderThanNumDaysYamlLog { get; set; } + + [YamlMember(Alias = "zipDateFormat")] + public string? ZipDateFormatYamlLog { get; set; } + + public override string Mode => this.ModeYamlLog is null ? + DefaultWinSWSettings.DefaultLogSettings.Mode : + this.ModeYamlLog; + + public override string Name + { + get + { + return this.NameYamlLog is null ? + DefaultWinSWSettings.DefaultLogSettings.Name : + Environment.ExpandEnvironmentVariables(this.NameYamlLog); + } + } + + public override string Directory + { + get + { + return this.LogPathYamlLog is null ? + DefaultWinSWSettings.DefaultLogSettings.Directory : + Environment.ExpandEnvironmentVariables(this.LogPathYamlLog); + } + } + + public override int? SizeThreshold + { + get + { + return this.SizeThresholdYamlLog is null ? + DefaultWinSWSettings.DefaultLogSettings.SizeThreshold : + this.SizeThresholdYamlLog * RollingSizeTimeLogAppender.BytesPerKB; + } + } + + public override int? KeepFiles + { + get + { + return this.KeepFilesYamlLog is null ? + DefaultWinSWSettings.DefaultLogSettings.KeepFiles : + this.KeepFilesYamlLog; + } + } + + public override string Pattern + { + get + { + if (this.PatternYamlLog != null) + { + return this.PatternYamlLog; + } + + return DefaultWinSWSettings.DefaultLogSettings.Pattern; + } + } + + public override int? Period => this.PeriodYamlLog is null ? 1 : this.PeriodYamlLog; + + public override bool OutFileDisabled + { + get + { + return this.OutFileDisabledYamlLog is null ? + DefaultWinSWSettings.DefaultLogSettings.OutFileDisabled : + (bool)this.OutFileDisabledYamlLog; + } + } + + public override bool ErrFileDisabled + { + get + { + return this.ErrFileDisabledYamlLog is null ? + this.configs.Defaults.ErrFileDisabled : + (bool)this.ErrFileDisabledYamlLog; + } + } + + public override string OutFilePattern + { + get + { + return this.OutFilePatternYamlLog is null ? + DefaultWinSWSettings.DefaultLogSettings.OutFilePattern : + Environment.ExpandEnvironmentVariables(this.OutFilePatternYamlLog); + } + } + + public override string ErrFilePattern + { + get + { + return this.ErrFilePatternYamlLog is null ? + DefaultWinSWSettings.DefaultLogSettings.ErrFilePattern : + Environment.ExpandEnvironmentVariables(this.ErrFilePatternYamlLog); + } + } + + public override string? AutoRollAtTime + { + get + { + return this.AutoRollAtTimeYamlLog is null ? + DefaultWinSWSettings.DefaultLogSettings.AutoRollAtTime : + this.AutoRollAtTimeYamlLog; + } + } + + public override int? ZipOlderThanNumDays + { + get + { + if (this.ZipOlderThanNumDaysYamlLog != null) + { + return this.ZipOlderThanNumDaysYamlLog; + } + + return DefaultWinSWSettings.DefaultLogSettings.ZipOlderThanNumDays; + } + } + + public override string? ZipDateFormat + { + get + { + return this.ZipDateFormatYamlLog is null ? + DefaultWinSWSettings.DefaultLogSettings.ZipDateFormat : + this.ZipDateFormatYamlLog; + } + } + } + + public class YamlDownload + { + [YamlMember(Alias = "from")] + public string FromYamlDownload { get; set; } = string.Empty; + + [YamlMember(Alias = "to")] + public string ToYamlDownload { get; set; } = string.Empty; + + [YamlMember(Alias = "auth")] + public AuthType AuthYamlDownload { get; set; } + + [YamlMember(Alias = "username")] + public string? UsernameYamlDownload { get; set; } + + [YamlMember(Alias = "password")] + public string? PasswordYamlDownload { get; set; } + + [YamlMember(Alias = "unsecureAuth")] + public bool UnsecureAuthYamlDownload { get; set; } + + [YamlMember(Alias = "failOnError")] + public bool FailOnErrorYamlDownload { get; set; } + + [YamlMember(Alias = "proxy")] + public string? ProxyYamlDownload { get; set; } + } + + public class YamlFailureAction + { + [YamlMember(Alias = "type")] + private SC_ACTION_TYPE type; + + [YamlMember(Alias = "delay")] + private TimeSpan delay; + + public SC_ACTION_TYPE Type { get => this.type; set => this.type = value; } + + public TimeSpan Delay { get => this.delay; set => this.delay = value; } + } + + private string? GetArguments(string? args, ArgType type) + { + if (args is null) + { + switch (type) + { + case ArgType.Arg: + return this.Defaults.Arguments; + case ArgType.Startarg: + return this.Defaults.StartArguments; + case ArgType.Stoparg: + return this.Defaults.StopArguments; + default: + return string.Empty; + } + } + + return Environment.ExpandEnvironmentVariables(args); + } + + private enum ArgType + { + Arg = 0, + Startarg = 1, + Stoparg = 2 + } + + private List GetDownloads(List? downloads) + { + if (downloads is null) + { + return this.Defaults.Downloads; + } + + var result = new List(downloads.Count); + + foreach (var item in downloads) + { + result.Add(new Download( + item.FromYamlDownload, + item.ToYamlDownload, + item.FailOnErrorYamlDownload, + item.AuthYamlDownload, + item.UsernameYamlDownload, + item.PasswordYamlDownload, + item.UnsecureAuthYamlDownload, + item.ProxyYamlDownload)); + } + + return result; + } + + public string Id => this.IdYaml is null ? this.Defaults.Id : this.IdYaml; + + public string Description => this.DescriptionYaml is null ? this.Defaults.Description : this.DescriptionYaml; + + public string Executable => this.ExecutableYaml is null ? this.Defaults.Executable : this.ExecutableYaml; + + public string ExecutablePath => this.ExecutablePathYaml is null ? this.Defaults.ExecutablePath : this.ExecutablePathYaml; + + public string Caption => this.CaptionYaml is null ? this.Defaults.Caption : this.CaptionYaml; + + public bool HideWindow => this.HideWindowYaml is null ? this.Defaults.HideWindow : (bool)this.HideWindowYaml; + + public bool StopParentProcessFirst + { + get + { + return this.StopParentProcessFirstYaml is null ? + this.Defaults.StopParentProcessFirst : + (bool)this.StopParentProcessFirstYaml; + } + } + + public StartMode StartMode => this.StartModeYaml is null ? this.Defaults.StartMode : (StartMode)this.StartModeYaml; + + public string Arguments + { + get + { + var args = this.GetArguments(this.ArgumentsYaml, ArgType.Arg); + return args is null ? this.Defaults.Arguments : args; + } + } + + public string? StartArguments => this.GetArguments(this.StartArgumentsYaml, ArgType.Startarg); + + public string? StopArguments => this.GetArguments(this.StopArgumentsYaml, ArgType.Stoparg); + + public string? StopExecutable + { + get + { + return this.StopExecutableYaml is null ? + this.Defaults.StopExecutable : + null; + } + } + + public SC_ACTION[] FailureActions + { + get + { + if (this.YamlFailureActions is null) + { + return new SC_ACTION[0]; + } + + var arr = new List(); + + foreach (var item in this.YamlFailureActions) + { + arr.Add(new SC_ACTION(item.Type, item.Delay)); + } + + return arr.ToArray(); + } + } + + public TimeSpan ResetFailureAfter => this.ResetFailureAfterYaml is null ? + this.Defaults.ResetFailureAfter : + (TimeSpan)this.ResetFailureAfterYaml; + + public string WorkingDirectory => this.WorkingDirectoryYaml is null ? + this.Defaults.WorkingDirectory : + this.WorkingDirectoryYaml; + + public ProcessPriorityClass Priority => this.PriorityYaml is null ? this.Defaults.Priority : (ProcessPriorityClass)this.PriorityYaml; + + public TimeSpan StopTimeout => this.StopTimeoutYaml is null ? this.Defaults.StopTimeout : (TimeSpan)this.StopTimeoutYaml; + + public string[] ServiceDependencies => this.ServiceDependenciesYaml is null ? + this.Defaults.ServiceDependencies : + this.ServiceDependenciesYaml; + + public TimeSpan WaitHint => this.WaitHintYaml is null ? this.Defaults.WaitHint : (TimeSpan)this.WaitHintYaml; + + public TimeSpan SleepTime => this.SleepTimeYaml is null ? this.Defaults.SleepTime : (TimeSpan)this.SleepTimeYaml; + + public bool Interactive => this.InteractiveYaml is null ? this.Defaults.Interactive : (bool)this.InteractiveYaml; + + public List Downloads => this.GetDownloads(this.DownloadsYaml); + + public Dictionary EnvironmentVariables { get; set; } = new Dictionary(); + + public void LoadEnvironmentVariables() + { + if (this.EnvironmentVariablesYaml is null) + { + this.EnvironmentVariables = this.Defaults.EnvironmentVariables; + } + else + { + foreach (var item in this.EnvironmentVariablesYaml) + { + var value = Environment.ExpandEnvironmentVariables(item.Value); + this.EnvironmentVariables[item.Key] = value; + Environment.SetEnvironmentVariable(item.Key, value); + } + } + } + + public ServiceAccount ServiceAccount => this.ServiceAccountYaml is null ? this.Defaults.ServiceAccount : this.ServiceAccountYaml; + + public Log Log => this.YAMLLog is null ? this.Defaults.Log : this.YAMLLog; + + public string LogDirectory => this.Log.Directory; + + public string LogMode => this.Log.Mode is null ? this.Defaults.LogMode : this.Log.Mode; + + // TODO + XmlNode? IWinSWConfiguration.ExtensionsConfiguration => throw new NotImplementedException(); + + public List ExtensionIds => throw new NotImplementedException(); + + public string BaseName => throw new NotImplementedException(); + + public string BasePath => throw new NotImplementedException(); + + public string? ServiceAccountDomain => throw new NotImplementedException(); + + public string? ServiceAccountName => throw new NotImplementedException(); + + public string? SecurityDescriptor => throw new NotImplementedException(); + } +} diff --git a/src/Core/WinSWCore/Extensions/AbstractWinSWExtension.cs b/src/Core/WinSWCore/Extensions/AbstractWinSWExtension.cs index bfc5f4a..a3b4733 100644 --- a/src/Core/WinSWCore/Extensions/AbstractWinSWExtension.cs +++ b/src/Core/WinSWCore/Extensions/AbstractWinSWExtension.cs @@ -1,4 +1,5 @@ using System.Xml; +using WinSW.Configuration; namespace WinSW.Extensions { @@ -10,7 +11,7 @@ namespace WinSW.Extensions public WinSWExtensionDescriptor Descriptor { get; set; } #pragma warning restore CS8618 // Non-nullable field is uninitialized. Consider declaring as nullable. - public virtual void Configure(ServiceDescriptor descriptor, XmlNode node) + public virtual void Configure(IWinSWConfiguration descriptor, XmlNode node) { // Do nothing } diff --git a/src/Core/WinSWCore/Extensions/IWinSWExtension.cs b/src/Core/WinSWCore/Extensions/IWinSWExtension.cs index a7c1f35..d187be8 100644 --- a/src/Core/WinSWCore/Extensions/IWinSWExtension.cs +++ b/src/Core/WinSWCore/Extensions/IWinSWExtension.cs @@ -1,4 +1,5 @@ using System.Xml; +using WinSW.Configuration; namespace WinSW.Extensions { @@ -27,7 +28,7 @@ namespace WinSW.Extensions /// /// Service descriptor /// Configuration node - void Configure(ServiceDescriptor descriptor, XmlNode node); + void Configure(IWinSWConfiguration descriptor, XmlNode node); /// /// Start handler. Called during startup of the service before the child process. diff --git a/src/Core/WinSWCore/Extensions/WinSWExtensionManager.cs b/src/Core/WinSWCore/Extensions/WinSWExtensionManager.cs index 081658c..3b0bf6a 100644 --- a/src/Core/WinSWCore/Extensions/WinSWExtensionManager.cs +++ b/src/Core/WinSWCore/Extensions/WinSWExtensionManager.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Xml; using log4net; +using WinSW.Configuration; namespace WinSW.Extensions { @@ -9,11 +10,11 @@ namespace WinSW.Extensions { public Dictionary Extensions { get; private set; } - public ServiceDescriptor ServiceDescriptor { get; private set; } + public IWinSWConfiguration ServiceDescriptor { get; private set; } private static readonly ILog Log = LogManager.GetLogger(typeof(WinSWExtensionManager)); - public WinSWExtensionManager(ServiceDescriptor serviceDescriptor) + public WinSWExtensionManager(IWinSWConfiguration serviceDescriptor) { this.ServiceDescriptor = serviceDescriptor; this.Extensions = new Dictionary(); diff --git a/src/Core/WinSWCore/ServiceDescriptor.cs b/src/Core/WinSWCore/ServiceDescriptor.cs index e7a41bf..849c7ce 100755 --- a/src/Core/WinSWCore/ServiceDescriptor.cs +++ b/src/Core/WinSWCore/ServiceDescriptor.cs @@ -1,746 +1,762 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Text; -using System.Xml; -using WinSW.Configuration; -using WinSW.Native; -using WinSW.Util; -using WMI; - -namespace WinSW -{ - /// - /// In-memory representation of the configuration file. - /// - public class ServiceDescriptor : IWinSWConfiguration - { - protected readonly XmlDocument dom = new XmlDocument(); - - private readonly Dictionary environmentVariables; - - public static DefaultWinSWSettings Defaults { get; } = new DefaultWinSWSettings(); - - /// - /// Where did we find the configuration file? - /// - /// This string is "c:\abc\def\ghi" when the configuration XML is "c:\abc\def\ghi.xml" - /// - public string BasePath { get; set; } - - /// - /// The file name portion of the configuration file. - /// - /// In the above example, this would be "ghi". - /// - public string BaseName { get; set; } - - // Currently there is no opportunity to alter the executable path - public virtual string ExecutablePath => Defaults.ExecutablePath; - - public ServiceDescriptor() - { - // find co-located configuration xml. We search up to the ancestor directories to simplify debugging, - // as well as trimming off ".vshost" suffix (which is used during debugging) - // Get the first parent to go into the recursive loop - string p = this.ExecutablePath; - string baseName = Path.GetFileNameWithoutExtension(p); - if (baseName.EndsWith(".vshost")) - { - baseName = baseName.Substring(0, baseName.Length - 7); - } - - DirectoryInfo d = new DirectoryInfo(Path.GetDirectoryName(p)); - while (true) - { - if (File.Exists(Path.Combine(d.FullName, baseName + ".xml"))) - { - break; - } - - if (d.Parent is null) - { - throw new FileNotFoundException("Unable to locate " + baseName + ".xml file within executable directory or any parents"); - } - - d = d.Parent; - } - - this.BaseName = baseName; - this.BasePath = Path.Combine(d.FullName, this.BaseName); - - try - { - this.dom.Load(this.BasePath + ".xml"); - } - catch (XmlException e) - { - throw new InvalidDataException(e.Message, e); - } - - // register the base directory as environment variable so that future expansions can refer to this. - Environment.SetEnvironmentVariable("BASE", d.FullName); - - // ditto for ID - Environment.SetEnvironmentVariable("SERVICE_ID", this.Id); - - // New name - Environment.SetEnvironmentVariable(WinSWSystem.EnvVarNameExecutablePath, this.ExecutablePath); - - // Also inject system environment variables - Environment.SetEnvironmentVariable(WinSWSystem.EnvVarNameServiceId, this.Id); - - this.environmentVariables = this.LoadEnvironmentVariables(); - } - - /// - /// Loads descriptor from existing DOM - /// -#pragma warning disable CS8618 // Non-nullable field is uninitialized. Consider declaring as nullable. - public ServiceDescriptor(XmlDocument dom) -#pragma warning restore CS8618 // Non-nullable field is uninitialized. Consider declaring as nullable. - { - this.dom = dom; - - this.environmentVariables = this.LoadEnvironmentVariables(); - } - - public static ServiceDescriptor FromXML(string xml) - { - var dom = new XmlDocument(); - dom.LoadXml(xml); - return new ServiceDescriptor(dom); - } - - private string SingleElement(string tagName) - { - return this.SingleElement(tagName, false)!; - } - - private string? SingleElement(string tagName, bool optional) - { - XmlNode? n = this.dom.SelectSingleNode("//" + tagName); - if (n is null && !optional) - { - throw new InvalidDataException("<" + tagName + "> is missing in configuration XML"); - } - - return n is null ? null : Environment.ExpandEnvironmentVariables(n.InnerText); - } - - private bool SingleBoolElement(string tagName, bool defaultValue) - { - XmlNode? e = this.dom.SelectSingleNode("//" + tagName); - - return e is null ? defaultValue : bool.Parse(e.InnerText); - } - - private int SingleIntElement(XmlNode parent, string tagName, int defaultValue) - { - XmlNode? e = parent.SelectSingleNode(tagName); - - return e is null ? defaultValue : int.Parse(e.InnerText); - } - - private TimeSpan SingleTimeSpanElement(XmlNode parent, string tagName, TimeSpan defaultValue) - { - string? value = this.SingleElement(tagName, true); - return value is null ? defaultValue : this.ParseTimeSpan(value); - } - - private TimeSpan ParseTimeSpan(string v) - { - v = v.Trim(); - foreach (var s in Suffix) - { - if (v.EndsWith(s.Key)) - { - return TimeSpan.FromMilliseconds(int.Parse(v.Substring(0, v.Length - s.Key.Length).Trim()) * s.Value); - } - } - - return TimeSpan.FromMilliseconds(int.Parse(v)); - } - - private static readonly Dictionary Suffix = new Dictionary - { - { "ms", 1 }, - { "sec", 1000L }, - { "secs", 1000L }, - { "min", 1000L * 60L }, - { "mins", 1000L * 60L }, - { "hr", 1000L * 60L * 60L }, - { "hrs", 1000L * 60L * 60L }, - { "hour", 1000L * 60L * 60L }, - { "hours", 1000L * 60L * 60L }, - { "day", 1000L * 60L * 60L * 24L }, - { "days", 1000L * 60L * 60L * 24L } - }; - - /// - /// Path to the executable. - /// - public string Executable => this.SingleElement("executable"); - - public bool HideWindow => this.SingleBoolElement("hidewindow", Defaults.HideWindow); - - /// - /// Optionally specify a different Path to an executable to shutdown the service. - /// - public string? StopExecutable => this.SingleElement("stopexecutable", true); - - /// - /// arguments or multiple optional argument elements which overrule the arguments element. - /// - public string Arguments - { - get - { - string? arguments = this.AppendTags("argument", null); - - if (!(arguments is null)) - { - return arguments; - } - - XmlNode? argumentsNode = this.dom.SelectSingleNode("//arguments"); - - return argumentsNode is null ? Defaults.Arguments : Environment.ExpandEnvironmentVariables(argumentsNode.InnerText); - } - } - - /// - /// startarguments or multiple optional startargument elements. - /// - public string? StartArguments - { - get - { - string? startArguments = this.AppendTags("startargument", null); - - if (!(startArguments is null)) - { - return startArguments; - } - - XmlNode? startArgumentsNode = this.dom.SelectSingleNode("//startarguments"); - - return startArgumentsNode is null ? null : Environment.ExpandEnvironmentVariables(startArgumentsNode.InnerText); - } - } - - /// - /// stoparguments or multiple optional stopargument elements. - /// - public string? StopArguments - { - get - { - string? stopArguments = this.AppendTags("stopargument", null); - - if (!(stopArguments is null)) - { - return stopArguments; - } - - XmlNode? stopArgumentsNode = this.dom.SelectSingleNode("//stoparguments"); - - return stopArgumentsNode is null ? null : Environment.ExpandEnvironmentVariables(stopArgumentsNode.InnerText); - } - } - - public string WorkingDirectory - { - get - { - var wd = this.SingleElement("workingdirectory", true); - return string.IsNullOrEmpty(wd) ? Defaults.WorkingDirectory : wd!; - } - } - - public List ExtensionIds - { - get - { - XmlNode? argumentNode = this.ExtensionsConfiguration; - XmlNodeList? extensions = argumentNode?.SelectNodes("extension"); - if (extensions is null) - { - return new List(0); - } - - List result = new List(extensions.Count); - for (int i = 0; i < extensions.Count; i++) - { - result.Add(XmlHelper.SingleAttribute((XmlElement)extensions[i], "id")); - } - - return result; - } - } - - public XmlNode? ExtensionsConfiguration => this.dom.SelectSingleNode("//extensions"); - - /// - /// Combines the contents of all the elements of the given name, - /// or return null if no element exists. Handles whitespace quotation. - /// - private string? AppendTags(string tagName, string? defaultValue = null) - { - XmlNode? argumentNode = this.dom.SelectSingleNode("//" + tagName); - if (argumentNode is null) - { - return defaultValue; - } - - StringBuilder arguments = new StringBuilder(); - - XmlNodeList argumentNodeList = this.dom.SelectNodes("//" + tagName); - for (int i = 0; i < argumentNodeList.Count; i++) - { - arguments.Append(' '); - - string token = Environment.ExpandEnvironmentVariables(argumentNodeList[i].InnerText); - - if (token.StartsWith("\"") && token.EndsWith("\"")) - { - // for backward compatibility, if the argument is already quoted, leave it as is. - // in earlier versions we didn't handle quotation, so the user might have worked - // around it by themselves - } - else - { - if (token.Contains(" ")) - { - arguments.Append('"').Append(token).Append('"'); - continue; - } - } - - arguments.Append(token); - } - - return arguments.ToString(); - } - - /// - /// LogDirectory is the service wrapper executable directory or the optionally specified logpath element. - /// - public string LogDirectory - { - get - { - XmlNode? loggingNode = this.dom.SelectSingleNode("//logpath"); - - return loggingNode is null - ? Defaults.LogDirectory - : Environment.ExpandEnvironmentVariables(loggingNode.InnerText); - } - } - - public string LogMode - { - get - { - string? mode = null; - - // first, backward compatibility with older configuration - XmlElement? e = (XmlElement?)this.dom.SelectSingleNode("//logmode"); - if (e != null) - { - mode = e.InnerText; - } - else - { - // this is more modern way, to support nested elements as configuration - e = (XmlElement?)this.dom.SelectSingleNode("//log"); - if (e != null) - { - mode = e.GetAttribute("mode"); - } - } - - return mode ?? Defaults.LogMode; - } - } - - public string LogName - { - get - { - XmlNode? loggingName = this.dom.SelectSingleNode("//logname"); - - return loggingName is null ? this.BaseName : Environment.ExpandEnvironmentVariables(loggingName.InnerText); - } - } - - public bool OutFileDisabled => this.SingleBoolElement("outfiledisabled", Defaults.OutFileDisabled); - - public bool ErrFileDisabled => this.SingleBoolElement("errfiledisabled", Defaults.ErrFileDisabled); - - public string OutFilePattern - { - get - { - XmlNode? loggingName = this.dom.SelectSingleNode("//outfilepattern"); - - return loggingName is null ? Defaults.OutFilePattern : Environment.ExpandEnvironmentVariables(loggingName.InnerText); - } - } - - public string ErrFilePattern - { - get - { - XmlNode? loggingName = this.dom.SelectSingleNode("//errfilepattern"); - - return loggingName is null ? Defaults.ErrFilePattern : Environment.ExpandEnvironmentVariables(loggingName.InnerText); - } - } - - public LogHandler LogHandler - { - get - { - XmlElement? e = (XmlElement?)this.dom.SelectSingleNode("//logmode"); - - // this is more modern way, to support nested elements as configuration - e ??= (XmlElement?)this.dom.SelectSingleNode("//log")!; // WARNING: NRE - - int sizeThreshold; - switch (this.LogMode) - { - case "rotate": - return new SizeBasedRollingLogAppender(this.LogDirectory, this.LogName, this.OutFileDisabled, this.ErrFileDisabled, this.OutFilePattern, this.ErrFilePattern); - - case "none": - return new IgnoreLogAppender(); - - case "reset": - return new ResetLogAppender(this.LogDirectory, this.LogName, this.OutFileDisabled, this.ErrFileDisabled, this.OutFilePattern, this.ErrFilePattern); - - case "roll": - return new RollingLogAppender(this.LogDirectory, this.LogName, this.OutFileDisabled, this.ErrFileDisabled, this.OutFilePattern, this.ErrFilePattern); - - case "roll-by-time": - XmlNode? patternNode = e.SelectSingleNode("pattern"); - if (patternNode is null) - { - throw new InvalidDataException("Time Based rolling policy is specified but no pattern can be found in configuration XML."); - } - - var pattern = patternNode.InnerText; - int period = this.SingleIntElement(e, "period", 1); - return new TimeBasedRollingLogAppender(this.LogDirectory, this.LogName, this.OutFileDisabled, this.ErrFileDisabled, this.OutFilePattern, this.ErrFilePattern, pattern, period); - - case "roll-by-size": - sizeThreshold = this.SingleIntElement(e, "sizeThreshold", 10 * 1024) * SizeBasedRollingLogAppender.BytesPerKB; - int keepFiles = this.SingleIntElement(e, "keepFiles", SizeBasedRollingLogAppender.DefaultFilesToKeep); - return new SizeBasedRollingLogAppender(this.LogDirectory, this.LogName, this.OutFileDisabled, this.ErrFileDisabled, this.OutFilePattern, this.ErrFilePattern, sizeThreshold, keepFiles); - - case "append": - return new DefaultLogAppender(this.LogDirectory, this.LogName, this.OutFileDisabled, this.ErrFileDisabled, this.OutFilePattern, this.ErrFilePattern); - - case "roll-by-size-time": - sizeThreshold = this.SingleIntElement(e, "sizeThreshold", 10 * 1024) * RollingSizeTimeLogAppender.BytesPerKB; - XmlNode? filePatternNode = e.SelectSingleNode("pattern"); - if (filePatternNode is null) - { - throw new InvalidDataException("Roll-Size-Time Based rolling policy is specified but no pattern can be found in configuration XML."); - } - - XmlNode? autoRollAtTimeNode = e.SelectSingleNode("autoRollAtTime"); - TimeSpan? autoRollAtTime = null; - if (autoRollAtTimeNode != null) - { - // validate it - if (!TimeSpan.TryParse(autoRollAtTimeNode.InnerText, out TimeSpan autoRollAtTimeValue)) - { - throw new InvalidDataException("Roll-Size-Time Based rolling policy is specified but autoRollAtTime does not match the TimeSpan format HH:mm:ss found in configuration XML."); - } - - autoRollAtTime = autoRollAtTimeValue; - } - - XmlNode? zipolderthannumdaysNode = e.SelectSingleNode("zipOlderThanNumDays"); - int? zipolderthannumdays = null; - if (zipolderthannumdaysNode != null) - { - // validate it - if (!int.TryParse(zipolderthannumdaysNode.InnerText, out int zipolderthannumdaysValue)) - { - throw new InvalidDataException("Roll-Size-Time Based rolling policy is specified but zipOlderThanNumDays does not match the int format found in configuration XML."); - } - - zipolderthannumdays = zipolderthannumdaysValue; - } - - XmlNode? zipdateformatNode = e.SelectSingleNode("zipDateFormat"); - string zipdateformat = zipdateformatNode is null ? "yyyyMM" : zipdateformatNode.InnerText; - - return new RollingSizeTimeLogAppender(this.LogDirectory, this.LogName, this.OutFileDisabled, this.ErrFileDisabled, this.OutFilePattern, this.ErrFilePattern, sizeThreshold, filePatternNode.InnerText, autoRollAtTime, zipolderthannumdays, zipdateformat); - - default: - throw new InvalidDataException("Undefined logging mode: " + this.LogMode); - } - } - } - - /// - /// Optionally specified depend services that must start before this service starts. - /// - public string[] ServiceDependencies - { - get - { - XmlNodeList? nodeList = this.dom.SelectNodes("//depend"); - if (nodeList is null) - { - return Defaults.ServiceDependencies; - } - - string[] serviceDependencies = new string[nodeList.Count]; - for (int i = 0; i < nodeList.Count; i++) - { - serviceDependencies[i] = nodeList[i].InnerText; - } - - return serviceDependencies; - } - } - - public string Id => this.SingleElement("id"); - - public string Caption => this.SingleElement("name"); - - public string Description => this.SingleElement("description"); - - /// - /// Start mode of the Service - /// - public StartMode StartMode - { - get - { - string? p = this.SingleElement("startmode", true); - if (p is null) - { - return Defaults.StartMode; - } - - try - { - return (StartMode)Enum.Parse(typeof(StartMode), p, true); - } - catch - { - Console.WriteLine("Start mode in XML must be one of the following:"); - foreach (string sm in Enum.GetNames(typeof(StartMode))) - { - Console.WriteLine(sm); - } - - throw; - } - } - } - - /// - /// True if the service should be installed with the DelayedAutoStart flag. - /// This setting will be applyed only during the install command and only when the Automatic start mode is configured. - /// - public bool DelayedAutoStart => this.dom.SelectSingleNode("//delayedAutoStart") != null; - - /// - /// True if the service should beep when finished on shutdown. - /// This doesn't work on some OSes. See http://msdn.microsoft.com/en-us/library/ms679277%28VS.85%29.aspx - /// - public bool BeepOnShutdown => this.dom.SelectSingleNode("//beeponshutdown") != null; - - /// - /// The estimated time required for a pending stop operation (default 15 secs). - /// Before the specified amount of time has elapsed, the service should make its next call to the SetServiceStatus function - /// with either an incremented checkPoint value or a change in currentState. (see http://msdn.microsoft.com/en-us/library/ms685996.aspx) - /// - public TimeSpan WaitHint => this.SingleTimeSpanElement(this.dom, "waithint", Defaults.WaitHint); - - /// - /// The time before the service should make its next call to the SetServiceStatus function - /// with an incremented checkPoint value (default 1 sec). - /// Do not wait longer than the wait hint. A good interval is one-tenth of the wait hint but not less than 1 second and not more than 10 seconds. - /// - public TimeSpan SleepTime => this.SingleTimeSpanElement(this.dom, "sleeptime", Defaults.SleepTime); - - /// - /// True if the service can interact with the desktop. - /// - public bool Interactive => this.dom.SelectSingleNode("//interactive") != null; - - /// - /// Environment variable overrides - /// - public Dictionary EnvironmentVariables => new Dictionary(this.environmentVariables); - - /// - /// List of downloads to be performed by the wrapper before starting - /// a service. - /// - public List Downloads - { - get - { - XmlNodeList? nodeList = this.dom.SelectNodes("//download"); - if (nodeList is null) - { - return Defaults.Downloads; - } - - List result = new List(nodeList.Count); - for (int i = 0; i < nodeList.Count; i++) - { - if (nodeList[i] is XmlElement element) - { - result.Add(new Download(element)); - } - } - - return result; - } - } - - public SC_ACTION[] FailureActions - { - get - { - XmlNodeList? childNodes = this.dom.SelectNodes("//onfailure"); - if (childNodes is null) - { - return new SC_ACTION[0]; - } - - SC_ACTION[] result = new SC_ACTION[childNodes.Count]; - for (int i = 0; i < childNodes.Count; i++) - { - XmlNode node = childNodes[i]; - string action = node.Attributes["action"].Value; - SC_ACTION_TYPE type = action switch - { - "restart" => SC_ACTION_TYPE.SC_ACTION_RESTART, - "none" => SC_ACTION_TYPE.SC_ACTION_NONE, - "reboot" => SC_ACTION_TYPE.SC_ACTION_REBOOT, - _ => throw new Exception("Invalid failure action: " + action) - }; - XmlAttribute? delay = node.Attributes["delay"]; - result[i] = new SC_ACTION(type, delay != null ? this.ParseTimeSpan(delay.Value) : TimeSpan.Zero); - } - - return result; - } - } - - public TimeSpan ResetFailureAfter => this.SingleTimeSpanElement(this.dom, "resetfailure", Defaults.ResetFailureAfter); - - protected string? GetServiceAccountPart(string subNodeName) - { - XmlNode? node = this.dom.SelectSingleNode("//serviceaccount"); - - if (node != null) - { - XmlNode? subNode = node.SelectSingleNode(subNodeName); - if (subNode != null) - { - return subNode.InnerText; - } - } - - return null; - } - - protected string? AllowServiceLogon => this.GetServiceAccountPart("allowservicelogon"); - - protected internal string? ServiceAccountDomain => this.GetServiceAccountPart("domain"); - - protected internal string? ServiceAccountName => this.GetServiceAccountPart("user"); - - public string? ServiceAccountPassword => this.GetServiceAccountPart("password"); - - public string? ServiceAccountUser => this.ServiceAccountName is null ? null : (this.ServiceAccountDomain ?? ".") + "\\" + this.ServiceAccountName; - - public bool HasServiceAccount() - { - return !string.IsNullOrEmpty(this.ServiceAccountName); - } - - public bool AllowServiceAcountLogonRight - { - get - { - if (this.AllowServiceLogon != null) - { - if (bool.TryParse(this.AllowServiceLogon, out bool parsedvalue)) - { - return parsedvalue; - } - } - - return false; - } - } - - /// - /// Time to wait for the service to gracefully shutdown the executable before we forcibly kill it - /// - public TimeSpan StopTimeout => this.SingleTimeSpanElement(this.dom, "stoptimeout", Defaults.StopTimeout); - - public bool StopParentProcessFirst - { - get - { - var value = this.SingleElement("stopparentprocessfirst", true); - if (bool.TryParse(value, out bool result)) - { - return result; - } - - return Defaults.StopParentProcessFirst; - } - } - - /// - /// Desired process priority or null if not specified. - /// - public ProcessPriorityClass Priority - { - get - { - string? p = this.SingleElement("priority", true); - if (p is null) - { - return Defaults.Priority; - } - - return (ProcessPriorityClass)Enum.Parse(typeof(ProcessPriorityClass), p, true); - } - } - - public string? SecurityDescriptor => this.SingleElement("securityDescriptor", true); - - private Dictionary LoadEnvironmentVariables() - { - XmlNodeList nodeList = this.dom.SelectNodes("//env"); - Dictionary environment = new Dictionary(nodeList.Count); - for (int i = 0; i < nodeList.Count; i++) - { - XmlNode node = nodeList[i]; - string key = node.Attributes["name"].Value; - string value = Environment.ExpandEnvironmentVariables(node.Attributes["value"].Value); - environment[key] = value; - - Environment.SetEnvironmentVariable(key, value); - } - - return environment; - } - } -} +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Text; +using System.Xml; +using WinSW.Configuration; +using WinSW.Native; +using WinSW.Util; +using WMI; + +namespace WinSW +{ + /// + /// In-memory representation of the configuration file. + /// + public class ServiceDescriptor : IWinSWConfiguration + { +#pragma warning disable S2755 // XML parsers should not be vulnerable to XXE attacks + protected readonly XmlDocument dom = new XmlDocument(); +#pragma warning restore S2755 // XML parsers should not be vulnerable to XXE attacks + + private readonly Dictionary environmentVariables; + + public static DefaultWinSWSettings Defaults { get; } = new DefaultWinSWSettings(); + + /// + /// Where did we find the configuration file? + /// + /// This string is "c:\abc\def\ghi" when the configuration XML is "c:\abc\def\ghi.xml" + /// + public string BasePath { get; set; } + + /// + /// The file name portion of the configuration file. + /// + /// In the above example, this would be "ghi". + /// + public string BaseName { get; set; } + + // Currently there is no opportunity to alter the executable path + public virtual string ExecutablePath => Defaults.ExecutablePath; + + public ServiceDescriptor() + { + // find co-located configuration xml. We search up to the ancestor directories to simplify debugging, + // as well as trimming off ".vshost" suffix (which is used during debugging) + // Get the first parent to go into the recursive loop + string p = this.ExecutablePath; + string baseName = Path.GetFileNameWithoutExtension(p); + if (baseName.EndsWith(".vshost")) + { + baseName = baseName.Substring(0, baseName.Length - 7); + } + + DirectoryInfo d = new DirectoryInfo(Path.GetDirectoryName(p)); + while (true) + { + if (File.Exists(Path.Combine(d.FullName, baseName + ".xml"))) + { + break; + } + + if (d.Parent is null) + { + throw new FileNotFoundException("Unable to locate " + baseName + ".xml file within executable directory or any parents"); + } + + d = d.Parent; + } + + this.BaseName = baseName; + this.BasePath = Path.Combine(d.FullName, this.BaseName); + + try + { + this.dom.Load(this.BasePath + ".xml"); + } + catch (XmlException e) + { + throw new InvalidDataException(e.Message, e); + } + + // register the base directory as environment variable so that future expansions can refer to this. + Environment.SetEnvironmentVariable("BASE", d.FullName); + + // ditto for ID + Environment.SetEnvironmentVariable("SERVICE_ID", this.Id); + + // New name + Environment.SetEnvironmentVariable(WinSWSystem.EnvVarNameExecutablePath, this.ExecutablePath); + + // Also inject system environment variables + Environment.SetEnvironmentVariable(WinSWSystem.EnvVarNameServiceId, this.Id); + + this.environmentVariables = this.LoadEnvironmentVariables(); + } + + /// + /// Loads descriptor from existing DOM + /// +#pragma warning disable CS8618 // Non-nullable field is uninitialized. Consider declaring as nullable. + public ServiceDescriptor(XmlDocument dom) +#pragma warning restore CS8618 // Non-nullable field is uninitialized. Consider declaring as nullable. + { + this.dom = dom; + + this.environmentVariables = this.LoadEnvironmentVariables(); + } + + public static ServiceDescriptor FromXML(string xml) + { +#pragma warning disable S2755 // XML parsers should not be vulnerable to XXE attacks + var xmlDom = new XmlDocument(); +#pragma warning restore S2755 // XML parsers should not be vulnerable to XXE attacks + xmlDom.LoadXml(xml); + return new ServiceDescriptor(xmlDom); + } + + private string SingleElement(string tagName) + { + return this.SingleElement(tagName, false)!; + } + + private string? SingleElement(string tagName, bool optional) + { + XmlNode? n = this.dom.SelectSingleNode("//" + tagName); + if (n is null && !optional) + { + throw new InvalidDataException("<" + tagName + "> is missing in configuration XML"); + } + + return n is null ? null : Environment.ExpandEnvironmentVariables(n.InnerText); + } + + private bool SingleBoolElement(string tagName, bool defaultValue) + { + XmlNode? e = this.dom.SelectSingleNode("//" + tagName); + + return e is null ? defaultValue : bool.Parse(e.InnerText); + } + + private int SingleIntElement(XmlNode parent, string tagName, int defaultValue) + { + XmlNode? e = parent.SelectSingleNode(tagName); + + return e is null ? defaultValue : int.Parse(e.InnerText); + } + + private TimeSpan SingleTimeSpanElement(string tagName, TimeSpan defaultValue) + { + string? value = this.SingleElement(tagName, true); + return value is null ? defaultValue : this.ParseTimeSpan(value); + } + + private TimeSpan ParseTimeSpan(string v) + { + v = v.Trim(); + foreach (var s in Suffix) + { + if (v.EndsWith(s.Key)) + { + return TimeSpan.FromMilliseconds(int.Parse(v.Substring(0, v.Length - s.Key.Length).Trim()) * s.Value); + } + } + + return TimeSpan.FromMilliseconds(int.Parse(v)); + } + + private static readonly Dictionary Suffix = new Dictionary + { + { "ms", 1 }, + { "sec", 1000L }, + { "secs", 1000L }, + { "min", 1000L * 60L }, + { "mins", 1000L * 60L }, + { "hr", 1000L * 60L * 60L }, + { "hrs", 1000L * 60L * 60L }, + { "hour", 1000L * 60L * 60L }, + { "hours", 1000L * 60L * 60L }, + { "day", 1000L * 60L * 60L * 24L }, + { "days", 1000L * 60L * 60L * 24L } + }; + + /// + /// Path to the executable. + /// + public string Executable => this.SingleElement("executable"); + + public bool HideWindow => this.SingleBoolElement("hidewindow", Defaults.HideWindow); + + /// + /// Optionally specify a different Path to an executable to shutdown the service. + /// + public string? StopExecutable => this.SingleElement("stopexecutable", true); + + /// + /// arguments or multiple optional argument elements which overrule the arguments element. + /// + public string Arguments + { + get + { + string? arguments = this.AppendTags("argument", null); + + if (!(arguments is null)) + { + return arguments; + } + + XmlNode? argumentsNode = this.dom.SelectSingleNode("//arguments"); + + return argumentsNode is null ? Defaults.Arguments : Environment.ExpandEnvironmentVariables(argumentsNode.InnerText); + } + } + + /// + /// startarguments or multiple optional startargument elements. + /// + public string? StartArguments + { + get + { + string? startArguments = this.AppendTags("startargument", null); + + if (!(startArguments is null)) + { + return startArguments; + } + + XmlNode? startArgumentsNode = this.dom.SelectSingleNode("//startarguments"); + + return startArgumentsNode is null ? null : Environment.ExpandEnvironmentVariables(startArgumentsNode.InnerText); + } + } + + /// + /// stoparguments or multiple optional stopargument elements. + /// + public string? StopArguments + { + get + { + string? stopArguments = this.AppendTags("stopargument", null); + + if (!(stopArguments is null)) + { + return stopArguments; + } + + XmlNode? stopArgumentsNode = this.dom.SelectSingleNode("//stoparguments"); + + return stopArgumentsNode is null ? null : Environment.ExpandEnvironmentVariables(stopArgumentsNode.InnerText); + } + } + + public string WorkingDirectory + { + get + { + var wd = this.SingleElement("workingdirectory", true); + return string.IsNullOrEmpty(wd) ? Defaults.WorkingDirectory : wd!; + } + } + + public List ExtensionIds + { + get + { + XmlNode? argumentNode = this.ExtensionsConfiguration; + XmlNodeList? extensions = argumentNode?.SelectNodes("extension"); + if (extensions is null) + { + return new List(0); + } + + List result = new List(extensions.Count); + for (int i = 0; i < extensions.Count; i++) + { + result.Add(XmlHelper.SingleAttribute((XmlElement)extensions[i], "id")); + } + + return result; + } + } + + public XmlNode? ExtensionsConfiguration => this.dom.SelectSingleNode("//extensions"); + + /// + /// Combines the contents of all the elements of the given name, + /// or return null if no element exists. Handles whitespace quotation. + /// + private string? AppendTags(string tagName, string? defaultValue = null) + { + XmlNode? argumentNode = this.dom.SelectSingleNode("//" + tagName); + if (argumentNode is null) + { + return defaultValue; + } + + StringBuilder arguments = new StringBuilder(); + + XmlNodeList argumentNodeList = this.dom.SelectNodes("//" + tagName); + for (int i = 0; i < argumentNodeList.Count; i++) + { + arguments.Append(' '); + + string token = Environment.ExpandEnvironmentVariables(argumentNodeList[i].InnerText); + + if (token.StartsWith("\"") && token.EndsWith("\"")) + { + // for backward compatibility, if the argument is already quoted, leave it as is. + // in earlier versions we didn't handle quotation, so the user might have worked + // around it by themselves + } + else + { + if (token.Contains(" ")) + { + arguments.Append('"').Append(token).Append('"'); + continue; + } + } + + arguments.Append(token); + } + + return arguments.ToString(); + } + + /// + /// LogDirectory is the service wrapper executable directory or the optionally specified logpath element. + /// + public string LogDirectory { get => this.Log.Directory; } + + public string LogMode + { + get + { + string? mode = null; + + // first, backward compatibility with older configuration + XmlElement? e = (XmlElement?)this.dom.SelectSingleNode("//logmode"); + if (e != null) + { + mode = e.InnerText; + } + else + { + // this is more modern way, to support nested elements as configuration + e = (XmlElement?)this.dom.SelectSingleNode("//log"); + if (e != null) + { + mode = e.GetAttribute("mode"); + } + } + + return mode ?? Defaults.LogMode; + } + } + + public string LogName + { + get + { + XmlNode? loggingName = this.dom.SelectSingleNode("//logname"); + + return loggingName is null ? this.BaseName : Environment.ExpandEnvironmentVariables(loggingName.InnerText); + } + } + + public Log Log + { + get + { + return new XmlLogSettings(this); + } + } + + private class XmlLogSettings : Log + { + private readonly ServiceDescriptor d; + + public XmlLogSettings(ServiceDescriptor d) + { + this.d = d; + } + + private XmlElement E + { + get + { + XmlElement? e = (XmlElement?)this.d.dom.SelectSingleNode("//logmode"); + + // this is more modern way, to support nested elements as configuration + e ??= (XmlElement?)this.d.dom.SelectSingleNode("//log")!; // WARNING: NRE + return e; + } + } + + public override string? Mode { get => this.d.LogMode; } + + public override string Name { get => this.d.LogName; } + + public override string Directory + { + get + { + XmlNode? loggingNode = this.d.dom.SelectSingleNode("//logpath"); + + return loggingNode is null + ? Defaults.LogDirectory + : Environment.ExpandEnvironmentVariables(loggingNode.InnerText); + } + } + + public override int? SizeThreshold { get => this.d.SingleIntElement(this.E, "sizeThreshold", 10 * 1024); } + + public override int? KeepFiles { get => this.d.SingleIntElement(this.E, "keepFiles", SizeBasedRollingLogAppender.DefaultFilesToKeep); } + + public override int? Period { get => this.d.SingleIntElement(this.E, "period", 1); } + + public override string Pattern + { + get + { + XmlNode? patternNode = this.E.SelectSingleNode("pattern"); + if (patternNode is null) + { +#pragma warning disable S2372 // Exceptions should not be thrown from property getters + throw new InvalidDataException("Time Based rolling policy is specified but no pattern can be found in configuration XML."); +#pragma warning restore S2372 // Exceptions should not be thrown from property getters + } + + return patternNode.InnerText; + } + } + + public override bool OutFileDisabled => this.d.SingleBoolElement("outfiledisabled", Defaults.OutFileDisabled); + + public override bool ErrFileDisabled => this.d.SingleBoolElement("errfiledisabled", Defaults.ErrFileDisabled); + + public override string OutFilePattern + { + get + { + XmlNode? loggingName = this.d.dom.SelectSingleNode("//outfilepattern"); + + return loggingName is null ? Defaults.OutFilePattern : Environment.ExpandEnvironmentVariables(loggingName.InnerText); + } + } + + public override string ErrFilePattern + { + get + { + XmlNode? loggingName = this.d.dom.SelectSingleNode("//errfilepattern"); + + return loggingName is null ? Defaults.ErrFilePattern : Environment.ExpandEnvironmentVariables(loggingName.InnerText); + } + } + + public override string? AutoRollAtTime + { + get + { + XmlNode? autoRollAtTimeNode = this.E.SelectSingleNode("autoRollAtTime"); + return autoRollAtTimeNode?.InnerText; + } + } + + public override int? ZipOlderThanNumDays + { + get + { + XmlNode? zipolderthannumdaysNode = this.E.SelectSingleNode("zipOlderThanNumDays"); + int? zipolderthannumdays = null; + if (zipolderthannumdaysNode != null) + { + // validate it + if (!int.TryParse(zipolderthannumdaysNode.InnerText, out int zipolderthannumdaysValue)) + { +#pragma warning disable S2372 // Exceptions should not be thrown from property getters + throw new InvalidDataException("Roll-Size-Time Based rolling policy is specified but zipOlderThanNumDays does not match the int format found in configuration XML."); +#pragma warning restore S2372 // Exceptions should not be thrown from property getters + } + + zipolderthannumdays = zipolderthannumdaysValue; + } + + return zipolderthannumdays; + } + } + + public override string? ZipDateFormat + { + get + { + XmlNode? zipdateformatNode = this.E.SelectSingleNode("zipDateFormat"); + return zipdateformatNode is null ? null : zipdateformatNode.InnerText; + } + } + } + + /// + /// Optionally specified depend services that must start before this service starts. + /// + public string[] ServiceDependencies + { + get + { + XmlNodeList? nodeList = this.dom.SelectNodes("//depend"); + if (nodeList is null) + { + return Defaults.ServiceDependencies; + } + + string[] serviceDependencies = new string[nodeList.Count]; + for (int i = 0; i < nodeList.Count; i++) + { + serviceDependencies[i] = nodeList[i].InnerText; + } + + return serviceDependencies; + } + } + + public string Id => this.SingleElement("id"); + + public string Caption => this.SingleElement("name"); + + public string Description => this.SingleElement("description"); + + /// + /// Start mode of the Service + /// + public StartMode StartMode + { + get + { + string? p = this.SingleElement("startmode", true); + if (p is null) + { + return Defaults.StartMode; + } + + try + { + return (StartMode)Enum.Parse(typeof(StartMode), p, true); + } + catch + { + Console.WriteLine("Start mode in XML must be one of the following:"); + foreach (string sm in Enum.GetNames(typeof(StartMode))) + { + Console.WriteLine(sm); + } + + throw; + } + } + } + + /// + /// True if the service should be installed with the DelayedAutoStart flag. + /// This setting will be applyed only during the install command and only when the Automatic start mode is configured. + /// + public bool DelayedAutoStart => this.dom.SelectSingleNode("//delayedAutoStart") != null; + + /// + /// True if the service should beep when finished on shutdown. + /// This doesn't work on some OSes. See http://msdn.microsoft.com/en-us/library/ms679277%28VS.85%29.aspx + /// + public bool BeepOnShutdown => this.dom.SelectSingleNode("//beeponshutdown") != null; + + /// + /// The estimated time required for a pending stop operation (default 15 secs). + /// Before the specified amount of time has elapsed, the service should make its next call to the SetServiceStatus function + /// with either an incremented checkPoint value or a change in currentState. (see http://msdn.microsoft.com/en-us/library/ms685996.aspx) + /// + public TimeSpan WaitHint => this.SingleTimeSpanElement("waithint", Defaults.WaitHint); + + /// + /// The time before the service should make its next call to the SetServiceStatus function + /// with an incremented checkPoint value (default 1 sec). + /// Do not wait longer than the wait hint. A good interval is one-tenth of the wait hint but not less than 1 second and not more than 10 seconds. + /// + public TimeSpan SleepTime => this.SingleTimeSpanElement("sleeptime", Defaults.SleepTime); + + /// + /// True if the service can interact with the desktop. + /// + public bool Interactive => this.dom.SelectSingleNode("//interactive") != null; + + /// + /// Environment variable overrides + /// + public Dictionary EnvironmentVariables => new Dictionary(this.environmentVariables); + + /// + /// List of downloads to be performed by the wrapper before starting + /// a service. + /// + public List Downloads + { + get + { + XmlNodeList? nodeList = this.dom.SelectNodes("//download"); + if (nodeList is null) + { + return Defaults.Downloads; + } + + List result = new List(nodeList.Count); + for (int i = 0; i < nodeList.Count; i++) + { + if (nodeList[i] is XmlElement element) + { + result.Add(new Download(element)); + } + } + + return result; + } + } + + public SC_ACTION[] FailureActions + { + get + { + XmlNodeList? childNodes = this.dom.SelectNodes("//onfailure"); + if (childNodes is null) + { + return new SC_ACTION[0]; + } + + SC_ACTION[] result = new SC_ACTION[childNodes.Count]; + for (int i = 0; i < childNodes.Count; i++) + { + XmlNode node = childNodes[i]; + string action = node.Attributes["action"].Value; + SC_ACTION_TYPE type = action switch + { + "restart" => SC_ACTION_TYPE.SC_ACTION_RESTART, + "none" => SC_ACTION_TYPE.SC_ACTION_NONE, + "reboot" => SC_ACTION_TYPE.SC_ACTION_REBOOT, + _ => throw new Exception("Invalid failure action: " + action) + }; + XmlAttribute? delay = node.Attributes["delay"]; + result[i] = new SC_ACTION(type, delay != null ? this.ParseTimeSpan(delay.Value) : TimeSpan.Zero); + } + + return result; + } + } + + public TimeSpan ResetFailureAfter => this.SingleTimeSpanElement("resetfailure", Defaults.ResetFailureAfter); + + protected string? GetServiceAccountPart(XmlNode node, string subNodeName) + { + XmlNode? subNode = node.SelectSingleNode(subNodeName); + if (subNode != null) + { + return subNode.InnerText; + } + + return null; + } + + private bool ParseAllowServiceAcountLogonRight(string? logonRight) + { + if (logonRight != null && bool.TryParse(logonRight, out bool parsedvalue)) + { + return parsedvalue; + } + + return false; + } + + public ServiceAccount ServiceAccount + { + get + { + XmlNode? node = this.dom.SelectSingleNode("//serviceaccount"); + + if (node is null) + { + return Defaults.ServiceAccount; + } + + var serviceAccount = Defaults.ServiceAccount; + + serviceAccount.ServiceAccountDomain = this.GetServiceAccountPart(node, "domain"); + + serviceAccount.ServiceAccountName = this.GetServiceAccountPart(node, "user"); + + serviceAccount.ServiceAccountPassword = this.GetServiceAccountPart(node, "password"); + + var loginRight = this.GetServiceAccountPart(node, "allowservicelogon"); + + serviceAccount.AllowServiceAcountLogonRight = this.ParseAllowServiceAcountLogonRight(loginRight); + + return serviceAccount; + } + } + + /// + /// Time to wait for the service to gracefully shutdown the executable before we forcibly kill it + /// + public TimeSpan StopTimeout => this.SingleTimeSpanElement("stoptimeout", Defaults.StopTimeout); + + public bool StopParentProcessFirst + { + get + { + var value = this.SingleElement("stopparentprocessfirst", true); + if (bool.TryParse(value, out bool result)) + { + return result; + } + + return Defaults.StopParentProcessFirst; + } + } + + /// + /// Desired process priority or null if not specified. + /// + public ProcessPriorityClass Priority + { + get + { + string? p = this.SingleElement("priority", true); + if (p is null) + { + return Defaults.Priority; + } + + return (ProcessPriorityClass)Enum.Parse(typeof(ProcessPriorityClass), p, true); + } + } + + public string? SecurityDescriptor => this.SingleElement("securityDescriptor", true); + + private Dictionary LoadEnvironmentVariables() + { + XmlNodeList nodeList = this.dom.SelectNodes("//env"); + Dictionary environment = new Dictionary(nodeList.Count); + for (int i = 0; i < nodeList.Count; i++) + { + XmlNode node = nodeList[i]; + string key = node.Attributes["name"].Value; + string value = Environment.ExpandEnvironmentVariables(node.Attributes["value"].Value); + environment[key] = value; + + Environment.SetEnvironmentVariable(key, value); + } + + return environment; + } + } +} diff --git a/src/Core/WinSWCore/ServiceDescriptorYaml.cs b/src/Core/WinSWCore/ServiceDescriptorYaml.cs new file mode 100644 index 0000000..aa47d5e --- /dev/null +++ b/src/Core/WinSWCore/ServiceDescriptorYaml.cs @@ -0,0 +1,82 @@ +using System; +using System.IO; +using WinSW.Configuration; +using YamlDotNet.Serialization; + +namespace WinSW +{ + public class ServiceDescriptorYaml + { + public readonly YamlConfiguration Configurations = new YamlConfiguration(); + + public static DefaultWinSWSettings Defaults { get; } = new DefaultWinSWSettings(); + + public string BasePath { get; set; } + + public virtual string ExecutablePath => Defaults.ExecutablePath; + + public ServiceDescriptorYaml() + { + string p = this.ExecutablePath; + string baseName = Path.GetFileNameWithoutExtension(p); + if (baseName.EndsWith(".vshost")) + { + baseName = baseName.Substring(0, baseName.Length - 7); + } + + DirectoryInfo d = new DirectoryInfo(Path.GetDirectoryName(p)); + while (true) + { + if (File.Exists(Path.Combine(d.FullName, baseName + ".yml"))) + { + break; + } + + if (d.Parent is null) + { + throw new FileNotFoundException("Unable to locate " + baseName + ".yml file within executable directory or any parents"); + } + + d = d.Parent; + } + + this.BasePath = Path.Combine(d.FullName, baseName); + + using (var reader = new StreamReader(this.BasePath + ".yml")) + { + var file = reader.ReadToEnd(); + var deserializer = new DeserializerBuilder().Build(); + + this.Configurations = deserializer.Deserialize(file); + } + + Environment.SetEnvironmentVariable("BASE", d.FullName); + + // ditto for ID + Environment.SetEnvironmentVariable("SERVICE_ID", this.Configurations.Id); + + // New name + Environment.SetEnvironmentVariable(WinSWSystem.EnvVarNameExecutablePath, this.ExecutablePath); + + // Also inject system environment variables + Environment.SetEnvironmentVariable(WinSWSystem.EnvVarNameServiceId, this.Configurations.Id); + + this.Configurations.LoadEnvironmentVariables(); + } + +#pragma warning disable CS8618 // Non-nullable field is uninitialized. Consider declaring as nullable. + public ServiceDescriptorYaml(YamlConfiguration configs) +#pragma warning restore CS8618 // Non-nullable field is uninitialized. Consider declaring as nullable. + { + this.Configurations = configs; + this.Configurations.LoadEnvironmentVariables(); + } + + public static ServiceDescriptorYaml FromYaml(string yaml) + { + var deserializer = new DeserializerBuilder().Build(); + var configs = deserializer.Deserialize(yaml); + return new ServiceDescriptorYaml(configs); + } + } +} diff --git a/src/Core/WinSWCore/WinSWCore.csproj b/src/Core/WinSWCore/WinSWCore.csproj index 4ace542..a3beb21 100644 --- a/src/Core/WinSWCore/WinSWCore.csproj +++ b/src/Core/WinSWCore/WinSWCore.csproj @@ -11,6 +11,7 @@ + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/src/NuGet.Config b/src/NuGet.Config new file mode 100644 index 0000000..765346e --- /dev/null +++ b/src/NuGet.Config @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/Plugins/RunawayProcessKiller/RunawayProcessKillerExtension.cs b/src/Plugins/RunawayProcessKiller/RunawayProcessKillerExtension.cs index d3196e4..3fa342f 100644 --- a/src/Plugins/RunawayProcessKiller/RunawayProcessKillerExtension.cs +++ b/src/Plugins/RunawayProcessKiller/RunawayProcessKillerExtension.cs @@ -5,6 +5,7 @@ using System.IO; using System.Text; using System.Xml; using log4net; +using WinSW.Configuration; using WinSW.Extensions; using WinSW.Util; using static WinSW.Plugins.RunawayProcessKiller.RunawayProcessKillerExtension.NativeMethods; @@ -179,7 +180,7 @@ namespace WinSW.Plugins.RunawayProcessKiller return parameters.Environment; } - public override void Configure(ServiceDescriptor descriptor, XmlNode node) + public override void Configure(IWinSWConfiguration descriptor, XmlNode node) { // We expect the upper logic to process any errors // TODO: a better parser API for types would be useful diff --git a/src/Plugins/SharedDirectoryMapper/SharedDirectoryMapper.cs b/src/Plugins/SharedDirectoryMapper/SharedDirectoryMapper.cs index 4fb45f3..4813ebb 100644 --- a/src/Plugins/SharedDirectoryMapper/SharedDirectoryMapper.cs +++ b/src/Plugins/SharedDirectoryMapper/SharedDirectoryMapper.cs @@ -1,9 +1,11 @@ using System.Collections.Generic; using System.Xml; using log4net; +using WinSW.Configuration; using WinSW.Extensions; using WinSW.Util; + namespace WinSW.Plugins.SharedDirectoryMapper { public class SharedDirectoryMapper : AbstractWinSWExtension @@ -25,7 +27,7 @@ namespace WinSW.Plugins.SharedDirectoryMapper this._entries.Add(config); } - public override void Configure(ServiceDescriptor descriptor, XmlNode node) + public override void Configure(IWinSWConfiguration descriptor, XmlNode node) { XmlNodeList? mapNodes = XmlHelper.SingleNode(node, "mapping", false)!.SelectNodes("map"); if (mapNodes != null) diff --git a/src/Test/winswTests/ServiceDescriptorTests.cs b/src/Test/winswTests/ServiceDescriptorTests.cs index 77580f9..c4ca8c8 100644 --- a/src/Test/winswTests/ServiceDescriptorTests.cs +++ b/src/Test/winswTests/ServiceDescriptorTests.cs @@ -1,445 +1,445 @@ -using System; -using System.Diagnostics; -using NUnit.Framework; -using WinSW; -using winswTests.Util; -using WMI; - -namespace winswTests -{ - [TestFixture] - public class ServiceDescriptorTests - { - private ServiceDescriptor _extendedServiceDescriptor; - - private const string ExpectedWorkingDirectory = @"Z:\Path\SubPath"; - private const string Username = "User"; - private const string Password = "Password"; - private const string Domain = "Domain"; - private const string AllowServiceAccountLogonRight = "true"; - - [SetUp] - public void SetUp() - { - string seedXml = -$@" - service.exe - Service - The service. - node.exe - My Arguments - - - {Domain} - {Username} - {Password} - {AllowServiceAccountLogonRight} - - {ExpectedWorkingDirectory} - C:\logs -"; - this._extendedServiceDescriptor = ServiceDescriptor.FromXML(seedXml); - } - - [Test] - public void DefaultStartMode() - { - Assert.That(this._extendedServiceDescriptor.StartMode, Is.EqualTo(StartMode.Automatic)); - } - - [Test] - public void IncorrectStartMode() - { - string seedXml = -$@" - service.exe - Service - The service. - node.exe - My Arguments - roll - - - {Domain} - {Username} - {Password} - {AllowServiceAccountLogonRight} - - {ExpectedWorkingDirectory} - C:\logs -"; - - this._extendedServiceDescriptor = ServiceDescriptor.FromXML(seedXml); - Assert.That(() => this._extendedServiceDescriptor.StartMode, Throws.ArgumentException); - } - - [Test] - public void ChangedStartMode() - { - string seedXml = -$@" - service.exe - Service - The service. - node.exe - My Arguments - manual - - - {Domain} - {Username} - {Password} - {AllowServiceAccountLogonRight} - - {ExpectedWorkingDirectory} - C:\logs -"; - - this._extendedServiceDescriptor = ServiceDescriptor.FromXML(seedXml); - Assert.That(this._extendedServiceDescriptor.StartMode, Is.EqualTo(StartMode.Manual)); - } - - [Test] - public void VerifyWorkingDirectory() - { - Debug.WriteLine("_extendedServiceDescriptor.WorkingDirectory :: " + this._extendedServiceDescriptor.WorkingDirectory); - Assert.That(this._extendedServiceDescriptor.WorkingDirectory, Is.EqualTo(ExpectedWorkingDirectory)); - } - - [Test] - public void VerifyServiceLogonRight() - { - Assert.That(this._extendedServiceDescriptor.AllowServiceAcountLogonRight, Is.True); - } - - [Test] - public void VerifyUsername() - { - Debug.WriteLine("_extendedServiceDescriptor.WorkingDirectory :: " + this._extendedServiceDescriptor.WorkingDirectory); - Assert.That(this._extendedServiceDescriptor.ServiceAccountUser, Is.EqualTo(Domain + "\\" + Username)); - } - - [Test] - public void VerifyPassword() - { - Debug.WriteLine("_extendedServiceDescriptor.WorkingDirectory :: " + this._extendedServiceDescriptor.WorkingDirectory); - Assert.That(this._extendedServiceDescriptor.ServiceAccountPassword, Is.EqualTo(Password)); - } - - [Test] - public void Priority() - { - var sd = ServiceDescriptor.FromXML("testnormal"); - Assert.That(sd.Priority, Is.EqualTo(ProcessPriorityClass.Normal)); - - sd = ServiceDescriptor.FromXML("testidle"); - Assert.That(sd.Priority, Is.EqualTo(ProcessPriorityClass.Idle)); - - sd = ServiceDescriptor.FromXML("test"); - Assert.That(sd.Priority, Is.EqualTo(ProcessPriorityClass.Normal)); - } - - [Test] - public void StopParentProcessFirstIsFalseByDefault() - { - Assert.That(this._extendedServiceDescriptor.StopParentProcessFirst, Is.False); - } - - [Test] - public void CanParseStopParentProcessFirst() - { - const string seedXml = "" - + "true" - + ""; - var serviceDescriptor = ServiceDescriptor.FromXML(seedXml); - - Assert.That(serviceDescriptor.StopParentProcessFirst, Is.True); - } - - [Test] - public void CanParseStopTimeout() - { - const string seedXml = "" - + "60sec" - + ""; - var serviceDescriptor = ServiceDescriptor.FromXML(seedXml); - - Assert.That(serviceDescriptor.StopTimeout, Is.EqualTo(TimeSpan.FromSeconds(60))); - } - - [Test] - public void CanParseStopTimeoutFromMinutes() - { - const string seedXml = "" - + "10min" - + ""; - var serviceDescriptor = ServiceDescriptor.FromXML(seedXml); - - Assert.That(serviceDescriptor.StopTimeout, Is.EqualTo(TimeSpan.FromMinutes(10))); - } - - [Test] - public void CanParseLogname() - { - const string seedXml = "" - + "MyTestApp" - + ""; - var serviceDescriptor = ServiceDescriptor.FromXML(seedXml); - - Assert.That(serviceDescriptor.LogName, Is.EqualTo("MyTestApp")); - } - - [Test] - public void CanParseOutfileDisabled() - { - const string seedXml = "" - + "true" - + ""; - var serviceDescriptor = ServiceDescriptor.FromXML(seedXml); - - Assert.That(serviceDescriptor.OutFileDisabled, Is.True); - } - - [Test] - public void CanParseErrfileDisabled() - { - const string seedXml = "" - + "true" - + ""; - var serviceDescriptor = ServiceDescriptor.FromXML(seedXml); - - Assert.That(serviceDescriptor.ErrFileDisabled, Is.True); - } - - [Test] - public void CanParseOutfilePattern() - { - const string seedXml = "" - + ".out.test.log" - + ""; - var serviceDescriptor = ServiceDescriptor.FromXML(seedXml); - - Assert.That(serviceDescriptor.OutFilePattern, Is.EqualTo(".out.test.log")); - } - - [Test] - public void CanParseErrfilePattern() - { - const string seedXml = "" - + ".err.test.log" - + ""; - var serviceDescriptor = ServiceDescriptor.FromXML(seedXml); - - Assert.That(serviceDescriptor.ErrFilePattern, Is.EqualTo(".err.test.log")); - } - - [Test] - public void LogModeRollBySize() - { - const string seedXml = "" - + "c:\\" - + "" - + "112" - + "113" - + "" - + ""; - - var serviceDescriptor = ServiceDescriptor.FromXML(seedXml); - serviceDescriptor.BaseName = "service"; - - var logHandler = serviceDescriptor.LogHandler as SizeBasedRollingLogAppender; - Assert.That(logHandler, Is.Not.Null); - Assert.That(logHandler.SizeTheshold, Is.EqualTo(112 * 1024)); - Assert.That(logHandler.FilesToKeep, Is.EqualTo(113)); - } - - [Test] - public void LogModeRollByTime() - { - const string seedXml = "" - + "c:\\" - + "" - + "7" - + "log pattern" - + "" - + ""; - - var serviceDescriptor = ServiceDescriptor.FromXML(seedXml); - serviceDescriptor.BaseName = "service"; - - var logHandler = serviceDescriptor.LogHandler as TimeBasedRollingLogAppender; - Assert.That(logHandler, Is.Not.Null); - Assert.That(logHandler.Period, Is.EqualTo(7)); - Assert.That(logHandler.Pattern, Is.EqualTo("log pattern")); - } - - [Test] - public void LogModeRollBySizeTime() - { - const string seedXml = "" - + "c:\\" - + "" - + "10240" - + "yyyy-MM-dd" - + "00:00:00" - + "" - + ""; - - var serviceDescriptor = ServiceDescriptor.FromXML(seedXml); - serviceDescriptor.BaseName = "service"; - - var logHandler = serviceDescriptor.LogHandler as RollingSizeTimeLogAppender; - Assert.That(logHandler, Is.Not.Null); - Assert.That(logHandler.SizeTheshold, Is.EqualTo(10240 * 1024)); - Assert.That(logHandler.FilePattern, Is.EqualTo("yyyy-MM-dd")); - Assert.That(logHandler.AutoRollAtTime, Is.EqualTo((TimeSpan?)new TimeSpan(0, 0, 0))); - } - - [Test] - public void VerifyServiceLogonRightGraceful() - { - const string seedXml = "" - + "" - + "" + Domain + "" - + "" + Username + "" - + "" + Password + "" - + "true1" - + "" - + ""; - var serviceDescriptor = ServiceDescriptor.FromXML(seedXml); - Assert.That(serviceDescriptor.AllowServiceAcountLogonRight, Is.False); - } - - [Test] - public void VerifyServiceLogonRightOmitted() - { - const string seedXml = "" - + "" - + "" + Domain + "" - + "" + Username + "" - + "" + Password + "" - + "" - + ""; - var serviceDescriptor = ServiceDescriptor.FromXML(seedXml); - Assert.That(serviceDescriptor.AllowServiceAcountLogonRight, Is.False); - } - - [Test] - public void VerifyWaitHint_FullXML() - { - var sd = ConfigXmlBuilder.create() - .WithTag("waithint", "20 min") - .ToServiceDescriptor(true); - Assert.That(sd.WaitHint, Is.EqualTo(TimeSpan.FromMinutes(20))); - } - - /// - /// Test for https://github.com/kohsuke/winsw/issues/159 - /// - [Test] - public void VerifyWaitHint_XMLWithoutVersion() - { - var sd = ConfigXmlBuilder.create(printXMLVersion: false) - .WithTag("waithint", "21 min") - .ToServiceDescriptor(true); - Assert.That(sd.WaitHint, Is.EqualTo(TimeSpan.FromMinutes(21))); - } - - [Test] - public void VerifyWaitHint_XMLWithoutComment() - { - var sd = ConfigXmlBuilder.create(xmlComment: null) - .WithTag("waithint", "22 min") - .ToServiceDescriptor(true); - Assert.That(sd.WaitHint, Is.EqualTo(TimeSpan.FromMinutes(22))); - } - - [Test] - public void VerifyWaitHint_XMLWithoutVersionAndComment() - { - var sd = ConfigXmlBuilder.create(xmlComment: null, printXMLVersion: false) - .WithTag("waithint", "23 min") - .ToServiceDescriptor(true); - Assert.That(sd.WaitHint, Is.EqualTo(TimeSpan.FromMinutes(23))); - } - - [Test] - public void VerifySleepTime() - { - var sd = ConfigXmlBuilder.create().WithTag("sleeptime", "3 hrs").ToServiceDescriptor(true); - Assert.That(sd.SleepTime, Is.EqualTo(TimeSpan.FromHours(3))); - } - - [Test] - public void VerifyResetFailureAfter() - { - var sd = ConfigXmlBuilder.create().WithTag("resetfailure", "75 sec").ToServiceDescriptor(true); - Assert.That(sd.ResetFailureAfter, Is.EqualTo(TimeSpan.FromSeconds(75))); - } - - [Test] - public void VerifyStopTimeout() - { - var sd = ConfigXmlBuilder.create().WithTag("stoptimeout", "35 secs").ToServiceDescriptor(true); - Assert.That(sd.StopTimeout, Is.EqualTo(TimeSpan.FromSeconds(35))); - } - - /// - /// https://github.com/kohsuke/winsw/issues/178 - /// - [Test] - public void Arguments_LegacyParam() - { - var sd = ConfigXmlBuilder.create().WithTag("arguments", "arg").ToServiceDescriptor(true); - Assert.That(sd.Arguments, Is.EqualTo("arg")); - } - - [Test] - public void Arguments_NewParam_Single() - { - var sd = ConfigXmlBuilder.create() - .WithTag("argument", "--arg1=2") - .ToServiceDescriptor(true); - Assert.That(sd.Arguments, Is.EqualTo(" --arg1=2")); - } - - [Test] - public void Arguments_NewParam_MultipleArgs() - { - var sd = ConfigXmlBuilder.create() - .WithTag("argument", "--arg1=2") - .WithTag("argument", "--arg2=123") - .WithTag("argument", "--arg3=null") - .ToServiceDescriptor(true); - Assert.That(sd.Arguments, Is.EqualTo(" --arg1=2 --arg2=123 --arg3=null")); - } - - /// - /// Ensures that the new single-argument field has a higher priority. - /// - [Test] - public void Arguments_Bothparam_Priorities() - { - var sd = ConfigXmlBuilder.create() - .WithTag("arguments", "--arg1=2 --arg2=3") - .WithTag("argument", "--arg2=123") - .WithTag("argument", "--arg3=null") - .ToServiceDescriptor(true); - Assert.That(sd.Arguments, Is.EqualTo(" --arg2=123 --arg3=null")); - } - - [TestCase(true)] - [TestCase(false)] - public void DelayedStart_RoundTrip(bool enabled) - { - var bldr = ConfigXmlBuilder.create(); - if (enabled) - { - bldr = bldr.WithDelayedAutoStart(); - } - - var sd = bldr.ToServiceDescriptor(); - Assert.That(sd.DelayedAutoStart, Is.EqualTo(enabled)); - } - } -} +using System; +using System.Diagnostics; +using NUnit.Framework; +using WinSW; +using winswTests.Util; +using WMI; + +namespace winswTests +{ + [TestFixture] + public class ServiceDescriptorTests + { + private ServiceDescriptor _extendedServiceDescriptor; + + private const string ExpectedWorkingDirectory = @"Z:\Path\SubPath"; + private const string Username = "User"; + private const string Password = "Password"; + private const string Domain = "Domain"; + private const string AllowServiceAccountLogonRight = "true"; + + [SetUp] + public void SetUp() + { + string seedXml = +$@" + service.exe + Service + The service. + node.exe + My Arguments + + + {Domain} + {Username} + {Password} + {AllowServiceAccountLogonRight} + + {ExpectedWorkingDirectory} + C:\logs +"; + this._extendedServiceDescriptor = ServiceDescriptor.FromXML(seedXml); + } + + [Test] + public void DefaultStartMode() + { + Assert.That(this._extendedServiceDescriptor.StartMode, Is.EqualTo(StartMode.Automatic)); + } + + [Test] + public void IncorrectStartMode() + { + string seedXml = +$@" + service.exe + Service + The service. + node.exe + My Arguments + roll + + + {Domain} + {Username} + {Password} + {AllowServiceAccountLogonRight} + + {ExpectedWorkingDirectory} + C:\logs +"; + + this._extendedServiceDescriptor = ServiceDescriptor.FromXML(seedXml); + Assert.That(() => this._extendedServiceDescriptor.StartMode, Throws.ArgumentException); + } + + [Test] + public void ChangedStartMode() + { + string seedXml = +$@" + service.exe + Service + The service. + node.exe + My Arguments + manual + + + {Domain} + {Username} + {Password} + {AllowServiceAccountLogonRight} + + {ExpectedWorkingDirectory} + C:\logs +"; + + this._extendedServiceDescriptor = ServiceDescriptor.FromXML(seedXml); + Assert.That(this._extendedServiceDescriptor.StartMode, Is.EqualTo(StartMode.Manual)); + } + + [Test] + public void VerifyWorkingDirectory() + { + Debug.WriteLine("_extendedServiceDescriptor.WorkingDirectory :: " + this._extendedServiceDescriptor.WorkingDirectory); + Assert.That(this._extendedServiceDescriptor.WorkingDirectory, Is.EqualTo(ExpectedWorkingDirectory)); + } + + [Test] + public void VerifyServiceLogonRight() + { + Assert.That(_extendedServiceDescriptor.ServiceAccount.AllowServiceAcountLogonRight, Is.True); + } + + [Test] + public void VerifyUsername() + { + Debug.WriteLine("_extendedServiceDescriptor.WorkingDirectory :: " + _extendedServiceDescriptor.WorkingDirectory); + Assert.That(_extendedServiceDescriptor.ServiceAccount.ServiceAccountUser, Is.EqualTo(Domain + "\\" + Username)); + } + + [Test] + public void VerifyPassword() + { + Debug.WriteLine("_extendedServiceDescriptor.WorkingDirectory :: " + _extendedServiceDescriptor.WorkingDirectory); + Assert.That(_extendedServiceDescriptor.ServiceAccount.ServiceAccountPassword, Is.EqualTo(Password)); + } + + [Test] + public void Priority() + { + var sd = ServiceDescriptor.FromXML("testnormal"); + Assert.That(sd.Priority, Is.EqualTo(ProcessPriorityClass.Normal)); + + sd = ServiceDescriptor.FromXML("testidle"); + Assert.That(sd.Priority, Is.EqualTo(ProcessPriorityClass.Idle)); + + sd = ServiceDescriptor.FromXML("test"); + Assert.That(sd.Priority, Is.EqualTo(ProcessPriorityClass.Normal)); + } + + [Test] + public void StopParentProcessFirstIsFalseByDefault() + { + Assert.That(this._extendedServiceDescriptor.StopParentProcessFirst, Is.False); + } + + [Test] + public void CanParseStopParentProcessFirst() + { + const string seedXml = "" + + "true" + + ""; + var serviceDescriptor = ServiceDescriptor.FromXML(seedXml); + + Assert.That(serviceDescriptor.StopParentProcessFirst, Is.True); + } + + [Test] + public void CanParseStopTimeout() + { + const string seedXml = "" + + "60sec" + + ""; + var serviceDescriptor = ServiceDescriptor.FromXML(seedXml); + + Assert.That(serviceDescriptor.StopTimeout, Is.EqualTo(TimeSpan.FromSeconds(60))); + } + + [Test] + public void CanParseStopTimeoutFromMinutes() + { + const string seedXml = "" + + "10min" + + ""; + var serviceDescriptor = ServiceDescriptor.FromXML(seedXml); + + Assert.That(serviceDescriptor.StopTimeout, Is.EqualTo(TimeSpan.FromMinutes(10))); + } + + [Test] + public void CanParseLogname() + { + const string seedXml = "" + + "MyTestApp" + + ""; + var serviceDescriptor = ServiceDescriptor.FromXML(seedXml); + + Assert.That(serviceDescriptor.LogName, Is.EqualTo("MyTestApp")); + } + + [Test] + public void CanParseOutfileDisabled() + { + const string seedXml = "" + + "true" + + ""; + var serviceDescriptor = ServiceDescriptor.FromXML(seedXml); + + Assert.That(serviceDescriptor.Log.OutFileDisabled, Is.True); + } + + [Test] + public void CanParseErrfileDisabled() + { + const string seedXml = "" + + "true" + + ""; + var serviceDescriptor = ServiceDescriptor.FromXML(seedXml); + + Assert.That(serviceDescriptor.Log.ErrFileDisabled, Is.True); + } + + [Test] + public void CanParseOutfilePattern() + { + const string seedXml = "" + + ".out.test.log" + + ""; + var serviceDescriptor = ServiceDescriptor.FromXML(seedXml); + + Assert.That(serviceDescriptor.Log.OutFilePattern, Is.EqualTo(".out.test.log")); + } + + [Test] + public void CanParseErrfilePattern() + { + const string seedXml = "" + + ".err.test.log" + + ""; + var serviceDescriptor = ServiceDescriptor.FromXML(seedXml); + + Assert.That(serviceDescriptor.Log.ErrFilePattern, Is.EqualTo(".err.test.log")); + } + + [Test] + public void LogModeRollBySize() + { + const string seedXml = "" + + "c:\\" + + "" + + "112" + + "113" + + "" + + ""; + + var serviceDescriptor = ServiceDescriptor.FromXML(seedXml); + serviceDescriptor.BaseName = "service"; + + var logHandler = serviceDescriptor.Log.CreateLogHandler() as SizeBasedRollingLogAppender; + Assert.That(logHandler, Is.Not.Null); + Assert.That(logHandler.SizeTheshold, Is.EqualTo(112 * 1024)); + Assert.That(logHandler.FilesToKeep, Is.EqualTo(113)); + } + + [Test] + public void LogModeRollByTime() + { + const string seedXml = "" + + "c:\\" + + "" + + "7" + + "log pattern" + + "" + + ""; + + var serviceDescriptor = ServiceDescriptor.FromXML(seedXml); + serviceDescriptor.BaseName = "service"; + + var logHandler = serviceDescriptor.Log.CreateLogHandler() as TimeBasedRollingLogAppender; + Assert.That(logHandler, Is.Not.Null); + Assert.That(logHandler.Period, Is.EqualTo(7)); + Assert.That(logHandler.Pattern, Is.EqualTo("log pattern")); + } + + [Test] + public void LogModeRollBySizeTime() + { + const string seedXml = "" + + "c:\\" + + "" + + "10240" + + "yyyy-MM-dd" + + "00:00:00" + + "" + + ""; + + var serviceDescriptor = ServiceDescriptor.FromXML(seedXml); + serviceDescriptor.BaseName = "service"; + + var logHandler = serviceDescriptor.Log.CreateLogHandler() as RollingSizeTimeLogAppender; + Assert.That(logHandler, Is.Not.Null); + Assert.That(logHandler.SizeTheshold, Is.EqualTo(10240 * 1024)); + Assert.That(logHandler.FilePattern, Is.EqualTo("yyyy-MM-dd")); + Assert.That(logHandler.AutoRollAtTime, Is.EqualTo((TimeSpan?)new TimeSpan(0, 0, 0))); + } + + [Test] + public void VerifyServiceLogonRightGraceful() + { + const string seedXml = "" + + "" + + "" + Domain + "" + + "" + Username + "" + + "" + Password + "" + + "true1" + + "" + + ""; + var serviceDescriptor = ServiceDescriptor.FromXML(seedXml); + Assert.That(serviceDescriptor.ServiceAccount.AllowServiceAcountLogonRight, Is.False); + } + + [Test] + public void VerifyServiceLogonRightOmitted() + { + const string seedXml = "" + + "" + + "" + Domain + "" + + "" + Username + "" + + "" + Password + "" + + "" + + ""; + var serviceDescriptor = ServiceDescriptor.FromXML(seedXml); + Assert.That(serviceDescriptor.ServiceAccount.AllowServiceAcountLogonRight, Is.False); + } + + [Test] + public void VerifyWaitHint_FullXML() + { + var sd = ConfigXmlBuilder.create() + .WithTag("waithint", "20 min") + .ToServiceDescriptor(true); + Assert.That(sd.WaitHint, Is.EqualTo(TimeSpan.FromMinutes(20))); + } + + /// + /// Test for https://github.com/kohsuke/winsw/issues/159 + /// + [Test] + public void VerifyWaitHint_XMLWithoutVersion() + { + var sd = ConfigXmlBuilder.create(printXMLVersion: false) + .WithTag("waithint", "21 min") + .ToServiceDescriptor(true); + Assert.That(sd.WaitHint, Is.EqualTo(TimeSpan.FromMinutes(21))); + } + + [Test] + public void VerifyWaitHint_XMLWithoutComment() + { + var sd = ConfigXmlBuilder.create(xmlComment: null) + .WithTag("waithint", "22 min") + .ToServiceDescriptor(true); + Assert.That(sd.WaitHint, Is.EqualTo(TimeSpan.FromMinutes(22))); + } + + [Test] + public void VerifyWaitHint_XMLWithoutVersionAndComment() + { + var sd = ConfigXmlBuilder.create(xmlComment: null, printXMLVersion: false) + .WithTag("waithint", "23 min") + .ToServiceDescriptor(true); + Assert.That(sd.WaitHint, Is.EqualTo(TimeSpan.FromMinutes(23))); + } + + [Test] + public void VerifySleepTime() + { + var sd = ConfigXmlBuilder.create().WithTag("sleeptime", "3 hrs").ToServiceDescriptor(true); + Assert.That(sd.SleepTime, Is.EqualTo(TimeSpan.FromHours(3))); + } + + [Test] + public void VerifyResetFailureAfter() + { + var sd = ConfigXmlBuilder.create().WithTag("resetfailure", "75 sec").ToServiceDescriptor(true); + Assert.That(sd.ResetFailureAfter, Is.EqualTo(TimeSpan.FromSeconds(75))); + } + + [Test] + public void VerifyStopTimeout() + { + var sd = ConfigXmlBuilder.create().WithTag("stoptimeout", "35 secs").ToServiceDescriptor(true); + Assert.That(sd.StopTimeout, Is.EqualTo(TimeSpan.FromSeconds(35))); + } + + /// + /// https://github.com/kohsuke/winsw/issues/178 + /// + [Test] + public void Arguments_LegacyParam() + { + var sd = ConfigXmlBuilder.create().WithTag("arguments", "arg").ToServiceDescriptor(true); + Assert.That(sd.Arguments, Is.EqualTo("arg")); + } + + [Test] + public void Arguments_NewParam_Single() + { + var sd = ConfigXmlBuilder.create() + .WithTag("argument", "--arg1=2") + .ToServiceDescriptor(true); + Assert.That(sd.Arguments, Is.EqualTo(" --arg1=2")); + } + + [Test] + public void Arguments_NewParam_MultipleArgs() + { + var sd = ConfigXmlBuilder.create() + .WithTag("argument", "--arg1=2") + .WithTag("argument", "--arg2=123") + .WithTag("argument", "--arg3=null") + .ToServiceDescriptor(true); + Assert.That(sd.Arguments, Is.EqualTo(" --arg1=2 --arg2=123 --arg3=null")); + } + + /// + /// Ensures that the new single-argument field has a higher priority. + /// + [Test] + public void Arguments_Bothparam_Priorities() + { + var sd = ConfigXmlBuilder.create() + .WithTag("arguments", "--arg1=2 --arg2=3") + .WithTag("argument", "--arg2=123") + .WithTag("argument", "--arg3=null") + .ToServiceDescriptor(true); + Assert.That(sd.Arguments, Is.EqualTo(" --arg2=123 --arg3=null")); + } + + [TestCase(true)] + [TestCase(false)] + public void DelayedStart_RoundTrip(bool enabled) + { + var bldr = ConfigXmlBuilder.create(); + if (enabled) + { + bldr = bldr.WithDelayedAutoStart(); + } + + var sd = bldr.ToServiceDescriptor(); + Assert.That(sd.DelayedAutoStart, Is.EqualTo(enabled)); + } + } +} diff --git a/src/Test/winswTests/ServiceDescriptorYamlTest.cs b/src/Test/winswTests/ServiceDescriptorYamlTest.cs new file mode 100644 index 0000000..b442ebc --- /dev/null +++ b/src/Test/winswTests/ServiceDescriptorYamlTest.cs @@ -0,0 +1,119 @@ +using System; +using NUnit.Framework; +using WinSW; + +namespace winswTests +{ + class ServiceDescriptorYamlTest + { + + private string MinimalYaml = @"id: myapp +caption: This is a test +executable: 'C:\Program Files\Java\jdk1.8.0_241\bin\java.exe' +description: This is test winsw"; + + + [Test] + public void Simple_yaml_parsing_test() + { + var configs = ServiceDescriptorYaml.FromYaml(MinimalYaml).Configurations; + + Assert.AreEqual("myapp", configs.Id); + Assert.AreEqual("This is a test", configs.Caption); + Assert.AreEqual("C:\\Program Files\\Java\\jdk1.8.0_241\\bin\\java.exe", configs.Executable); + Assert.AreEqual("This is test winsw", configs.Description); + } + + [Test] + public void Must_implemented_value_test() + { + string yml = @"caption: This is a test +executable: 'C:\Program Files\Java\jdk1.8.0_241\bin\java.exe' +description: This is test winsw"; + + void getId() + { + var id = ServiceDescriptorYaml.FromYaml(yml).Configurations.Id; + } + + Assert.That(() => getId(), Throws.TypeOf()); + } + + [Test] + public void Default_value_map_test() + { + var executablePath = ServiceDescriptorYaml.FromYaml(MinimalYaml).Configurations.ExecutablePath; + + Assert.IsNotNull(executablePath); + } + + [Test] + public void Simple_download_parsing_test() + { + var yml = @"download: + - + from: www.sample.com + to: c://tmp + - + from: www.sample2.com + to: d://tmp + - + from: www.sample3.com + to: d://temp"; + + var configs = ServiceDescriptorYaml.FromYaml(yml).Configurations; + + Assert.AreEqual(3, configs.Downloads.Count); + } + + [Test] + public void Download_not_specified_test() + { + var yml = @"id: jenkins +name: No Service Account +"; + + var configs = ServiceDescriptorYaml.FromYaml(yml).Configurations; + + Assert.DoesNotThrow(() => + { + var dowloads = configs.Downloads; + }); + } + + [Test] + public void Service_account_not_specified_test() + { + var yml = @"id: jenkins +name: No Service Account +"; + + var configs = ServiceDescriptorYaml.FromYaml(yml).Configurations; + + Assert.DoesNotThrow(() => + { + var serviceAccount = configs.ServiceAccount.AllowServiceAcountLogonRight; + }); + } + + [Test] + public void Service_account_specified_but_fields_not_specified() + { + var yml = @"id: jenkins +name: No Service Account +serviceaccount: + user: testuser +"; + + var configs = ServiceDescriptorYaml.FromYaml(yml).Configurations; + + Assert.DoesNotThrow(() => + { + var user = configs.ServiceAccount.ServiceAccountUser; + var password = configs.ServiceAccount.ServiceAccountPassword; + var allowLogon = configs.ServiceAccount.AllowServiceAcountLogonRight; + var hasAccount = configs.ServiceAccount.HasServiceAccount(); + }); + } + } +} diff --git a/src/Test/winswTests/Util/ServiceDescriptorAssert.cs b/src/Test/winswTests/Util/ServiceDescriptorAssert.cs index 37b1a11..ed0ece3 100644 --- a/src/Test/winswTests/Util/ServiceDescriptorAssert.cs +++ b/src/Test/winswTests/Util/ServiceDescriptorAssert.cs @@ -59,6 +59,10 @@ namespace winswTests.Util properties.Remove("Caption"); properties.Remove("Description"); properties.Remove("Executable"); + properties.Remove("BaseName"); + properties.Remove("BasePath"); + properties.Remove("Log"); + properties.Remove("ServiceAccount"); return properties; } }