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 f150de13b0.

* 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<Download>

* 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 <o.v.nenashev@gmail.com>
pull/590/head
Buddhika Chathuranga 2020-07-21 22:27:27 +05:30 committed by GitHub
parent ae10abe857
commit 10d3a6113f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 2213 additions and 1230 deletions

View File

@ -18,9 +18,9 @@ using log4net.Appender;
using log4net.Config; using log4net.Config;
using log4net.Core; using log4net.Core;
using log4net.Layout; using log4net.Layout;
using WinSW.Configuration;
using WinSW.Logging; using WinSW.Logging;
using WinSW.Native; using WinSW.Native;
using WinSW.Util;
using WMI; using WMI;
using ServiceType = WMI.ServiceType; 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; bool inConsoleMode = argsArray.Length > 0;
@ -181,7 +181,9 @@ namespace WinSW
default: default:
Console.WriteLine("Unknown command: " + args[0]); Console.WriteLine("Unknown command: " + args[0]);
PrintAvailableCommands(); PrintAvailableCommands();
#pragma warning disable S112 // General exceptions should never be thrown
throw new Exception("Unknown command: " + args[0]); throw new Exception("Unknown command: " + args[0]);
#pragma warning restore S112 // General exceptions should never be thrown
} }
void Install() void Install()
@ -199,7 +201,9 @@ namespace WinSW
{ {
Console.WriteLine("Service with id '" + descriptor.Id + "' already exists"); 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"); 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"); throw new Exception("Installation failure: Service with id '" + descriptor.Id + "' already exists");
#pragma warning restore S112 // General exceptions should never be thrown
} }
string? username = null; string? username = null;
@ -222,17 +226,17 @@ namespace WinSW
} }
else else
{ {
if (descriptor.HasServiceAccount()) if (descriptor.ServiceAccount.HasServiceAccount())
{ {
username = descriptor.ServiceAccountUser; username = descriptor.ServiceAccount.ServiceAccountUser;
password = descriptor.ServiceAccountPassword; password = descriptor.ServiceAccount.ServiceAccountPassword;
allowServiceLogonRight = descriptor.AllowServiceAcountLogonRight; allowServiceLogonRight = descriptor.ServiceAccount.AllowServiceAcountLogonRight;
} }
} }
if (allowServiceLogonRight) if (allowServiceLogonRight)
{ {
Security.AddServiceLogonRight(descriptor.ServiceAccountDomain!, descriptor.ServiceAccountName!); Security.AddServiceLogonRight(descriptor.ServiceAccount.ServiceAccountDomain!, descriptor.ServiceAccount.ServiceAccountName!);
} }
svcs.Create( 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 + "'"); 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 _); 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) if (!result)
{ {
#pragma warning disable S112 // General exceptions should never be thrown
throw new Exception("Failed to invoke restart: " + Marshal.GetLastWin32Error()); throw new Exception("Failed to invoke restart: " + Marshal.GetLastWin32Error());
#pragma warning restore S112 // General exceptions should never be thrown
} }
} }
void Status() void Status()
{ {
Log.Debug("User requested the status of the process with id '" + descriptor.Id + "'"); 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"); Console.WriteLine(svc is null ? "NonExistent" : svc.Started ? "Started" : "Stopped");
#pragma warning restore S3358 // Ternary operators should not be nested
} }
void Test() void Test()
@ -532,7 +540,7 @@ namespace WinSW
[DoesNotReturn] [DoesNotReturn]
private static void ThrowNoSuchService() => throw new WmiException(ReturnValue.NoSuchService); 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 // TODO: Make logging levels configurable
Level fileLogLevel = Level.Debug; Level fileLogLevel = Level.Debug;

View File

