Merge branch 'master' into filelen

pull/582/head
Oleg Nenashev 2020-08-05 11:03:39 +02:00 committed by GitHub
commit 62d10fe2fb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 4386 additions and 2841 deletions

4
.github/codecov.yml vendored Normal file
View File

@ -0,0 +1,4 @@
coverage:
status:
project: off
patch: off

304
doc/YamlConfigFile.md Normal file
View File

@ -0,0 +1,304 @@
# YAML configuration file
This page describes the configuration file, which controls the behavior of the Windows service.
You can find configuration file samples in the [examples](../examples) directory of the source code repository.
Actual samples are also being published as part of releases on GitHub and NuGet.
## File structure
YAML Configuration file shuold be in following format
Example:
```yaml
id: jenkins
name: Jenkins
description: This service runs Jenkins continuous integration system.
env:
- name: JENKINS_HOME
value: '%BASE%'
executable: java
arguments: >
-Xrs
-Xmx256m
-jar "%BASE%\jenkins.war"
--httpPort=8080
log:
mode: roll
```
## Environment variable expansion
Configuration YAML files can include environment variable expansions of the form `%Name%`.
Such occurrences, if found, will be automatically replaced by the actual values of the variables.
[Read more about Environment variable expansion](xmlConfigFile.md#environment-variable-expansion)
## Configuration entries
### id
Specifies the ID that Windows uses internally to identify the service.
This has to be unique among all the services installed in a system, and it should consist entirely out of alpha-numeric characters.
### name
Short display name of the service, which can contain spaces and other characters.
This shouldn't be too long, like `id`, and this also needs to be unique among all the services in a given system.
### description
Long human-readable description of the service.
This gets displayed in Windows service manager when the service is selected.
### executable
This element specifies the executable to be launched.
It can be either absolute path, or you can just specify the executable name and let it be searched from `PATH` (although note that the services often run in a different user account and therefore it might have different `PATH` than your shell does.)
### startmode
This element specifies the start mode of the Windows service.
It can be one of the following values: Boot, System, Automatic, or Manual.
For more information, see the [ChangeStartMode method](https://docs.microsoft.com/windows/win32/cimwin32prov/changestartmode-method-in-class-win32-service).
The default value is `Automatic`.
### delayedAutoStart
This Boolean option enables the delayed start mode if the `Automatic` start mode is defined.
[Read more about delayedAutoStart](xmlConfigFile.md#delayedautostart)
```yaml
delayedAutoStart: false
```
### depend
Specify IDs of other services that this service depends on.
When service `X` depends on service `Y`, `X` can only run if `Y` is running.
YAML list can be used to specify multiple dependencies.
```yaml
depend:
- Eventlog
- W32Time
```
### log
Optionally set a different logging directory with `logpath` and startup `mode`: append (default), reset (clear log), ignore, roll (move to `\*.old`).
User can specify all log configurations as a single YAML dictionary
```yaml
log:
mode: roll-by-size
logpath: '%BASE/log%'
sizeThreshold: 10240
keepFiles: 8
```
See the [Logging and error reporting](loggingAndErrorReporting.md) page for more info.
### Arguments
`arguments` element specifies the arguments to be passed to the executable. User can specify all the commands as a single line.
```yaml
arguments: arg1 arg2 arg3
```
Also user can specify the arguemtns in more structured way with YAML multline strings.
```yaml
arguments: >
arg1
arg2
arg3
```
### stoparguments/stopexecutable
~~When the service is requested to stop, winsw simply calls [TerminateProcess function](https://docs.microsoft.com/windows/win32/api/processthreadsapi/nf-processthreadsapi-terminateprocess) to kill the service instantly.~~
However, if `stoparguments` elements is present, winsw will instead launch another process of `executable` (or `stopexecutable` if that's specified) with the specified arguments, and expects that to initiate the graceful shutdown of the service process.
Winsw will then wait for the two processes to exit on its own, before reporting back to Windows that the service has terminated.
When you use the `stoparguments`, you must use `startarguments` instead of `arguments`. See the complete example below:
```yaml
executable: catalina.sh
startarguments: >
jpda
run
stopexecutable: catalina.sh
stoparguments: stop
```
### stoptimeout
This optional element allows you to change this "15 seconds" value, so that you can control how long winsw gives the service to shut itself down.
[Read more about stoptimeout](xmlConfigFile.md#stoptimeout)
See `onfailure` below for how to specify time duration:
```yaml
stoptimeout: 15 sec
```
### Environment
User can use list of YAML dictionaries, if necessary to specify environment variables to be set for the child process. The syntax is:
```yaml
env:
-
name: MY_TOOL_HOME
value: 'C:\etc\tools\myTool'
-
name: LM_LICENSE_FILE
value: host1;host2
```
### interactive
If this optional element is specified, the service will be allowed to interact with the desktop, such as by showing a new window and dialog boxes.
If your program requires GUI, set this like the following:
```yaml
interactive: true
```
Note that since the introduction UAC (Windows Vista and onward), services are no longer really allowed to interact with the desktop.
In those OSes, all that this does is to allow the user to switch to a separate window station to interact with the service.
### beeponshutdown
This optional element is to emit [simple tones](https://docs.microsoft.com/windows/win32/api/utilapiset/nf-utilapiset-beep) when the service shuts down.
This feature should be used only for debugging, as some operating systems and hardware do not support this functionality.
### download
This optional element can be specified to have the service wrapper retrieve resources from URL and place it locally as a file.
This operation runs when the service is started, before the application specified by `executable` is launched.
[Read more about download](xmlConfigFile.md#download)
Examples:
```yaml
download:
-
from: "http://www.google.com/"
to: '%BASE%\index.html'
-
from: "http://www.nosuchhostexists.com/"
to: '%BASE%\dummy.html'
failOnError: true
-
from: "http://example.com/some.dat"
to: '%BASE%\some.dat'
auth: basic
unsecureAuth: true
username: aUser
password: aPa55w0rd
-
from: "https://example.com/some.dat"
to: '%BASE%\some.dat'
auth: basic
username: aUser
password: aPa55w0rd
-
from: "https://example.com/some.dat"
to: '%BASE%\some.dat'
auth: sspi
```
### onfailure
This optional element controls the behaviour when the process launched by winsw fails (i.e., exits with non-zero exit code).
```yaml
onFailure:
-
action: restart
delay: 10 sec
-
action: restart
delay: 20 sec
-
action: reboot
```
[Read more about onFailure](xmlConfigFile.md#onfailure)
### resetfailure
This optional element controls the timing in which Windows SCM resets the failure count.
For example, if you specify `resetfailure: 1 hour` and your service continues to run longer than one hour, then the failure count is reset to zero.
This affects the behaviour of the failure actions (see `onfailure` above).
In other words, this is the duration in which you consider the service has been running successfully.
Defaults to 1 day.
### Security descriptor
The security descriptor string for the service in SDDL form.
For more information, see [Security Descriptor Definition Language](https://docs.microsoft.com/windows/win32/secauthz/security-descriptor-definition-language).
```yaml
securtityDescriptor: 'D:(A;;DCSWRPDTRC;;;BA)(A;;DCSWRPDTRC;;;SY)S:NO\_ACCESS\_CONTROL'
```
### Service account
The service is installed as the [LocalSystem account](https://docs.microsoft.com/windows/win32/services/localsystem-account) by default. If your service does not need a high privilege level, consider using the [LocalService account](https://docs.microsoft.com/windows/win32/services/localservice-account), the [NetworkService account](https://docs.microsoft.com/windows/win32/services/networkservice-account) or a user account.
To use a user account, specify a `serviceaccount` element like this:
```yaml
serviceaccount:
domain: YOURDOMAIN
user: useraccount
password: Pa55w0rd
allowservicelogon: true
```
[Read more about Service account](xmlConfigFile.md#service-account)
### Working directory
Some services need to run with a working directory specified.
To do this, specify a `workingdirectory` element like this:
```yaml
workingdirectory: 'C:\application'
```
### Priority
Optionally specify the scheduling priority of the service process (equivalent of Unix nice)
Possible values are `idle`, `belownormal`, `normal`, `abovenormal`, `high`, `realtime` (case insensitive.)
```yaml
priority: idle
```
Specifying a priority higher than normal has unintended consequences.
For more information, see [ProcessPriorityClass Enumeration](https://docs.microsoft.com/dotnet/api/system.diagnostics.processpriorityclass) in .NET docs.
This feature is intended primarily to launch a process in a lower priority so as not to interfere with the computer's interactive usage.
### Stop parent process first
Optionally specify the order of service shutdown.
If `true`, the parent process is shutdown first.
This is useful when the main process is a console, which can respond to Ctrl+C command and will gracefully shutdown child processes.
```yaml
stopparentprocessfirst: true
```

View File

@ -0,0 +1,75 @@
id: myapp
name: MyApp Service (powered by WinSW)
description: This service is a service created from a sample configuration
executable: java
#serviceaccount:
# domain: YOURDOMAIN
# user: useraccount
# password: Pa55w0rd
# allowservicelogon: yes
#onFailure:
# -
# action: restart
# delay: 10 sec
# -
# action: restart
# delay: 20 sec
# -
# action: reboot
#resetFailureAfter: 01:00:00
#securityDescriptor: security descriptor string
#arguments: >
# -classpath
# c:\cygwin\home\kohsuke\ws\hello-world\out\production\hello-world
# test.Main
#startArguments: start arguments
#workingdirectory: C:\myApp\work
priority: Normal
stopTimeout: 15 sec
stopParentProcessFirst: true
#stopExecutable: '%BASE%\stop.exe'
#stopArguments: -stop true
startMode: Automatic
#delayedAutoStart: true
#serviceDependencies:
# - Eventlog
# - W32Time
waitHint: 15 sec
sleepTime: 1 sec
#interactive: true
log:
# logpath: '%BASE%\logs'
mode: append
#env:
# -
# name: MY_TOOL_HOME
# value: 'C:\etc\tools\myTool'
# -
# name: LM_LICENSE_FILE
# value: host1;host2
#download:
# -
# from: "http://www.google.com/"
# to: '%BASE%\index.html'
# -
# from: "http://www.nosuchhostexists.com/"
# to: '%BASE%\dummy.html'
# failOnError: true
# -
# from: "http://example.com/some.dat"
# to: '%BASE%\some.dat'
# auth: basic
# unsecureAuth: true
# username: aUser
# password: aPa55w0rd
# -
# from: "https://example.com/some.dat"
# to: '%BASE%\some.dat'
# auth: basic
# username: aUser
# password: aPa55w0rd
# -
# from: "https://example.com/some.dat"
# to: '%BASE%\some.dat'
# auth: sspi
#beepOnShutdown: true

View File

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

View File

@ -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;
@ -22,7 +23,9 @@ namespace WinSW
private ServiceApis.SERVICE_STATUS wrapperServiceStatus;
private readonly Process process = new Process();
private readonly ServiceDescriptor descriptor;
private readonly IWinSWConfiguration descriptor;
private Dictionary<string, string>? envs;
internal WinSWExtensionManager ExtensionManager { get; private set; }
@ -55,7 +58,7 @@ namespace WinSW
/// </summary>
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
/// <returns>Log Handler, which should be used for the spawned process</returns>
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;
}

View File

@ -92,6 +92,7 @@
<InputAssemblies>$(InputAssemblies) "$(OutDir)SharedDirectoryMapper.dll"</InputAssemblies>
<InputAssemblies>$(InputAssemblies) "$(OutDir)RunawayProcessKiller.dll"</InputAssemblies>
<InputAssemblies>$(InputAssemblies) "$(OutDir)log4net.dll"</InputAssemblies>
<InputAssemblies>$(InputAssemblies) "$(OutDir)YamlDotNet.dll"</InputAssemblies>
<OutputAssembly>"$(ArtifactsDir)WinSW.$(IdentifierSuffix).exe"</OutputAssembly>
</PropertyGroup>

View File

@ -12,6 +12,8 @@ namespace WinSW.Configuration
/// </summary>
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<Download> Downloads => new List<Download>(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<string> ExtensionIds => new List<string>(0);
public string? SecurityDescriptor => null;
}
}

View File

@ -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
/// <summary>
/// Destination for logging.
/// If undefined, a default one should be used.
/// </summary>
string LogDirectory { get; }
// TODO: replace by enum
string LogMode { get; }
Log Log { get; }
// Environment
List<Download> Downloads { get; }
@ -76,5 +75,18 @@ namespace WinSW.Configuration
// Extensions
XmlNode? ExtensionsConfiguration { get; }
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,666 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Xml;
using WinSW.Native;
using WinSW.Util;
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 = "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 string? ResetFailureAfterYaml { get; set; }
[YamlMember(Alias = "stopTimeout")]
public string? StopTimeoutYaml { get; set; }
[YamlMember(Alias = "startMode")]
public string? StartModeYaml { get; set; }
[YamlMember(Alias = "serviceDependencies")]
public string[]? ServiceDependenciesYaml { get; set; }
[YamlMember(Alias = "waitHint")]
public string? WaitHintYaml { get; set; }
[YamlMember(Alias = "sleepTime")]
public string? SleepTimeYaml { get; set; }
[YamlMember(Alias = "interactive")]
public bool? InteractiveYaml { get; set; }
[YamlMember(Alias = "priority")]
public string? PriorityYaml { get; set; }
[YamlMember(Alias = "beepOnShutdown")]
public bool BeepOnShutdown { get; set; }
[YamlMember(Alias = "env")]
public List<YamlEnv>? EnvironmentVariablesYaml { get; set; }
[YamlMember(Alias = "onFailure")]
public List<YamlFailureAction>? YamlFailureActions { get; set; }
[YamlMember(Alias = "delayedAutoStart")]
public bool DelayedAutoStart { get; set; }
[YamlMember(Alias = "securityDescriptor")]
public string? SecurityDescriptorYaml { get; set; }
[YamlMember(Alias = "extensions")]
public List<string>? YamlExtensionIds { get; set; }
public class YamlEnv
{
[YamlMember(Alias = "name")]
public string? Name { get; set; }
[YamlMember(Alias = "value")]
public string? Value { 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 :
ExpandEnv(this.NameYamlLog);
}
}
public override string Directory
{
get
{
return this.LogPathYamlLog is null ?
DefaultWinSWSettings.DefaultLogSettings.Directory :
ExpandEnv(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 :
ExpandEnv(this.OutFilePatternYamlLog);
}
}
public override string ErrFilePattern
{
get
{
return this.ErrFilePatternYamlLog is null ?
DefaultWinSWSettings.DefaultLogSettings.ErrFilePattern :
ExpandEnv(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 string? 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 string FromDownload => ExpandEnv(this.FromYamlDownload);
public string ToDownload => ExpandEnv(this.ToYamlDownload);
public string? UsernameDownload => this.UsernameYamlDownload is null ? null : ExpandEnv(this.UsernameYamlDownload);
public string? PasswordDownload => this.PasswordYamlDownload is null ? null : ExpandEnv(this.PasswordYamlDownload);
public string? ProxyDownload => this.ProxyYamlDownload is null ? null : ExpandEnv(this.ProxyYamlDownload);
public AuthType AuthDownload
{
get
{
if (this.AuthYamlDownload is null)
{
return AuthType.None;
}
var auth = ExpandEnv(this.AuthYamlDownload);
try
{
return (AuthType)Enum.Parse(typeof(AuthType), auth, true);
}
catch
{
Console.WriteLine("Auth type in YAML must be one of the following:");
foreach (string at in Enum.GetNames(typeof(AuthType)))
{
Console.WriteLine(at);
}
throw;
}
}
}
}
public class YamlFailureAction
{
[YamlMember(Alias = "action")]
public string? FailureAction { get; set; }
[YamlMember(Alias = "delay")]
public string? FailureActionDelay { get; set; }
public SC_ACTION_TYPE Type
{
get
{
SC_ACTION_TYPE actionType = this.FailureAction switch
{
"restart" => SC_ACTION_TYPE.SC_ACTION_RESTART,
"none" => SC_ACTION_TYPE.SC_ACTION_NONE,
"reboot" => SC_ACTION_TYPE.SC_ACTION_REBOOT,
_ => throw new InvalidDataException("Invalid failure action: " + this.FailureAction)
};
return actionType;
}
}
public TimeSpan Delay => this.FailureActionDelay is null ? TimeSpan.Zero : ConfigHelper.ParseTimeSpan(this.FailureActionDelay);
}
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 ExpandEnv(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.FromDownload,
item.ToDownload,
item.FailOnErrorYamlDownload,
item.AuthDownload,
item.UsernameDownload,
item.PasswordDownload,
item.UnsecureAuthYamlDownload,
item.ProxyDownload));
}
return result;
}
internal static string ExpandEnv(string str)
{
return Environment.ExpandEnvironmentVariables(str);
}
public string Id => this.IdYaml is null ? this.Defaults.Id : ExpandEnv(this.IdYaml);
public string Description => this.DescriptionYaml is null ? this.Defaults.Description : ExpandEnv(this.DescriptionYaml);
public string Executable => this.ExecutableYaml is null ? this.Defaults.Executable : ExpandEnv(this.ExecutableYaml);
public string ExecutablePath => this.ExecutablePathYaml is null ?
this.Defaults.ExecutablePath :
ExpandEnv(this.ExecutablePathYaml);
public string Caption => this.NameYaml is null ? this.Defaults.Caption : ExpandEnv(this.NameYaml);
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
{
get
{
if (this.StartModeYaml is null)
{
return this.Defaults.StartMode;
}
var p = ExpandEnv(this.StartModeYaml);
try
{
return (StartMode)Enum.Parse(typeof(StartMode), p, true);
}
catch
{
Console.WriteLine("Start mode in YAML must be one of the following:");
foreach (string sm in Enum.GetNames(typeof(StartMode)))
{
Console.WriteLine(sm);
}
throw;
}
}
}
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 :
ExpandEnv(this.StopExecutableYaml);
}
}
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 :
ConfigHelper.ParseTimeSpan(this.ResetFailureAfterYaml);
public string WorkingDirectory => this.WorkingDirectoryYaml is null ?
this.Defaults.WorkingDirectory :
ExpandEnv(this.WorkingDirectoryYaml);
public ProcessPriorityClass Priority
{
get
{
if (this.PriorityYaml is null)
{
return this.Defaults.Priority;
}
var p = ExpandEnv(this.PriorityYaml);
try
{
return (ProcessPriorityClass)Enum.Parse(typeof(ProcessPriorityClass), p, true);
}
catch
{
Console.WriteLine("Priority in YAML must be one of the following:");
foreach (string pr in Enum.GetNames(typeof(ProcessPriorityClass)))
{
Console.WriteLine(pr);
}
throw;
}
}
}
public TimeSpan StopTimeout => this.StopTimeoutYaml is null ? this.Defaults.StopTimeout : ConfigHelper.ParseTimeSpan(this.StopTimeoutYaml);
public string[] ServiceDependencies
{
get
{
if (this.ServiceDependenciesYaml is null)
{
return this.Defaults.ServiceDependencies;
}
var result = new List<string>(0);
foreach (var item in this.ServiceDependenciesYaml)
{
result.Add(ExpandEnv(item));
}
return result.ToArray();
}
}
public TimeSpan WaitHint => this.WaitHintYaml is null ? this.Defaults.WaitHint : ConfigHelper.ParseTimeSpan(this.WaitHintYaml);
public TimeSpan SleepTime => this.SleepTimeYaml is null ? this.Defaults.SleepTime : ConfigHelper.ParseTimeSpan(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)
{
if (item.Name is null || item.Value is null)
{
continue;
}
var key = item.Name;
var value = ExpandEnv(item.Value);
this.EnvironmentVariables[key] = value;
Environment.SetEnvironmentVariable(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 - Extensions
XmlNode? IWinSWConfiguration.ExtensionsConfiguration => throw new NotImplementedException();
public List<string> ExtensionIds => this.YamlExtensionIds ?? this.Defaults.ExtensionIds;
public string BaseName => this.Defaults.BaseName;
public string BasePath => this.Defaults.BasePath;
public string? SecurityDescriptor
{
get
{
if (this.SecurityDescriptorYaml is null)
{
return this.Defaults.SecurityDescriptor;
}
return ExpandEnv(this.SecurityDescriptorYaml);
}
}
}
}

0
src/Core/WinSWCore/Download.cs Executable file → Normal file
View File

View File

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

View File

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

View File

@ -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<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));
public WinSWExtensionManager(ServiceDescriptor serviceDescriptor)
public WinSWExtensionManager(IWinSWConfiguration serviceDescriptor)
{
this.ServiceDescriptor = serviceDescriptor;
this.Extensions = new Dictionary<string, IWinSWExtension>();

0
src/Core/WinSWCore/Native/Kernel32.cs Executable file → Normal file
View File

287
src/Core/WinSWCore/ServiceDescriptor.cs Executable file → Normal file
View File

@ -16,7 +16,9 @@ namespace WinSW
/// </summary>
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<string, string> environmentVariables;
@ -108,9 +110,11 @@ namespace WinSW
public static ServiceDescriptor FromXML(string xml)
{
var dom = new XmlDocument();
dom.LoadXml(xml);
return new ServiceDescriptor(dom);
#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)
@ -143,41 +147,12 @@ namespace WinSW
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);
return value is null ? defaultValue : this.ParseTimeSpan(value);
return value is null ? defaultValue : ConfigHelper.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<string, long> Suffix = new Dictionary<string, long>
{
{ "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 }
};
/// <summary>
/// Path to the executable.
/// </summary>
@ -327,17 +302,7 @@ namespace WinSW
/// <summary>
/// LogDirectory is the service wrapper executable directory or the optionally specified logpath element.
/// </summary>
public string LogDirectory
{
get
{
XmlNode? loggingNode = this.dom.SelectSingleNode("//logpath");
return loggingNode is null
? Defaults.LogDirectory
: Environment.ExpandEnvironmentVariables(loggingNode.InnerText);
}
}
public string LogDirectory { get => this.Log.Directory; }
public string LogMode
{
@ -375,114 +340,135 @@ namespace WinSW
}
}
public bool OutFileDisabled => this.SingleBoolElement("outfiledisabled", Defaults.OutFileDisabled);
public bool ErrFileDisabled => this.SingleBoolElement("errfiledisabled", Defaults.ErrFileDisabled);
public string OutFilePattern
public Log Log
{
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);
}
}
public string ErrFilePattern
public override string ErrFilePattern
{
get
{
XmlNode? loggingName = this.dom.SelectSingleNode("//errfilepattern");
XmlNode? loggingName = this.d.dom.SelectSingleNode("//errfilepattern");
return loggingName is null ? Defaults.ErrFilePattern : Environment.ExpandEnvironmentVariables(loggingName.InnerText);
}
}
public LogHandler LogHandler
public override string? AutoRollAtTime
{
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.");
XmlNode? autoRollAtTimeNode = this.E.SelectSingleNode("autoRollAtTime");
return autoRollAtTimeNode?.InnerText;
}
}
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)
public override int? ZipOlderThanNumDays
{
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)
get
{
// 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");
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;
}
XmlNode? zipdateformatNode = e.SelectSingleNode("zipDateFormat");
string zipdateformat = zipdateformatNode is null ? "yyyyMM" : zipdateformatNode.InnerText;
return zipolderthannumdays;
}
}
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);
public override string? ZipDateFormat
{
get
{
XmlNode? zipdateformatNode = this.E.SelectSingleNode("zipDateFormat");
return zipdateformatNode is null ? null : zipdateformatNode.InnerText;
}
}
}
@ -563,14 +549,14 @@ namespace WinSW
/// 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)
/// </summary>
public TimeSpan WaitHint => this.SingleTimeSpanElement(this.dom, "waithint", Defaults.WaitHint);
public TimeSpan WaitHint => this.SingleTimeSpanElement("waithint", Defaults.WaitHint);
/// <summary>
/// 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.
/// </summary>
public TimeSpan SleepTime => this.SingleTimeSpanElement(this.dom, "sleeptime", Defaults.SleepTime);
public TimeSpan SleepTime => this.SingleTimeSpanElement("sleeptime", Defaults.SleepTime);
/// <summary>
/// True if the service can interact with the desktop.
@ -632,66 +618,67 @@ namespace WinSW
_ => 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);
result[i] = new SC_ACTION(type, delay != null ? ConfigHelper.ParseTimeSpan(delay.Value) : TimeSpan.Zero);
}
return result;
}
}
public TimeSpan ResetFailureAfter => this.SingleTimeSpanElement(this.dom, "resetfailure", Defaults.ResetFailureAfter);
public TimeSpan ResetFailureAfter => this.SingleTimeSpanElement("resetfailure", Defaults.ResetFailureAfter);
protected string? GetServiceAccountPart(string subNodeName)
{
XmlNode? node = this.dom.SelectSingleNode("//serviceaccount");
if (node != null)
protected string? GetServiceAccountPart(XmlNode node, string subNodeName)
{
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()
private bool ParseAllowServiceAcountLogonRight(string? logonRight)
{
return !string.IsNullOrEmpty(this.ServiceAccountName);
}
public bool AllowServiceAcountLogonRight
{
get
{
if (this.AllowServiceLogon != null)
{
if (bool.TryParse(this.AllowServiceLogon, out bool parsedvalue))
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;
}
}
/// <summary>
/// Time to wait for the service to gracefully shutdown the executable before we forcibly kill it
/// </summary>
public TimeSpan StopTimeout => this.SingleTimeSpanElement(this.dom, "stoptimeout", Defaults.StopTimeout);
public TimeSpan StopTimeout => this.SingleTimeSpanElement("stoptimeout", Defaults.StopTimeout);
public bool StopParentProcessFirst
{

View File

@ -0,0 +1,78 @@
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 ServiceDescriptorYaml()
{
string p = Defaults.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;
}
var basepath = Path.Combine(d.FullName, baseName);
using (var reader = new StreamReader(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, Defaults.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

@ -0,0 +1,37 @@
using System;
using System.Collections.Generic;
namespace WinSW.Util
{
public static class ConfigHelper
{
public static 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<string, long> Suffix = new Dictionary<string, long>
{
{ "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 }
};
}
}

View File

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

0
src/Core/WinSWCore/Wmi.cs Executable file → Normal file
View File

0
src/Core/WinSWCore/WmiSchema.cs Executable file → Normal file
View File

View File

@ -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

View File

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

View File

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

View File

@ -0,0 +1,149 @@
using System;
using NUnit.Framework;
using WinSW;
using WinSW.Configuration;
using WinSW.Native;
namespace winswTests
{
class ServiceDescriptorYamlTest
{
private readonly string MinimalYaml = @"id: myapp
name: This is a test
executable: 'C:\Program Files\Java\jdk1.8.0_241\bin\java.exe'
description: This is test winsw";
private readonly DefaultWinSWSettings Defaults = new DefaultWinSWSettings();
[Test]
public void Parse_must_implemented_value_test()
{
var yml = @"name: This is a test
executable: 'C:\Program Files\Java\jdk1.8.0_241\bin\java.exe'
description: This is test winsw";
Assert.That(() =>
{
_ = ServiceDescriptorYaml.FromYaml(yml).Configurations.Id;
}, Throws.TypeOf<InvalidOperationException>());
}
[Test]
public void Default_value_map_test()
{
var configs = ServiceDescriptorYaml.FromYaml(MinimalYaml).Configurations;
Assert.IsNotNull(configs.ExecutablePath);
Assert.IsNotNull(configs.BaseName);
Assert.IsNotNull(configs.BasePath);
}
[Test]
public void Parse_downloads()
{
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);
Assert.AreEqual("www.sample.com", configs.Downloads[0].From);
Assert.AreEqual("c://tmp", configs.Downloads[0].To);
}
[Test]
public void Parse_serviceaccount()
{
var yml = @"id: myapp
name: winsw
description: yaml test
executable: java
serviceaccount:
user: testuser
domain: mydomain
password: pa55w0rd
allowservicelogon: yes";
var serviceAccount = ServiceDescriptorYaml.FromYaml(yml).Configurations.ServiceAccount;
Assert.AreEqual("mydomain\\testuser", serviceAccount.ServiceAccountUser);
Assert.AreEqual(true, serviceAccount.AllowServiceAcountLogonRight);
Assert.AreEqual("pa55w0rd", serviceAccount.ServiceAccountPassword);
Assert.AreEqual(true, serviceAccount.HasServiceAccount());
}
[Test]
public void Parse_environment_variables()
{
var yml = @"id: myapp
name: WinSW
executable: java
description: env test
env:
-
name: MY_TOOL_HOME
value: 'C:\etc\tools\myTool'
-
name: LM_LICENSE_FILE
value: host1;host2";
var envs = ServiceDescriptorYaml.FromYaml(yml).Configurations.EnvironmentVariables;
Assert.That(@"C:\etc\tools\myTool", Is.EqualTo(envs["MY_TOOL_HOME"]));
Assert.That("host1;host2", Is.EqualTo(envs["LM_LICENSE_FILE"]));
}
[Test]
public void Parse_log()
{
var yml = @"id: myapp
name: winsw
description: yaml test
executable: java
log:
mode: roll
logpath: 'D://winsw/logs'";
var config = ServiceDescriptorYaml.FromYaml(yml).Configurations;
Assert.AreEqual("roll", config.LogMode);
Assert.AreEqual("D://winsw/logs", config.LogDirectory);
}
[Test]
public void Parse_onfailure_actions()
{
var yml = @"id: myapp
name: winsw
description: yaml test
executable: java
onFailure:
-
action: restart
delay: 5 sec
-
action: reboot
delay: 10 min";
var onFailure = ServiceDescriptorYaml.FromYaml(yml).Configurations.FailureActions;
Assert.That(onFailure[0].Type, Is.EqualTo(SC_ACTION_TYPE.SC_ACTION_RESTART));
Assert.That(onFailure[1].Type, Is.EqualTo(SC_ACTION_TYPE.SC_ACTION_REBOOT));
Assert.That(TimeSpan.FromMilliseconds(onFailure[0].Delay), Is.EqualTo(TimeSpan.FromSeconds(5)));
Assert.That(TimeSpan.FromMilliseconds(onFailure[1].Delay), Is.EqualTo(TimeSpan.FromMinutes(10)));
}
}
}

View File

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