diff --git a/doc/YamlConfigFile.md b/doc/YamlConfigFile.md
new file mode 100644
index 0000000..e24aadf
--- /dev/null
+++ b/doc/YamlConfigFile.md
@@ -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
+```
diff --git a/examples/sample-allOption.yml b/examples/sample-allOption.yml
new file mode 100644
index 0000000..d63d9fe
--- /dev/null
+++ b/examples/sample-allOption.yml
@@ -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
\ No newline at end of file
diff --git a/src/Core/ServiceWrapper/winsw.csproj b/src/Core/ServiceWrapper/winsw.csproj
index f9324ab..7620bcf 100644
--- a/src/Core/ServiceWrapper/winsw.csproj
+++ b/src/Core/ServiceWrapper/winsw.csproj
@@ -92,6 +92,7 @@
$(InputAssemblies) "$(OutDir)SharedDirectoryMapper.dll"
$(InputAssemblies) "$(OutDir)RunawayProcessKiller.dll"
$(InputAssemblies) "$(OutDir)log4net.dll"
+ $(InputAssemblies) "$(OutDir)YamlDotNet.dll"
"$(ArtifactsDir)WinSW.$(IdentifierSuffix).exe"
diff --git a/src/Core/WinSWCore/Configuration/IWinSWConfiguration.cs b/src/Core/WinSWCore/Configuration/IWinSWConfiguration.cs
index a59bd86..1b21450 100644
--- a/src/Core/WinSWCore/Configuration/IWinSWConfiguration.cs
+++ b/src/Core/WinSWCore/Configuration/IWinSWConfiguration.cs
@@ -76,7 +76,6 @@ namespace WinSW.Configuration
// Extensions
XmlNode? ExtensionsConfiguration { get; }
- // IWinSWConfiguration Support
List ExtensionIds { get; }
// Service Account
diff --git a/src/Core/WinSWCore/Configuration/ServiceAccount.cs b/src/Core/WinSWCore/Configuration/ServiceAccount.cs
index 9b638f0..d44b366 100644
--- a/src/Core/WinSWCore/Configuration/ServiceAccount.cs
+++ b/src/Core/WinSWCore/Configuration/ServiceAccount.cs
@@ -1,4 +1,4 @@
-using YamlDotNet.Serialization;
+using YamlDotNet.Serialization;
namespace WinSW.Configuration
{
@@ -10,7 +10,7 @@ namespace WinSW.Configuration
[YamlMember(Alias = "domain")]
public string? ServiceAccountDomain { get; set; }
- [YamlMember(Alias = "Password")]
+ [YamlMember(Alias = "password")]
public string? ServiceAccountPassword { get; set; }
[YamlMember(Alias = "allowservicelogon")]
diff --git a/src/Core/WinSWCore/Configuration/YamlConfiguration.cs b/src/Core/WinSWCore/Configuration/YamlConfiguration.cs
index 3551a7e..49e5610 100644
--- a/src/Core/WinSWCore/Configuration/YamlConfiguration.cs
+++ b/src/Core/WinSWCore/Configuration/YamlConfiguration.cs
@@ -1,8 +1,10 @@
-using System;
+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;
@@ -28,9 +30,6 @@ namespace WinSW.Configuration
[YamlMember(Alias = "executablePath")]
public string? ExecutablePathYaml { get; set; }
- [YamlMember(Alias = "caption")]
- public string? CaptionYaml { get; set; }
-
[YamlMember(Alias = "hideWindow")]
public bool? HideWindowYaml { get; set; }
@@ -62,36 +61,36 @@ namespace WinSW.Configuration
public bool? StopParentProcessFirstYaml { get; set; }
[YamlMember(Alias = "resetFailureAfter")]
- public TimeSpan? ResetFailureAfterYaml { get; set; }
+ public string? ResetFailureAfterYaml { get; set; }
[YamlMember(Alias = "stopTimeout")]
- public TimeSpan? StopTimeoutYaml { get; set; }
+ public string? StopTimeoutYaml { get; set; }
[YamlMember(Alias = "startMode")]
- public StartMode? StartModeYaml { get; set; }
+ public string? StartModeYaml { get; set; }
[YamlMember(Alias = "serviceDependencies")]
public string[]? ServiceDependenciesYaml { get; set; }
[YamlMember(Alias = "waitHint")]
- public TimeSpan? WaitHintYaml { get; set; }
+ public string? WaitHintYaml { get; set; }
[YamlMember(Alias = "sleepTime")]
- public TimeSpan? SleepTimeYaml { get; set; }
+ public string? SleepTimeYaml { get; set; }
[YamlMember(Alias = "interactive")]
public bool? InteractiveYaml { get; set; }
[YamlMember(Alias = "priority")]
- public ProcessPriorityClass? PriorityYaml { get; set; }
+ public string? PriorityYaml { get; set; }
[YamlMember(Alias = "beepOnShutdown")]
public bool BeepOnShutdown { get; set; }
[YamlMember(Alias = "env")]
- public Dictionary? EnvironmentVariablesYaml { get; set; }
+ public List? EnvironmentVariablesYaml { get; set; }
- [YamlMember(Alias = "failureActions")]
+ [YamlMember(Alias = "onFailure")]
public List? YamlFailureActions { get; set; }
[YamlMember(Alias = "delayedAutoStart")]
@@ -100,6 +99,18 @@ namespace WinSW.Configuration
[YamlMember(Alias = "securityDescriptor")]
public string? SecurityDescriptorYaml { get; set; }
+ [YamlMember(Alias = "extensions")]
+ public List? 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;
@@ -163,7 +174,7 @@ namespace WinSW.Configuration
{
return this.NameYamlLog is null ?
DefaultWinSWSettings.DefaultLogSettings.Name :
- Environment.ExpandEnvironmentVariables(this.NameYamlLog);
+ ExpandEnv(this.NameYamlLog);
}
}
@@ -173,7 +184,7 @@ namespace WinSW.Configuration
{
return this.LogPathYamlLog is null ?
DefaultWinSWSettings.DefaultLogSettings.Directory :
- Environment.ExpandEnvironmentVariables(this.LogPathYamlLog);
+ ExpandEnv(this.LogPathYamlLog);
}
}
@@ -238,7 +249,7 @@ namespace WinSW.Configuration
{
return this.OutFilePatternYamlLog is null ?
DefaultWinSWSettings.DefaultLogSettings.OutFilePattern :
- Environment.ExpandEnvironmentVariables(this.OutFilePatternYamlLog);
+ ExpandEnv(this.OutFilePatternYamlLog);
}
}
@@ -248,7 +259,7 @@ namespace WinSW.Configuration
{
return this.ErrFilePatternYamlLog is null ?
DefaultWinSWSettings.DefaultLogSettings.ErrFilePattern :
- Environment.ExpandEnvironmentVariables(this.ErrFilePatternYamlLog);
+ ExpandEnv(this.ErrFilePatternYamlLog);
}
}
@@ -295,7 +306,7 @@ namespace WinSW.Configuration
public string ToYamlDownload { get; set; } = string.Empty;
[YamlMember(Alias = "auth")]
- public AuthType AuthYamlDownload { get; set; }
+ public string? AuthYamlDownload { get; set; }
[YamlMember(Alias = "username")]
public string? UsernameYamlDownload { get; set; }
@@ -311,19 +322,71 @@ namespace WinSW.Configuration
[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 = "type")]
- private SC_ACTION_TYPE type;
+ [YamlMember(Alias = "action")]
+ public string? FailureAction { get; set; }
[YamlMember(Alias = "delay")]
- private TimeSpan delay;
+ public string? FailureActionDelay { get; set; }
- public SC_ACTION_TYPE Type { get => this.type; set => this.type = value; }
+ 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)
+ };
- public TimeSpan Delay { get => this.delay; set => this.delay = value; }
+ return actionType;
+ }
+ }
+
+ public TimeSpan Delay => this.FailureActionDelay is null ? TimeSpan.Zero : ConfigHelper.ParseTimeSpan(this.FailureActionDelay);
}
private string? GetArguments(string? args, ArgType type)
@@ -343,7 +406,7 @@ namespace WinSW.Configuration
}
}
- return Environment.ExpandEnvironmentVariables(args);
+ return ExpandEnv(args);
}
private enum ArgType
@@ -365,28 +428,35 @@ namespace WinSW.Configuration
foreach (var item in downloads)
{
result.Add(new Download(
- item.FromYamlDownload,
- item.ToYamlDownload,
+ item.FromDownload,
+ item.ToDownload,
item.FailOnErrorYamlDownload,
- item.AuthYamlDownload,
- item.UsernameYamlDownload,
- item.PasswordYamlDownload,
+ item.AuthDownload,
+ item.UsernameDownload,
+ item.PasswordDownload,
item.UnsecureAuthYamlDownload,
- item.ProxyYamlDownload));
+ item.ProxyDownload));
}
return result;
}
- public string Id => this.IdYaml is null ? this.Defaults.Id : this.IdYaml;
+ internal static string ExpandEnv(string str)
+ {
+ return Environment.ExpandEnvironmentVariables(str);
+ }
- public string Description => this.DescriptionYaml is null ? this.Defaults.Description : this.DescriptionYaml;
+ public string Id => this.IdYaml is null ? this.Defaults.Id : ExpandEnv(this.IdYaml);
- public string Executable => this.ExecutableYaml is null ? this.Defaults.Executable : this.ExecutableYaml;
+ public string Description => this.DescriptionYaml is null ? this.Defaults.Description : ExpandEnv(this.DescriptionYaml);
- public string ExecutablePath => this.ExecutablePathYaml is null ? this.Defaults.ExecutablePath : this.ExecutablePathYaml;
+ public string Executable => this.ExecutableYaml is null ? this.Defaults.Executable : ExpandEnv(this.ExecutableYaml);
- public string Caption => this.CaptionYaml is null ? this.Defaults.Caption : this.CaptionYaml;
+ 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;
@@ -400,7 +470,33 @@ namespace WinSW.Configuration
}
}
- public StartMode StartMode => this.StartModeYaml is null ? this.Defaults.StartMode : (StartMode)this.StartModeYaml;
+ 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
{
@@ -421,7 +517,7 @@ namespace WinSW.Configuration
{
return this.StopExecutableYaml is null ?
this.Defaults.StopExecutable :
- null;
+ ExpandEnv(this.StopExecutableYaml);
}
}
@@ -447,23 +543,65 @@ namespace WinSW.Configuration
public TimeSpan ResetFailureAfter => this.ResetFailureAfterYaml is null ?
this.Defaults.ResetFailureAfter :
- (TimeSpan)this.ResetFailureAfterYaml;
+ ConfigHelper.ParseTimeSpan(this.ResetFailureAfterYaml);
public string WorkingDirectory => this.WorkingDirectoryYaml is null ?
this.Defaults.WorkingDirectory :
- this.WorkingDirectoryYaml;
+ ExpandEnv(this.WorkingDirectoryYaml);
- public ProcessPriorityClass Priority => this.PriorityYaml is null ? this.Defaults.Priority : (ProcessPriorityClass)this.PriorityYaml;
+ public ProcessPriorityClass Priority
+ {
+ get
+ {
+ if (this.PriorityYaml is null)
+ {
+ return this.Defaults.Priority;
+ }
- public TimeSpan StopTimeout => this.StopTimeoutYaml is null ? this.Defaults.StopTimeout : (TimeSpan)this.StopTimeoutYaml;
+ var p = ExpandEnv(this.PriorityYaml);
- public string[] ServiceDependencies => this.ServiceDependenciesYaml is null ?
- this.Defaults.ServiceDependencies :
- this.ServiceDependenciesYaml;
+ 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);
+ }
- public TimeSpan WaitHint => this.WaitHintYaml is null ? this.Defaults.WaitHint : (TimeSpan)this.WaitHintYaml;
+ throw;
+ }
+ }
+ }
- public TimeSpan SleepTime => this.SleepTimeYaml is null ? this.Defaults.SleepTime : (TimeSpan)this.SleepTimeYaml;
+ 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(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;
@@ -481,9 +619,16 @@ namespace WinSW.Configuration
{
foreach (var item in this.EnvironmentVariablesYaml)
{
- var value = Environment.ExpandEnvironmentVariables(item.Value);
- this.EnvironmentVariables[item.Key] = value;
- Environment.SetEnvironmentVariable(item.Key, value);
+ 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);
}
}
}
@@ -496,19 +641,26 @@ namespace WinSW.Configuration
public string LogMode => this.Log.Mode is null ? this.Defaults.LogMode : this.Log.Mode;
- // TODO
+ // TODO - Extensions
XmlNode? IWinSWConfiguration.ExtensionsConfiguration => throw new NotImplementedException();
- public List ExtensionIds => throw new NotImplementedException();
+ public List ExtensionIds => this.YamlExtensionIds ?? this.Defaults.ExtensionIds;
- public string BaseName => throw new NotImplementedException();
+ public string BaseName => this.Defaults.BaseName;
- public string BasePath => throw new NotImplementedException();
+ public string BasePath => this.Defaults.BasePath;
- public string? ServiceAccountDomain => throw new NotImplementedException();
+ public string? SecurityDescriptor
+ {
+ get
+ {
+ if (this.SecurityDescriptorYaml is null)
+ {
+ return this.Defaults.SecurityDescriptor;
+ }
- public string? ServiceAccountName => throw new NotImplementedException();
-
- public string? SecurityDescriptor => throw new NotImplementedException();
+ return ExpandEnv(this.SecurityDescriptorYaml);
+ }
+ }
}
}
diff --git a/src/Core/WinSWCore/ServiceDescriptor.cs b/src/Core/WinSWCore/ServiceDescriptor.cs
index 849c7ce..4d31d83 100644
--- a/src/Core/WinSWCore/ServiceDescriptor.cs
+++ b/src/Core/WinSWCore/ServiceDescriptor.cs
@@ -150,38 +150,9 @@ namespace WinSW
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 Suffix = new Dictionary
- {
- { "ms", 1 },
- { "sec", 1000L },
- { "secs", 1000L },
- { "min", 1000L * 60L },
- { "mins", 1000L * 60L },
- { "hr", 1000L * 60L * 60L },
- { "hrs", 1000L * 60L * 60L },
- { "hour", 1000L * 60L * 60L },
- { "hours", 1000L * 60L * 60L },
- { "day", 1000L * 60L * 60L * 24L },
- { "days", 1000L * 60L * 60L * 24L }
- };
-
///
/// Path to the executable.
///
@@ -647,7 +618,7 @@ 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;
diff --git a/src/Core/WinSWCore/ServiceDescriptorYaml.cs b/src/Core/WinSWCore/ServiceDescriptorYaml.cs
index aa47d5e..0dec860 100644
--- a/src/Core/WinSWCore/ServiceDescriptorYaml.cs
+++ b/src/Core/WinSWCore/ServiceDescriptorYaml.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.IO;
using WinSW.Configuration;
using YamlDotNet.Serialization;
@@ -11,13 +11,9 @@ namespace WinSW
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 p = Defaults.ExecutablePath;
string baseName = Path.GetFileNameWithoutExtension(p);
if (baseName.EndsWith(".vshost"))
{
@@ -40,9 +36,9 @@ namespace WinSW
d = d.Parent;
}
- this.BasePath = Path.Combine(d.FullName, baseName);
+ var basepath = Path.Combine(d.FullName, baseName);
- using (var reader = new StreamReader(this.BasePath + ".yml"))
+ using (var reader = new StreamReader(basepath + ".yml"))
{
var file = reader.ReadToEnd();
var deserializer = new DeserializerBuilder().Build();
@@ -56,7 +52,7 @@ namespace WinSW
Environment.SetEnvironmentVariable("SERVICE_ID", this.Configurations.Id);
// New name
- Environment.SetEnvironmentVariable(WinSWSystem.EnvVarNameExecutablePath, this.ExecutablePath);
+ Environment.SetEnvironmentVariable(WinSWSystem.EnvVarNameExecutablePath, Defaults.ExecutablePath);
// Also inject system environment variables
Environment.SetEnvironmentVariable(WinSWSystem.EnvVarNameServiceId, this.Configurations.Id);
diff --git a/src/Core/WinSWCore/Util/ConfigHelper.cs b/src/Core/WinSWCore/Util/ConfigHelper.cs
new file mode 100644
index 0000000..8ca87fa
--- /dev/null
+++ b/src/Core/WinSWCore/Util/ConfigHelper.cs
@@ -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 Suffix = new Dictionary
+ {
+ { "ms", 1 },
+ { "sec", 1000L },
+ { "secs", 1000L },
+ { "min", 1000L * 60L },
+ { "mins", 1000L * 60L },
+ { "hr", 1000L * 60L * 60L },
+ { "hrs", 1000L * 60L * 60L },
+ { "hour", 1000L * 60L * 60L },
+ { "hours", 1000L * 60L * 60L },
+ { "day", 1000L * 60L * 60L * 24L },
+ { "days", 1000L * 60L * 60L * 24L }
+ };
+ }
+}
diff --git a/src/Test/winswTests/ServiceDescriptorYamlTest.cs b/src/Test/winswTests/ServiceDescriptorYamlTest.cs
index b442ebc..c2ca3f2 100644
--- a/src/Test/winswTests/ServiceDescriptorYamlTest.cs
+++ b/src/Test/winswTests/ServiceDescriptorYamlTest.cs
@@ -1,54 +1,46 @@
using System;
using NUnit.Framework;
using WinSW;
+using WinSW.Configuration;
+using WinSW.Native;
namespace winswTests
{
class ServiceDescriptorYamlTest
{
- private string MinimalYaml = @"id: myapp
-caption: This is a test
+ 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 Simple_yaml_parsing_test()
+ public void Parse_must_implemented_value_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
+ var yml = @"name: This is a test
executable: 'C:\Program Files\Java\jdk1.8.0_241\bin\java.exe'
description: This is test winsw";
- void getId()
+ Assert.That(() =>
{
- var id = ServiceDescriptorYaml.FromYaml(yml).Configurations.Id;
- }
-
- Assert.That(() => getId(), Throws.TypeOf());
+ _ = ServiceDescriptorYaml.FromYaml(yml).Configurations.Id;
+ }, Throws.TypeOf());
}
[Test]
public void Default_value_map_test()
{
- var executablePath = ServiceDescriptorYaml.FromYaml(MinimalYaml).Configurations.ExecutablePath;
+ var configs = ServiceDescriptorYaml.FromYaml(MinimalYaml).Configurations;
- Assert.IsNotNull(executablePath);
+ Assert.IsNotNull(configs.ExecutablePath);
+ Assert.IsNotNull(configs.BaseName);
+ Assert.IsNotNull(configs.BasePath);
}
[Test]
- public void Simple_download_parsing_test()
+ public void Parse_downloads()
{
var yml = @"download:
-
@@ -64,56 +56,94 @@ description: This is test winsw";
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 Download_not_specified_test()
+ public void Parse_serviceaccount()
{
- 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
+ var yml = @"id: myapp
+name: winsw
+description: yaml test
+executable: java
serviceaccount:
- user: testuser
-";
+ user: testuser
+ domain: mydomain
+ password: pa55w0rd
+ allowservicelogon: yes";
- var configs = ServiceDescriptorYaml.FromYaml(yml).Configurations;
+ 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)));
- Assert.DoesNotThrow(() =>
- {
- var user = configs.ServiceAccount.ServiceAccountUser;
- var password = configs.ServiceAccount.ServiceAccountPassword;
- var allowLogon = configs.ServiceAccount.AllowServiceAcountLogonRight;
- var hasAccount = configs.ServiceAccount.HasServiceAccount();
- });
}
}
}