@ -10,6 +10,7 @@ using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
#endif #endif
using log4net; using log4net;
using WinSW.Configuration;
using WinSW.Extensions; using WinSW.Extensions;
using WinSW.Logging; using WinSW.Logging;
using WinSW.Native; using WinSW.Native;
@ -22,7 +23,9 @@ namespace WinSW
private ServiceApis.SERVICE_STATUS wrapperServiceStatus; private ServiceApis.SERVICE_STATUS wrapperServiceStatus;
private readonly Process process = new Process(); private readonly Process process = new Process();
private readonly ServiceDescriptor descriptor;
private readonly IWinSWConfiguration descriptor;
private Dictionary<string, string>? envs; private Dictionary<string, string>? envs;
internal WinSWExtensionManager ExtensionManager { get; private set; } internal WinSWExtensionManager ExtensionManager { get; private set; }
@ -55,7 +58,7 @@ namespace WinSW
/// </summary> /// </summary>
public bool IsShuttingDown => this.systemShuttingdown; public bool IsShuttingDown => this.systemShuttingdown;
public WrapperService(ServiceDescriptor descriptor) public WrapperService(IWinSWConfiguration descriptor)
{ {
this.descriptor = descriptor; this.descriptor = descriptor;
this.ServiceName = this.descriptor.Id; this.ServiceName = this.descriptor.Id;
@ -131,14 +134,15 @@ namespace WinSW
/// <returns>Log Handler, which should be used for the spawned process</returns> /// <returns>Log Handler, which should be used for the spawned process</returns>
private LogHandler CreateExecutableLogHandler() private LogHandler CreateExecutableLogHandler()
{ {
string logDirectory = this.descriptor.LogDirectory; string? logDirectory = this.descriptor.LogDirectory;
if (!Directory.Exists(logDirectory)) if (!Directory.Exists(logDirectory))
{ {
Directory.CreateDirectory(logDirectory); Directory.CreateDirectory(logDirectory);
} }
LogHandler logAppender = this.descriptor.LogHandler; LogHandler logAppender = this.descriptor.Log.CreateLogHandler();
logAppender.EventLogger = this; logAppender.EventLogger = this;
return logAppender; return logAppender;
} }

View File

@ -12,6 +12,8 @@ namespace WinSW.Configuration
/// </summary> /// </summary>
public sealed class DefaultWinSWSettings : IWinSWConfiguration 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 Id => throw new InvalidOperationException(nameof(this.Id) + " must be specified.");
public string Caption => throw new InvalidOperationException(nameof(this.Caption) + " 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; public string ExecutablePath => Process.GetCurrentProcess().MainModule.FileName;
// Installation // Installation
public bool AllowServiceAcountLogonRight => false;
public string? ServiceAccountPassword => null;
public string? ServiceAccountUser => null;
public Native.SC_ACTION[] FailureActions => new Native.SC_ACTION[0]; public Native.SC_ACTION[] FailureActions => new Native.SC_ACTION[0];
public TimeSpan ResetFailureAfter => TimeSpan.FromDays(1); public TimeSpan ResetFailureAfter => TimeSpan.FromDays(1);
@ -66,17 +62,67 @@ namespace WinSW.Configuration
public bool Interactive => false; public bool Interactive => false;
// Logging // 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 // Environment
public List<Download> Downloads => new List<Download>(0); public List<Download> Downloads => new List<Download>(0);
@ -88,5 +134,32 @@ namespace WinSW.Configuration
// Extensions // Extensions
public XmlNode? ExtensionsConfiguration => null; 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<string> ExtensionIds => new List<string>(0);
public string? SecurityDescriptor => null;
} }
} }

View File

@ -22,12 +22,6 @@ namespace WinSW.Configuration
bool HideWindow { get; } bool HideWindow { get; }
// Installation // Installation
bool AllowServiceAcountLogonRight { get; }
string? ServiceAccountPassword { get; }
string? ServiceAccountUser { get; }
Native.SC_ACTION[] FailureActions { get; } Native.SC_ACTION[] FailureActions { get; }
TimeSpan ResetFailureAfter { get; } TimeSpan ResetFailureAfter { get; }
@ -60,12 +54,17 @@ namespace WinSW.Configuration
bool Interactive { get; } bool Interactive { get; }
// Logging /// <summary>
/// Destination for logging.
/// If undefined, a default one should be used.
/// </summary>
string LogDirectory { get; } string LogDirectory { get; }
// TODO: replace by enum // TODO: replace by enum
string LogMode { get; } string LogMode { get; }
Log Log { get; }
// Environment // Environment
List<Download> Downloads { get; } List<Download> Downloads { get; }
@ -76,5 +75,19 @@ namespace WinSW.Configuration
// Extensions // Extensions
XmlNode? ExtensionsConfiguration { get; } XmlNode? ExtensionsConfiguration { get; }
// IWinSWConfiguration Support
List<string> ExtensionIds { get; }
// Service Account
ServiceAccount ServiceAccount { get; }
string BaseName { get; }
string BasePath { get; }
bool DelayedAutoStart { get; }
string? SecurityDescriptor { get; }
} }
} }

