From 13cfe45490f06c6ef13b45564c91dd3b6f53a238 Mon Sep 17 00:00:00 2001 From: Buddhika Chathuranga Date: Wed, 29 Jul 2020 17:30:39 +0530 Subject: [PATCH] Update Yaml Configuration support (#596) * Sync with upstream (#3) * Add a Dependabot configuration (#558) * Add a Dependabot configuration * Update dependabot.yml * Bump NUnit3TestAdapter from 3.16.0 to 3.16.1 (#561) Bumps [NUnit3TestAdapter](https://github.com/nunit/nunit3-vs-adapter) from 3.16.0 to 3.16.1. - [Release notes](https://github.com/nunit/nunit3-vs-adapter/releases) - [Commits](https://github.com/nunit/nunit3-vs-adapter/compare/V3.16...V3.16.1) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump Microsoft.NET.Test.Sdk from 16.4.0 to 16.6.1 (#563) Bumps [Microsoft.NET.Test.Sdk](https://github.com/microsoft/vstest) from 16.4.0 to 16.6.1. - [Release notes](https://github.com/microsoft/vstest/releases) - [Commits](https://github.com/microsoft/vstest/compare/v16.4.0...v16.6.1) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump ilmerge from 3.0.29 to 3.0.40 (#559) * Bump ilmerge from 3.0.29 to 3.0.40 Bumps [ilmerge](https://github.com/dotnet/ILMerge) from 3.0.29 to 3.0.40. - [Release notes](https://github.com/dotnet/ILMerge/releases) - [Commits](https://github.com/dotnet/ILMerge/commits) Signed-off-by: dependabot[bot] * Define $(ILMergeVersion) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Next Turn <45985406+NextTurn@users.noreply.github.com> * Bump coverlet.collector from 1.2.0 to 1.3.0 (#560) Bumps [coverlet.collector](https://github.com/coverlet-coverage/coverlet) from 1.2.0 to 1.3.0. - [Release notes](https://github.com/coverlet-coverage/coverlet/releases) - [Commits](https://github.com/coverlet-coverage/coverlet/commits) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump ilmerge from 3.0.40 to 3.0.41 (#571) Bumps [ilmerge](https://github.com/dotnet/ILMerge) from 3.0.40 to 3.0.41. - [Release notes](https://github.com/dotnet/ILMerge/releases) - [Commits](https://github.com/dotnet/ILMerge/commits) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Oleg Nenashev Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Next Turn <45985406+NextTurn@users.noreply.github.com> * Update WinSW Yaml Support Add sample-allOption.yml Update timespan values to aprse from strings. Now user can specify timespan values like 5sec. Implement unimplemented properties in YmlConfiguration * Update configurations namings * Move ParseTimeSpan to seperate utility class * Update Environment Variable Syntax Now env variables support name: value: syntax * Add YamlConfigFile.md Add yaml user documentation Update YamlSupport Unit test * Update YamlConfigFile.md Summerize the yaml documentation and reference the XmlConfigFile.md for more details * Update YamlDoc * Update doc/YamlConfigFile.md Co-authored-by: Next Turn <45985406+NextTurn@users.noreply.github.com> * Update yaml configurations to expand environment variables * Update doc/YamlConfigFile.md Co-authored-by: Next Turn <45985406+NextTurn@users.noreply.github.com> Co-authored-by: Oleg Nenashev Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Next Turn <45985406+NextTurn@users.noreply.github.com> --- doc/YamlConfigFile.md | 304 ++++++++++++++++++ examples/sample-allOption.yml | 75 +++++ src/Core/ServiceWrapper/winsw.csproj | 1 + .../Configuration/IWinSWConfiguration.cs | 1 - .../WinSWCore/Configuration/ServiceAccount.cs | 4 +- .../Configuration/YamlConfiguration.cs | 264 +++++++++++---- src/Core/WinSWCore/ServiceDescriptor.cs | 33 +- src/Core/WinSWCore/ServiceDescriptorYaml.cs | 14 +- src/Core/WinSWCore/Util/ConfigHelper.cs | 37 +++ .../winswTests/ServiceDescriptorYamlTest.cs | 162 ++++++---- 10 files changed, 730 insertions(+), 165 deletions(-) create mode 100644 doc/YamlConfigFile.md create mode 100644 examples/sample-allOption.yml create mode 100644 src/Core/WinSWCore/Util/ConfigHelper.cs 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(); - }); } } }