View File

@ -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);
}
}
}
}

View File

@ -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);
}
}
}

View File

@ -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<YamlDownload>? 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<string, string>? EnvironmentVariablesYaml { get; set; }
[YamlMember(Alias = "failureActions")]
public List<YamlFailureAction>? 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<Download> GetDownloads(List<YamlDownload>? downloads)
{
if (downloads is null)
{
return this.Defaults.Downloads;
}
var result = new List<Download>(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<SC_ACTION>();
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<Download> Downloads => this.GetDownloads(this.DownloadsYaml);
public Dictionary<string, string> EnvironmentVariables { get; set; } = new Dictionary<string, string>();
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<string> 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();
}
}

View File

@ -1,4 +1,5 @@
using System.Xml; using System.Xml;
using WinSW.Configuration;
namespace WinSW.Extensions namespace WinSW.Extensions
{ {
@ -10,7 +11,7 @@ namespace WinSW.Extensions
public WinSWExtensionDescriptor Descriptor { get; set; } public WinSWExtensionDescriptor Descriptor { get; set; }
#pragma warning restore CS8618 // Non-nullable field is uninitialized. Consider declaring as nullable. #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 // Do nothing
} }

View File

@ -1,4 +1,5 @@
using System.Xml; using System.Xml;
using WinSW.Configuration;
namespace WinSW.Extensions namespace WinSW.Extensions
{ {
@ -27,7 +28,7 @@ namespace WinSW.Extensions
/// </summary> /// </summary>
/// <param name="descriptor">Service descriptor</param> /// <param name="descriptor">Service descriptor</param>
/// <param name="node">Configuration node</param> /// <param name="node">Configuration node</param>
void Configure(ServiceDescriptor descriptor, XmlNode node); void Configure(IWinSWConfiguration descriptor, XmlNode node);
/// <summary> /// <summary>
/// Start handler. Called during startup of the service before the child process. /// Start handler. Called during startup of the service before the child process.

View File

@ -2,6 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Xml; using System.Xml;
using log4net; using log4net;
using WinSW.Configuration;
namespace WinSW.Extensions namespace WinSW.Extensions
{ {
@ -9,11 +10,11 @@ namespace WinSW.Extensions
{ {
public Dictionary<string, IWinSWExtension> Extensions { get; private set; } public Dictionary<string, IWinSWExtension> 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)); private static readonly ILog Log = LogManager.GetLogger(typeof(WinSWExtensionManager));
public WinSWExtensionManager(ServiceDescriptor serviceDescriptor) public WinSWExtensionManager(IWinSWConfiguration serviceDescriptor)
{ {
this.ServiceDescriptor = serviceDescriptor; this.ServiceDescriptor = serviceDescriptor;
this.Extensions = new Dictionary<string, IWinSWExtension>(); this.Extensions = new Dictionary<string, IWinSWExtension>();

View File

@ -16,7 +16,9 @@ namespace WinSW
/// </summary> /// </summary>
public class ServiceDescriptor : IWinSWConfiguration public class ServiceDescriptor : IWinSWConfiguration
{ {
#pragma warning disable S2755 // XML parsers should not be vulnerable to XXE attacks
protected readonly XmlDocument dom = new XmlDocument(); protected readonly XmlDocument dom = new XmlDocument();
#pragma warning restore S2755 // XML parsers should not be vulnerable to XXE attacks
private readonly Dictionary<string, string> environmentVariables; private readonly Dictionary<string, string> environmentVariables;
@ -108,9 +110,11 @@ namespace WinSW
public static ServiceDescriptor FromXML(string xml) public static ServiceDescriptor FromXML(string xml)
{ {
var dom = new XmlDocument(); #pragma warning disable S2755 // XML parsers should not be vulnerable to XXE attacks
dom.LoadXml(xml); var xmlDom = new XmlDocument();
return new ServiceDescriptor(dom); #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) private string SingleElement(string tagName)
@ -143,7 +147,7 @@ namespace WinSW
return e is null ? defaultValue : int.Parse(e.InnerText); return e is null ? defaultValue : int.Parse(e.InnerText);
} }
private TimeSpan SingleTimeSpanElement(XmlNode parent, string tagName, TimeSpan defaultValue) private TimeSpan SingleTimeSpanElement(string tagName, TimeSpan defaultValue)
{ {
string? value = this.SingleElement(tagName, true); string? value = this.SingleElement(tagName, true);
return value is null ? defaultValue : this.ParseTimeSpan(value); return value is null ? defaultValue : this.ParseTimeSpan(value);
@ -327,17 +331,7 @@ namespace WinSW
/// <summary> /// <summary>
/// LogDirectory is the service wrapper executable directory or the optionally specified logpath element. /// LogDirectory is the service wrapper executable directory or the optionally specified logpath element.
/// </summary> /// </summary>
public string LogDirectory public string LogDirectory { get => this.Log.Directory; }
{
get
{
XmlNode? loggingNode = this.dom.SelectSingleNode("//logpath");
return loggingNode is null
? Defaults.LogDirectory
: Environment.ExpandEnvironmentVariables(loggingNode.InnerText);
}
}
public string LogMode public string LogMode
{ {
@ -375,114 +369,135 @@ namespace WinSW
} }
} }
public bool OutFileDisabled => this.SingleBoolElement("outfiledisabled", Defaults.OutFileDisabled); public Log Log
public bool ErrFileDisabled => this.SingleBoolElement("errfiledisabled", Defaults.ErrFileDisabled);
public string OutFilePattern
{ {
get get
{ {
XmlNode? loggingName = this.dom.SelectSingleNode("//outfilepattern"); 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); return loggingName is null ? Defaults.OutFilePattern : Environment.ExpandEnvironmentVariables(loggingName.InnerText);
} }
} }
public string ErrFilePattern public override string ErrFilePattern
{ {
get get
{ {
XmlNode? loggingName = this.dom.SelectSingleNode("//errfilepattern"); XmlNode? loggingName = this.d.dom.SelectSingleNode("//errfilepattern");
return loggingName is null ? Defaults.ErrFilePattern : Environment.ExpandEnvironmentVariables(loggingName.InnerText); return loggingName is null ? Defaults.ErrFilePattern : Environment.ExpandEnvironmentVariables(loggingName.InnerText);
} }
} }
public LogHandler LogHandler public override string? AutoRollAtTime
{ {
get get
{ {
XmlElement? e = (XmlElement?)this.dom.SelectSingleNode("//logmode"); XmlNode? autoRollAtTimeNode = this.E.SelectSingleNode("autoRollAtTime");
return autoRollAtTimeNode?.InnerText;
// 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; public override int? ZipOlderThanNumDays
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."); get
}
XmlNode? autoRollAtTimeNode = e.SelectSingleNode("autoRollAtTime");
TimeSpan? autoRollAtTime = null;
if (autoRollAtTimeNode != null)
{ {
// validate it XmlNode? zipolderthannumdaysNode = this.E.SelectSingleNode("zipOlderThanNumDays");
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; int? zipolderthannumdays = null;
if (zipolderthannumdaysNode != null) if (zipolderthannumdaysNode != null)
{ {
// validate it // validate it
if (!int.TryParse(zipolderthannumdaysNode.InnerText, out int zipolderthannumdaysValue)) 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."); 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; zipolderthannumdays = zipolderthannumdaysValue;
} }
XmlNode? zipdateformatNode = e.SelectSingleNode("zipDateFormat"); return zipolderthannumdays;
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); public override string? ZipDateFormat
{
default: get
throw new InvalidDataException("Undefined logging mode: " + this.LogMode); {
XmlNode? zipdateformatNode = this.E.SelectSingleNode("zipDateFormat");
return zipdateformatNode is null ? null : zipdateformatNode.InnerText;
} }
} }
} }
@ -563,14 +578,14 @@ namespace WinSW
/// Before the specified amount of time has elapsed, the service should make its next call to the SetServiceStatus function /// 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) /// with either an incremented checkPoint value or a change in currentState. (see http://msdn.microsoft.com/en-us/library/ms685996.aspx)
/// </summary> /// </summary>
public TimeSpan WaitHint => this.SingleTimeSpanElement(this.dom, "waithint", Defaults.WaitHint); public TimeSpan WaitHint => this.SingleTimeSpanElement("waithint", Defaults.WaitHint);
/// <summary> /// <summary>
/// The time before the service should make its next call to the SetServiceStatus function /// The time before the service should make its next call to the SetServiceStatus function
/// with an incremented checkPoint value (default 1 sec). /// 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. /// 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.
/// </summary> /// </summary>
public TimeSpan SleepTime => this.SingleTimeSpanElement(this.dom, "sleeptime", Defaults.SleepTime); public TimeSpan SleepTime => this.SingleTimeSpanElement("sleeptime", Defaults.SleepTime);
/// <summary> /// <summary>
/// True if the service can interact with the desktop. /// True if the service can interact with the desktop.
@ -639,59 +654,60 @@ namespace WinSW
} }
} }
public TimeSpan ResetFailureAfter => this.SingleTimeSpanElement(this.dom, "resetfailure", Defaults.ResetFailureAfter); public TimeSpan ResetFailureAfter => this.SingleTimeSpanElement("resetfailure", Defaults.ResetFailureAfter);
protected string? GetServiceAccountPart(string subNodeName) protected string? GetServiceAccountPart(XmlNode node, string subNodeName)
{
XmlNode? node = this.dom.SelectSingleNode("//serviceaccount");
if (node != null)
{ {
XmlNode? subNode = node.SelectSingleNode(subNodeName); XmlNode? subNode = node.SelectSingleNode(subNodeName);
if (subNode != null) if (subNode != null)
{ {
return subNode.InnerText; return subNode.InnerText;
} }
}
return null; return null;
} }
protected string? AllowServiceLogon => this.GetServiceAccountPart("allowservicelogon"); private bool ParseAllowServiceAcountLogonRight(string? logonRight)
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); if (logonRight != null && bool.TryParse(logonRight, out bool parsedvalue))
}
public bool AllowServiceAcountLogonRight
{
get
{
if (this.AllowServiceLogon != null)
{
if (bool.TryParse(this.AllowServiceLogon, out bool parsedvalue))
{ {
return parsedvalue; return parsedvalue;
} }
}
return false; 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;
}
} }
/// <summary> /// <summary>
/// Time to wait for the service to gracefully shutdown the executable before we forcibly kill it /// Time to wait for the service to gracefully shutdown the executable before we forcibly kill it
/// </summary> /// </summary>
public TimeSpan StopTimeout => this.SingleTimeSpanElement(this.dom, "stoptimeout", Defaults.StopTimeout); public TimeSpan StopTimeout => this.SingleTimeSpanElement("stoptimeout", Defaults.StopTimeout);
public bool StopParentProcessFirst public bool StopParentProcessFirst
{ {

View File

@ -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<YamlConfiguration>(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<YamlConfiguration>(yaml);
return new ServiceDescriptorYaml(configs);
}
}
}

View File

@ -11,6 +11,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="log4net" Version="2.0.8" /> <PackageReference Include="log4net" Version="2.0.8" />
<PackageReference Include="YamlDotNet" Version="8.1.2" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.*"> <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.*">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>

7
src/NuGet.Config Normal file
View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<clear />
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
</packageSources>
</configuration>

View File

@ -5,6 +5,7 @@ using System.IO;
using System.Text; using System.Text;
using System.Xml; using System.Xml;
using log4net; using log4net;
using WinSW.Configuration;
using WinSW.Extensions; using WinSW.Extensions;
using WinSW.Util; using WinSW.Util;
using static WinSW.Plugins.RunawayProcessKiller.RunawayProcessKillerExtension.NativeMethods; using static WinSW.Plugins.RunawayProcessKiller.RunawayProcessKillerExtension.NativeMethods;
@ -179,7 +180,7 @@ namespace WinSW.Plugins.RunawayProcessKiller
return parameters.Environment; 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 // We expect the upper logic to process any errors
// TODO: a better parser API for types would be useful // TODO: a better parser API for types would be useful

View File

@ -1,9 +1,11 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Xml; using System.Xml;
using log4net; using log4net;
using WinSW.Configuration;
using WinSW.Extensions; using WinSW.Extensions;
using WinSW.Util; using WinSW.Util;
namespace WinSW.Plugins.SharedDirectoryMapper namespace WinSW.Plugins.SharedDirectoryMapper
{ {
public class SharedDirectoryMapper : AbstractWinSWExtension public class SharedDirectoryMapper : AbstractWinSWExtension
@ -25,7 +27,7 @@ namespace WinSW.Plugins.SharedDirectoryMapper
this._entries.Add(config); 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"); XmlNodeList? mapNodes = XmlHelper.SingleNode(node, "mapping", false)!.SelectNodes("map");
if (mapNodes != null) if (mapNodes != null)

View File

@ -1,4 +1,4 @@
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using NUnit.Framework; using NUnit.Framework;
using WinSW; using WinSW;
@ -109,21 +109,21 @@ $@"<service>
[Test] [Test]
public void VerifyServiceLogonRight() public void VerifyServiceLogonRight()
{ {
Assert.That(this._extendedServiceDescriptor.AllowServiceAcountLogonRight, Is.True); Assert.That(_extendedServiceDescriptor.ServiceAccount.AllowServiceAcountLogonRight, Is.True);
} }
[Test] [Test]
public void VerifyUsername() public void VerifyUsername()
{ {
Debug.WriteLine("_extendedServiceDescriptor.WorkingDirectory :: " + this._extendedServiceDescriptor.WorkingDirectory); Debug.WriteLine("_extendedServiceDescriptor.WorkingDirectory :: " + _extendedServiceDescriptor.WorkingDirectory);
Assert.That(this._extendedServiceDescriptor.ServiceAccountUser, Is.EqualTo(Domain + "\\" + Username)); Assert.That(_extendedServiceDescriptor.ServiceAccount.ServiceAccountUser, Is.EqualTo(Domain + "\\" + Username));
} }
[Test] [Test]
public void VerifyPassword() public void VerifyPassword()
{ {
Debug.WriteLine("_extendedServiceDescriptor.WorkingDirectory :: " + this._extendedServiceDescriptor.WorkingDirectory); Debug.WriteLine("_extendedServiceDescriptor.WorkingDirectory :: " + _extendedServiceDescriptor.WorkingDirectory);
Assert.That(this._extendedServiceDescriptor.ServiceAccountPassword, Is.EqualTo(Password)); Assert.That(_extendedServiceDescriptor.ServiceAccount.ServiceAccountPassword, Is.EqualTo(Password));
} }
[Test] [Test]
@ -197,7 +197,7 @@ $@"<service>
+ "</service>"; + "</service>";
var serviceDescriptor = ServiceDescriptor.FromXML(seedXml); var serviceDescriptor = ServiceDescriptor.FromXML(seedXml);
Assert.That(serviceDescriptor.OutFileDisabled, Is.True); Assert.That(serviceDescriptor.Log.OutFileDisabled, Is.True);
} }
[Test] [Test]
@ -208,7 +208,7 @@ $@"<service>
+ "</service>"; + "</service>";
var serviceDescriptor = ServiceDescriptor.FromXML(seedXml); var serviceDescriptor = ServiceDescriptor.FromXML(seedXml);
Assert.That(serviceDescriptor.ErrFileDisabled, Is.True); Assert.That(serviceDescriptor.Log.ErrFileDisabled, Is.True);
} }
[Test] [Test]
@ -219,7 +219,7 @@ $@"<service>
+ "</service>"; + "</service>";
var serviceDescriptor = ServiceDescriptor.FromXML(seedXml); var serviceDescriptor = ServiceDescriptor.FromXML(seedXml);
Assert.That(serviceDescriptor.OutFilePattern, Is.EqualTo(".out.test.log")); Assert.That(serviceDescriptor.Log.OutFilePattern, Is.EqualTo(".out.test.log"));
} }
[Test] [Test]
@ -230,7 +230,7 @@ $@"<service>
+ "</service>"; + "</service>";
var serviceDescriptor = ServiceDescriptor.FromXML(seedXml); var serviceDescriptor = ServiceDescriptor.FromXML(seedXml);
Assert.That(serviceDescriptor.ErrFilePattern, Is.EqualTo(".err.test.log")); Assert.That(serviceDescriptor.Log.ErrFilePattern, Is.EqualTo(".err.test.log"));
} }
[Test] [Test]
@ -247,7 +247,7 @@ $@"<service>
var serviceDescriptor = ServiceDescriptor.FromXML(seedXml); var serviceDescriptor = ServiceDescriptor.FromXML(seedXml);
serviceDescriptor.BaseName = "service"; serviceDescriptor.BaseName = "service";
var logHandler = serviceDescriptor.LogHandler as SizeBasedRollingLogAppender; var logHandler = serviceDescriptor.Log.CreateLogHandler() as SizeBasedRollingLogAppender;
Assert.That(logHandler, Is.Not.Null); Assert.That(logHandler, Is.Not.Null);
Assert.That(logHandler.SizeTheshold, Is.EqualTo(112 * 1024)); Assert.That(logHandler.SizeTheshold, Is.EqualTo(112 * 1024));
Assert.That(logHandler.FilesToKeep, Is.EqualTo(113)); Assert.That(logHandler.FilesToKeep, Is.EqualTo(113));
@ -267,7 +267,7 @@ $@"<service>
var serviceDescriptor = ServiceDescriptor.FromXML(seedXml); var serviceDescriptor = ServiceDescriptor.FromXML(seedXml);
serviceDescriptor.BaseName = "service"; serviceDescriptor.BaseName = "service";
var logHandler = serviceDescriptor.LogHandler as TimeBasedRollingLogAppender; var logHandler = serviceDescriptor.Log.CreateLogHandler() as TimeBasedRollingLogAppender;
Assert.That(logHandler, Is.Not.Null); Assert.That(logHandler, Is.Not.Null);
Assert.That(logHandler.Period, Is.EqualTo(7)); Assert.That(logHandler.Period, Is.EqualTo(7));
Assert.That(logHandler.Pattern, Is.EqualTo("log pattern")); Assert.That(logHandler.Pattern, Is.EqualTo("log pattern"));
@ -288,7 +288,7 @@ $@"<service>
var serviceDescriptor = ServiceDescriptor.FromXML(seedXml); var serviceDescriptor = ServiceDescriptor.FromXML(seedXml);
serviceDescriptor.BaseName = "service"; serviceDescriptor.BaseName = "service";
var logHandler = serviceDescriptor.LogHandler as RollingSizeTimeLogAppender; var logHandler = serviceDescriptor.Log.CreateLogHandler() as RollingSizeTimeLogAppender;
Assert.That(logHandler, Is.Not.Null); Assert.That(logHandler, Is.Not.Null);
Assert.That(logHandler.SizeTheshold, Is.EqualTo(10240 * 1024)); Assert.That(logHandler.SizeTheshold, Is.EqualTo(10240 * 1024));
Assert.That(logHandler.FilePattern, Is.EqualTo("yyyy-MM-dd")); Assert.That(logHandler.FilePattern, Is.EqualTo("yyyy-MM-dd"));
@ -307,7 +307,7 @@ $@"<service>
+ "</serviceaccount>" + "</serviceaccount>"
+ "</service>"; + "</service>";
var serviceDescriptor = ServiceDescriptor.FromXML(seedXml); var serviceDescriptor = ServiceDescriptor.FromXML(seedXml);
Assert.That(serviceDescriptor.AllowServiceAcountLogonRight, Is.False); Assert.That(serviceDescriptor.ServiceAccount.AllowServiceAcountLogonRight, Is.False);
} }
[Test] [Test]
@ -321,7 +321,7 @@ $@"<service>
+ "</serviceaccount>" + "</serviceaccount>"
+ "</service>"; + "</service>";
var serviceDescriptor = ServiceDescriptor.FromXML(seedXml); var serviceDescriptor = ServiceDescriptor.FromXML(seedXml);
Assert.That(serviceDescriptor.AllowServiceAcountLogonRight, Is.False); Assert.That(serviceDescriptor.ServiceAccount.AllowServiceAcountLogonRight, Is.False);
} }
[Test] [Test]

View File

@ -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<InvalidOperationException>());
}
[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();
});
}
}
}

View File

@ -59,6 +59,10 @@ namespace winswTests.Util
properties.Remove("Caption"); properties.Remove("Caption");
properties.Remove("Description"); properties.Remove("Description");
properties.Remove("Executable"); properties.Remove("Executable");
properties.Remove("BaseName");
properties.Remove("BasePath");
properties.Remove("Log");
properties.Remove("ServiceAccount");
return properties; return properties;
} }
} }