diff --git a/.github/codecov.yml b/.github/codecov.yml new file mode 100644 index 0000000..35cde5c --- /dev/null +++ b/.github/codecov.yml @@ -0,0 +1,4 @@ +coverage: + status: + project: off + patch: off 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/Program.cs b/src/Core/ServiceWrapper/Program.cs index 661a2f5..05152e8 100644 --- a/src/Core/ServiceWrapper/Program.cs +++ b/src/Core/ServiceWrapper/Program.cs @@ -18,9 +18,9 @@ using log4net.Appender; using log4net.Config; using log4net.Core; using log4net.Layout; +using WinSW.Configuration; using WinSW.Logging; using WinSW.Native; -using WinSW.Util; using WMI; using ServiceType = WMI.ServiceType; @@ -59,7 +59,7 @@ namespace WinSW } } - public static void Run(string[] argsArray, ServiceDescriptor? descriptor = null) + public static void Run(string[] argsArray, IWinSWConfiguration? descriptor = null) { bool inConsoleMode = argsArray.Length > 0; @@ -181,7 +181,9 @@ namespace WinSW default: Console.WriteLine("Unknown command: " + args[0]); PrintAvailableCommands(); +#pragma warning disable S112 // General exceptions should never be thrown throw new Exception("Unknown command: " + args[0]); +#pragma warning restore S112 // General exceptions should never be thrown } void Install() @@ -199,7 +201,9 @@ namespace WinSW { Console.WriteLine("Service with id '" + descriptor.Id + "' already exists"); Console.WriteLine("To install the service, delete the existing one or change service Id in the configuration file"); +#pragma warning disable S112 // General exceptions should never be thrown throw new Exception("Installation failure: Service with id '" + descriptor.Id + "' already exists"); +#pragma warning restore S112 // General exceptions should never be thrown } string? username = null; @@ -222,17 +226,17 @@ namespace WinSW } else { - if (descriptor.HasServiceAccount()) + if (descriptor.ServiceAccount.HasServiceAccount()) { - username = descriptor.ServiceAccountUser; - password = descriptor.ServiceAccountPassword; - allowServiceLogonRight = descriptor.AllowServiceAcountLogonRight; + username = descriptor.ServiceAccount.ServiceAccountUser; + password = descriptor.ServiceAccount.ServiceAccountPassword; + allowServiceLogonRight = descriptor.ServiceAccount.AllowServiceAcountLogonRight; } } if (allowServiceLogonRight) { - Security.AddServiceLogonRight(descriptor.ServiceAccountDomain!, descriptor.ServiceAccountName!); + Security.AddServiceLogonRight(descriptor.ServiceAccount.ServiceAccountDomain!, descriptor.ServiceAccount.ServiceAccountName!); } svcs.Create( @@ -319,7 +323,7 @@ namespace WinSW Log.Fatal("Failed to uninstall the service with id '" + descriptor.Id + "'. WMI Error code is '" + e.ErrorCode + "'"); } - throw e; + throw; } } @@ -455,14 +459,18 @@ namespace WinSW bool result = ProcessApis.CreateProcess(null, descriptor.ExecutablePath + " restart", IntPtr.Zero, IntPtr.Zero, false, ProcessApis.CREATE_NEW_PROCESS_GROUP, IntPtr.Zero, null, default, out _); if (!result) { +#pragma warning disable S112 // General exceptions should never be thrown throw new Exception("Failed to invoke restart: " + Marshal.GetLastWin32Error()); +#pragma warning restore S112 // General exceptions should never be thrown } } void Status() { Log.Debug("User requested the status of the process with id '" + descriptor.Id + "'"); +#pragma warning disable S3358 // Ternary operators should not be nested Console.WriteLine(svc is null ? "NonExistent" : svc.Started ? "Started" : "Stopped"); +#pragma warning restore S3358 // Ternary operators should not be nested } void Test() @@ -532,7 +540,7 @@ namespace WinSW [DoesNotReturn] private static void ThrowNoSuchService() => throw new WmiException(ReturnValue.NoSuchService); - private static void InitLoggers(ServiceDescriptor descriptor, bool enableConsoleLogging) + private static void InitLoggers(IWinSWConfiguration descriptor, bool enableConsoleLogging) { // TODO: Make logging levels configurable Level fileLogLevel = Level.Debug; diff --git a/src/Core/ServiceWrapper/WrapperService.cs b/src/Core/ServiceWrapper/WrapperService.cs index a8a3ed8..5b53bba 100644 --- a/src/Core/ServiceWrapper/WrapperService.cs +++ b/src/Core/ServiceWrapper/WrapperService.cs @@ -10,6 +10,7 @@ using System.Text.RegularExpressions; using System.Threading.Tasks; #endif using log4net; +using WinSW.Configuration; using WinSW.Extensions; using WinSW.Logging; using WinSW.Native; @@ -20,9 +21,11 @@ namespace WinSW public class WrapperService : ServiceBase, IEventLogger { private ServiceApis.SERVICE_STATUS wrapperServiceStatus; - + private readonly Process process = new Process(); - private readonly ServiceDescriptor descriptor; + + private readonly IWinSWConfiguration descriptor; + private Dictionary? envs; internal WinSWExtensionManager ExtensionManager { get; private set; } @@ -55,7 +58,7 @@ namespace WinSW /// public bool IsShuttingDown => this.systemShuttingdown; - public WrapperService(ServiceDescriptor descriptor) + public WrapperService(IWinSWConfiguration descriptor) { this.descriptor = descriptor; this.ServiceName = this.descriptor.Id; @@ -131,14 +134,15 @@ namespace WinSW /// Log Handler, which should be used for the spawned process private LogHandler CreateExecutableLogHandler() { - string logDirectory = this.descriptor.LogDirectory; + string? logDirectory = this.descriptor.LogDirectory; if (!Directory.Exists(logDirectory)) { Directory.CreateDirectory(logDirectory); } - LogHandler logAppender = this.descriptor.LogHandler; + LogHandler logAppender = this.descriptor.Log.CreateLogHandler(); + logAppender.EventLogger = this; return logAppender; } diff --git a/src/Core/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/DefaultSettings.cs b/src/Core/WinSWCore/Configuration/DefaultSettings.cs index 845b505..43d0a7c 100644 --- a/src/Core/WinSWCore/Configuration/DefaultSettings.cs +++ b/src/Core/WinSWCore/Configuration/DefaultSettings.cs @@ -12,6 +12,8 @@ namespace WinSW.Configuration /// public sealed class DefaultWinSWSettings : IWinSWConfiguration { + public static LogDefaults DefaultLogSettings { get; } = new LogDefaults(); + public string Id => throw new InvalidOperationException(nameof(this.Id) + " must be specified."); public string Caption => throw new InvalidOperationException(nameof(this.Caption) + " must be specified."); @@ -25,12 +27,6 @@ namespace WinSW.Configuration public string ExecutablePath => Process.GetCurrentProcess().MainModule.FileName; // Installation - public bool AllowServiceAcountLogonRight => false; - - public string? ServiceAccountPassword => null; - - public string? ServiceAccountUser => null; - public Native.SC_ACTION[] FailureActions => new Native.SC_ACTION[0]; public TimeSpan ResetFailureAfter => TimeSpan.FromDays(1); @@ -66,17 +62,67 @@ namespace WinSW.Configuration public bool Interactive => false; // Logging - public string LogDirectory => Path.GetDirectoryName(this.ExecutablePath)!; + public Log Log { get => new LogDefaults(); } - public string LogMode => "append"; + public string LogDirectory => DefaultLogSettings.Directory; - public bool OutFileDisabled => false; + public string LogMode => DefaultLogSettings.Mode; - public bool ErrFileDisabled => false; + public bool OutFileDisabled => this.Log.OutFileDisabled; - public string OutFilePattern => ".out.log"; + public bool ErrFileDisabled => this.Log.ErrFileDisabled; - public string ErrFilePattern => ".err.log"; + public string OutFilePattern => this.Log.OutFilePattern; + + public string ErrFilePattern => this.Log.ErrFilePattern; + + public ServiceAccount ServiceAccount => new ServiceAccount() + { + ServiceAccountName = null, + ServiceAccountDomain = null, + ServiceAccountPassword = null, + AllowServiceAcountLogonRight = false + }; + + public class LogDefaults : Log + { + private readonly DefaultWinSWSettings defaults; + + public LogDefaults() + { + this.defaults = new DefaultWinSWSettings(); + } + + public override string Mode => "append"; + + public override string Name => this.defaults.BaseName; + + public override string Directory => Path.GetDirectoryName(this.defaults.ExecutablePath)!; + + public override int? SizeThreshold => 1024 * 10 * RollingSizeTimeLogAppender.BytesPerKB; + + public override int? KeepFiles => SizeBasedRollingLogAppender.DefaultFilesToKeep; + + public override string Pattern => + throw new InvalidDataException("Time Based rolling policy is specified but no pattern can be found in configuration XML."); + + public override int? Period => 1; + + public override bool OutFileDisabled { get => false; } + + public override bool ErrFileDisabled { get => false; } + + public override string OutFilePattern { get => ".out.log"; } + + public override string ErrFilePattern { get => ".err.log"; } + + public override string? AutoRollAtTime => null; + + public override int? ZipOlderThanNumDays => + throw new InvalidDataException("Roll-Size-Time Based rolling policy is specified but zipOlderThanNumDays does not match the int format found in configuration XML."); + + public override string? ZipDateFormat => null; + } // Environment public List Downloads => new List(0); @@ -88,5 +134,32 @@ namespace WinSW.Configuration // Extensions public XmlNode? ExtensionsConfiguration => null; + + public string BaseName + { + get + { + string baseName = Path.GetFileNameWithoutExtension(this.ExecutablePath); + if (baseName.EndsWith(".vshost")) + { + baseName = baseName.Substring(0, baseName.Length - 7); + } + + return baseName; + } + } + + public string BasePath + { + get + { + var d = new DirectoryInfo(Path.GetDirectoryName(this.ExecutablePath)); + return Path.Combine(d.FullName, this.BaseName); + } + } + + public List ExtensionIds => new List(0); + + public string? SecurityDescriptor => null; } } diff --git a/src/Core/WinSWCore/Configuration/IWinSWConfiguration.cs b/src/Core/WinSWCore/Configuration/IWinSWConfiguration.cs index 0cd85ee..1b21450 100644 --- a/src/Core/WinSWCore/Configuration/IWinSWConfiguration.cs +++ b/src/Core/WinSWCore/Configuration/IWinSWConfiguration.cs @@ -22,12 +22,6 @@ namespace WinSW.Configuration bool HideWindow { get; } // Installation - bool AllowServiceAcountLogonRight { get; } - - string? ServiceAccountPassword { get; } - - string? ServiceAccountUser { get; } - Native.SC_ACTION[] FailureActions { get; } TimeSpan ResetFailureAfter { get; } @@ -60,12 +54,17 @@ namespace WinSW.Configuration bool Interactive { get; } - // Logging + /// + /// Destination for logging. + /// If undefined, a default one should be used. + /// string LogDirectory { get; } // TODO: replace by enum string LogMode { get; } + Log Log { get; } + // Environment List Downloads { get; } @@ -76,5 +75,18 @@ namespace WinSW.Configuration // Extensions XmlNode? ExtensionsConfiguration { get; } + + List ExtensionIds { get; } + + // Service Account + ServiceAccount ServiceAccount { get; } + + string BaseName { get; } + + string BasePath { get; } + + bool DelayedAutoStart { get; } + + string? SecurityDescriptor { get; } } } diff --git a/src/Core/WinSWCore/Configuration/Log.cs b/src/Core/WinSWCore/Configuration/Log.cs new file mode 100644 index 0000000..a73dff6 --- /dev/null +++ b/src/Core/WinSWCore/Configuration/Log.cs @@ -0,0 +1,107 @@ +using System; +using System.IO; + +namespace WinSW.Configuration +{ + public abstract class Log + { + public abstract string? Mode { get; } + + public abstract string Name { get; } + + public abstract string Directory { get; } + + public abstract int? SizeThreshold { get; } + + public abstract int? KeepFiles { get; } + + public abstract string Pattern { get; } + + public abstract int? Period { get; } + + // Filters + public abstract bool OutFileDisabled { get; } + + public abstract bool ErrFileDisabled { get; } + + public abstract string OutFilePattern { get; } + + public abstract string ErrFilePattern { get; } + + // Zip options + public abstract string? AutoRollAtTime { get; } + + public abstract int? ZipOlderThanNumDays { get; } + + public abstract string? ZipDateFormat { get; } + + public LogHandler CreateLogHandler() + { + switch (this.Mode) + { + case "rotate": + return new SizeBasedRollingLogAppender(this.Directory, this.Name, this.OutFileDisabled, this.ErrFileDisabled, this.OutFilePattern, this.ErrFilePattern); + + case "none": + return new IgnoreLogAppender(); + + case "reset": + return new ResetLogAppender(this.Directory, this.Name, this.OutFileDisabled, this.ErrFileDisabled, this.OutFilePattern, this.ErrFilePattern); + + case "roll": + return new RollingLogAppender(this.Directory, this.Name, this.OutFileDisabled, this.ErrFileDisabled, this.OutFilePattern, this.ErrFilePattern); + + case "roll-by-time": + return new TimeBasedRollingLogAppender(this.Directory, this.Name, this.OutFileDisabled, this.ErrFileDisabled, this.OutFilePattern, this.ErrFilePattern, this.Pattern, this.Period.GetValueOrDefault(1)); + + case "roll-by-size": + return new SizeBasedRollingLogAppender( + this.Directory, + this.Name, + this.OutFileDisabled, + this.ErrFileDisabled, + this.OutFilePattern, + this.ErrFilePattern, + this.SizeThreshold.GetValueOrDefault(10 * 1024) * SizeBasedRollingLogAppender.BytesPerKB, + this.KeepFiles.GetValueOrDefault(SizeBasedRollingLogAppender.DefaultFilesToKeep)); + + case "append": + return new DefaultLogAppender(this.Directory, this.Name, this.OutFileDisabled, this.ErrFileDisabled, this.OutFilePattern, this.ErrFilePattern); + + case "roll-by-size-time": + if (this.Pattern is null) + { + throw new InvalidDataException("Roll-Size-Time Based rolling policy is specified but no pattern can be found in configuration"); + } + + TimeSpan? autoRollAtTime = null; + if (this.AutoRollAtTime != null) + { + // validate it + if (!TimeSpan.TryParse(this.AutoRollAtTime, out TimeSpan autoRollAtTimeValue)) + { + throw new InvalidDataException("Roll-Size-Time Based rolling policy is specified but autoRollAtTime does not match the TimeSpan format HH:mm:ss found in configuration XML."); + } + + autoRollAtTime = autoRollAtTimeValue; + } + + return new RollingSizeTimeLogAppender( + this.Directory, + this.Name, + this.OutFileDisabled, + this.ErrFileDisabled, + this.OutFilePattern, + this.ErrFilePattern, + this.SizeThreshold.GetValueOrDefault(10 * 1024) * SizeBasedRollingLogAppender.BytesPerKB, + this.Pattern, + autoRollAtTime, + this.ZipOlderThanNumDays, + this.ZipDateFormat != null ? this.ZipDateFormat : "yyyyMM"); + + default: + throw new InvalidDataException("Undefined logging mode: " + this.Mode); + } + } + } +} \ No newline at end of file diff --git a/src/Core/WinSWCore/Configuration/ServiceAccount.cs b/src/Core/WinSWCore/Configuration/ServiceAccount.cs new file mode 100644 index 0000000..d44b366 --- /dev/null +++ b/src/Core/WinSWCore/Configuration/ServiceAccount.cs @@ -0,0 +1,29 @@ +using YamlDotNet.Serialization; + +namespace WinSW.Configuration +{ + public class ServiceAccount + { + [YamlMember(Alias = "user")] + public string? ServiceAccountName { get; set; } + + [YamlMember(Alias = "domain")] + public string? ServiceAccountDomain { get; set; } + + [YamlMember(Alias = "password")] + public string? ServiceAccountPassword { get; set; } + + [YamlMember(Alias = "allowservicelogon")] + public bool AllowServiceAcountLogonRight { get; set; } + + public string? ServiceAccountUser + { + get => this.ServiceAccountName is null ? null : (this.ServiceAccountDomain ?? ".") + "\\" + this.ServiceAccountName; + } + + public bool HasServiceAccount() + { + return !string.IsNullOrEmpty(this.ServiceAccountName); + } + } +} diff --git a/src/Core/WinSWCore/Configuration/YamlConfiguration.cs b/src/Core/WinSWCore/Configuration/YamlConfiguration.cs new file mode 100644 index 0000000..49e5610 --- /dev/null +++ b/src/Core/WinSWCore/Configuration/YamlConfiguration.cs @@ -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? 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? EnvironmentVariablesYaml { get; set; } + + [YamlMember(Alias = "onFailure")] + public List? YamlFailureActions { get; set; } + + [YamlMember(Alias = "delayedAutoStart")] + public bool DelayedAutoStart { get; set; } + + [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; + + 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 GetDownloads(List? downloads) + { + if (downloads is null) + { + return this.Defaults.Downloads; + } + + var result = new List(downloads.Count); + + foreach (var item in downloads) + { + result.Add(new Download( + item.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(); + + 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(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 Downloads => this.GetDownloads(this.DownloadsYaml); + + public Dictionary EnvironmentVariables { get; set; } = new Dictionary(); + + public void LoadEnvironmentVariables() + { + if (this.EnvironmentVariablesYaml is null) + { + this.EnvironmentVariables = this.Defaults.EnvironmentVariables; + } + else + { + foreach (var item in this.EnvironmentVariablesYaml) + { + 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 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); + } + } + } +} diff --git a/src/Core/WinSWCore/Download.cs b/src/Core/WinSWCore/Download.cs old mode 100755 new mode 100644 index dc70b2f..c1b8c9d --- a/src/Core/WinSWCore/Download.cs +++ b/src/Core/WinSWCore/Download.cs @@ -1,280 +1,280 @@ -using System; -using System.IO; -using System.Net; -#if !VNEXT -using System.Reflection; -#endif -using System.Text; -#if VNEXT -using System.Threading.Tasks; -#endif -using System.Xml; -using log4net; -using WinSW.Util; - -namespace WinSW -{ - /// - /// Specify the download activities prior to the launch. - /// This enables self-updating services. - /// - public class Download - { - public enum AuthType - { - None = 0, - Sspi, - Basic - } - - private static readonly ILog Logger = LogManager.GetLogger(typeof(Download)); - - public readonly string From; - public readonly string To; - public readonly AuthType Auth; - public readonly string? Username; - public readonly string? Password; - public readonly bool UnsecureAuth; - public readonly bool FailOnError; - public readonly string? Proxy; - - public string ShortId => $"(download from {this.From})"; - - static Download() - { -#if NET461 - // If your app runs on .NET Framework 4.7 or later versions, but targets an earlier version - AppContext.SetSwitch("Switch.System.Net.DontEnableSystemDefaultTlsVersions", false); -#elif !VNEXT - // If your app runs on .NET Framework 4.6, but targets an earlier version - Type.GetType("System.AppContext")?.InvokeMember("SetSwitch", BindingFlags.InvokeMethod | BindingFlags.Public | BindingFlags.Static, null, null, new object[] { "Switch.System.Net.DontEnableSchUseStrongCrypto", false }); - - const SecurityProtocolType Tls12 = (SecurityProtocolType)0x00000C00; - const SecurityProtocolType Tls11 = (SecurityProtocolType)0x00000300; - - // Windows 7 and Windows Server 2008 R2 - if (Environment.OSVersion.Version.Major == 6 && Environment.OSVersion.Version.Minor == 1) - { - try - { - ServicePointManager.SecurityProtocol |= Tls11 | Tls12; - Logger.Info("TLS 1.1/1.2 enabled"); - } - catch (NotSupportedException) - { - Logger.Info("TLS 1.1/1.2 disabled"); - } - } -#endif - } - - // internal - public Download( - string from, - string to, - bool failOnError = false, - AuthType auth = AuthType.None, - string? username = null, - string? password = null, - bool unsecureAuth = false, - string? proxy = null) - { - this.From = from; - this.To = to; - this.FailOnError = failOnError; - this.Proxy = proxy; - this.Auth = auth; - this.Username = username; - this.Password = password; - this.UnsecureAuth = unsecureAuth; - } - - /// - /// Constructs the download setting sfrom the XML entry - /// - /// XML element - /// The required attribute is missing or the configuration is invalid. - internal Download(XmlElement n) - { - this.From = XmlHelper.SingleAttribute(n, "from"); - this.To = XmlHelper.SingleAttribute(n, "to"); - - // All arguments below are optional - this.FailOnError = XmlHelper.SingleAttribute(n, "failOnError", false); - this.Proxy = XmlHelper.SingleAttribute(n, "proxy", null); - - this.Auth = XmlHelper.EnumAttribute(n, "auth", AuthType.None); - this.Username = XmlHelper.SingleAttribute(n, "user", null); - this.Password = XmlHelper.SingleAttribute(n, "password", null); - this.UnsecureAuth = XmlHelper.SingleAttribute(n, "unsecureAuth", false); - - if (this.Auth == AuthType.Basic) - { - // Allow it only for HTTPS or for UnsecureAuth - if (!this.From.StartsWith("https:") && !this.UnsecureAuth) - { - throw new InvalidDataException("Warning: you're sending your credentials in clear text to the server " + this.ShortId + - "If you really want this you must enable 'unsecureAuth' in the configuration"); - } - - // Also fail if there is no user/password - if (this.Username is null) - { - throw new InvalidDataException("Basic Auth is enabled, but username is not specified " + this.ShortId); - } - - if (this.Password is null) - { - throw new InvalidDataException("Basic Auth is enabled, but password is not specified " + this.ShortId); - } - } - } - - // Source: http://stackoverflow.com/questions/2764577/forcing-basic-authentication-in-webrequest - private void SetBasicAuthHeader(WebRequest request, string username, string password) - { - string authInfo = username + ":" + password; - authInfo = Convert.ToBase64String(Encoding.GetEncoding("ISO-8859-1").GetBytes(authInfo)); - request.Headers["Authorization"] = "Basic " + authInfo; - } - - /// - /// Downloads the requested file and puts it to the specified target. - /// - /// - /// Download failure. FailOnError flag should be processed outside. - /// -#if VNEXT - public async Task PerformAsync() -#else - public void Perform() -#endif - { - WebRequest request = WebRequest.Create(this.From); - if (!string.IsNullOrEmpty(this.Proxy)) - { - CustomProxyInformation proxyInformation = new CustomProxyInformation(this.Proxy!); - if (proxyInformation.Credentials != null) - { - request.Proxy = new WebProxy(proxyInformation.ServerAddress, false, null, proxyInformation.Credentials); - } - else - { - request.Proxy = new WebProxy(proxyInformation.ServerAddress); - } - } - - switch (this.Auth) - { - case AuthType.None: - // Do nothing - break; - - case AuthType.Sspi: - request.UseDefaultCredentials = true; - request.PreAuthenticate = true; - request.Credentials = CredentialCache.DefaultCredentials; - break; - - case AuthType.Basic: - this.SetBasicAuthHeader(request, this.Username!, this.Password!); - break; - - default: - throw new WebException("Code defect. Unsupported authentication type: " + this.Auth); - } - - bool supportsIfModifiedSince = false; - if (request is HttpWebRequest httpRequest && File.Exists(this.To)) - { - supportsIfModifiedSince = true; - httpRequest.IfModifiedSince = File.GetLastWriteTime(this.To); - } - - DateTime lastModified = default; - string tmpFilePath = this.To + ".tmp"; - try - { -#if VNEXT - using (WebResponse response = await request.GetResponseAsync()) -#else - using (WebResponse response = request.GetResponse()) -#endif - using (Stream responseStream = response.GetResponseStream()) - using (FileStream tmpStream = new FileStream(tmpFilePath, FileMode.Create)) - { - if (supportsIfModifiedSince) - { - lastModified = ((HttpWebResponse)response).LastModified; - } - -#if VNEXT - await responseStream.CopyToAsync(tmpStream); -#elif NET20 - CopyStream(responseStream, tmpStream); -#else - responseStream.CopyTo(tmpStream); -#endif - } - - FileHelper.MoveOrReplaceFile(this.To + ".tmp", this.To); - - if (supportsIfModifiedSince) - { - File.SetLastWriteTime(this.To, lastModified); - } - } - catch (WebException e) - { - if (supportsIfModifiedSince && ((HttpWebResponse)e.Response).StatusCode == HttpStatusCode.NotModified) - { - Logger.Info($"Skipped downloading unmodified resource '{this.From}'"); - } - else - { - throw; - } - } - } - -#if NET20 - private static void CopyStream(Stream source, Stream destination) - { - byte[] buffer = new byte[8192]; - int read; - while ((read = source.Read(buffer, 0, buffer.Length)) != 0) - { - destination.Write(buffer, 0, read); - } - } -#endif - } - - public class CustomProxyInformation - { - public string ServerAddress { get; set; } - - public NetworkCredential? Credentials { get; set; } - - public CustomProxyInformation(string proxy) - { - if (proxy.Contains("@")) - { - // Extract proxy credentials - int credsFrom = proxy.IndexOf("://") + 3; - int credsTo = proxy.LastIndexOf("@"); - string completeCredsStr = proxy.Substring(credsFrom, credsTo - credsFrom); - int credsSeparator = completeCredsStr.IndexOf(":"); - - string username = completeCredsStr.Substring(0, credsSeparator); - string password = completeCredsStr.Substring(credsSeparator + 1); - this.Credentials = new NetworkCredential(username, password); - this.ServerAddress = proxy.Replace(completeCredsStr + "@", string.Empty); - } - else - { - this.ServerAddress = proxy; - } - } - } -} +using System; +using System.IO; +using System.Net; +#if !VNEXT +using System.Reflection; +#endif +using System.Text; +#if VNEXT +using System.Threading.Tasks; +#endif +using System.Xml; +using log4net; +using WinSW.Util; + +namespace WinSW +{ + /// + /// Specify the download activities prior to the launch. + /// This enables self-updating services. + /// + public class Download + { + public enum AuthType + { + None = 0, + Sspi, + Basic + } + + private static readonly ILog Logger = LogManager.GetLogger(typeof(Download)); + + public readonly string From; + public readonly string To; + public readonly AuthType Auth; + public readonly string? Username; + public readonly string? Password; + public readonly bool UnsecureAuth; + public readonly bool FailOnError; + public readonly string? Proxy; + + public string ShortId => $"(download from {this.From})"; + + static Download() + { +#if NET461 + // If your app runs on .NET Framework 4.7 or later versions, but targets an earlier version + AppContext.SetSwitch("Switch.System.Net.DontEnableSystemDefaultTlsVersions", false); +#elif !VNEXT + // If your app runs on .NET Framework 4.6, but targets an earlier version + Type.GetType("System.AppContext")?.InvokeMember("SetSwitch", BindingFlags.InvokeMethod | BindingFlags.Public | BindingFlags.Static, null, null, new object[] { "Switch.System.Net.DontEnableSchUseStrongCrypto", false }); + + const SecurityProtocolType Tls12 = (SecurityProtocolType)0x00000C00; + const SecurityProtocolType Tls11 = (SecurityProtocolType)0x00000300; + + // Windows 7 and Windows Server 2008 R2 + if (Environment.OSVersion.Version.Major == 6 && Environment.OSVersion.Version.Minor == 1) + { + try + { + ServicePointManager.SecurityProtocol |= Tls11 | Tls12; + Logger.Info("TLS 1.1/1.2 enabled"); + } + catch (NotSupportedException) + { + Logger.Info("TLS 1.1/1.2 disabled"); + } + } +#endif + } + + // internal + public Download( + string from, + string to, + bool failOnError = false, + AuthType auth = AuthType.None, + string? username = null, + string? password = null, + bool unsecureAuth = false, + string? proxy = null) + { + this.From = from; + this.To = to; + this.FailOnError = failOnError; + this.Proxy = proxy; + this.Auth = auth; + this.Username = username; + this.Password = password; + this.UnsecureAuth = unsecureAuth; + } + + /// + /// Constructs the download setting sfrom the XML entry + /// + /// XML element + /// The required attribute is missing or the configuration is invalid. + internal Download(XmlElement n) + { + this.From = XmlHelper.SingleAttribute(n, "from"); + this.To = XmlHelper.SingleAttribute(n, "to"); + + // All arguments below are optional + this.FailOnError = XmlHelper.SingleAttribute(n, "failOnError", false); + this.Proxy = XmlHelper.SingleAttribute(n, "proxy", null); + + this.Auth = XmlHelper.EnumAttribute(n, "auth", AuthType.None); + this.Username = XmlHelper.SingleAttribute(n, "user", null); + this.Password = XmlHelper.SingleAttribute(n, "password", null); + this.UnsecureAuth = XmlHelper.SingleAttribute(n, "unsecureAuth", false); + + if (this.Auth == AuthType.Basic) + { + // Allow it only for HTTPS or for UnsecureAuth + if (!this.From.StartsWith("https:") && !this.UnsecureAuth) + { + throw new InvalidDataException("Warning: you're sending your credentials in clear text to the server " + this.ShortId + + "If you really want this you must enable 'unsecureAuth' in the configuration"); + } + + // Also fail if there is no user/password + if (this.Username is null) + { + throw new InvalidDataException("Basic Auth is enabled, but username is not specified " + this.ShortId); + } + + if (this.Password is null) + { + throw new InvalidDataException("Basic Auth is enabled, but password is not specified " + this.ShortId); + } + } + } + + // Source: http://stackoverflow.com/questions/2764577/forcing-basic-authentication-in-webrequest + private void SetBasicAuthHeader(WebRequest request, string username, string password) + { + string authInfo = username + ":" + password; + authInfo = Convert.ToBase64String(Encoding.GetEncoding("ISO-8859-1").GetBytes(authInfo)); + request.Headers["Authorization"] = "Basic " + authInfo; + } + + /// + /// Downloads the requested file and puts it to the specified target. + /// + /// + /// Download failure. FailOnError flag should be processed outside. + /// +#if VNEXT + public async Task PerformAsync() +#else + public void Perform() +#endif + { + WebRequest request = WebRequest.Create(this.From); + if (!string.IsNullOrEmpty(this.Proxy)) + { + CustomProxyInformation proxyInformation = new CustomProxyInformation(this.Proxy!); + if (proxyInformation.Credentials != null) + { + request.Proxy = new WebProxy(proxyInformation.ServerAddress, false, null, proxyInformation.Credentials); + } + else + { + request.Proxy = new WebProxy(proxyInformation.ServerAddress); + } + } + + switch (this.Auth) + { + case AuthType.None: + // Do nothing + break; + + case AuthType.Sspi: + request.UseDefaultCredentials = true; + request.PreAuthenticate = true; + request.Credentials = CredentialCache.DefaultCredentials; + break; + + case AuthType.Basic: + this.SetBasicAuthHeader(request, this.Username!, this.Password!); + break; + + default: + throw new WebException("Code defect. Unsupported authentication type: " + this.Auth); + } + + bool supportsIfModifiedSince = false; + if (request is HttpWebRequest httpRequest && File.Exists(this.To)) + { + supportsIfModifiedSince = true; + httpRequest.IfModifiedSince = File.GetLastWriteTime(this.To); + } + + DateTime lastModified = default; + string tmpFilePath = this.To + ".tmp"; + try + { +#if VNEXT + using (WebResponse response = await request.GetResponseAsync()) +#else + using (WebResponse response = request.GetResponse()) +#endif + using (Stream responseStream = response.GetResponseStream()) + using (FileStream tmpStream = new FileStream(tmpFilePath, FileMode.Create)) + { + if (supportsIfModifiedSince) + { + lastModified = ((HttpWebResponse)response).LastModified; + } + +#if VNEXT + await responseStream.CopyToAsync(tmpStream); +#elif NET20 + CopyStream(responseStream, tmpStream); +#else + responseStream.CopyTo(tmpStream); +#endif + } + + FileHelper.MoveOrReplaceFile(this.To + ".tmp", this.To); + + if (supportsIfModifiedSince) + { + File.SetLastWriteTime(this.To, lastModified); + } + } + catch (WebException e) + { + if (supportsIfModifiedSince && ((HttpWebResponse)e.Response).StatusCode == HttpStatusCode.NotModified) + { + Logger.Info($"Skipped downloading unmodified resource '{this.From}'"); + } + else + { + throw; + } + } + } + +#if NET20 + private static void CopyStream(Stream source, Stream destination) + { + byte[] buffer = new byte[8192]; + int read; + while ((read = source.Read(buffer, 0, buffer.Length)) != 0) + { + destination.Write(buffer, 0, read); + } + } +#endif + } + + public class CustomProxyInformation + { + public string ServerAddress { get; set; } + + public NetworkCredential? Credentials { get; set; } + + public CustomProxyInformation(string proxy) + { + if (proxy.Contains("@")) + { + // Extract proxy credentials + int credsFrom = proxy.IndexOf("://") + 3; + int credsTo = proxy.LastIndexOf("@"); + string completeCredsStr = proxy.Substring(credsFrom, credsTo - credsFrom); + int credsSeparator = completeCredsStr.IndexOf(":"); + + string username = completeCredsStr.Substring(0, credsSeparator); + string password = completeCredsStr.Substring(credsSeparator + 1); + this.Credentials = new NetworkCredential(username, password); + this.ServerAddress = proxy.Replace(completeCredsStr + "@", string.Empty); + } + else + { + this.ServerAddress = proxy; + } + } + } +} diff --git a/src/Core/WinSWCore/DynamicProxy.cs b/src/Core/WinSWCore/DynamicProxy.cs index d1ba71a..283d758 100644 --- a/src/Core/WinSWCore/DynamicProxy.cs +++ b/src/Core/WinSWCore/DynamicProxy.cs @@ -1,205 +1,205 @@ -using System; -using System.Collections.Generic; -using System.Reflection; -using System.Reflection.Emit; - -namespace DynamicProxy -{ - /// - /// Interface that a user defined proxy handler needs to implement. This interface - /// defines one method that gets invoked by the generated proxy. - /// - public interface IProxyInvocationHandler - { - /// The instance of the proxy - /// The method info that can be used to invoke the actual method on the object implementation - /// Parameters to pass to the method - /// Object - object? Invoke(object proxy, MethodInfo method, object[] parameters); - } - - /// - /// - public static class ProxyFactory - { - private const string ProxySuffix = "Proxy"; - private const string AssemblyName = "ProxyAssembly"; - private const string ModuleName = "ProxyModule"; - private const string HandlerName = "handler"; - - private static readonly Dictionary TypeCache = new Dictionary(); - - private static readonly AssemblyBuilder AssemblyBuilder = -#if VNEXT - AssemblyBuilder.DefineDynamicAssembly( -#else - AppDomain.CurrentDomain.DefineDynamicAssembly( -#endif - new AssemblyName(AssemblyName), AssemblyBuilderAccess.Run); - - private static readonly ModuleBuilder ModuleBuilder = AssemblyBuilder.DefineDynamicModule(ModuleName); - - public static object Create(IProxyInvocationHandler handler, Type objType, bool isObjInterface = false) - { - string typeName = objType.FullName + ProxySuffix; - Type? type = null; - lock (TypeCache) - { - if (!TypeCache.TryGetValue(typeName, out type)) - { - type = CreateType(typeName, isObjInterface ? new Type[] { objType } : objType.GetInterfaces()); - TypeCache.Add(typeName, type); - } - } - - return Activator.CreateInstance(type, new object[] { handler })!; - } - - private static Type CreateType(string dynamicTypeName, Type[] interfaces) - { - Type objType = typeof(object); - Type handlerType = typeof(IProxyInvocationHandler); - - TypeAttributes typeAttributes = TypeAttributes.Public | TypeAttributes.Sealed; - - // Gather up the proxy information and create a new type builder. One that - // inherits from Object and implements the interface passed in - TypeBuilder typeBuilder = ModuleBuilder.DefineType( - dynamicTypeName, typeAttributes, objType, interfaces); - - // Define a member variable to hold the delegate - FieldBuilder handlerField = typeBuilder.DefineField( - HandlerName, handlerType, FieldAttributes.Private | FieldAttributes.InitOnly); - - // build a constructor that takes the delegate object as the only argument - ConstructorInfo baseConstructor = objType.GetConstructor(Type.EmptyTypes)!; - ConstructorBuilder delegateConstructor = typeBuilder.DefineConstructor( - MethodAttributes.Public, CallingConventions.Standard, new Type[] { handlerType }); - - ILGenerator constructorIL = delegateConstructor.GetILGenerator(); - - // Load "this" - constructorIL.Emit(OpCodes.Ldarg_0); - - // Load first constructor parameter - constructorIL.Emit(OpCodes.Ldarg_1); - - // Set the first parameter into the handler field - constructorIL.Emit(OpCodes.Stfld, handlerField); - - // Load "this" - constructorIL.Emit(OpCodes.Ldarg_0); - - // Call the super constructor - constructorIL.Emit(OpCodes.Call, baseConstructor); - - // Constructor return - constructorIL.Emit(OpCodes.Ret); - - // for every method that the interfaces define, build a corresponding - // method in the dynamic type that calls the handlers invoke method. - foreach (Type interfaceType in interfaces) - { - GenerateMethod(interfaceType, handlerField, typeBuilder); - } - - return typeBuilder.CreateType()!; - } - - /// - /// . - /// - private static readonly MethodInfo InvokeMethod = typeof(IProxyInvocationHandler).GetMethod(nameof(IProxyInvocationHandler.Invoke))!; - - /// - /// . - /// - private static readonly MethodInfo GetMethodFromHandleMethod = typeof(MethodBase).GetMethod(nameof(MethodBase.GetMethodFromHandle), new[] { typeof(RuntimeMethodHandle) })!; - - private static void GenerateMethod(Type interfaceType, FieldBuilder handlerField, TypeBuilder typeBuilder) - { - MethodInfo[] interfaceMethods = interfaceType.GetMethods(); - - for (int i = 0; i < interfaceMethods.Length; i++) - { - MethodInfo methodInfo = interfaceMethods[i]; - - // Get the method parameters since we need to create an array - // of parameter types - ParameterInfo[] methodParams = methodInfo.GetParameters(); - int numOfParams = methodParams.Length; - Type[] methodParameters = new Type[numOfParams]; - - // convert the ParameterInfo objects into Type - for (int j = 0; j < numOfParams; j++) - { - methodParameters[j] = methodParams[j].ParameterType; - } - - // create a new builder for the method in the interface - MethodBuilder methodBuilder = typeBuilder.DefineMethod( - methodInfo.Name, - /*MethodAttributes.Public | MethodAttributes.Virtual | */ methodInfo.Attributes & ~MethodAttributes.Abstract, - CallingConventions.Standard, - methodInfo.ReturnType, - methodParameters); - - ILGenerator methodIL = methodBuilder.GetILGenerator(); - - // invoke target: IProxyInvocationHandler - methodIL.Emit(OpCodes.Ldarg_0); - methodIL.Emit(OpCodes.Ldfld, handlerField); - - // 1st parameter: object proxy - methodIL.Emit(OpCodes.Ldarg_0); - - // 2nd parameter: MethodInfo method - methodIL.Emit(OpCodes.Ldtoken, methodInfo); - methodIL.Emit(OpCodes.Call, GetMethodFromHandleMethod); - methodIL.Emit(OpCodes.Castclass, typeof(MethodInfo)); - - // 3rd parameter: object[] parameters - methodIL.Emit(OpCodes.Ldc_I4, numOfParams); - methodIL.Emit(OpCodes.Newarr, typeof(object)); - - // if we have any parameters, then iterate through and set the values - // of each element to the corresponding arguments - for (int j = 0; j < numOfParams; j++) - { - methodIL.Emit(OpCodes.Dup); // copy the array - methodIL.Emit(OpCodes.Ldc_I4, j); - methodIL.Emit(OpCodes.Ldarg, j + 1); // +1 for "this" - if (methodParameters[j].IsValueType) - { - methodIL.Emit(OpCodes.Box, methodParameters[j]); - } - - methodIL.Emit(OpCodes.Stelem_Ref); - } - - // call the Invoke method - methodIL.Emit(OpCodes.Callvirt, InvokeMethod); - - if (methodInfo.ReturnType != typeof(void)) - { - methodIL.Emit(OpCodes.Unbox_Any, methodInfo.ReturnType); - } - else - { - // pop the return value that Invoke returned from the stack since - // the method's return type is void. - methodIL.Emit(OpCodes.Pop); - } - - // Return - methodIL.Emit(OpCodes.Ret); - } - - // Iterate through the parent interfaces and recursively call this method - foreach (Type parentType in interfaceType.GetInterfaces()) - { - GenerateMethod(parentType, handlerField, typeBuilder); - } - } - } -} +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Reflection.Emit; + +namespace DynamicProxy +{ + /// + /// Interface that a user defined proxy handler needs to implement. This interface + /// defines one method that gets invoked by the generated proxy. + /// + public interface IProxyInvocationHandler + { + /// The instance of the proxy + /// The method info that can be used to invoke the actual method on the object implementation + /// Parameters to pass to the method + /// Object + object? Invoke(object proxy, MethodInfo method, object[] parameters); + } + + /// + /// + public static class ProxyFactory + { + private const string ProxySuffix = "Proxy"; + private const string AssemblyName = "ProxyAssembly"; + private const string ModuleName = "ProxyModule"; + private const string HandlerName = "handler"; + + private static readonly Dictionary TypeCache = new Dictionary(); + + private static readonly AssemblyBuilder AssemblyBuilder = +#if VNEXT + AssemblyBuilder.DefineDynamicAssembly( +#else + AppDomain.CurrentDomain.DefineDynamicAssembly( +#endif + new AssemblyName(AssemblyName), AssemblyBuilderAccess.Run); + + private static readonly ModuleBuilder ModuleBuilder = AssemblyBuilder.DefineDynamicModule(ModuleName); + + public static object Create(IProxyInvocationHandler handler, Type objType, bool isObjInterface = false) + { + string typeName = objType.FullName + ProxySuffix; + Type? type = null; + lock (TypeCache) + { + if (!TypeCache.TryGetValue(typeName, out type)) + { + type = CreateType(typeName, isObjInterface ? new Type[] { objType } : objType.GetInterfaces()); + TypeCache.Add(typeName, type); + } + } + + return Activator.CreateInstance(type, new object[] { handler })!; + } + + private static Type CreateType(string dynamicTypeName, Type[] interfaces) + { + Type objType = typeof(object); + Type handlerType = typeof(IProxyInvocationHandler); + + TypeAttributes typeAttributes = TypeAttributes.Public | TypeAttributes.Sealed; + + // Gather up the proxy information and create a new type builder. One that + // inherits from Object and implements the interface passed in + TypeBuilder typeBuilder = ModuleBuilder.DefineType( + dynamicTypeName, typeAttributes, objType, interfaces); + + // Define a member variable to hold the delegate + FieldBuilder handlerField = typeBuilder.DefineField( + HandlerName, handlerType, FieldAttributes.Private | FieldAttributes.InitOnly); + + // build a constructor that takes the delegate object as the only argument + ConstructorInfo baseConstructor = objType.GetConstructor(Type.EmptyTypes)!; + ConstructorBuilder delegateConstructor = typeBuilder.DefineConstructor( + MethodAttributes.Public, CallingConventions.Standard, new Type[] { handlerType }); + + ILGenerator constructorIL = delegateConstructor.GetILGenerator(); + + // Load "this" + constructorIL.Emit(OpCodes.Ldarg_0); + + // Load first constructor parameter + constructorIL.Emit(OpCodes.Ldarg_1); + + // Set the first parameter into the handler field + constructorIL.Emit(OpCodes.Stfld, handlerField); + + // Load "this" + constructorIL.Emit(OpCodes.Ldarg_0); + + // Call the super constructor + constructorIL.Emit(OpCodes.Call, baseConstructor); + + // Constructor return + constructorIL.Emit(OpCodes.Ret); + + // for every method that the interfaces define, build a corresponding + // method in the dynamic type that calls the handlers invoke method. + foreach (Type interfaceType in interfaces) + { + GenerateMethod(interfaceType, handlerField, typeBuilder); + } + + return typeBuilder.CreateType()!; + } + + /// + /// . + /// + private static readonly MethodInfo InvokeMethod = typeof(IProxyInvocationHandler).GetMethod(nameof(IProxyInvocationHandler.Invoke))!; + + /// + /// . + /// + private static readonly MethodInfo GetMethodFromHandleMethod = typeof(MethodBase).GetMethod(nameof(MethodBase.GetMethodFromHandle), new[] { typeof(RuntimeMethodHandle) })!; + + private static void GenerateMethod(Type interfaceType, FieldBuilder handlerField, TypeBuilder typeBuilder) + { + MethodInfo[] interfaceMethods = interfaceType.GetMethods(); + + for (int i = 0; i < interfaceMethods.Length; i++) + { + MethodInfo methodInfo = interfaceMethods[i]; + + // Get the method parameters since we need to create an array + // of parameter types + ParameterInfo[] methodParams = methodInfo.GetParameters(); + int numOfParams = methodParams.Length; + Type[] methodParameters = new Type[numOfParams]; + + // convert the ParameterInfo objects into Type + for (int j = 0; j < numOfParams; j++) + { + methodParameters[j] = methodParams[j].ParameterType; + } + + // create a new builder for the method in the interface + MethodBuilder methodBuilder = typeBuilder.DefineMethod( + methodInfo.Name, + /*MethodAttributes.Public | MethodAttributes.Virtual | */ methodInfo.Attributes & ~MethodAttributes.Abstract, + CallingConventions.Standard, + methodInfo.ReturnType, + methodParameters); + + ILGenerator methodIL = methodBuilder.GetILGenerator(); + + // invoke target: IProxyInvocationHandler + methodIL.Emit(OpCodes.Ldarg_0); + methodIL.Emit(OpCodes.Ldfld, handlerField); + + // 1st parameter: object proxy + methodIL.Emit(OpCodes.Ldarg_0); + + // 2nd parameter: MethodInfo method + methodIL.Emit(OpCodes.Ldtoken, methodInfo); + methodIL.Emit(OpCodes.Call, GetMethodFromHandleMethod); + methodIL.Emit(OpCodes.Castclass, typeof(MethodInfo)); + + // 3rd parameter: object[] parameters + methodIL.Emit(OpCodes.Ldc_I4, numOfParams); + methodIL.Emit(OpCodes.Newarr, typeof(object)); + + // if we have any parameters, then iterate through and set the values + // of each element to the corresponding arguments + for (int j = 0; j < numOfParams; j++) + { + methodIL.Emit(OpCodes.Dup); // copy the array + methodIL.Emit(OpCodes.Ldc_I4, j); + methodIL.Emit(OpCodes.Ldarg, j + 1); // +1 for "this" + if (methodParameters[j].IsValueType) + { + methodIL.Emit(OpCodes.Box, methodParameters[j]); + } + + methodIL.Emit(OpCodes.Stelem_Ref); + } + + // call the Invoke method + methodIL.Emit(OpCodes.Callvirt, InvokeMethod); + + if (methodInfo.ReturnType != typeof(void)) + { + methodIL.Emit(OpCodes.Unbox_Any, methodInfo.ReturnType); + } + else + { + // pop the return value that Invoke returned from the stack since + // the method's return type is void. + methodIL.Emit(OpCodes.Pop); + } + + // Return + methodIL.Emit(OpCodes.Ret); + } + + // Iterate through the parent interfaces and recursively call this method + foreach (Type parentType in interfaceType.GetInterfaces()) + { + GenerateMethod(parentType, handlerField, typeBuilder); + } + } + } +} diff --git a/src/Core/WinSWCore/Extensions/AbstractWinSWExtension.cs b/src/Core/WinSWCore/Extensions/AbstractWinSWExtension.cs index bfc5f4a..a3b4733 100644 --- a/src/Core/WinSWCore/Extensions/AbstractWinSWExtension.cs +++ b/src/Core/WinSWCore/Extensions/AbstractWinSWExtension.cs @@ -1,4 +1,5 @@ using System.Xml; +using WinSW.Configuration; namespace WinSW.Extensions { @@ -10,7 +11,7 @@ namespace WinSW.Extensions public WinSWExtensionDescriptor Descriptor { get; set; } #pragma warning restore CS8618 // Non-nullable field is uninitialized. Consider declaring as nullable. - public virtual void Configure(ServiceDescriptor descriptor, XmlNode node) + public virtual void Configure(IWinSWConfiguration descriptor, XmlNode node) { // Do nothing } diff --git a/src/Core/WinSWCore/Extensions/IWinSWExtension.cs b/src/Core/WinSWCore/Extensions/IWinSWExtension.cs index a7c1f35..d187be8 100644 --- a/src/Core/WinSWCore/Extensions/IWinSWExtension.cs +++ b/src/Core/WinSWCore/Extensions/IWinSWExtension.cs @@ -1,4 +1,5 @@ using System.Xml; +using WinSW.Configuration; namespace WinSW.Extensions { @@ -27,7 +28,7 @@ namespace WinSW.Extensions /// /// Service descriptor /// Configuration node - void Configure(ServiceDescriptor descriptor, XmlNode node); + void Configure(IWinSWConfiguration descriptor, XmlNode node); /// /// Start handler. Called during startup of the service before the child process. diff --git a/src/Core/WinSWCore/Extensions/WinSWExtensionManager.cs b/src/Core/WinSWCore/Extensions/WinSWExtensionManager.cs index 081658c..3b0bf6a 100644 --- a/src/Core/WinSWCore/Extensions/WinSWExtensionManager.cs +++ b/src/Core/WinSWCore/Extensions/WinSWExtensionManager.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Xml; using log4net; +using WinSW.Configuration; namespace WinSW.Extensions { @@ -9,11 +10,11 @@ namespace WinSW.Extensions { public Dictionary Extensions { get; private set; } - public ServiceDescriptor ServiceDescriptor { get; private set; } + public IWinSWConfiguration ServiceDescriptor { get; private set; } private static readonly ILog Log = LogManager.GetLogger(typeof(WinSWExtensionManager)); - public WinSWExtensionManager(ServiceDescriptor serviceDescriptor) + public WinSWExtensionManager(IWinSWConfiguration serviceDescriptor) { this.ServiceDescriptor = serviceDescriptor; this.Extensions = new Dictionary(); diff --git a/src/Core/WinSWCore/LogAppenders.cs b/src/Core/WinSWCore/LogAppenders.cs index a2751d3..1b0ca5a 100644 --- a/src/Core/WinSWCore/LogAppenders.cs +++ b/src/Core/WinSWCore/LogAppenders.cs @@ -1,621 +1,621 @@ -using System; -using System.Diagnostics; -#if VNEXT -using System.IO.Compression; -#endif -using System.IO; -using System.Text; -using System.Threading; -#if !VNEXT -using ICSharpCode.SharpZipLib.Zip; -#endif -using WinSW.Util; - -namespace WinSW -{ - public interface IEventLogger - { - void LogEvent(string message); - - void LogEvent(string message, EventLogEntryType type); - } - - /// - /// Abstraction for handling log. - /// - public abstract class LogHandler - { - public abstract void Log(StreamReader outputReader, StreamReader errorReader); - - /// - /// Error and information about logging should be reported here. - /// -#pragma warning disable CS8618 // Non-nullable field is uninitialized. Consider declaring as nullable. - public IEventLogger EventLogger { get; set; } -#pragma warning restore CS8618 // Non-nullable field is uninitialized. Consider declaring as nullable. - - /// - /// Convenience method to copy stuff from StreamReader to StreamWriter - /// - protected void CopyStream(StreamReader reader, StreamWriter writer) - { - string? line; - while ((line = reader.ReadLine()) != null) - { - writer.WriteLine(line); - } - - reader.Dispose(); - writer.Dispose(); - } - - /// - /// File replacement. - /// - protected void MoveFile(string sourceFileName, string destFileName) - { - try - { - FileHelper.MoveOrReplaceFile(sourceFileName, destFileName); - } - catch (IOException e) - { - this.EventLogger.LogEvent("Failed to move :" + sourceFileName + " to " + destFileName + " because " + e.Message); - } - } - } - - /// - /// Base class for file-based loggers - /// - public abstract class AbstractFileLogAppender : LogHandler - { - protected string BaseLogFileName { get; private set; } - - protected bool OutFileDisabled { get; private set; } - - protected bool ErrFileDisabled { get; private set; } - - protected string OutFilePattern { get; private set; } - - protected string ErrFilePattern { get; private set; } - - protected AbstractFileLogAppender(string logDirectory, string baseName, bool outFileDisabled, bool errFileDisabled, string outFilePattern, string errFilePattern) - { - this.BaseLogFileName = Path.Combine(logDirectory, baseName); - this.OutFileDisabled = outFileDisabled; - this.OutFilePattern = outFilePattern; - this.ErrFileDisabled = errFileDisabled; - this.ErrFilePattern = errFilePattern; - } - - public override void Log(StreamReader outputReader, StreamReader errorReader) - { - if (this.OutFileDisabled) - { - outputReader.Dispose(); - } - else - { - this.LogOutput(outputReader); - } - - if (this.ErrFileDisabled) - { - errorReader.Dispose(); - } - else - { - this.LogError(errorReader); - } - } - - protected StreamWriter CreateWriter(FileStream stream) => new StreamWriter(stream) { AutoFlush = true }; - - protected abstract void LogOutput(StreamReader outputReader); - - protected abstract void LogError(StreamReader errorReader); - } - - public abstract class SimpleLogAppender : AbstractFileLogAppender - { - public FileMode FileMode { get; private set; } - - public string OutputLogFileName { get; private set; } - - public string ErrorLogFileName { get; private set; } - - protected SimpleLogAppender(string logDirectory, string baseName, FileMode fileMode, bool outFileDisabled, bool errFileDisabled, string outFilePattern, string errFilePattern) - : base(logDirectory, baseName, outFileDisabled, errFileDisabled, outFilePattern, errFilePattern) - { - this.FileMode = fileMode; - this.OutputLogFileName = this.BaseLogFileName + ".out.log"; - this.ErrorLogFileName = this.BaseLogFileName + ".err.log"; - } - - protected override void LogOutput(StreamReader outputReader) - { - new Thread(() => this.CopyStream(outputReader, this.CreateWriter(new FileStream(this.OutputLogFileName, this.FileMode)))).Start(); - } - - protected override void LogError(StreamReader errorReader) - { - new Thread(() => this.CopyStream(errorReader, this.CreateWriter(new FileStream(this.ErrorLogFileName, this.FileMode)))).Start(); - } - } - - public class DefaultLogAppender : SimpleLogAppender - { - public DefaultLogAppender(string logDirectory, string baseName, bool outFileDisabled, bool errFileDisabled, string outFilePattern, string errFilePattern) - : base(logDirectory, baseName, FileMode.Append, outFileDisabled, errFileDisabled, outFilePattern, errFilePattern) - { - } - } - - public class ResetLogAppender : SimpleLogAppender - { - public ResetLogAppender(string logDirectory, string baseName, bool outFileDisabled, bool errFileDisabled, string outFilePattern, string errFilePattern) - : base(logDirectory, baseName, FileMode.Create, outFileDisabled, errFileDisabled, outFilePattern, errFilePattern) - { - } - } - - /// - /// LogHandler that throws away output - /// - public class IgnoreLogAppender : LogHandler - { - public override void Log(StreamReader outputReader, StreamReader errorReader) - { - outputReader.Dispose(); - errorReader.Dispose(); - } - } - - public class TimeBasedRollingLogAppender : AbstractFileLogAppender - { - public string Pattern { get; private set; } - - public int Period { get; private set; } - - public TimeBasedRollingLogAppender(string logDirectory, string baseName, bool outFileDisabled, bool errFileDisabled, string outFilePattern, string errFilePattern, string pattern, int period) - : base(logDirectory, baseName, outFileDisabled, errFileDisabled, outFilePattern, errFilePattern) - { - this.Pattern = pattern; - this.Period = period; - } - - protected override void LogOutput(StreamReader outputReader) - { - new Thread(() => this.CopyStreamWithDateRotation(outputReader, this.OutFilePattern)).Start(); - } - - protected override void LogError(StreamReader errorReader) - { - new Thread(() => this.CopyStreamWithDateRotation(errorReader, this.ErrFilePattern)).Start(); - } - - /// - /// Works like the CopyStream method but does a log rotation based on time. - /// - private void CopyStreamWithDateRotation(StreamReader reader, string ext) - { - PeriodicRollingCalendar periodicRollingCalendar = new PeriodicRollingCalendar(this.Pattern, this.Period); - periodicRollingCalendar.Init(); - - StreamWriter writer = this.CreateWriter(new FileStream(this.BaseLogFileName + "_" + periodicRollingCalendar.Format + ext, FileMode.Append)); - string? line; - while ((line = reader.ReadLine()) != null) - { - if (periodicRollingCalendar.ShouldRoll) - { - writer.Dispose(); - writer = this.CreateWriter(new FileStream(this.BaseLogFileName + "_" + periodicRollingCalendar.Format + ext, FileMode.Create)); - } - - writer.WriteLine(line); - } - - reader.Dispose(); - writer.Dispose(); - } - } - - public class SizeBasedRollingLogAppender : AbstractFileLogAppender - { - public static int BytesPerKB = 1024; - public static int BytesPerMB = 1024 * BytesPerKB; - public static int DefaultSizeThreshold = 10 * BytesPerMB; // roll every 10MB. - public static int DefaultFilesToKeep = 8; - - public int SizeTheshold { get; private set; } - - public int FilesToKeep { get; private set; } - - public SizeBasedRollingLogAppender(string logDirectory, string baseName, bool outFileDisabled, bool errFileDisabled, string outFilePattern, string errFilePattern, int sizeThreshold, int filesToKeep) - : base(logDirectory, baseName, outFileDisabled, errFileDisabled, outFilePattern, errFilePattern) - { - this.SizeTheshold = sizeThreshold; - this.FilesToKeep = filesToKeep; - } - - public SizeBasedRollingLogAppender(string logDirectory, string baseName, bool outFileDisabled, bool errFileDisabled, string outFilePattern, string errFilePattern) - : this(logDirectory, baseName, outFileDisabled, errFileDisabled, outFilePattern, errFilePattern, DefaultSizeThreshold, DefaultFilesToKeep) - { - } - - protected override void LogOutput(StreamReader outputReader) - { - new Thread(() => this.CopyStreamWithRotation(outputReader, this.OutFilePattern)).Start(); - } - - protected override void LogError(StreamReader errorReader) - { - new Thread(() => this.CopyStreamWithRotation(errorReader, this.ErrFilePattern)).Start(); - } - - /// - /// Works like the CopyStream method but does a log rotation. - /// - private void CopyStreamWithRotation(StreamReader reader, string ext) - { - StreamWriter writer = this.CreateWriter(new FileStream(this.BaseLogFileName + ext, FileMode.Append)); - long fileLength = new FileInfo(this.BaseLogFileName + ext).Length; - - string? line; - while ((line = reader.ReadLine()) != null) - { - int lengthToWrite = Encoding.UTF8.GetByteCount(line) + 2; // CRLF - if (fileLength + lengthToWrite > this.SizeTheshold) - { - writer.Dispose(); - - try - { - for (int j = this.FilesToKeep; j >= 1; j--) - { - string dst = this.BaseLogFileName + "." + (j - 1) + ext; - string src = this.BaseLogFileName + "." + (j - 2) + ext; - if (File.Exists(dst)) - { - File.Delete(dst); - } - - if (File.Exists(src)) - { - File.Move(src, dst); - } - } - - File.Move(this.BaseLogFileName + ext, this.BaseLogFileName + ".0" + ext); - } - catch (IOException e) - { - this.EventLogger.LogEvent("Failed to roll log: " + e.Message); - } - - // even if the log rotation fails, create a new one, or else - // we'll infinitely try to roll. - writer = this.CreateWriter(new FileStream(this.BaseLogFileName + ext, FileMode.Create)); - fileLength = new FileInfo(this.BaseLogFileName + ext).Length; - } - - writer.WriteLine(line); - fileLength += lengthToWrite; - } - - reader.Dispose(); - writer.Dispose(); - } - } - - /// - /// Roll log when a service is newly started. - /// - public class RollingLogAppender : SimpleLogAppender - { - public RollingLogAppender(string logDirectory, string baseName, bool outFileDisabled, bool errFileDisabled, string outFilePattern, string errFilePattern) - : base(logDirectory, baseName, FileMode.Append, outFileDisabled, errFileDisabled, outFilePattern, errFilePattern) - { - } - - public override void Log(StreamReader outputReader, StreamReader errorReader) - { - if (!this.OutFileDisabled) - { - this.MoveFile(this.OutputLogFileName, this.OutputLogFileName + ".old"); - } - - if (!this.ErrFileDisabled) - { - this.MoveFile(this.ErrorLogFileName, this.ErrorLogFileName + ".old"); - } - - base.Log(outputReader, errorReader); - } - } - - public class RollingSizeTimeLogAppender : AbstractFileLogAppender - { - public static int BytesPerKB = 1024; - - public int SizeTheshold { get; private set; } - - public string FilePattern { get; private set; } - - public TimeSpan? AutoRollAtTime { get; private set; } - - public int? ZipOlderThanNumDays { get; private set; } - - public string ZipDateFormat { get; private set; } - - public RollingSizeTimeLogAppender( - string logDirectory, - string baseName, - bool outFileDisabled, - bool errFileDisabled, - string outFilePattern, - string errFilePattern, - int sizeThreshold, - string filePattern, - TimeSpan? autoRollAtTime, - int? zipolderthannumdays, - string zipdateformat) - : base(logDirectory, baseName, outFileDisabled, errFileDisabled, outFilePattern, errFilePattern) - { - this.SizeTheshold = sizeThreshold; - this.FilePattern = filePattern; - this.AutoRollAtTime = autoRollAtTime; - this.ZipOlderThanNumDays = zipolderthannumdays; - this.ZipDateFormat = zipdateformat; - } - - protected override void LogOutput(StreamReader outputReader) - { - new Thread(() => this.CopyStreamWithRotation(outputReader, this.OutFilePattern)).Start(); - } - - protected override void LogError(StreamReader errorReader) - { - new Thread(() => this.CopyStreamWithRotation(errorReader, this.ErrFilePattern)).Start(); - } - - private void CopyStreamWithRotation(StreamReader reader, string extension) - { - // lock required as the timer thread and the thread that will write to the stream could try and access the file stream at the same time - var fileLock = new object(); - - var baseDirectory = Path.GetDirectoryName(this.BaseLogFileName)!; - var baseFileName = Path.GetFileName(this.BaseLogFileName); - var logFile = this.BaseLogFileName + extension; - - var writer = this.CreateWriter(new FileStream(logFile, FileMode.Append)); - var fileLength = new FileInfo(logFile).Length; - - // We auto roll at time is configured then we need to create a timer and wait until time is elasped and roll the file over - if (this.AutoRollAtTime is TimeSpan autoRollAtTime) - { - // Run at start - var tickTime = this.SetupRollTimer(autoRollAtTime); - var timer = new System.Timers.Timer(tickTime); - timer.Elapsed += (s, e) => - { - try - { - timer.Stop(); - lock (fileLock) - { - writer.Dispose(); - - var now = DateTime.Now.AddDays(-1); - var nextFileNumber = this.GetNextFileNumber(extension, baseDirectory, baseFileName, now); - var nextFileName = Path.Combine(baseDirectory, string.Format("{0}.{1}.#{2:D4}{3}", baseFileName, now.ToString(this.FilePattern), nextFileNumber, extension)); - File.Move(logFile, nextFileName); - - writer = this.CreateWriter(new FileStream(logFile, FileMode.Create)); - fileLength = new FileInfo(logFile).Length; - } - - // Next day so check if file can be zipped - this.ZipFiles(baseDirectory, extension, baseFileName); - } - catch (Exception ex) - { - this.EventLogger.LogEvent($"Failed to to trigger auto roll at time event due to: {ex.Message}"); - } - finally - { - // Recalculate the next interval - timer.Interval = this.SetupRollTimer(autoRollAtTime); - timer.Start(); - } - }; - timer.Start(); - } - - string? line; - while ((line = reader.ReadLine()) != null) - { - lock (fileLock) - { - int lengthToWrite = Encoding.UTF8.GetByteCount(line) + 2; // CRLF - if (fileLength + lengthToWrite > this.SizeTheshold) - { - try - { - // roll file - var now = DateTime.Now; - var nextFileNumber = this.GetNextFileNumber(extension, baseDirectory, baseFileName, now); - var nextFileName = Path.Combine( - baseDirectory, - string.Format("{0}.{1}.#{2:D4}{3}", baseFileName, now.ToString(this.FilePattern), nextFileNumber, extension)); - File.Move(logFile, nextFileName); - - // even if the log rotation fails, create a new one, or else - // we'll infinitely try to roll. - writer = this.CreateWriter(new FileStream(logFile, FileMode.Create)); - fileLength = new FileInfo(logFile).Length; - } - catch (Exception e) - { - this.EventLogger.LogEvent($"Failed to roll size time log: {e.Message}"); - } - } - - writer.WriteLine(line); - fileLength += lengthToWrite; - } - } - - reader.Dispose(); - writer.Dispose(); - } - - private void ZipFiles(string directory, string fileExtension, string zipFileBaseName) - { - if (this.ZipOlderThanNumDays is null || this.ZipOlderThanNumDays <= 0) - { - return; - } - - try - { - foreach (string path in Directory.GetFiles(directory, "*" + fileExtension)) - { - var fileInfo = new FileInfo(path); - if (fileInfo.LastWriteTimeUtc >= DateTime.UtcNow.AddDays(-this.ZipOlderThanNumDays.Value)) - { - continue; - } - - string sourceFileName = Path.GetFileName(path); - string zipFilePattern = fileInfo.LastAccessTimeUtc.ToString(this.ZipDateFormat); - string zipFilePath = Path.Combine(directory, $"{zipFileBaseName}.{zipFilePattern}.zip"); - this.ZipOneFile(path, sourceFileName, zipFilePath); - - File.Delete(path); - } - } - catch (Exception e) - { - this.EventLogger.LogEvent($"Failed to Zip files. Error {e.Message}"); - } - } - -#if VNEXT - private void ZipOneFile(string sourceFilePath, string entryName, string zipFilePath) - { - ZipArchive? zipArchive = null; - try - { - zipArchive = ZipFile.Open(zipFilePath, ZipArchiveMode.Update); - - if (zipArchive.GetEntry(entryName) is null) - { - zipArchive.CreateEntryFromFile(sourceFilePath, entryName); - } - } - catch (Exception e) - { - this.EventLogger.LogEvent($"Failed to Zip the File {sourceFilePath}. Error {e.Message}"); - } - finally - { - zipArchive?.Dispose(); - } - } -#else - private void ZipOneFile(string sourceFilePath, string entryName, string zipFilePath) - { - ZipFile? zipFile = null; - try - { - zipFile = new ZipFile(File.Open(zipFilePath, FileMode.OpenOrCreate)); - zipFile.BeginUpdate(); - - if (zipFile.FindEntry(entryName, false) < 0) - { - zipFile.Add(sourceFilePath, entryName); - } - - zipFile.CommitUpdate(); - } - catch (Exception e) - { - this.EventLogger.LogEvent($"Failed to Zip the File {sourceFilePath}. Error {e.Message}"); - zipFile?.AbortUpdate(); - } - finally - { - zipFile?.Close(); - } - } -#endif - - private double SetupRollTimer(TimeSpan autoRollAtTime) - { - var nowTime = DateTime.Now; - var scheduledTime = new DateTime( - nowTime.Year, - nowTime.Month, - nowTime.Day, - autoRollAtTime.Hours, - autoRollAtTime.Minutes, - autoRollAtTime.Seconds, - 0); - if (nowTime > scheduledTime) - { - scheduledTime = scheduledTime.AddDays(1); - } - - double tickTime = (scheduledTime - DateTime.Now).TotalMilliseconds; - return tickTime; - } - - private int GetNextFileNumber(string ext, string baseDirectory, string baseFileName, DateTime now) - { - var nextFileNumber = 0; - var files = Directory.GetFiles(baseDirectory, string.Format("{0}.{1}.#*{2}", baseFileName, now.ToString(this.FilePattern), ext)); - if (files.Length == 0) - { - nextFileNumber = 1; - } - else - { - foreach (var f in files) - { - try - { - var filenameOnly = Path.GetFileNameWithoutExtension(f); - var hashIndex = filenameOnly.IndexOf('#'); - var lastNumberAsString = filenameOnly.Substring(hashIndex + 1, 4); - if (int.TryParse(lastNumberAsString, out int lastNumber)) - { - if (lastNumber > nextFileNumber) - { - nextFileNumber = lastNumber; - } - } - else - { - throw new IOException($"File {f} does not follow the pattern provided"); - } - } - catch (Exception e) - { - throw new IOException($"Failed to process file {f} due to error {e.Message}", e); - } - } - - if (nextFileNumber == 0) - { - throw new IOException("Cannot roll the file because matching pattern not found"); - } - - nextFileNumber++; - } - - return nextFileNumber; - } - } -} +using System; +using System.Diagnostics; +#if VNEXT +using System.IO.Compression; +#endif +using System.IO; +using System.Text; +using System.Threading; +#if !VNEXT +using ICSharpCode.SharpZipLib.Zip; +#endif +using WinSW.Util; + +namespace WinSW +{ + public interface IEventLogger + { + void LogEvent(string message); + + void LogEvent(string message, EventLogEntryType type); + } + + /// + /// Abstraction for handling log. + /// + public abstract class LogHandler + { + public abstract void Log(StreamReader outputReader, StreamReader errorReader); + + /// + /// Error and information about logging should be reported here. + /// +#pragma warning disable CS8618 // Non-nullable field is uninitialized. Consider declaring as nullable. + public IEventLogger EventLogger { get; set; } +#pragma warning restore CS8618 // Non-nullable field is uninitialized. Consider declaring as nullable. + + /// + /// Convenience method to copy stuff from StreamReader to StreamWriter + /// + protected void CopyStream(StreamReader reader, StreamWriter writer) + { + string? line; + while ((line = reader.ReadLine()) != null) + { + writer.WriteLine(line); + } + + reader.Dispose(); + writer.Dispose(); + } + + /// + /// File replacement. + /// + protected void MoveFile(string sourceFileName, string destFileName) + { + try + { + FileHelper.MoveOrReplaceFile(sourceFileName, destFileName); + } + catch (IOException e) + { + this.EventLogger.LogEvent("Failed to move :" + sourceFileName + " to " + destFileName + " because " + e.Message); + } + } + } + + /// + /// Base class for file-based loggers + /// + public abstract class AbstractFileLogAppender : LogHandler + { + protected string BaseLogFileName { get; private set; } + + protected bool OutFileDisabled { get; private set; } + + protected bool ErrFileDisabled { get; private set; } + + protected string OutFilePattern { get; private set; } + + protected string ErrFilePattern { get; private set; } + + protected AbstractFileLogAppender(string logDirectory, string baseName, bool outFileDisabled, bool errFileDisabled, string outFilePattern, string errFilePattern) + { + this.BaseLogFileName = Path.Combine(logDirectory, baseName); + this.OutFileDisabled = outFileDisabled; + this.OutFilePattern = outFilePattern; + this.ErrFileDisabled = errFileDisabled; + this.ErrFilePattern = errFilePattern; + } + + public override void Log(StreamReader outputReader, StreamReader errorReader) + { + if (this.OutFileDisabled) + { + outputReader.Dispose(); + } + else + { + this.LogOutput(outputReader); + } + + if (this.ErrFileDisabled) + { + errorReader.Dispose(); + } + else + { + this.LogError(errorReader); + } + } + + protected StreamWriter CreateWriter(FileStream stream) => new StreamWriter(stream) { AutoFlush = true }; + + protected abstract void LogOutput(StreamReader outputReader); + + protected abstract void LogError(StreamReader errorReader); + } + + public abstract class SimpleLogAppender : AbstractFileLogAppender + { + public FileMode FileMode { get; private set; } + + public string OutputLogFileName { get; private set; } + + public string ErrorLogFileName { get; private set; } + + protected SimpleLogAppender(string logDirectory, string baseName, FileMode fileMode, bool outFileDisabled, bool errFileDisabled, string outFilePattern, string errFilePattern) + : base(logDirectory, baseName, outFileDisabled, errFileDisabled, outFilePattern, errFilePattern) + { + this.FileMode = fileMode; + this.OutputLogFileName = this.BaseLogFileName + ".out.log"; + this.ErrorLogFileName = this.BaseLogFileName + ".err.log"; + } + + protected override void LogOutput(StreamReader outputReader) + { + new Thread(() => this.CopyStream(outputReader, this.CreateWriter(new FileStream(this.OutputLogFileName, this.FileMode)))).Start(); + } + + protected override void LogError(StreamReader errorReader) + { + new Thread(() => this.CopyStream(errorReader, this.CreateWriter(new FileStream(this.ErrorLogFileName, this.FileMode)))).Start(); + } + } + + public class DefaultLogAppender : SimpleLogAppender + { + public DefaultLogAppender(string logDirectory, string baseName, bool outFileDisabled, bool errFileDisabled, string outFilePattern, string errFilePattern) + : base(logDirectory, baseName, FileMode.Append, outFileDisabled, errFileDisabled, outFilePattern, errFilePattern) + { + } + } + + public class ResetLogAppender : SimpleLogAppender + { + public ResetLogAppender(string logDirectory, string baseName, bool outFileDisabled, bool errFileDisabled, string outFilePattern, string errFilePattern) + : base(logDirectory, baseName, FileMode.Create, outFileDisabled, errFileDisabled, outFilePattern, errFilePattern) + { + } + } + + /// + /// LogHandler that throws away output + /// + public class IgnoreLogAppender : LogHandler + { + public override void Log(StreamReader outputReader, StreamReader errorReader) + { + outputReader.Dispose(); + errorReader.Dispose(); + } + } + + public class TimeBasedRollingLogAppender : AbstractFileLogAppender + { + public string Pattern { get; private set; } + + public int Period { get; private set; } + + public TimeBasedRollingLogAppender(string logDirectory, string baseName, bool outFileDisabled, bool errFileDisabled, string outFilePattern, string errFilePattern, string pattern, int period) + : base(logDirectory, baseName, outFileDisabled, errFileDisabled, outFilePattern, errFilePattern) + { + this.Pattern = pattern; + this.Period = period; + } + + protected override void LogOutput(StreamReader outputReader) + { + new Thread(() => this.CopyStreamWithDateRotation(outputReader, this.OutFilePattern)).Start(); + } + + protected override void LogError(StreamReader errorReader) + { + new Thread(() => this.CopyStreamWithDateRotation(errorReader, this.ErrFilePattern)).Start(); + } + + /// + /// Works like the CopyStream method but does a log rotation based on time. + /// + private void CopyStreamWithDateRotation(StreamReader reader, string ext) + { + PeriodicRollingCalendar periodicRollingCalendar = new PeriodicRollingCalendar(this.Pattern, this.Period); + periodicRollingCalendar.Init(); + + StreamWriter writer = this.CreateWriter(new FileStream(this.BaseLogFileName + "_" + periodicRollingCalendar.Format + ext, FileMode.Append)); + string? line; + while ((line = reader.ReadLine()) != null) + { + if (periodicRollingCalendar.ShouldRoll) + { + writer.Dispose(); + writer = this.CreateWriter(new FileStream(this.BaseLogFileName + "_" + periodicRollingCalendar.Format + ext, FileMode.Create)); + } + + writer.WriteLine(line); + } + + reader.Dispose(); + writer.Dispose(); + } + } + + public class SizeBasedRollingLogAppender : AbstractFileLogAppender + { + public static int BytesPerKB = 1024; + public static int BytesPerMB = 1024 * BytesPerKB; + public static int DefaultSizeThreshold = 10 * BytesPerMB; // roll every 10MB. + public static int DefaultFilesToKeep = 8; + + public int SizeTheshold { get; private set; } + + public int FilesToKeep { get; private set; } + + public SizeBasedRollingLogAppender(string logDirectory, string baseName, bool outFileDisabled, bool errFileDisabled, string outFilePattern, string errFilePattern, int sizeThreshold, int filesToKeep) + : base(logDirectory, baseName, outFileDisabled, errFileDisabled, outFilePattern, errFilePattern) + { + this.SizeTheshold = sizeThreshold; + this.FilesToKeep = filesToKeep; + } + + public SizeBasedRollingLogAppender(string logDirectory, string baseName, bool outFileDisabled, bool errFileDisabled, string outFilePattern, string errFilePattern) + : this(logDirectory, baseName, outFileDisabled, errFileDisabled, outFilePattern, errFilePattern, DefaultSizeThreshold, DefaultFilesToKeep) + { + } + + protected override void LogOutput(StreamReader outputReader) + { + new Thread(() => this.CopyStreamWithRotation(outputReader, this.OutFilePattern)).Start(); + } + + protected override void LogError(StreamReader errorReader) + { + new Thread(() => this.CopyStreamWithRotation(errorReader, this.ErrFilePattern)).Start(); + } + + /// + /// Works like the CopyStream method but does a log rotation. + /// + private void CopyStreamWithRotation(StreamReader reader, string ext) + { + StreamWriter writer = this.CreateWriter(new FileStream(this.BaseLogFileName + ext, FileMode.Append)); + long fileLength = new FileInfo(this.BaseLogFileName + ext).Length; + + string? line; + while ((line = reader.ReadLine()) != null) + { + int lengthToWrite = Encoding.UTF8.GetByteCount(line) + 2; // CRLF + if (fileLength + lengthToWrite > this.SizeTheshold) + { + writer.Dispose(); + + try + { + for (int j = this.FilesToKeep; j >= 1; j--) + { + string dst = this.BaseLogFileName + "." + (j - 1) + ext; + string src = this.BaseLogFileName + "." + (j - 2) + ext; + if (File.Exists(dst)) + { + File.Delete(dst); + } + + if (File.Exists(src)) + { + File.Move(src, dst); + } + } + + File.Move(this.BaseLogFileName + ext, this.BaseLogFileName + ".0" + ext); + } + catch (IOException e) + { + this.EventLogger.LogEvent("Failed to roll log: " + e.Message); + } + + // even if the log rotation fails, create a new one, or else + // we'll infinitely try to roll. + writer = this.CreateWriter(new FileStream(this.BaseLogFileName + ext, FileMode.Create)); + fileLength = new FileInfo(this.BaseLogFileName + ext).Length; + } + + writer.WriteLine(line); + fileLength += lengthToWrite; + } + + reader.Dispose(); + writer.Dispose(); + } + } + + /// + /// Roll log when a service is newly started. + /// + public class RollingLogAppender : SimpleLogAppender + { + public RollingLogAppender(string logDirectory, string baseName, bool outFileDisabled, bool errFileDisabled, string outFilePattern, string errFilePattern) + : base(logDirectory, baseName, FileMode.Append, outFileDisabled, errFileDisabled, outFilePattern, errFilePattern) + { + } + + public override void Log(StreamReader outputReader, StreamReader errorReader) + { + if (!this.OutFileDisabled) + { + this.MoveFile(this.OutputLogFileName, this.OutputLogFileName + ".old"); + } + + if (!this.ErrFileDisabled) + { + this.MoveFile(this.ErrorLogFileName, this.ErrorLogFileName + ".old"); + } + + base.Log(outputReader, errorReader); + } + } + + public class RollingSizeTimeLogAppender : AbstractFileLogAppender + { + public static int BytesPerKB = 1024; + + public int SizeTheshold { get; private set; } + + public string FilePattern { get; private set; } + + public TimeSpan? AutoRollAtTime { get; private set; } + + public int? ZipOlderThanNumDays { get; private set; } + + public string ZipDateFormat { get; private set; } + + public RollingSizeTimeLogAppender( + string logDirectory, + string baseName, + bool outFileDisabled, + bool errFileDisabled, + string outFilePattern, + string errFilePattern, + int sizeThreshold, + string filePattern, + TimeSpan? autoRollAtTime, + int? zipolderthannumdays, + string zipdateformat) + : base(logDirectory, baseName, outFileDisabled, errFileDisabled, outFilePattern, errFilePattern) + { + this.SizeTheshold = sizeThreshold; + this.FilePattern = filePattern; + this.AutoRollAtTime = autoRollAtTime; + this.ZipOlderThanNumDays = zipolderthannumdays; + this.ZipDateFormat = zipdateformat; + } + + protected override void LogOutput(StreamReader outputReader) + { + new Thread(() => this.CopyStreamWithRotation(outputReader, this.OutFilePattern)).Start(); + } + + protected override void LogError(StreamReader errorReader) + { + new Thread(() => this.CopyStreamWithRotation(errorReader, this.ErrFilePattern)).Start(); + } + + private void CopyStreamWithRotation(StreamReader reader, string extension) + { + // lock required as the timer thread and the thread that will write to the stream could try and access the file stream at the same time + var fileLock = new object(); + + var baseDirectory = Path.GetDirectoryName(this.BaseLogFileName)!; + var baseFileName = Path.GetFileName(this.BaseLogFileName); + var logFile = this.BaseLogFileName + extension; + + var writer = this.CreateWriter(new FileStream(logFile, FileMode.Append)); + var fileLength = new FileInfo(logFile).Length; + + // We auto roll at time is configured then we need to create a timer and wait until time is elasped and roll the file over + if (this.AutoRollAtTime is TimeSpan autoRollAtTime) + { + // Run at start + var tickTime = this.SetupRollTimer(autoRollAtTime); + var timer = new System.Timers.Timer(tickTime); + timer.Elapsed += (s, e) => + { + try + { + timer.Stop(); + lock (fileLock) + { + writer.Dispose(); + + var now = DateTime.Now.AddDays(-1); + var nextFileNumber = this.GetNextFileNumber(extension, baseDirectory, baseFileName, now); + var nextFileName = Path.Combine(baseDirectory, string.Format("{0}.{1}.#{2:D4}{3}", baseFileName, now.ToString(this.FilePattern), nextFileNumber, extension)); + File.Move(logFile, nextFileName); + + writer = this.CreateWriter(new FileStream(logFile, FileMode.Create)); + fileLength = new FileInfo(logFile).Length; + } + + // Next day so check if file can be zipped + this.ZipFiles(baseDirectory, extension, baseFileName); + } + catch (Exception ex) + { + this.EventLogger.LogEvent($"Failed to to trigger auto roll at time event due to: {ex.Message}"); + } + finally + { + // Recalculate the next interval + timer.Interval = this.SetupRollTimer(autoRollAtTime); + timer.Start(); + } + }; + timer.Start(); + } + + string? line; + while ((line = reader.ReadLine()) != null) + { + lock (fileLock) + { + int lengthToWrite = Encoding.UTF8.GetByteCount(line) + 2; // CRLF + if (fileLength + lengthToWrite > this.SizeTheshold) + { + try + { + // roll file + var now = DateTime.Now; + var nextFileNumber = this.GetNextFileNumber(extension, baseDirectory, baseFileName, now); + var nextFileName = Path.Combine( + baseDirectory, + string.Format("{0}.{1}.#{2:D4}{3}", baseFileName, now.ToString(this.FilePattern), nextFileNumber, extension)); + File.Move(logFile, nextFileName); + + // even if the log rotation fails, create a new one, or else + // we'll infinitely try to roll. + writer = this.CreateWriter(new FileStream(logFile, FileMode.Create)); + fileLength = new FileInfo(logFile).Length; + } + catch (Exception e) + { + this.EventLogger.LogEvent($"Failed to roll size time log: {e.Message}"); + } + } + + writer.WriteLine(line); + fileLength += lengthToWrite; + } + } + + reader.Dispose(); + writer.Dispose(); + } + + private void ZipFiles(string directory, string fileExtension, string zipFileBaseName) + { + if (this.ZipOlderThanNumDays is null || this.ZipOlderThanNumDays <= 0) + { + return; + } + + try + { + foreach (string path in Directory.GetFiles(directory, "*" + fileExtension)) + { + var fileInfo = new FileInfo(path); + if (fileInfo.LastWriteTimeUtc >= DateTime.UtcNow.AddDays(-this.ZipOlderThanNumDays.Value)) + { + continue; + } + + string sourceFileName = Path.GetFileName(path); + string zipFilePattern = fileInfo.LastAccessTimeUtc.ToString(this.ZipDateFormat); + string zipFilePath = Path.Combine(directory, $"{zipFileBaseName}.{zipFilePattern}.zip"); + this.ZipOneFile(path, sourceFileName, zipFilePath); + + File.Delete(path); + } + } + catch (Exception e) + { + this.EventLogger.LogEvent($"Failed to Zip files. Error {e.Message}"); + } + } + +#if VNEXT + private void ZipOneFile(string sourceFilePath, string entryName, string zipFilePath) + { + ZipArchive? zipArchive = null; + try + { + zipArchive = ZipFile.Open(zipFilePath, ZipArchiveMode.Update); + + if (zipArchive.GetEntry(entryName) is null) + { + zipArchive.CreateEntryFromFile(sourceFilePath, entryName); + } + } + catch (Exception e) + { + this.EventLogger.LogEvent($"Failed to Zip the File {sourceFilePath}. Error {e.Message}"); + } + finally + { + zipArchive?.Dispose(); + } + } +#else + private void ZipOneFile(string sourceFilePath, string entryName, string zipFilePath) + { + ZipFile? zipFile = null; + try + { + zipFile = new ZipFile(File.Open(zipFilePath, FileMode.OpenOrCreate)); + zipFile.BeginUpdate(); + + if (zipFile.FindEntry(entryName, false) < 0) + { + zipFile.Add(sourceFilePath, entryName); + } + + zipFile.CommitUpdate(); + } + catch (Exception e) + { + this.EventLogger.LogEvent($"Failed to Zip the File {sourceFilePath}. Error {e.Message}"); + zipFile?.AbortUpdate(); + } + finally + { + zipFile?.Close(); + } + } +#endif + + private double SetupRollTimer(TimeSpan autoRollAtTime) + { + var nowTime = DateTime.Now; + var scheduledTime = new DateTime( + nowTime.Year, + nowTime.Month, + nowTime.Day, + autoRollAtTime.Hours, + autoRollAtTime.Minutes, + autoRollAtTime.Seconds, + 0); + if (nowTime > scheduledTime) + { + scheduledTime = scheduledTime.AddDays(1); + } + + double tickTime = (scheduledTime - DateTime.Now).TotalMilliseconds; + return tickTime; + } + + private int GetNextFileNumber(string ext, string baseDirectory, string baseFileName, DateTime now) + { + var nextFileNumber = 0; + var files = Directory.GetFiles(baseDirectory, string.Format("{0}.{1}.#*{2}", baseFileName, now.ToString(this.FilePattern), ext)); + if (files.Length == 0) + { + nextFileNumber = 1; + } + else + { + foreach (var f in files) + { + try + { + var filenameOnly = Path.GetFileNameWithoutExtension(f); + var hashIndex = filenameOnly.IndexOf('#'); + var lastNumberAsString = filenameOnly.Substring(hashIndex + 1, 4); + if (int.TryParse(lastNumberAsString, out int lastNumber)) + { + if (lastNumber > nextFileNumber) + { + nextFileNumber = lastNumber; + } + } + else + { + throw new IOException($"File {f} does not follow the pattern provided"); + } + } + catch (Exception e) + { + throw new IOException($"Failed to process file {f} due to error {e.Message}", e); + } + } + + if (nextFileNumber == 0) + { + throw new IOException("Cannot roll the file because matching pattern not found"); + } + + nextFileNumber++; + } + + return nextFileNumber; + } + } +} diff --git a/src/Core/WinSWCore/Native/Kernel32.cs b/src/Core/WinSWCore/Native/Kernel32.cs old mode 100755 new mode 100644 index 4ce74ed..733467b --- a/src/Core/WinSWCore/Native/Kernel32.cs +++ b/src/Core/WinSWCore/Native/Kernel32.cs @@ -1,11 +1,11 @@ -using System.Runtime.InteropServices; -using Microsoft.Win32.SafeHandles; - -namespace WinSW.Native -{ - internal static class Kernel32 - { - [DllImport(Libraries.Kernel32)] - internal static extern bool SetStdHandle(int stdHandle, SafeFileHandle handle); - } -} +using System.Runtime.InteropServices; +using Microsoft.Win32.SafeHandles; + +namespace WinSW.Native +{ + internal static class Kernel32 + { + [DllImport(Libraries.Kernel32)] + internal static extern bool SetStdHandle(int stdHandle, SafeFileHandle handle); + } +} diff --git a/src/Core/WinSWCore/PeriodicRollingCalendar.cs b/src/Core/WinSWCore/PeriodicRollingCalendar.cs index 41b868c..03a84f6 100644 --- a/src/Core/WinSWCore/PeriodicRollingCalendar.cs +++ b/src/Core/WinSWCore/PeriodicRollingCalendar.cs @@ -1,108 +1,108 @@ -using System; - -namespace WinSW -{ - // This is largely borrowed from the logback Rolling Calendar. - public class PeriodicRollingCalendar - { - private readonly string format; - private readonly long period; - private DateTime currentRoll; - private DateTime nextRoll; - - public PeriodicRollingCalendar(string format, long period) - { - this.format = format; - this.period = period; - this.currentRoll = DateTime.Now; - } - - public void Init() - { - this.PeriodicityType = this.DeterminePeriodicityType(); - this.nextRoll = this.NextTriggeringTime(this.currentRoll, this.period); - } - - public enum Periodicity - { - ERRONEOUS, - TOP_OF_MILLISECOND, - TOP_OF_SECOND, - TOP_OF_MINUTE, - TOP_OF_HOUR, - TOP_OF_DAY - } - - private static readonly Periodicity[] ValidOrderedList = - { - Periodicity.TOP_OF_MILLISECOND, Periodicity.TOP_OF_SECOND, Periodicity.TOP_OF_MINUTE, Periodicity.TOP_OF_HOUR, Periodicity.TOP_OF_DAY - }; - - private Periodicity DeterminePeriodicityType() - { - PeriodicRollingCalendar periodicRollingCalendar = new PeriodicRollingCalendar(this.format, this.period); - DateTime epoch = new DateTime(1970, 1, 1); - - foreach (Periodicity i in ValidOrderedList) - { - string r0 = epoch.ToString(this.format); - periodicRollingCalendar.PeriodicityType = i; - - DateTime next = periodicRollingCalendar.NextTriggeringTime(epoch, 1); - string r1 = next.ToString(this.format); - - if (r0 != r1) - { - return i; - } - } - - return Periodicity.ERRONEOUS; - } - - private DateTime NextTriggeringTime(DateTime input, long increment) => this.PeriodicityType switch - { - Periodicity.TOP_OF_MILLISECOND => - new DateTime(input.Year, input.Month, input.Day, input.Hour, input.Minute, input.Second, input.Millisecond) - .AddMilliseconds(increment), - - Periodicity.TOP_OF_SECOND => - new DateTime(input.Year, input.Month, input.Day, input.Hour, input.Minute, input.Second) - .AddSeconds(increment), - - Periodicity.TOP_OF_MINUTE => - new DateTime(input.Year, input.Month, input.Day, input.Hour, input.Minute, 0) - .AddMinutes(increment), - - Periodicity.TOP_OF_HOUR => - new DateTime(input.Year, input.Month, input.Day, input.Hour, 0, 0) - .AddHours(increment), - - Periodicity.TOP_OF_DAY => - new DateTime(input.Year, input.Month, input.Day) - .AddDays(increment), - - _ => throw new Exception("invalid periodicity type: " + this.PeriodicityType), - }; - - public Periodicity PeriodicityType { get; set; } - - public bool ShouldRoll - { - get - { - DateTime now = DateTime.Now; - if (now > this.nextRoll) - { - this.currentRoll = now; - this.nextRoll = this.NextTriggeringTime(now, this.period); - return true; - } - - return false; - } - } - - public string Format => this.currentRoll.ToString(this.format); - } -} +using System; + +namespace WinSW +{ + // This is largely borrowed from the logback Rolling Calendar. + public class PeriodicRollingCalendar + { + private readonly string format; + private readonly long period; + private DateTime currentRoll; + private DateTime nextRoll; + + public PeriodicRollingCalendar(string format, long period) + { + this.format = format; + this.period = period; + this.currentRoll = DateTime.Now; + } + + public void Init() + { + this.PeriodicityType = this.DeterminePeriodicityType(); + this.nextRoll = this.NextTriggeringTime(this.currentRoll, this.period); + } + + public enum Periodicity + { + ERRONEOUS, + TOP_OF_MILLISECOND, + TOP_OF_SECOND, + TOP_OF_MINUTE, + TOP_OF_HOUR, + TOP_OF_DAY + } + + private static readonly Periodicity[] ValidOrderedList = + { + Periodicity.TOP_OF_MILLISECOND, Periodicity.TOP_OF_SECOND, Periodicity.TOP_OF_MINUTE, Periodicity.TOP_OF_HOUR, Periodicity.TOP_OF_DAY + }; + + private Periodicity DeterminePeriodicityType() + { + PeriodicRollingCalendar periodicRollingCalendar = new PeriodicRollingCalendar(this.format, this.period); + DateTime epoch = new DateTime(1970, 1, 1); + + foreach (Periodicity i in ValidOrderedList) + { + string r0 = epoch.ToString(this.format); + periodicRollingCalendar.PeriodicityType = i; + + DateTime next = periodicRollingCalendar.NextTriggeringTime(epoch, 1); + string r1 = next.ToString(this.format); + + if (r0 != r1) + { + return i; + } + } + + return Periodicity.ERRONEOUS; + } + + private DateTime NextTriggeringTime(DateTime input, long increment) => this.PeriodicityType switch + { + Periodicity.TOP_OF_MILLISECOND => + new DateTime(input.Year, input.Month, input.Day, input.Hour, input.Minute, input.Second, input.Millisecond) + .AddMilliseconds(increment), + + Periodicity.TOP_OF_SECOND => + new DateTime(input.Year, input.Month, input.Day, input.Hour, input.Minute, input.Second) + .AddSeconds(increment), + + Periodicity.TOP_OF_MINUTE => + new DateTime(input.Year, input.Month, input.Day, input.Hour, input.Minute, 0) + .AddMinutes(increment), + + Periodicity.TOP_OF_HOUR => + new DateTime(input.Year, input.Month, input.Day, input.Hour, 0, 0) + .AddHours(increment), + + Periodicity.TOP_OF_DAY => + new DateTime(input.Year, input.Month, input.Day) + .AddDays(increment), + + _ => throw new Exception("invalid periodicity type: " + this.PeriodicityType), + }; + + public Periodicity PeriodicityType { get; set; } + + public bool ShouldRoll + { + get + { + DateTime now = DateTime.Now; + if (now > this.nextRoll) + { + this.currentRoll = now; + this.nextRoll = this.NextTriggeringTime(now, this.period); + return true; + } + + return false; + } + } + + public string Format => this.currentRoll.ToString(this.format); + } +} diff --git a/src/Core/WinSWCore/ServiceDescriptor.cs b/src/Core/WinSWCore/ServiceDescriptor.cs old mode 100755 new mode 100644 index e7a41bf..4d31d83 --- a/src/Core/WinSWCore/ServiceDescriptor.cs +++ b/src/Core/WinSWCore/ServiceDescriptor.cs @@ -1,746 +1,733 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Text; -using System.Xml; -using WinSW.Configuration; -using WinSW.Native; -using WinSW.Util; -using WMI; - -namespace WinSW -{ - /// - /// In-memory representation of the configuration file. - /// - public class ServiceDescriptor : IWinSWConfiguration - { - protected readonly XmlDocument dom = new XmlDocument(); - - private readonly Dictionary environmentVariables; - - public static DefaultWinSWSettings Defaults { get; } = new DefaultWinSWSettings(); - - /// - /// Where did we find the configuration file? - /// - /// This string is "c:\abc\def\ghi" when the configuration XML is "c:\abc\def\ghi.xml" - /// - public string BasePath { get; set; } - - /// - /// The file name portion of the configuration file. - /// - /// In the above example, this would be "ghi". - /// - public string BaseName { get; set; } - - // Currently there is no opportunity to alter the executable path - public virtual string ExecutablePath => Defaults.ExecutablePath; - - public ServiceDescriptor() - { - // find co-located configuration xml. We search up to the ancestor directories to simplify debugging, - // as well as trimming off ".vshost" suffix (which is used during debugging) - // Get the first parent to go into the recursive loop - string p = this.ExecutablePath; - string baseName = Path.GetFileNameWithoutExtension(p); - if (baseName.EndsWith(".vshost")) - { - baseName = baseName.Substring(0, baseName.Length - 7); - } - - DirectoryInfo d = new DirectoryInfo(Path.GetDirectoryName(p)); - while (true) - { - if (File.Exists(Path.Combine(d.FullName, baseName + ".xml"))) - { - break; - } - - if (d.Parent is null) - { - throw new FileNotFoundException("Unable to locate " + baseName + ".xml file within executable directory or any parents"); - } - - d = d.Parent; - } - - this.BaseName = baseName; - this.BasePath = Path.Combine(d.FullName, this.BaseName); - - try - { - this.dom.Load(this.BasePath + ".xml"); - } - catch (XmlException e) - { - throw new InvalidDataException(e.Message, e); - } - - // register the base directory as environment variable so that future expansions can refer to this. - Environment.SetEnvironmentVariable("BASE", d.FullName); - - // ditto for ID - Environment.SetEnvironmentVariable("SERVICE_ID", this.Id); - - // New name - Environment.SetEnvironmentVariable(WinSWSystem.EnvVarNameExecutablePath, this.ExecutablePath); - - // Also inject system environment variables - Environment.SetEnvironmentVariable(WinSWSystem.EnvVarNameServiceId, this.Id); - - this.environmentVariables = this.LoadEnvironmentVariables(); - } - - /// - /// Loads descriptor from existing DOM - /// -#pragma warning disable CS8618 // Non-nullable field is uninitialized. Consider declaring as nullable. - public ServiceDescriptor(XmlDocument dom) -#pragma warning restore CS8618 // Non-nullable field is uninitialized. Consider declaring as nullable. - { - this.dom = dom; - - this.environmentVariables = this.LoadEnvironmentVariables(); - } - - public static ServiceDescriptor FromXML(string xml) - { - var dom = new XmlDocument(); - dom.LoadXml(xml); - return new ServiceDescriptor(dom); - } - - private string SingleElement(string tagName) - { - return this.SingleElement(tagName, false)!; - } - - private string? SingleElement(string tagName, bool optional) - { - XmlNode? n = this.dom.SelectSingleNode("//" + tagName); - if (n is null && !optional) - { - throw new InvalidDataException("<" + tagName + "> is missing in configuration XML"); - } - - return n is null ? null : Environment.ExpandEnvironmentVariables(n.InnerText); - } - - private bool SingleBoolElement(string tagName, bool defaultValue) - { - XmlNode? e = this.dom.SelectSingleNode("//" + tagName); - - return e is null ? defaultValue : bool.Parse(e.InnerText); - } - - private int SingleIntElement(XmlNode parent, string tagName, int defaultValue) - { - XmlNode? e = parent.SelectSingleNode(tagName); - - return e is null ? defaultValue : int.Parse(e.InnerText); - } - - private TimeSpan SingleTimeSpanElement(XmlNode parent, string tagName, TimeSpan defaultValue) - { - string? value = this.SingleElement(tagName, true); - return value is null ? defaultValue : this.ParseTimeSpan(value); - } - - private TimeSpan ParseTimeSpan(string v) - { - v = v.Trim(); - foreach (var s in Suffix) - { - if (v.EndsWith(s.Key)) - { - return TimeSpan.FromMilliseconds(int.Parse(v.Substring(0, v.Length - s.Key.Length).Trim()) * s.Value); - } - } - - return TimeSpan.FromMilliseconds(int.Parse(v)); - } - - private static readonly Dictionary Suffix = new Dictionary - { - { "ms", 1 }, - { "sec", 1000L }, - { "secs", 1000L }, - { "min", 1000L * 60L }, - { "mins", 1000L * 60L }, - { "hr", 1000L * 60L * 60L }, - { "hrs", 1000L * 60L * 60L }, - { "hour", 1000L * 60L * 60L }, - { "hours", 1000L * 60L * 60L }, - { "day", 1000L * 60L * 60L * 24L }, - { "days", 1000L * 60L * 60L * 24L } - }; - - /// - /// Path to the executable. - /// - public string Executable => this.SingleElement("executable"); - - public bool HideWindow => this.SingleBoolElement("hidewindow", Defaults.HideWindow); - - /// - /// Optionally specify a different Path to an executable to shutdown the service. - /// - public string? StopExecutable => this.SingleElement("stopexecutable", true); - - /// - /// arguments or multiple optional argument elements which overrule the arguments element. - /// - public string Arguments - { - get - { - string? arguments = this.AppendTags("argument", null); - - if (!(arguments is null)) - { - return arguments; - } - - XmlNode? argumentsNode = this.dom.SelectSingleNode("//arguments"); - - return argumentsNode is null ? Defaults.Arguments : Environment.ExpandEnvironmentVariables(argumentsNode.InnerText); - } - } - - /// - /// startarguments or multiple optional startargument elements. - /// - public string? StartArguments - { - get - { - string? startArguments = this.AppendTags("startargument", null); - - if (!(startArguments is null)) - { - return startArguments; - } - - XmlNode? startArgumentsNode = this.dom.SelectSingleNode("//startarguments"); - - return startArgumentsNode is null ? null : Environment.ExpandEnvironmentVariables(startArgumentsNode.InnerText); - } - } - - /// - /// stoparguments or multiple optional stopargument elements. - /// - public string? StopArguments - { - get - { - string? stopArguments = this.AppendTags("stopargument", null); - - if (!(stopArguments is null)) - { - return stopArguments; - } - - XmlNode? stopArgumentsNode = this.dom.SelectSingleNode("//stoparguments"); - - return stopArgumentsNode is null ? null : Environment.ExpandEnvironmentVariables(stopArgumentsNode.InnerText); - } - } - - public string WorkingDirectory - { - get - { - var wd = this.SingleElement("workingdirectory", true); - return string.IsNullOrEmpty(wd) ? Defaults.WorkingDirectory : wd!; - } - } - - public List ExtensionIds - { - get - { - XmlNode? argumentNode = this.ExtensionsConfiguration; - XmlNodeList? extensions = argumentNode?.SelectNodes("extension"); - if (extensions is null) - { - return new List(0); - } - - List result = new List(extensions.Count); - for (int i = 0; i < extensions.Count; i++) - { - result.Add(XmlHelper.SingleAttribute((XmlElement)extensions[i], "id")); - } - - return result; - } - } - - public XmlNode? ExtensionsConfiguration => this.dom.SelectSingleNode("//extensions"); - - /// - /// Combines the contents of all the elements of the given name, - /// or return null if no element exists. Handles whitespace quotation. - /// - private string? AppendTags(string tagName, string? defaultValue = null) - { - XmlNode? argumentNode = this.dom.SelectSingleNode("//" + tagName); - if (argumentNode is null) - { - return defaultValue; - } - - StringBuilder arguments = new StringBuilder(); - - XmlNodeList argumentNodeList = this.dom.SelectNodes("//" + tagName); - for (int i = 0; i < argumentNodeList.Count; i++) - { - arguments.Append(' '); - - string token = Environment.ExpandEnvironmentVariables(argumentNodeList[i].InnerText); - - if (token.StartsWith("\"") && token.EndsWith("\"")) - { - // for backward compatibility, if the argument is already quoted, leave it as is. - // in earlier versions we didn't handle quotation, so the user might have worked - // around it by themselves - } - else - { - if (token.Contains(" ")) - { - arguments.Append('"').Append(token).Append('"'); - continue; - } - } - - arguments.Append(token); - } - - return arguments.ToString(); - } - - /// - /// LogDirectory is the service wrapper executable directory or the optionally specified logpath element. - /// - public string LogDirectory - { - get - { - XmlNode? loggingNode = this.dom.SelectSingleNode("//logpath"); - - return loggingNode is null - ? Defaults.LogDirectory - : Environment.ExpandEnvironmentVariables(loggingNode.InnerText); - } - } - - public string LogMode - { - get - { - string? mode = null; - - // first, backward compatibility with older configuration - XmlElement? e = (XmlElement?)this.dom.SelectSingleNode("//logmode"); - if (e != null) - { - mode = e.InnerText; - } - else - { - // this is more modern way, to support nested elements as configuration - e = (XmlElement?)this.dom.SelectSingleNode("//log"); - if (e != null) - { - mode = e.GetAttribute("mode"); - } - } - - return mode ?? Defaults.LogMode; - } - } - - public string LogName - { - get - { - XmlNode? loggingName = this.dom.SelectSingleNode("//logname"); - - return loggingName is null ? this.BaseName : Environment.ExpandEnvironmentVariables(loggingName.InnerText); - } - } - - public bool OutFileDisabled => this.SingleBoolElement("outfiledisabled", Defaults.OutFileDisabled); - - public bool ErrFileDisabled => this.SingleBoolElement("errfiledisabled", Defaults.ErrFileDisabled); - - public string OutFilePattern - { - get - { - XmlNode? loggingName = this.dom.SelectSingleNode("//outfilepattern"); - - return loggingName is null ? Defaults.OutFilePattern : Environment.ExpandEnvironmentVariables(loggingName.InnerText); - } - } - - public string ErrFilePattern - { - get - { - XmlNode? loggingName = this.dom.SelectSingleNode("//errfilepattern"); - - return loggingName is null ? Defaults.ErrFilePattern : Environment.ExpandEnvironmentVariables(loggingName.InnerText); - } - } - - public LogHandler LogHandler - { - get - { - XmlElement? e = (XmlElement?)this.dom.SelectSingleNode("//logmode"); - - // this is more modern way, to support nested elements as configuration - e ??= (XmlElement?)this.dom.SelectSingleNode("//log")!; // WARNING: NRE - - int sizeThreshold; - switch (this.LogMode) - { - case "rotate": - return new SizeBasedRollingLogAppender(this.LogDirectory, this.LogName, this.OutFileDisabled, this.ErrFileDisabled, this.OutFilePattern, this.ErrFilePattern); - - case "none": - return new IgnoreLogAppender(); - - case "reset": - return new ResetLogAppender(this.LogDirectory, this.LogName, this.OutFileDisabled, this.ErrFileDisabled, this.OutFilePattern, this.ErrFilePattern); - - case "roll": - return new RollingLogAppender(this.LogDirectory, this.LogName, this.OutFileDisabled, this.ErrFileDisabled, this.OutFilePattern, this.ErrFilePattern); - - case "roll-by-time": - XmlNode? patternNode = e.SelectSingleNode("pattern"); - if (patternNode is null) - { - throw new InvalidDataException("Time Based rolling policy is specified but no pattern can be found in configuration XML."); - } - - var pattern = patternNode.InnerText; - int period = this.SingleIntElement(e, "period", 1); - return new TimeBasedRollingLogAppender(this.LogDirectory, this.LogName, this.OutFileDisabled, this.ErrFileDisabled, this.OutFilePattern, this.ErrFilePattern, pattern, period); - - case "roll-by-size": - sizeThreshold = this.SingleIntElement(e, "sizeThreshold", 10 * 1024) * SizeBasedRollingLogAppender.BytesPerKB; - int keepFiles = this.SingleIntElement(e, "keepFiles", SizeBasedRollingLogAppender.DefaultFilesToKeep); - return new SizeBasedRollingLogAppender(this.LogDirectory, this.LogName, this.OutFileDisabled, this.ErrFileDisabled, this.OutFilePattern, this.ErrFilePattern, sizeThreshold, keepFiles); - - case "append": - return new DefaultLogAppender(this.LogDirectory, this.LogName, this.OutFileDisabled, this.ErrFileDisabled, this.OutFilePattern, this.ErrFilePattern); - - case "roll-by-size-time": - sizeThreshold = this.SingleIntElement(e, "sizeThreshold", 10 * 1024) * RollingSizeTimeLogAppender.BytesPerKB; - XmlNode? filePatternNode = e.SelectSingleNode("pattern"); - if (filePatternNode is null) - { - throw new InvalidDataException("Roll-Size-Time Based rolling policy is specified but no pattern can be found in configuration XML."); - } - - XmlNode? autoRollAtTimeNode = e.SelectSingleNode("autoRollAtTime"); - TimeSpan? autoRollAtTime = null; - if (autoRollAtTimeNode != null) - { - // validate it - if (!TimeSpan.TryParse(autoRollAtTimeNode.InnerText, out TimeSpan autoRollAtTimeValue)) - { - throw new InvalidDataException("Roll-Size-Time Based rolling policy is specified but autoRollAtTime does not match the TimeSpan format HH:mm:ss found in configuration XML."); - } - - autoRollAtTime = autoRollAtTimeValue; - } - - XmlNode? zipolderthannumdaysNode = e.SelectSingleNode("zipOlderThanNumDays"); - int? zipolderthannumdays = null; - if (zipolderthannumdaysNode != null) - { - // validate it - if (!int.TryParse(zipolderthannumdaysNode.InnerText, out int zipolderthannumdaysValue)) - { - throw new InvalidDataException("Roll-Size-Time Based rolling policy is specified but zipOlderThanNumDays does not match the int format found in configuration XML."); - } - - zipolderthannumdays = zipolderthannumdaysValue; - } - - XmlNode? zipdateformatNode = e.SelectSingleNode("zipDateFormat"); - string zipdateformat = zipdateformatNode is null ? "yyyyMM" : zipdateformatNode.InnerText; - - return new RollingSizeTimeLogAppender(this.LogDirectory, this.LogName, this.OutFileDisabled, this.ErrFileDisabled, this.OutFilePattern, this.ErrFilePattern, sizeThreshold, filePatternNode.InnerText, autoRollAtTime, zipolderthannumdays, zipdateformat); - - default: - throw new InvalidDataException("Undefined logging mode: " + this.LogMode); - } - } - } - - /// - /// Optionally specified depend services that must start before this service starts. - /// - public string[] ServiceDependencies - { - get - { - XmlNodeList? nodeList = this.dom.SelectNodes("//depend"); - if (nodeList is null) - { - return Defaults.ServiceDependencies; - } - - string[] serviceDependencies = new string[nodeList.Count]; - for (int i = 0; i < nodeList.Count; i++) - { - serviceDependencies[i] = nodeList[i].InnerText; - } - - return serviceDependencies; - } - } - - public string Id => this.SingleElement("id"); - - public string Caption => this.SingleElement("name"); - - public string Description => this.SingleElement("description"); - - /// - /// Start mode of the Service - /// - public StartMode StartMode - { - get - { - string? p = this.SingleElement("startmode", true); - if (p is null) - { - return Defaults.StartMode; - } - - try - { - return (StartMode)Enum.Parse(typeof(StartMode), p, true); - } - catch - { - Console.WriteLine("Start mode in XML must be one of the following:"); - foreach (string sm in Enum.GetNames(typeof(StartMode))) - { - Console.WriteLine(sm); - } - - throw; - } - } - } - - /// - /// True if the service should be installed with the DelayedAutoStart flag. - /// This setting will be applyed only during the install command and only when the Automatic start mode is configured. - /// - public bool DelayedAutoStart => this.dom.SelectSingleNode("//delayedAutoStart") != null; - - /// - /// True if the service should beep when finished on shutdown. - /// This doesn't work on some OSes. See http://msdn.microsoft.com/en-us/library/ms679277%28VS.85%29.aspx - /// - public bool BeepOnShutdown => this.dom.SelectSingleNode("//beeponshutdown") != null; - - /// - /// The estimated time required for a pending stop operation (default 15 secs). - /// Before the specified amount of time has elapsed, the service should make its next call to the SetServiceStatus function - /// with either an incremented checkPoint value or a change in currentState. (see http://msdn.microsoft.com/en-us/library/ms685996.aspx) - /// - public TimeSpan WaitHint => this.SingleTimeSpanElement(this.dom, "waithint", Defaults.WaitHint); - - /// - /// The time before the service should make its next call to the SetServiceStatus function - /// with an incremented checkPoint value (default 1 sec). - /// Do not wait longer than the wait hint. A good interval is one-tenth of the wait hint but not less than 1 second and not more than 10 seconds. - /// - public TimeSpan SleepTime => this.SingleTimeSpanElement(this.dom, "sleeptime", Defaults.SleepTime); - - /// - /// True if the service can interact with the desktop. - /// - public bool Interactive => this.dom.SelectSingleNode("//interactive") != null; - - /// - /// Environment variable overrides - /// - public Dictionary EnvironmentVariables => new Dictionary(this.environmentVariables); - - /// - /// List of downloads to be performed by the wrapper before starting - /// a service. - /// - public List Downloads - { - get - { - XmlNodeList? nodeList = this.dom.SelectNodes("//download"); - if (nodeList is null) - { - return Defaults.Downloads; - } - - List result = new List(nodeList.Count); - for (int i = 0; i < nodeList.Count; i++) - { - if (nodeList[i] is XmlElement element) - { - result.Add(new Download(element)); - } - } - - return result; - } - } - - public SC_ACTION[] FailureActions - { - get - { - XmlNodeList? childNodes = this.dom.SelectNodes("//onfailure"); - if (childNodes is null) - { - return new SC_ACTION[0]; - } - - SC_ACTION[] result = new SC_ACTION[childNodes.Count]; - for (int i = 0; i < childNodes.Count; i++) - { - XmlNode node = childNodes[i]; - string action = node.Attributes["action"].Value; - SC_ACTION_TYPE type = action switch - { - "restart" => SC_ACTION_TYPE.SC_ACTION_RESTART, - "none" => SC_ACTION_TYPE.SC_ACTION_NONE, - "reboot" => SC_ACTION_TYPE.SC_ACTION_REBOOT, - _ => throw new Exception("Invalid failure action: " + action) - }; - XmlAttribute? delay = node.Attributes["delay"]; - result[i] = new SC_ACTION(type, delay != null ? this.ParseTimeSpan(delay.Value) : TimeSpan.Zero); - } - - return result; - } - } - - public TimeSpan ResetFailureAfter => this.SingleTimeSpanElement(this.dom, "resetfailure", Defaults.ResetFailureAfter); - - protected string? GetServiceAccountPart(string subNodeName) - { - XmlNode? node = this.dom.SelectSingleNode("//serviceaccount"); - - if (node != null) - { - XmlNode? subNode = node.SelectSingleNode(subNodeName); - if (subNode != null) - { - return subNode.InnerText; - } - } - - return null; - } - - protected string? AllowServiceLogon => this.GetServiceAccountPart("allowservicelogon"); - - protected internal string? ServiceAccountDomain => this.GetServiceAccountPart("domain"); - - protected internal string? ServiceAccountName => this.GetServiceAccountPart("user"); - - public string? ServiceAccountPassword => this.GetServiceAccountPart("password"); - - public string? ServiceAccountUser => this.ServiceAccountName is null ? null : (this.ServiceAccountDomain ?? ".") + "\\" + this.ServiceAccountName; - - public bool HasServiceAccount() - { - return !string.IsNullOrEmpty(this.ServiceAccountName); - } - - public bool AllowServiceAcountLogonRight - { - get - { - if (this.AllowServiceLogon != null) - { - if (bool.TryParse(this.AllowServiceLogon, out bool parsedvalue)) - { - return parsedvalue; - } - } - - return false; - } - } - - /// - /// Time to wait for the service to gracefully shutdown the executable before we forcibly kill it - /// - public TimeSpan StopTimeout => this.SingleTimeSpanElement(this.dom, "stoptimeout", Defaults.StopTimeout); - - public bool StopParentProcessFirst - { - get - { - var value = this.SingleElement("stopparentprocessfirst", true); - if (bool.TryParse(value, out bool result)) - { - return result; - } - - return Defaults.StopParentProcessFirst; - } - } - - /// - /// Desired process priority or null if not specified. - /// - public ProcessPriorityClass Priority - { - get - { - string? p = this.SingleElement("priority", true); - if (p is null) - { - return Defaults.Priority; - } - - return (ProcessPriorityClass)Enum.Parse(typeof(ProcessPriorityClass), p, true); - } - } - - public string? SecurityDescriptor => this.SingleElement("securityDescriptor", true); - - private Dictionary LoadEnvironmentVariables() - { - XmlNodeList nodeList = this.dom.SelectNodes("//env"); - Dictionary environment = new Dictionary(nodeList.Count); - for (int i = 0; i < nodeList.Count; i++) - { - XmlNode node = nodeList[i]; - string key = node.Attributes["name"].Value; - string value = Environment.ExpandEnvironmentVariables(node.Attributes["value"].Value); - environment[key] = value; - - Environment.SetEnvironmentVariable(key, value); - } - - return environment; - } - } -} +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Text; +using System.Xml; +using WinSW.Configuration; +using WinSW.Native; +using WinSW.Util; +using WMI; + +namespace WinSW +{ + /// + /// In-memory representation of the configuration file. + /// + public class ServiceDescriptor : IWinSWConfiguration + { +#pragma warning disable S2755 // XML parsers should not be vulnerable to XXE attacks + protected readonly XmlDocument dom = new XmlDocument(); +#pragma warning restore S2755 // XML parsers should not be vulnerable to XXE attacks + + private readonly Dictionary environmentVariables; + + public static DefaultWinSWSettings Defaults { get; } = new DefaultWinSWSettings(); + + /// + /// Where did we find the configuration file? + /// + /// This string is "c:\abc\def\ghi" when the configuration XML is "c:\abc\def\ghi.xml" + /// + public string BasePath { get; set; } + + /// + /// The file name portion of the configuration file. + /// + /// In the above example, this would be "ghi". + /// + public string BaseName { get; set; } + + // Currently there is no opportunity to alter the executable path + public virtual string ExecutablePath => Defaults.ExecutablePath; + + public ServiceDescriptor() + { + // find co-located configuration xml. We search up to the ancestor directories to simplify debugging, + // as well as trimming off ".vshost" suffix (which is used during debugging) + // Get the first parent to go into the recursive loop + string p = this.ExecutablePath; + string baseName = Path.GetFileNameWithoutExtension(p); + if (baseName.EndsWith(".vshost")) + { + baseName = baseName.Substring(0, baseName.Length - 7); + } + + DirectoryInfo d = new DirectoryInfo(Path.GetDirectoryName(p)); + while (true) + { + if (File.Exists(Path.Combine(d.FullName, baseName + ".xml"))) + { + break; + } + + if (d.Parent is null) + { + throw new FileNotFoundException("Unable to locate " + baseName + ".xml file within executable directory or any parents"); + } + + d = d.Parent; + } + + this.BaseName = baseName; + this.BasePath = Path.Combine(d.FullName, this.BaseName); + + try + { + this.dom.Load(this.BasePath + ".xml"); + } + catch (XmlException e) + { + throw new InvalidDataException(e.Message, e); + } + + // register the base directory as environment variable so that future expansions can refer to this. + Environment.SetEnvironmentVariable("BASE", d.FullName); + + // ditto for ID + Environment.SetEnvironmentVariable("SERVICE_ID", this.Id); + + // New name + Environment.SetEnvironmentVariable(WinSWSystem.EnvVarNameExecutablePath, this.ExecutablePath); + + // Also inject system environment variables + Environment.SetEnvironmentVariable(WinSWSystem.EnvVarNameServiceId, this.Id); + + this.environmentVariables = this.LoadEnvironmentVariables(); + } + + /// + /// Loads descriptor from existing DOM + /// +#pragma warning disable CS8618 // Non-nullable field is uninitialized. Consider declaring as nullable. + public ServiceDescriptor(XmlDocument dom) +#pragma warning restore CS8618 // Non-nullable field is uninitialized. Consider declaring as nullable. + { + this.dom = dom; + + this.environmentVariables = this.LoadEnvironmentVariables(); + } + + public static ServiceDescriptor FromXML(string xml) + { +#pragma warning disable S2755 // XML parsers should not be vulnerable to XXE attacks + var xmlDom = new XmlDocument(); +#pragma warning restore S2755 // XML parsers should not be vulnerable to XXE attacks + xmlDom.LoadXml(xml); + return new ServiceDescriptor(xmlDom); + } + + private string SingleElement(string tagName) + { + return this.SingleElement(tagName, false)!; + } + + private string? SingleElement(string tagName, bool optional) + { + XmlNode? n = this.dom.SelectSingleNode("//" + tagName); + if (n is null && !optional) + { + throw new InvalidDataException("<" + tagName + "> is missing in configuration XML"); + } + + return n is null ? null : Environment.ExpandEnvironmentVariables(n.InnerText); + } + + private bool SingleBoolElement(string tagName, bool defaultValue) + { + XmlNode? e = this.dom.SelectSingleNode("//" + tagName); + + return e is null ? defaultValue : bool.Parse(e.InnerText); + } + + private int SingleIntElement(XmlNode parent, string tagName, int defaultValue) + { + XmlNode? e = parent.SelectSingleNode(tagName); + + return e is null ? defaultValue : int.Parse(e.InnerText); + } + + private TimeSpan SingleTimeSpanElement(string tagName, TimeSpan defaultValue) + { + string? value = this.SingleElement(tagName, true); + return value is null ? defaultValue : ConfigHelper.ParseTimeSpan(value); + } + + /// + /// Path to the executable. + /// + public string Executable => this.SingleElement("executable"); + + public bool HideWindow => this.SingleBoolElement("hidewindow", Defaults.HideWindow); + + /// + /// Optionally specify a different Path to an executable to shutdown the service. + /// + public string? StopExecutable => this.SingleElement("stopexecutable", true); + + /// + /// arguments or multiple optional argument elements which overrule the arguments element. + /// + public string Arguments + { + get + { + string? arguments = this.AppendTags("argument", null); + + if (!(arguments is null)) + { + return arguments; + } + + XmlNode? argumentsNode = this.dom.SelectSingleNode("//arguments"); + + return argumentsNode is null ? Defaults.Arguments : Environment.ExpandEnvironmentVariables(argumentsNode.InnerText); + } + } + + /// + /// startarguments or multiple optional startargument elements. + /// + public string? StartArguments + { + get + { + string? startArguments = this.AppendTags("startargument", null); + + if (!(startArguments is null)) + { + return startArguments; + } + + XmlNode? startArgumentsNode = this.dom.SelectSingleNode("//startarguments"); + + return startArgumentsNode is null ? null : Environment.ExpandEnvironmentVariables(startArgumentsNode.InnerText); + } + } + + /// + /// stoparguments or multiple optional stopargument elements. + /// + public string? StopArguments + { + get + { + string? stopArguments = this.AppendTags("stopargument", null); + + if (!(stopArguments is null)) + { + return stopArguments; + } + + XmlNode? stopArgumentsNode = this.dom.SelectSingleNode("//stoparguments"); + + return stopArgumentsNode is null ? null : Environment.ExpandEnvironmentVariables(stopArgumentsNode.InnerText); + } + } + + public string WorkingDirectory + { + get + { + var wd = this.SingleElement("workingdirectory", true); + return string.IsNullOrEmpty(wd) ? Defaults.WorkingDirectory : wd!; + } + } + + public List ExtensionIds + { + get + { + XmlNode? argumentNode = this.ExtensionsConfiguration; + XmlNodeList? extensions = argumentNode?.SelectNodes("extension"); + if (extensions is null) + { + return new List(0); + } + + List result = new List(extensions.Count); + for (int i = 0; i < extensions.Count; i++) + { + result.Add(XmlHelper.SingleAttribute((XmlElement)extensions[i], "id")); + } + + return result; + } + } + + public XmlNode? ExtensionsConfiguration => this.dom.SelectSingleNode("//extensions"); + + /// + /// Combines the contents of all the elements of the given name, + /// or return null if no element exists. Handles whitespace quotation. + /// + private string? AppendTags(string tagName, string? defaultValue = null) + { + XmlNode? argumentNode = this.dom.SelectSingleNode("//" + tagName); + if (argumentNode is null) + { + return defaultValue; + } + + StringBuilder arguments = new StringBuilder(); + + XmlNodeList argumentNodeList = this.dom.SelectNodes("//" + tagName); + for (int i = 0; i < argumentNodeList.Count; i++) + { + arguments.Append(' '); + + string token = Environment.ExpandEnvironmentVariables(argumentNodeList[i].InnerText); + + if (token.StartsWith("\"") && token.EndsWith("\"")) + { + // for backward compatibility, if the argument is already quoted, leave it as is. + // in earlier versions we didn't handle quotation, so the user might have worked + // around it by themselves + } + else + { + if (token.Contains(" ")) + { + arguments.Append('"').Append(token).Append('"'); + continue; + } + } + + arguments.Append(token); + } + + return arguments.ToString(); + } + + /// + /// LogDirectory is the service wrapper executable directory or the optionally specified logpath element. + /// + public string LogDirectory { get => this.Log.Directory; } + + public string LogMode + { + get + { + string? mode = null; + + // first, backward compatibility with older configuration + XmlElement? e = (XmlElement?)this.dom.SelectSingleNode("//logmode"); + if (e != null) + { + mode = e.InnerText; + } + else + { + // this is more modern way, to support nested elements as configuration + e = (XmlElement?)this.dom.SelectSingleNode("//log"); + if (e != null) + { + mode = e.GetAttribute("mode"); + } + } + + return mode ?? Defaults.LogMode; + } + } + + public string LogName + { + get + { + XmlNode? loggingName = this.dom.SelectSingleNode("//logname"); + + return loggingName is null ? this.BaseName : Environment.ExpandEnvironmentVariables(loggingName.InnerText); + } + } + + public Log Log + { + get + { + return new XmlLogSettings(this); + } + } + + private class XmlLogSettings : Log + { + private readonly ServiceDescriptor d; + + public XmlLogSettings(ServiceDescriptor d) + { + this.d = d; + } + + private XmlElement E + { + get + { + XmlElement? e = (XmlElement?)this.d.dom.SelectSingleNode("//logmode"); + + // this is more modern way, to support nested elements as configuration + e ??= (XmlElement?)this.d.dom.SelectSingleNode("//log")!; // WARNING: NRE + return e; + } + } + + public override string? Mode { get => this.d.LogMode; } + + public override string Name { get => this.d.LogName; } + + public override string Directory + { + get + { + XmlNode? loggingNode = this.d.dom.SelectSingleNode("//logpath"); + + return loggingNode is null + ? Defaults.LogDirectory + : Environment.ExpandEnvironmentVariables(loggingNode.InnerText); + } + } + + public override int? SizeThreshold { get => this.d.SingleIntElement(this.E, "sizeThreshold", 10 * 1024); } + + public override int? KeepFiles { get => this.d.SingleIntElement(this.E, "keepFiles", SizeBasedRollingLogAppender.DefaultFilesToKeep); } + + public override int? Period { get => this.d.SingleIntElement(this.E, "period", 1); } + + public override string Pattern + { + get + { + XmlNode? patternNode = this.E.SelectSingleNode("pattern"); + if (patternNode is null) + { +#pragma warning disable S2372 // Exceptions should not be thrown from property getters + throw new InvalidDataException("Time Based rolling policy is specified but no pattern can be found in configuration XML."); +#pragma warning restore S2372 // Exceptions should not be thrown from property getters + } + + return patternNode.InnerText; + } + } + + public override bool OutFileDisabled => this.d.SingleBoolElement("outfiledisabled", Defaults.OutFileDisabled); + + public override bool ErrFileDisabled => this.d.SingleBoolElement("errfiledisabled", Defaults.ErrFileDisabled); + + public override string OutFilePattern + { + get + { + XmlNode? loggingName = this.d.dom.SelectSingleNode("//outfilepattern"); + + return loggingName is null ? Defaults.OutFilePattern : Environment.ExpandEnvironmentVariables(loggingName.InnerText); + } + } + + public override string ErrFilePattern + { + get + { + XmlNode? loggingName = this.d.dom.SelectSingleNode("//errfilepattern"); + + return loggingName is null ? Defaults.ErrFilePattern : Environment.ExpandEnvironmentVariables(loggingName.InnerText); + } + } + + public override string? AutoRollAtTime + { + get + { + XmlNode? autoRollAtTimeNode = this.E.SelectSingleNode("autoRollAtTime"); + return autoRollAtTimeNode?.InnerText; + } + } + + public override int? ZipOlderThanNumDays + { + get + { + XmlNode? zipolderthannumdaysNode = this.E.SelectSingleNode("zipOlderThanNumDays"); + int? zipolderthannumdays = null; + if (zipolderthannumdaysNode != null) + { + // validate it + if (!int.TryParse(zipolderthannumdaysNode.InnerText, out int zipolderthannumdaysValue)) + { +#pragma warning disable S2372 // Exceptions should not be thrown from property getters + throw new InvalidDataException("Roll-Size-Time Based rolling policy is specified but zipOlderThanNumDays does not match the int format found in configuration XML."); +#pragma warning restore S2372 // Exceptions should not be thrown from property getters + } + + zipolderthannumdays = zipolderthannumdaysValue; + } + + return zipolderthannumdays; + } + } + + public override string? ZipDateFormat + { + get + { + XmlNode? zipdateformatNode = this.E.SelectSingleNode("zipDateFormat"); + return zipdateformatNode is null ? null : zipdateformatNode.InnerText; + } + } + } + + /// + /// Optionally specified depend services that must start before this service starts. + /// + public string[] ServiceDependencies + { + get + { + XmlNodeList? nodeList = this.dom.SelectNodes("//depend"); + if (nodeList is null) + { + return Defaults.ServiceDependencies; + } + + string[] serviceDependencies = new string[nodeList.Count]; + for (int i = 0; i < nodeList.Count; i++) + { + serviceDependencies[i] = nodeList[i].InnerText; + } + + return serviceDependencies; + } + } + + public string Id => this.SingleElement("id"); + + public string Caption => this.SingleElement("name"); + + public string Description => this.SingleElement("description"); + + /// + /// Start mode of the Service + /// + public StartMode StartMode + { + get + { + string? p = this.SingleElement("startmode", true); + if (p is null) + { + return Defaults.StartMode; + } + + try + { + return (StartMode)Enum.Parse(typeof(StartMode), p, true); + } + catch + { + Console.WriteLine("Start mode in XML must be one of the following:"); + foreach (string sm in Enum.GetNames(typeof(StartMode))) + { + Console.WriteLine(sm); + } + + throw; + } + } + } + + /// + /// True if the service should be installed with the DelayedAutoStart flag. + /// This setting will be applyed only during the install command and only when the Automatic start mode is configured. + /// + public bool DelayedAutoStart => this.dom.SelectSingleNode("//delayedAutoStart") != null; + + /// + /// True if the service should beep when finished on shutdown. + /// This doesn't work on some OSes. See http://msdn.microsoft.com/en-us/library/ms679277%28VS.85%29.aspx + /// + public bool BeepOnShutdown => this.dom.SelectSingleNode("//beeponshutdown") != null; + + /// + /// The estimated time required for a pending stop operation (default 15 secs). + /// Before the specified amount of time has elapsed, the service should make its next call to the SetServiceStatus function + /// with either an incremented checkPoint value or a change in currentState. (see http://msdn.microsoft.com/en-us/library/ms685996.aspx) + /// + public TimeSpan WaitHint => this.SingleTimeSpanElement("waithint", Defaults.WaitHint); + + /// + /// The time before the service should make its next call to the SetServiceStatus function + /// with an incremented checkPoint value (default 1 sec). + /// Do not wait longer than the wait hint. A good interval is one-tenth of the wait hint but not less than 1 second and not more than 10 seconds. + /// + public TimeSpan SleepTime => this.SingleTimeSpanElement("sleeptime", Defaults.SleepTime); + + /// + /// True if the service can interact with the desktop. + /// + public bool Interactive => this.dom.SelectSingleNode("//interactive") != null; + + /// + /// Environment variable overrides + /// + public Dictionary EnvironmentVariables => new Dictionary(this.environmentVariables); + + /// + /// List of downloads to be performed by the wrapper before starting + /// a service. + /// + public List Downloads + { + get + { + XmlNodeList? nodeList = this.dom.SelectNodes("//download"); + if (nodeList is null) + { + return Defaults.Downloads; + } + + List result = new List(nodeList.Count); + for (int i = 0; i < nodeList.Count; i++) + { + if (nodeList[i] is XmlElement element) + { + result.Add(new Download(element)); + } + } + + return result; + } + } + + public SC_ACTION[] FailureActions + { + get + { + XmlNodeList? childNodes = this.dom.SelectNodes("//onfailure"); + if (childNodes is null) + { + return new SC_ACTION[0]; + } + + SC_ACTION[] result = new SC_ACTION[childNodes.Count]; + for (int i = 0; i < childNodes.Count; i++) + { + XmlNode node = childNodes[i]; + string action = node.Attributes["action"].Value; + SC_ACTION_TYPE type = action switch + { + "restart" => SC_ACTION_TYPE.SC_ACTION_RESTART, + "none" => SC_ACTION_TYPE.SC_ACTION_NONE, + "reboot" => SC_ACTION_TYPE.SC_ACTION_REBOOT, + _ => throw new Exception("Invalid failure action: " + action) + }; + XmlAttribute? delay = node.Attributes["delay"]; + result[i] = new SC_ACTION(type, delay != null ? ConfigHelper.ParseTimeSpan(delay.Value) : TimeSpan.Zero); + } + + return result; + } + } + + public TimeSpan ResetFailureAfter => this.SingleTimeSpanElement("resetfailure", Defaults.ResetFailureAfter); + + protected string? GetServiceAccountPart(XmlNode node, string subNodeName) + { + XmlNode? subNode = node.SelectSingleNode(subNodeName); + if (subNode != null) + { + return subNode.InnerText; + } + + return null; + } + + private bool ParseAllowServiceAcountLogonRight(string? logonRight) + { + if (logonRight != null && bool.TryParse(logonRight, out bool parsedvalue)) + { + return parsedvalue; + } + + return false; + } + + public ServiceAccount ServiceAccount + { + get + { + XmlNode? node = this.dom.SelectSingleNode("//serviceaccount"); + + if (node is null) + { + return Defaults.ServiceAccount; + } + + var serviceAccount = Defaults.ServiceAccount; + + serviceAccount.ServiceAccountDomain = this.GetServiceAccountPart(node, "domain"); + + serviceAccount.ServiceAccountName = this.GetServiceAccountPart(node, "user"); + + serviceAccount.ServiceAccountPassword = this.GetServiceAccountPart(node, "password"); + + var loginRight = this.GetServiceAccountPart(node, "allowservicelogon"); + + serviceAccount.AllowServiceAcountLogonRight = this.ParseAllowServiceAcountLogonRight(loginRight); + + return serviceAccount; + } + } + + /// + /// Time to wait for the service to gracefully shutdown the executable before we forcibly kill it + /// + public TimeSpan StopTimeout => this.SingleTimeSpanElement("stoptimeout", Defaults.StopTimeout); + + public bool StopParentProcessFirst + { + get + { + var value = this.SingleElement("stopparentprocessfirst", true); + if (bool.TryParse(value, out bool result)) + { + return result; + } + + return Defaults.StopParentProcessFirst; + } + } + + /// + /// Desired process priority or null if not specified. + /// + public ProcessPriorityClass Priority + { + get + { + string? p = this.SingleElement("priority", true); + if (p is null) + { + return Defaults.Priority; + } + + return (ProcessPriorityClass)Enum.Parse(typeof(ProcessPriorityClass), p, true); + } + } + + public string? SecurityDescriptor => this.SingleElement("securityDescriptor", true); + + private Dictionary LoadEnvironmentVariables() + { + XmlNodeList nodeList = this.dom.SelectNodes("//env"); + Dictionary environment = new Dictionary(nodeList.Count); + for (int i = 0; i < nodeList.Count; i++) + { + XmlNode node = nodeList[i]; + string key = node.Attributes["name"].Value; + string value = Environment.ExpandEnvironmentVariables(node.Attributes["value"].Value); + environment[key] = value; + + Environment.SetEnvironmentVariable(key, value); + } + + return environment; + } + } +} diff --git a/src/Core/WinSWCore/ServiceDescriptorYaml.cs b/src/Core/WinSWCore/ServiceDescriptorYaml.cs new file mode 100644 index 0000000..0dec860 --- /dev/null +++ b/src/Core/WinSWCore/ServiceDescriptorYaml.cs @@ -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(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(yaml); + return new ServiceDescriptorYaml(configs); + } + } +} 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/Core/WinSWCore/WinSWCore.csproj b/src/Core/WinSWCore/WinSWCore.csproj index 4ace542..a3beb21 100644 --- a/src/Core/WinSWCore/WinSWCore.csproj +++ b/src/Core/WinSWCore/WinSWCore.csproj @@ -11,6 +11,7 @@ + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/src/Core/WinSWCore/Wmi.cs b/src/Core/WinSWCore/Wmi.cs old mode 100755 new mode 100644 index 5259a19..fadec91 --- a/src/Core/WinSWCore/Wmi.cs +++ b/src/Core/WinSWCore/Wmi.cs @@ -1,227 +1,227 @@ -using System; -using System.Management; -using System.Reflection; -using System.Text; -using DynamicProxy; - -namespace WMI -{ - // https://docs.microsoft.com/windows/win32/cimwin32prov/create-method-in-class-win32-service - public enum ReturnValue : uint - { - Success = 0, - NotSupported = 1, - AccessDenied = 2, - DependentServicesRunning = 3, - InvalidServiceControl = 4, - ServiceCannotAcceptControl = 5, - ServiceNotActive = 6, - ServiceRequestTimeout = 7, - UnknownFailure = 8, - PathNotFound = 9, - ServiceAlreadyRunning = 10, - ServiceDatabaseLocked = 11, - ServiceDependencyDeleted = 12, - ServiceDependencyFailure = 13, - ServiceDisabled = 14, - ServiceLogonFailure = 15, - ServiceMarkedForDeletion = 16, - ServiceNoThread = 17, - StatusCircularDependency = 18, - StatusDuplicateName = 19, - StatusInvalidName = 20, - StatusInvalidParameter = 21, - StatusInvalidServiceAccount = 22, - StatusServiceExists = 23, - ServiceAlreadyPaused = 24, - - NoSuchService = 200 - } - - /// - /// Signals a problem in WMI related operations - /// - public class WmiException : Exception - { - public readonly ReturnValue ErrorCode; - - public WmiException(string message, ReturnValue code) - : base(message) - { - this.ErrorCode = code; - } - - public WmiException(ReturnValue code) - : this(code.ToString(), code) - { - } - } - - /// - /// Associated a WMI class name to the proxy interface (which should extend from IWmiCollection) - /// - public class WmiClassName : Attribute - { - public readonly string Name; - - public WmiClassName(string name) => this.Name = name; - } - - /// - /// Marker interface to denote a collection in WMI. - /// - public interface IWmiCollection - { - } - - /// - /// Marker interface to denote an individual managed object - /// - public interface IWmiObject - { - } - - public sealed class WmiRoot - { - private readonly ManagementScope wmiScope; - - public WmiRoot() - { - ConnectionOptions options = new ConnectionOptions - { - EnablePrivileges = true, - Impersonation = ImpersonationLevel.Impersonate, - Authentication = AuthenticationLevel.PacketPrivacy, - }; - - this.wmiScope = new ManagementScope(@"\\.\root\cimv2", options); - this.wmiScope.Connect(); - } - - private static string Capitalize(string s) - { - return char.ToUpper(s[0]) + s.Substring(1); - } - - private abstract class BaseHandler : IProxyInvocationHandler - { - public abstract object? Invoke(object proxy, MethodInfo method, object[] arguments); - - protected void CheckError(ManagementBaseObject result) - { - uint code = (uint)result["returnValue"]; - if (code != 0) - { - throw new WmiException((ReturnValue)code); - } - } - - protected ManagementBaseObject GetMethodParameters(ManagementObject wmiObject, string methodName, ParameterInfo[] methodParameters, object[] arguments) - { - ManagementBaseObject wmiParameters = wmiObject.GetMethodParameters(methodName); - for (int i = 0; i < arguments.Length; i++) - { - string capitalizedName = Capitalize(methodParameters[i].Name!); - wmiParameters[capitalizedName] = arguments[i]; - } - - return wmiParameters; - } - } - - private class InstanceHandler : BaseHandler, IWmiObject - { - private readonly ManagementObject wmiObject; - - public InstanceHandler(ManagementObject wmiObject) => this.wmiObject = wmiObject; - - public override object? Invoke(object proxy, MethodInfo method, object[] arguments) - { - if (method.DeclaringType == typeof(IWmiObject)) - { - return method.Invoke(this, arguments); - } - - // TODO: proper property support - if (method.Name.StartsWith("set_")) - { - this.wmiObject[method.Name.Substring(4)] = arguments[0]; - return null; - } - - if (method.Name.StartsWith("get_")) - { - return this.wmiObject[method.Name.Substring(4)]; - } - - string methodName = method.Name; - using ManagementBaseObject? wmiParameters = arguments.Length == 0 ? null : - this.GetMethodParameters(this.wmiObject, methodName, method.GetParameters(), arguments); - using ManagementBaseObject result = this.wmiObject.InvokeMethod(methodName, wmiParameters, null); - this.CheckError(result); - return null; - } - } - - private class ClassHandler : BaseHandler - { - private readonly ManagementClass wmiClass; - private readonly string className; - - public ClassHandler(ManagementScope wmiScope, string className) - { - this.wmiClass = new ManagementClass(wmiScope, new ManagementPath(className), null); - this.className = className; - } - - public override object? Invoke(object proxy, MethodInfo method, object[] arguments) - { - ParameterInfo[] methodParameters = method.GetParameters(); - - if (method.Name == nameof(IWin32Services.Select)) - { - // select method to find instances - StringBuilder query = new StringBuilder("SELECT * FROM ").Append(this.className).Append(" WHERE "); - for (int i = 0; i < arguments.Length; i++) - { - if (i != 0) - { - query.Append(" AND "); - } - - query.Append(' ').Append(Capitalize(methodParameters[i].Name!)).Append(" = '").Append(arguments[i]).Append('\''); - } - - using ManagementObjectSearcher searcher = new ManagementObjectSearcher(this.wmiClass.Scope, new ObjectQuery(query.ToString())); - using ManagementObjectCollection results = searcher.Get(); - - // TODO: support collections - foreach (ManagementObject wmiObject in results) - { - return ProxyFactory.Create(new InstanceHandler(wmiObject), method.ReturnType, true); - } - - return null; - } - - string methodName = method.Name; - using ManagementBaseObject? wmiParameters = arguments.Length == 0 ? null : - this.GetMethodParameters(this.wmiClass, methodName, methodParameters, arguments); - using ManagementBaseObject result = this.wmiClass.InvokeMethod(methodName, wmiParameters, null); - this.CheckError(result); - return null; - } - } - - /// - /// Obtains an object that corresponds to a table in WMI, which is a collection of a managed object. - /// - public T GetCollection() - where T : IWmiCollection - { - WmiClassName className = (WmiClassName)typeof(T).GetCustomAttributes(typeof(WmiClassName), false)[0]; - - return (T)ProxyFactory.Create(new ClassHandler(this.wmiScope, className.Name), typeof(T), true); - } - } -} +using System; +using System.Management; +using System.Reflection; +using System.Text; +using DynamicProxy; + +namespace WMI +{ + // https://docs.microsoft.com/windows/win32/cimwin32prov/create-method-in-class-win32-service + public enum ReturnValue : uint + { + Success = 0, + NotSupported = 1, + AccessDenied = 2, + DependentServicesRunning = 3, + InvalidServiceControl = 4, + ServiceCannotAcceptControl = 5, + ServiceNotActive = 6, + ServiceRequestTimeout = 7, + UnknownFailure = 8, + PathNotFound = 9, + ServiceAlreadyRunning = 10, + ServiceDatabaseLocked = 11, + ServiceDependencyDeleted = 12, + ServiceDependencyFailure = 13, + ServiceDisabled = 14, + ServiceLogonFailure = 15, + ServiceMarkedForDeletion = 16, + ServiceNoThread = 17, + StatusCircularDependency = 18, + StatusDuplicateName = 19, + StatusInvalidName = 20, + StatusInvalidParameter = 21, + StatusInvalidServiceAccount = 22, + StatusServiceExists = 23, + ServiceAlreadyPaused = 24, + + NoSuchService = 200 + } + + /// + /// Signals a problem in WMI related operations + /// + public class WmiException : Exception + { + public readonly ReturnValue ErrorCode; + + public WmiException(string message, ReturnValue code) + : base(message) + { + this.ErrorCode = code; + } + + public WmiException(ReturnValue code) + : this(code.ToString(), code) + { + } + } + + /// + /// Associated a WMI class name to the proxy interface (which should extend from IWmiCollection) + /// + public class WmiClassName : Attribute + { + public readonly string Name; + + public WmiClassName(string name) => this.Name = name; + } + + /// + /// Marker interface to denote a collection in WMI. + /// + public interface IWmiCollection + { + } + + /// + /// Marker interface to denote an individual managed object + /// + public interface IWmiObject + { + } + + public sealed class WmiRoot + { + private readonly ManagementScope wmiScope; + + public WmiRoot() + { + ConnectionOptions options = new ConnectionOptions + { + EnablePrivileges = true, + Impersonation = ImpersonationLevel.Impersonate, + Authentication = AuthenticationLevel.PacketPrivacy, + }; + + this.wmiScope = new ManagementScope(@"\\.\root\cimv2", options); + this.wmiScope.Connect(); + } + + private static string Capitalize(string s) + { + return char.ToUpper(s[0]) + s.Substring(1); + } + + private abstract class BaseHandler : IProxyInvocationHandler + { + public abstract object? Invoke(object proxy, MethodInfo method, object[] arguments); + + protected void CheckError(ManagementBaseObject result) + { + uint code = (uint)result["returnValue"]; + if (code != 0) + { + throw new WmiException((ReturnValue)code); + } + } + + protected ManagementBaseObject GetMethodParameters(ManagementObject wmiObject, string methodName, ParameterInfo[] methodParameters, object[] arguments) + { + ManagementBaseObject wmiParameters = wmiObject.GetMethodParameters(methodName); + for (int i = 0; i < arguments.Length; i++) + { + string capitalizedName = Capitalize(methodParameters[i].Name!); + wmiParameters[capitalizedName] = arguments[i]; + } + + return wmiParameters; + } + } + + private class InstanceHandler : BaseHandler, IWmiObject + { + private readonly ManagementObject wmiObject; + + public InstanceHandler(ManagementObject wmiObject) => this.wmiObject = wmiObject; + + public override object? Invoke(object proxy, MethodInfo method, object[] arguments) + { + if (method.DeclaringType == typeof(IWmiObject)) + { + return method.Invoke(this, arguments); + } + + // TODO: proper property support + if (method.Name.StartsWith("set_")) + { + this.wmiObject[method.Name.Substring(4)] = arguments[0]; + return null; + } + + if (method.Name.StartsWith("get_")) + { + return this.wmiObject[method.Name.Substring(4)]; + } + + string methodName = method.Name; + using ManagementBaseObject? wmiParameters = arguments.Length == 0 ? null : + this.GetMethodParameters(this.wmiObject, methodName, method.GetParameters(), arguments); + using ManagementBaseObject result = this.wmiObject.InvokeMethod(methodName, wmiParameters, null); + this.CheckError(result); + return null; + } + } + + private class ClassHandler : BaseHandler + { + private readonly ManagementClass wmiClass; + private readonly string className; + + public ClassHandler(ManagementScope wmiScope, string className) + { + this.wmiClass = new ManagementClass(wmiScope, new ManagementPath(className), null); + this.className = className; + } + + public override object? Invoke(object proxy, MethodInfo method, object[] arguments) + { + ParameterInfo[] methodParameters = method.GetParameters(); + + if (method.Name == nameof(IWin32Services.Select)) + { + // select method to find instances + StringBuilder query = new StringBuilder("SELECT * FROM ").Append(this.className).Append(" WHERE "); + for (int i = 0; i < arguments.Length; i++) + { + if (i != 0) + { + query.Append(" AND "); + } + + query.Append(' ').Append(Capitalize(methodParameters[i].Name!)).Append(" = '").Append(arguments[i]).Append('\''); + } + + using ManagementObjectSearcher searcher = new ManagementObjectSearcher(this.wmiClass.Scope, new ObjectQuery(query.ToString())); + using ManagementObjectCollection results = searcher.Get(); + + // TODO: support collections + foreach (ManagementObject wmiObject in results) + { + return ProxyFactory.Create(new InstanceHandler(wmiObject), method.ReturnType, true); + } + + return null; + } + + string methodName = method.Name; + using ManagementBaseObject? wmiParameters = arguments.Length == 0 ? null : + this.GetMethodParameters(this.wmiClass, methodName, methodParameters, arguments); + using ManagementBaseObject result = this.wmiClass.InvokeMethod(methodName, wmiParameters, null); + this.CheckError(result); + return null; + } + } + + /// + /// Obtains an object that corresponds to a table in WMI, which is a collection of a managed object. + /// + public T GetCollection() + where T : IWmiCollection + { + WmiClassName className = (WmiClassName)typeof(T).GetCustomAttributes(typeof(WmiClassName), false)[0]; + + return (T)ProxyFactory.Create(new ClassHandler(this.wmiScope, className.Name), typeof(T), true); + } + } +} diff --git a/src/Core/WinSWCore/WmiSchema.cs b/src/Core/WinSWCore/WmiSchema.cs old mode 100755 new mode 100644 index 2686204..a9e962f --- a/src/Core/WinSWCore/WmiSchema.cs +++ b/src/Core/WinSWCore/WmiSchema.cs @@ -1,72 +1,72 @@ -namespace WMI -{ - public enum ServiceType - { - KernalDriver = 1, - FileSystemDriver = 2, - Adapter = 4, - RecognizerDriver = 8, - OwnProcess = 16, - ShareProcess = 32, - InteractiveProcess = 256, - } - - public enum ErrorControl - { - UserNotNotified = 0, - UserNotified = 1, - SystemRestartedWithLastKnownGoodConfiguration = 2, - SystemAttemptsToStartWithAGoodConfiguration = 3 - } - - public enum StartMode - { - /// - /// Device driver started by the operating system loader. This value is valid only for driver services. - /// - Boot, - - /// - /// Device driver started by the operating system initialization process. This value is valid only for driver services. - /// - System, - - /// - /// Service to be started automatically by the Service Control Manager during system startup. - /// - Automatic, - - /// - /// Service to be started by the Service Control Manager when a process calls the StartService method. - /// - Manual, - - /// - /// Service that can no longer be started. - /// - Disabled, - } - - [WmiClassName("Win32_Service")] - public interface IWin32Services : IWmiCollection - { - // ReturnValue Create(bool desktopInteract, string displayName, int errorControl, string loadOrderGroup, string loadOrderGroupDependencies, string name, string pathName, string serviceDependencies, string serviceType, string startMode, string startName, string startPassword); - void Create(string name, string displayName, string pathName, ServiceType serviceType, ErrorControl errorControl, string startMode, bool desktopInteract, string? startName, string? startPassword, string[] serviceDependencies); - - void Create(string name, string displayName, string pathName, ServiceType serviceType, ErrorControl errorControl, string startMode, bool desktopInteract, string[] serviceDependencies); - - IWin32Service? Select(string name); - } - - // https://docs.microsoft.com/windows/win32/cimwin32prov/win32-service - public interface IWin32Service : IWmiObject - { - bool Started { get; } - - void Delete(); - - void StartService(); - - void StopService(); - } -} +namespace WMI +{ + public enum ServiceType + { + KernalDriver = 1, + FileSystemDriver = 2, + Adapter = 4, + RecognizerDriver = 8, + OwnProcess = 16, + ShareProcess = 32, + InteractiveProcess = 256, + } + + public enum ErrorControl + { + UserNotNotified = 0, + UserNotified = 1, + SystemRestartedWithLastKnownGoodConfiguration = 2, + SystemAttemptsToStartWithAGoodConfiguration = 3 + } + + public enum StartMode + { + /// + /// Device driver started by the operating system loader. This value is valid only for driver services. + /// + Boot, + + /// + /// Device driver started by the operating system initialization process. This value is valid only for driver services. + /// + System, + + /// + /// Service to be started automatically by the Service Control Manager during system startup. + /// + Automatic, + + /// + /// Service to be started by the Service Control Manager when a process calls the StartService method. + /// + Manual, + + /// + /// Service that can no longer be started. + /// + Disabled, + } + + [WmiClassName("Win32_Service")] + public interface IWin32Services : IWmiCollection + { + // ReturnValue Create(bool desktopInteract, string displayName, int errorControl, string loadOrderGroup, string loadOrderGroupDependencies, string name, string pathName, string serviceDependencies, string serviceType, string startMode, string startName, string startPassword); + void Create(string name, string displayName, string pathName, ServiceType serviceType, ErrorControl errorControl, string startMode, bool desktopInteract, string? startName, string? startPassword, string[] serviceDependencies); + + void Create(string name, string displayName, string pathName, ServiceType serviceType, ErrorControl errorControl, string startMode, bool desktopInteract, string[] serviceDependencies); + + IWin32Service? Select(string name); + } + + // https://docs.microsoft.com/windows/win32/cimwin32prov/win32-service + public interface IWin32Service : IWmiObject + { + bool Started { get; } + + void Delete(); + + void StartService(); + + void StopService(); + } +} diff --git a/src/NuGet.config b/src/NuGet.Config similarity index 100% rename from src/NuGet.config rename to src/NuGet.Config diff --git a/src/Plugins/RunawayProcessKiller/RunawayProcessKillerExtension.cs b/src/Plugins/RunawayProcessKiller/RunawayProcessKillerExtension.cs index d3196e4..3fa342f 100644 --- a/src/Plugins/RunawayProcessKiller/RunawayProcessKillerExtension.cs +++ b/src/Plugins/RunawayProcessKiller/RunawayProcessKillerExtension.cs @@ -5,6 +5,7 @@ using System.IO; using System.Text; using System.Xml; using log4net; +using WinSW.Configuration; using WinSW.Extensions; using WinSW.Util; using static WinSW.Plugins.RunawayProcessKiller.RunawayProcessKillerExtension.NativeMethods; @@ -179,7 +180,7 @@ namespace WinSW.Plugins.RunawayProcessKiller return parameters.Environment; } - public override void Configure(ServiceDescriptor descriptor, XmlNode node) + public override void Configure(IWinSWConfiguration descriptor, XmlNode node) { // We expect the upper logic to process any errors // TODO: a better parser API for types would be useful diff --git a/src/Plugins/SharedDirectoryMapper/SharedDirectoryMapper.cs b/src/Plugins/SharedDirectoryMapper/SharedDirectoryMapper.cs index 4fb45f3..4813ebb 100644 --- a/src/Plugins/SharedDirectoryMapper/SharedDirectoryMapper.cs +++ b/src/Plugins/SharedDirectoryMapper/SharedDirectoryMapper.cs @@ -1,9 +1,11 @@ using System.Collections.Generic; using System.Xml; using log4net; +using WinSW.Configuration; using WinSW.Extensions; using WinSW.Util; + namespace WinSW.Plugins.SharedDirectoryMapper { public class SharedDirectoryMapper : AbstractWinSWExtension @@ -25,7 +27,7 @@ namespace WinSW.Plugins.SharedDirectoryMapper this._entries.Add(config); } - public override void Configure(ServiceDescriptor descriptor, XmlNode node) + public override void Configure(IWinSWConfiguration descriptor, XmlNode node) { XmlNodeList? mapNodes = XmlHelper.SingleNode(node, "mapping", false)!.SelectNodes("map"); if (mapNodes != null) diff --git a/src/Test/winswTests/ServiceDescriptorTests.cs b/src/Test/winswTests/ServiceDescriptorTests.cs index 77580f9..c4ca8c8 100644 --- a/src/Test/winswTests/ServiceDescriptorTests.cs +++ b/src/Test/winswTests/ServiceDescriptorTests.cs @@ -1,445 +1,445 @@ -using System; -using System.Diagnostics; -using NUnit.Framework; -using WinSW; -using winswTests.Util; -using WMI; - -namespace winswTests -{ - [TestFixture] - public class ServiceDescriptorTests - { - private ServiceDescriptor _extendedServiceDescriptor; - - private const string ExpectedWorkingDirectory = @"Z:\Path\SubPath"; - private const string Username = "User"; - private const string Password = "Password"; - private const string Domain = "Domain"; - private const string AllowServiceAccountLogonRight = "true"; - - [SetUp] - public void SetUp() - { - string seedXml = -$@" - service.exe - Service - The service. - node.exe - My Arguments - - - {Domain} - {Username} - {Password} - {AllowServiceAccountLogonRight} - - {ExpectedWorkingDirectory} - C:\logs -"; - this._extendedServiceDescriptor = ServiceDescriptor.FromXML(seedXml); - } - - [Test] - public void DefaultStartMode() - { - Assert.That(this._extendedServiceDescriptor.StartMode, Is.EqualTo(StartMode.Automatic)); - } - - [Test] - public void IncorrectStartMode() - { - string seedXml = -$@" - service.exe - Service - The service. - node.exe - My Arguments - roll - - - {Domain} - {Username} - {Password} - {AllowServiceAccountLogonRight} - - {ExpectedWorkingDirectory} - C:\logs -"; - - this._extendedServiceDescriptor = ServiceDescriptor.FromXML(seedXml); - Assert.That(() => this._extendedServiceDescriptor.StartMode, Throws.ArgumentException); - } - - [Test] - public void ChangedStartMode() - { - string seedXml = -$@" - service.exe - Service - The service. - node.exe - My Arguments - manual - - - {Domain} - {Username} - {Password} - {AllowServiceAccountLogonRight} - - {ExpectedWorkingDirectory} - C:\logs -"; - - this._extendedServiceDescriptor = ServiceDescriptor.FromXML(seedXml); - Assert.That(this._extendedServiceDescriptor.StartMode, Is.EqualTo(StartMode.Manual)); - } - - [Test] - public void VerifyWorkingDirectory() - { - Debug.WriteLine("_extendedServiceDescriptor.WorkingDirectory :: " + this._extendedServiceDescriptor.WorkingDirectory); - Assert.That(this._extendedServiceDescriptor.WorkingDirectory, Is.EqualTo(ExpectedWorkingDirectory)); - } - - [Test] - public void VerifyServiceLogonRight() - { - Assert.That(this._extendedServiceDescriptor.AllowServiceAcountLogonRight, Is.True); - } - - [Test] - public void VerifyUsername() - { - Debug.WriteLine("_extendedServiceDescriptor.WorkingDirectory :: " + this._extendedServiceDescriptor.WorkingDirectory); - Assert.That(this._extendedServiceDescriptor.ServiceAccountUser, Is.EqualTo(Domain + "\\" + Username)); - } - - [Test] - public void VerifyPassword() - { - Debug.WriteLine("_extendedServiceDescriptor.WorkingDirectory :: " + this._extendedServiceDescriptor.WorkingDirectory); - Assert.That(this._extendedServiceDescriptor.ServiceAccountPassword, Is.EqualTo(Password)); - } - - [Test] - public void Priority() - { - var sd = ServiceDescriptor.FromXML("testnormal"); - Assert.That(sd.Priority, Is.EqualTo(ProcessPriorityClass.Normal)); - - sd = ServiceDescriptor.FromXML("testidle"); - Assert.That(sd.Priority, Is.EqualTo(ProcessPriorityClass.Idle)); - - sd = ServiceDescriptor.FromXML("test"); - Assert.That(sd.Priority, Is.EqualTo(ProcessPriorityClass.Normal)); - } - - [Test] - public void StopParentProcessFirstIsFalseByDefault() - { - Assert.That(this._extendedServiceDescriptor.StopParentProcessFirst, Is.False); - } - - [Test] - public void CanParseStopParentProcessFirst() - { - const string seedXml = "" - + "true" - + ""; - var serviceDescriptor = ServiceDescriptor.FromXML(seedXml); - - Assert.That(serviceDescriptor.StopParentProcessFirst, Is.True); - } - - [Test] - public void CanParseStopTimeout() - { - const string seedXml = "" - + "60sec" - + ""; - var serviceDescriptor = ServiceDescriptor.FromXML(seedXml); - - Assert.That(serviceDescriptor.StopTimeout, Is.EqualTo(TimeSpan.FromSeconds(60))); - } - - [Test] - public void CanParseStopTimeoutFromMinutes() - { - const string seedXml = "" - + "10min" - + ""; - var serviceDescriptor = ServiceDescriptor.FromXML(seedXml); - - Assert.That(serviceDescriptor.StopTimeout, Is.EqualTo(TimeSpan.FromMinutes(10))); - } - - [Test] - public void CanParseLogname() - { - const string seedXml = "" - + "MyTestApp" - + ""; - var serviceDescriptor = ServiceDescriptor.FromXML(seedXml); - - Assert.That(serviceDescriptor.LogName, Is.EqualTo("MyTestApp")); - } - - [Test] - public void CanParseOutfileDisabled() - { - const string seedXml = "" - + "true" - + ""; - var serviceDescriptor = ServiceDescriptor.FromXML(seedXml); - - Assert.That(serviceDescriptor.OutFileDisabled, Is.True); - } - - [Test] - public void CanParseErrfileDisabled() - { - const string seedXml = "" - + "true" - + ""; - var serviceDescriptor = ServiceDescriptor.FromXML(seedXml); - - Assert.That(serviceDescriptor.ErrFileDisabled, Is.True); - } - - [Test] - public void CanParseOutfilePattern() - { - const string seedXml = "" - + ".out.test.log" - + ""; - var serviceDescriptor = ServiceDescriptor.FromXML(seedXml); - - Assert.That(serviceDescriptor.OutFilePattern, Is.EqualTo(".out.test.log")); - } - - [Test] - public void CanParseErrfilePattern() - { - const string seedXml = "" - + ".err.test.log" - + ""; - var serviceDescriptor = ServiceDescriptor.FromXML(seedXml); - - Assert.That(serviceDescriptor.ErrFilePattern, Is.EqualTo(".err.test.log")); - } - - [Test] - public void LogModeRollBySize() - { - const string seedXml = "" - + "c:\\" - + "" - + "112" - + "113" - + "" - + ""; - - var serviceDescriptor = ServiceDescriptor.FromXML(seedXml); - serviceDescriptor.BaseName = "service"; - - var logHandler = serviceDescriptor.LogHandler as SizeBasedRollingLogAppender; - Assert.That(logHandler, Is.Not.Null); - Assert.That(logHandler.SizeTheshold, Is.EqualTo(112 * 1024)); - Assert.That(logHandler.FilesToKeep, Is.EqualTo(113)); - } - - [Test] - public void LogModeRollByTime() - { - const string seedXml = "" - + "c:\\" - + "" - + "7" - + "log pattern" - + "" - + ""; - - var serviceDescriptor = ServiceDescriptor.FromXML(seedXml); - serviceDescriptor.BaseName = "service"; - - var logHandler = serviceDescriptor.LogHandler as TimeBasedRollingLogAppender; - Assert.That(logHandler, Is.Not.Null); - Assert.That(logHandler.Period, Is.EqualTo(7)); - Assert.That(logHandler.Pattern, Is.EqualTo("log pattern")); - } - - [Test] - public void LogModeRollBySizeTime() - { - const string seedXml = "" - + "c:\\" - + "" - + "10240" - + "yyyy-MM-dd" - + "00:00:00" - + "" - + ""; - - var serviceDescriptor = ServiceDescriptor.FromXML(seedXml); - serviceDescriptor.BaseName = "service"; - - var logHandler = serviceDescriptor.LogHandler as RollingSizeTimeLogAppender; - Assert.That(logHandler, Is.Not.Null); - Assert.That(logHandler.SizeTheshold, Is.EqualTo(10240 * 1024)); - Assert.That(logHandler.FilePattern, Is.EqualTo("yyyy-MM-dd")); - Assert.That(logHandler.AutoRollAtTime, Is.EqualTo((TimeSpan?)new TimeSpan(0, 0, 0))); - } - - [Test] - public void VerifyServiceLogonRightGraceful() - { - const string seedXml = "" - + "" - + "" + Domain + "" - + "" + Username + "" - + "" + Password + "" - + "true1" - + "" - + ""; - var serviceDescriptor = ServiceDescriptor.FromXML(seedXml); - Assert.That(serviceDescriptor.AllowServiceAcountLogonRight, Is.False); - } - - [Test] - public void VerifyServiceLogonRightOmitted() - { - const string seedXml = "" - + "" - + "" + Domain + "" - + "" + Username + "" - + "" + Password + "" - + "" - + ""; - var serviceDescriptor = ServiceDescriptor.FromXML(seedXml); - Assert.That(serviceDescriptor.AllowServiceAcountLogonRight, Is.False); - } - - [Test] - public void VerifyWaitHint_FullXML() - { - var sd = ConfigXmlBuilder.create() - .WithTag("waithint", "20 min") - .ToServiceDescriptor(true); - Assert.That(sd.WaitHint, Is.EqualTo(TimeSpan.FromMinutes(20))); - } - - /// - /// Test for https://github.com/kohsuke/winsw/issues/159 - /// - [Test] - public void VerifyWaitHint_XMLWithoutVersion() - { - var sd = ConfigXmlBuilder.create(printXMLVersion: false) - .WithTag("waithint", "21 min") - .ToServiceDescriptor(true); - Assert.That(sd.WaitHint, Is.EqualTo(TimeSpan.FromMinutes(21))); - } - - [Test] - public void VerifyWaitHint_XMLWithoutComment() - { - var sd = ConfigXmlBuilder.create(xmlComment: null) - .WithTag("waithint", "22 min") - .ToServiceDescriptor(true); - Assert.That(sd.WaitHint, Is.EqualTo(TimeSpan.FromMinutes(22))); - } - - [Test] - public void VerifyWaitHint_XMLWithoutVersionAndComment() - { - var sd = ConfigXmlBuilder.create(xmlComment: null, printXMLVersion: false) - .WithTag("waithint", "23 min") - .ToServiceDescriptor(true); - Assert.That(sd.WaitHint, Is.EqualTo(TimeSpan.FromMinutes(23))); - } - - [Test] - public void VerifySleepTime() - { - var sd = ConfigXmlBuilder.create().WithTag("sleeptime", "3 hrs").ToServiceDescriptor(true); - Assert.That(sd.SleepTime, Is.EqualTo(TimeSpan.FromHours(3))); - } - - [Test] - public void VerifyResetFailureAfter() - { - var sd = ConfigXmlBuilder.create().WithTag("resetfailure", "75 sec").ToServiceDescriptor(true); - Assert.That(sd.ResetFailureAfter, Is.EqualTo(TimeSpan.FromSeconds(75))); - } - - [Test] - public void VerifyStopTimeout() - { - var sd = ConfigXmlBuilder.create().WithTag("stoptimeout", "35 secs").ToServiceDescriptor(true); - Assert.That(sd.StopTimeout, Is.EqualTo(TimeSpan.FromSeconds(35))); - } - - /// - /// https://github.com/kohsuke/winsw/issues/178 - /// - [Test] - public void Arguments_LegacyParam() - { - var sd = ConfigXmlBuilder.create().WithTag("arguments", "arg").ToServiceDescriptor(true); - Assert.That(sd.Arguments, Is.EqualTo("arg")); - } - - [Test] - public void Arguments_NewParam_Single() - { - var sd = ConfigXmlBuilder.create() - .WithTag("argument", "--arg1=2") - .ToServiceDescriptor(true); - Assert.That(sd.Arguments, Is.EqualTo(" --arg1=2")); - } - - [Test] - public void Arguments_NewParam_MultipleArgs() - { - var sd = ConfigXmlBuilder.create() - .WithTag("argument", "--arg1=2") - .WithTag("argument", "--arg2=123") - .WithTag("argument", "--arg3=null") - .ToServiceDescriptor(true); - Assert.That(sd.Arguments, Is.EqualTo(" --arg1=2 --arg2=123 --arg3=null")); - } - - /// - /// Ensures that the new single-argument field has a higher priority. - /// - [Test] - public void Arguments_Bothparam_Priorities() - { - var sd = ConfigXmlBuilder.create() - .WithTag("arguments", "--arg1=2 --arg2=3") - .WithTag("argument", "--arg2=123") - .WithTag("argument", "--arg3=null") - .ToServiceDescriptor(true); - Assert.That(sd.Arguments, Is.EqualTo(" --arg2=123 --arg3=null")); - } - - [TestCase(true)] - [TestCase(false)] - public void DelayedStart_RoundTrip(bool enabled) - { - var bldr = ConfigXmlBuilder.create(); - if (enabled) - { - bldr = bldr.WithDelayedAutoStart(); - } - - var sd = bldr.ToServiceDescriptor(); - Assert.That(sd.DelayedAutoStart, Is.EqualTo(enabled)); - } - } -} +using System; +using System.Diagnostics; +using NUnit.Framework; +using WinSW; +using winswTests.Util; +using WMI; + +namespace winswTests +{ + [TestFixture] + public class ServiceDescriptorTests + { + private ServiceDescriptor _extendedServiceDescriptor; + + private const string ExpectedWorkingDirectory = @"Z:\Path\SubPath"; + private const string Username = "User"; + private const string Password = "Password"; + private const string Domain = "Domain"; + private const string AllowServiceAccountLogonRight = "true"; + + [SetUp] + public void SetUp() + { + string seedXml = +$@" + service.exe + Service + The service. + node.exe + My Arguments + + + {Domain} + {Username} + {Password} + {AllowServiceAccountLogonRight} + + {ExpectedWorkingDirectory} + C:\logs +"; + this._extendedServiceDescriptor = ServiceDescriptor.FromXML(seedXml); + } + + [Test] + public void DefaultStartMode() + { + Assert.That(this._extendedServiceDescriptor.StartMode, Is.EqualTo(StartMode.Automatic)); + } + + [Test] + public void IncorrectStartMode() + { + string seedXml = +$@" + service.exe + Service + The service. + node.exe + My Arguments + roll + + + {Domain} + {Username} + {Password} + {AllowServiceAccountLogonRight} + + {ExpectedWorkingDirectory} + C:\logs +"; + + this._extendedServiceDescriptor = ServiceDescriptor.FromXML(seedXml); + Assert.That(() => this._extendedServiceDescriptor.StartMode, Throws.ArgumentException); + } + + [Test] + public void ChangedStartMode() + { + string seedXml = +$@" + service.exe + Service + The service. + node.exe + My Arguments + manual + + + {Domain} + {Username} + {Password} + {AllowServiceAccountLogonRight} + + {ExpectedWorkingDirectory} + C:\logs +"; + + this._extendedServiceDescriptor = ServiceDescriptor.FromXML(seedXml); + Assert.That(this._extendedServiceDescriptor.StartMode, Is.EqualTo(StartMode.Manual)); + } + + [Test] + public void VerifyWorkingDirectory() + { + Debug.WriteLine("_extendedServiceDescriptor.WorkingDirectory :: " + this._extendedServiceDescriptor.WorkingDirectory); + Assert.That(this._extendedServiceDescriptor.WorkingDirectory, Is.EqualTo(ExpectedWorkingDirectory)); + } + + [Test] + public void VerifyServiceLogonRight() + { + Assert.That(_extendedServiceDescriptor.ServiceAccount.AllowServiceAcountLogonRight, Is.True); + } + + [Test] + public void VerifyUsername() + { + Debug.WriteLine("_extendedServiceDescriptor.WorkingDirectory :: " + _extendedServiceDescriptor.WorkingDirectory); + Assert.That(_extendedServiceDescriptor.ServiceAccount.ServiceAccountUser, Is.EqualTo(Domain + "\\" + Username)); + } + + [Test] + public void VerifyPassword() + { + Debug.WriteLine("_extendedServiceDescriptor.WorkingDirectory :: " + _extendedServiceDescriptor.WorkingDirectory); + Assert.That(_extendedServiceDescriptor.ServiceAccount.ServiceAccountPassword, Is.EqualTo(Password)); + } + + [Test] + public void Priority() + { + var sd = ServiceDescriptor.FromXML("testnormal"); + Assert.That(sd.Priority, Is.EqualTo(ProcessPriorityClass.Normal)); + + sd = ServiceDescriptor.FromXML("testidle"); + Assert.That(sd.Priority, Is.EqualTo(ProcessPriorityClass.Idle)); + + sd = ServiceDescriptor.FromXML("test"); + Assert.That(sd.Priority, Is.EqualTo(ProcessPriorityClass.Normal)); + } + + [Test] + public void StopParentProcessFirstIsFalseByDefault() + { + Assert.That(this._extendedServiceDescriptor.StopParentProcessFirst, Is.False); + } + + [Test] + public void CanParseStopParentProcessFirst() + { + const string seedXml = "" + + "true" + + ""; + var serviceDescriptor = ServiceDescriptor.FromXML(seedXml); + + Assert.That(serviceDescriptor.StopParentProcessFirst, Is.True); + } + + [Test] + public void CanParseStopTimeout() + { + const string seedXml = "" + + "60sec" + + ""; + var serviceDescriptor = ServiceDescriptor.FromXML(seedXml); + + Assert.That(serviceDescriptor.StopTimeout, Is.EqualTo(TimeSpan.FromSeconds(60))); + } + + [Test] + public void CanParseStopTimeoutFromMinutes() + { + const string seedXml = "" + + "10min" + + ""; + var serviceDescriptor = ServiceDescriptor.FromXML(seedXml); + + Assert.That(serviceDescriptor.StopTimeout, Is.EqualTo(TimeSpan.FromMinutes(10))); + } + + [Test] + public void CanParseLogname() + { + const string seedXml = "" + + "MyTestApp" + + ""; + var serviceDescriptor = ServiceDescriptor.FromXML(seedXml); + + Assert.That(serviceDescriptor.LogName, Is.EqualTo("MyTestApp")); + } + + [Test] + public void CanParseOutfileDisabled() + { + const string seedXml = "" + + "true" + + ""; + var serviceDescriptor = ServiceDescriptor.FromXML(seedXml); + + Assert.That(serviceDescriptor.Log.OutFileDisabled, Is.True); + } + + [Test] + public void CanParseErrfileDisabled() + { + const string seedXml = "" + + "true" + + ""; + var serviceDescriptor = ServiceDescriptor.FromXML(seedXml); + + Assert.That(serviceDescriptor.Log.ErrFileDisabled, Is.True); + } + + [Test] + public void CanParseOutfilePattern() + { + const string seedXml = "" + + ".out.test.log" + + ""; + var serviceDescriptor = ServiceDescriptor.FromXML(seedXml); + + Assert.That(serviceDescriptor.Log.OutFilePattern, Is.EqualTo(".out.test.log")); + } + + [Test] + public void CanParseErrfilePattern() + { + const string seedXml = "" + + ".err.test.log" + + ""; + var serviceDescriptor = ServiceDescriptor.FromXML(seedXml); + + Assert.That(serviceDescriptor.Log.ErrFilePattern, Is.EqualTo(".err.test.log")); + } + + [Test] + public void LogModeRollBySize() + { + const string seedXml = "" + + "c:\\" + + "" + + "112" + + "113" + + "" + + ""; + + var serviceDescriptor = ServiceDescriptor.FromXML(seedXml); + serviceDescriptor.BaseName = "service"; + + var logHandler = serviceDescriptor.Log.CreateLogHandler() as SizeBasedRollingLogAppender; + Assert.That(logHandler, Is.Not.Null); + Assert.That(logHandler.SizeTheshold, Is.EqualTo(112 * 1024)); + Assert.That(logHandler.FilesToKeep, Is.EqualTo(113)); + } + + [Test] + public void LogModeRollByTime() + { + const string seedXml = "" + + "c:\\" + + "" + + "7" + + "log pattern" + + "" + + ""; + + var serviceDescriptor = ServiceDescriptor.FromXML(seedXml); + serviceDescriptor.BaseName = "service"; + + var logHandler = serviceDescriptor.Log.CreateLogHandler() as TimeBasedRollingLogAppender; + Assert.That(logHandler, Is.Not.Null); + Assert.That(logHandler.Period, Is.EqualTo(7)); + Assert.That(logHandler.Pattern, Is.EqualTo("log pattern")); + } + + [Test] + public void LogModeRollBySizeTime() + { + const string seedXml = "" + + "c:\\" + + "" + + "10240" + + "yyyy-MM-dd" + + "00:00:00" + + "" + + ""; + + var serviceDescriptor = ServiceDescriptor.FromXML(seedXml); + serviceDescriptor.BaseName = "service"; + + var logHandler = serviceDescriptor.Log.CreateLogHandler() as RollingSizeTimeLogAppender; + Assert.That(logHandler, Is.Not.Null); + Assert.That(logHandler.SizeTheshold, Is.EqualTo(10240 * 1024)); + Assert.That(logHandler.FilePattern, Is.EqualTo("yyyy-MM-dd")); + Assert.That(logHandler.AutoRollAtTime, Is.EqualTo((TimeSpan?)new TimeSpan(0, 0, 0))); + } + + [Test] + public void VerifyServiceLogonRightGraceful() + { + const string seedXml = "" + + "" + + "" + Domain + "" + + "" + Username + "" + + "" + Password + "" + + "true1" + + "" + + ""; + var serviceDescriptor = ServiceDescriptor.FromXML(seedXml); + Assert.That(serviceDescriptor.ServiceAccount.AllowServiceAcountLogonRight, Is.False); + } + + [Test] + public void VerifyServiceLogonRightOmitted() + { + const string seedXml = "" + + "" + + "" + Domain + "" + + "" + Username + "" + + "" + Password + "" + + "" + + ""; + var serviceDescriptor = ServiceDescriptor.FromXML(seedXml); + Assert.That(serviceDescriptor.ServiceAccount.AllowServiceAcountLogonRight, Is.False); + } + + [Test] + public void VerifyWaitHint_FullXML() + { + var sd = ConfigXmlBuilder.create() + .WithTag("waithint", "20 min") + .ToServiceDescriptor(true); + Assert.That(sd.WaitHint, Is.EqualTo(TimeSpan.FromMinutes(20))); + } + + /// + /// Test for https://github.com/kohsuke/winsw/issues/159 + /// + [Test] + public void VerifyWaitHint_XMLWithoutVersion() + { + var sd = ConfigXmlBuilder.create(printXMLVersion: false) + .WithTag("waithint", "21 min") + .ToServiceDescriptor(true); + Assert.That(sd.WaitHint, Is.EqualTo(TimeSpan.FromMinutes(21))); + } + + [Test] + public void VerifyWaitHint_XMLWithoutComment() + { + var sd = ConfigXmlBuilder.create(xmlComment: null) + .WithTag("waithint", "22 min") + .ToServiceDescriptor(true); + Assert.That(sd.WaitHint, Is.EqualTo(TimeSpan.FromMinutes(22))); + } + + [Test] + public void VerifyWaitHint_XMLWithoutVersionAndComment() + { + var sd = ConfigXmlBuilder.create(xmlComment: null, printXMLVersion: false) + .WithTag("waithint", "23 min") + .ToServiceDescriptor(true); + Assert.That(sd.WaitHint, Is.EqualTo(TimeSpan.FromMinutes(23))); + } + + [Test] + public void VerifySleepTime() + { + var sd = ConfigXmlBuilder.create().WithTag("sleeptime", "3 hrs").ToServiceDescriptor(true); + Assert.That(sd.SleepTime, Is.EqualTo(TimeSpan.FromHours(3))); + } + + [Test] + public void VerifyResetFailureAfter() + { + var sd = ConfigXmlBuilder.create().WithTag("resetfailure", "75 sec").ToServiceDescriptor(true); + Assert.That(sd.ResetFailureAfter, Is.EqualTo(TimeSpan.FromSeconds(75))); + } + + [Test] + public void VerifyStopTimeout() + { + var sd = ConfigXmlBuilder.create().WithTag("stoptimeout", "35 secs").ToServiceDescriptor(true); + Assert.That(sd.StopTimeout, Is.EqualTo(TimeSpan.FromSeconds(35))); + } + + /// + /// https://github.com/kohsuke/winsw/issues/178 + /// + [Test] + public void Arguments_LegacyParam() + { + var sd = ConfigXmlBuilder.create().WithTag("arguments", "arg").ToServiceDescriptor(true); + Assert.That(sd.Arguments, Is.EqualTo("arg")); + } + + [Test] + public void Arguments_NewParam_Single() + { + var sd = ConfigXmlBuilder.create() + .WithTag("argument", "--arg1=2") + .ToServiceDescriptor(true); + Assert.That(sd.Arguments, Is.EqualTo(" --arg1=2")); + } + + [Test] + public void Arguments_NewParam_MultipleArgs() + { + var sd = ConfigXmlBuilder.create() + .WithTag("argument", "--arg1=2") + .WithTag("argument", "--arg2=123") + .WithTag("argument", "--arg3=null") + .ToServiceDescriptor(true); + Assert.That(sd.Arguments, Is.EqualTo(" --arg1=2 --arg2=123 --arg3=null")); + } + + /// + /// Ensures that the new single-argument field has a higher priority. + /// + [Test] + public void Arguments_Bothparam_Priorities() + { + var sd = ConfigXmlBuilder.create() + .WithTag("arguments", "--arg1=2 --arg2=3") + .WithTag("argument", "--arg2=123") + .WithTag("argument", "--arg3=null") + .ToServiceDescriptor(true); + Assert.That(sd.Arguments, Is.EqualTo(" --arg2=123 --arg3=null")); + } + + [TestCase(true)] + [TestCase(false)] + public void DelayedStart_RoundTrip(bool enabled) + { + var bldr = ConfigXmlBuilder.create(); + if (enabled) + { + bldr = bldr.WithDelayedAutoStart(); + } + + var sd = bldr.ToServiceDescriptor(); + Assert.That(sd.DelayedAutoStart, Is.EqualTo(enabled)); + } + } +} diff --git a/src/Test/winswTests/ServiceDescriptorYamlTest.cs b/src/Test/winswTests/ServiceDescriptorYamlTest.cs new file mode 100644 index 0000000..c2ca3f2 --- /dev/null +++ b/src/Test/winswTests/ServiceDescriptorYamlTest.cs @@ -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()); + } + + [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))); + + } + } +} diff --git a/src/Test/winswTests/Util/ServiceDescriptorAssert.cs b/src/Test/winswTests/Util/ServiceDescriptorAssert.cs index 37b1a11..ed0ece3 100644 --- a/src/Test/winswTests/Util/ServiceDescriptorAssert.cs +++ b/src/Test/winswTests/Util/ServiceDescriptorAssert.cs @@ -59,6 +59,10 @@ namespace winswTests.Util properties.Remove("Caption"); properties.Remove("Description"); properties.Remove("Executable"); + properties.Remove("BaseName"); + properties.Remove("BasePath"); + properties.Remove("Log"); + properties.Remove("ServiceAccount"); return properties; } } diff --git a/src/winsw.sln b/src/winsw.sln index 3aba401..e41dfdf 100644 --- a/src/winsw.sln +++ b/src/winsw.sln @@ -1,87 +1,87 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 2013 -VisualStudioVersion = 12.0.31101.0 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "winsw", "Core\ServiceWrapper\winsw.csproj", "{0DE77F55-ADE5-43C1-999A-0BC81153B039}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "winswTests", "Test\winswTests\winswTests.csproj", "{93843402-842B-44B4-B303-AEE829BE0B43}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharedDirectoryMapper", "Plugins\SharedDirectoryMapper\SharedDirectoryMapper.csproj", "{CA5C71DB-C5A8-4C27-BF83-8E6DAED9D6B5}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{077C2CEC-B687-4B53-86E9-C1A1BF5554E5}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Plugins", "Plugins", "{BC4AD891-E87E-4F30-867C-FD8084A29E5D}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Core", "Core", "{5297623A-1A95-4F89-9AAE-DA634081EC86}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WinSWCore", "Core\WinSWCore\WinSWCore.csproj", "{9D0C63E2-B6FF-4A85-BD36-B3E5D7F27D06}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RunawayProcessKiller", "Plugins\RunawayProcessKiller\RunawayProcessKiller.csproj", "{57284B7A-82A4-407A-B706-EBEA6BF8EA13}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{AA414F46-B863-473A-A0E0-C2971B3396AE}" - ProjectSection(SolutionItems) = preProject - .editorconfig = .editorconfig - ..\examples\sample-allOptions.xml = ..\examples\sample-allOptions.xml - ..\examples\sample-minimal.xml = ..\examples\sample-minimal.xml - EndProjectSection -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Debug|Win32 = Debug|Win32 - Release|Any CPU = Release|Any CPU - Release|Win32 = Release|Win32 - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {0DE77F55-ADE5-43C1-999A-0BC81153B039}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0DE77F55-ADE5-43C1-999A-0BC81153B039}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0DE77F55-ADE5-43C1-999A-0BC81153B039}.Debug|Win32.ActiveCfg = Debug|Any CPU - {0DE77F55-ADE5-43C1-999A-0BC81153B039}.Debug|Win32.Build.0 = Debug|Any CPU - {0DE77F55-ADE5-43C1-999A-0BC81153B039}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0DE77F55-ADE5-43C1-999A-0BC81153B039}.Release|Any CPU.Build.0 = Release|Any CPU - {0DE77F55-ADE5-43C1-999A-0BC81153B039}.Release|Win32.ActiveCfg = Release|Any CPU - {0DE77F55-ADE5-43C1-999A-0BC81153B039}.Release|Win32.Build.0 = Release|Any CPU - {93843402-842B-44B4-B303-AEE829BE0B43}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {93843402-842B-44B4-B303-AEE829BE0B43}.Debug|Any CPU.Build.0 = Debug|Any CPU - {93843402-842B-44B4-B303-AEE829BE0B43}.Debug|Win32.ActiveCfg = Debug|Any CPU - {93843402-842B-44B4-B303-AEE829BE0B43}.Debug|Win32.Build.0 = Debug|Any CPU - {93843402-842B-44B4-B303-AEE829BE0B43}.Release|Any CPU.ActiveCfg = Release|Any CPU - {93843402-842B-44B4-B303-AEE829BE0B43}.Release|Any CPU.Build.0 = Release|Any CPU - {93843402-842B-44B4-B303-AEE829BE0B43}.Release|Win32.ActiveCfg = Release|Any CPU - {93843402-842B-44B4-B303-AEE829BE0B43}.Release|Win32.Build.0 = Release|Any CPU - {CA5C71DB-C5A8-4C27-BF83-8E6DAED9D6B5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {CA5C71DB-C5A8-4C27-BF83-8E6DAED9D6B5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {CA5C71DB-C5A8-4C27-BF83-8E6DAED9D6B5}.Debug|Win32.ActiveCfg = Debug|Any CPU - {CA5C71DB-C5A8-4C27-BF83-8E6DAED9D6B5}.Debug|Win32.Build.0 = Debug|Any CPU - {CA5C71DB-C5A8-4C27-BF83-8E6DAED9D6B5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {CA5C71DB-C5A8-4C27-BF83-8E6DAED9D6B5}.Release|Any CPU.Build.0 = Release|Any CPU - {CA5C71DB-C5A8-4C27-BF83-8E6DAED9D6B5}.Release|Win32.ActiveCfg = Release|Any CPU - {CA5C71DB-C5A8-4C27-BF83-8E6DAED9D6B5}.Release|Win32.Build.0 = Release|Any CPU - {9D0C63E2-B6FF-4A85-BD36-B3E5D7F27D06}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9D0C63E2-B6FF-4A85-BD36-B3E5D7F27D06}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9D0C63E2-B6FF-4A85-BD36-B3E5D7F27D06}.Debug|Win32.ActiveCfg = Debug|Any CPU - {9D0C63E2-B6FF-4A85-BD36-B3E5D7F27D06}.Debug|Win32.Build.0 = Debug|Any CPU - {9D0C63E2-B6FF-4A85-BD36-B3E5D7F27D06}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9D0C63E2-B6FF-4A85-BD36-B3E5D7F27D06}.Release|Any CPU.Build.0 = Release|Any CPU - {9D0C63E2-B6FF-4A85-BD36-B3E5D7F27D06}.Release|Win32.ActiveCfg = Release|Any CPU - {9D0C63E2-B6FF-4A85-BD36-B3E5D7F27D06}.Release|Win32.Build.0 = Release|Any CPU - {57284B7A-82A4-407A-B706-EBEA6BF8EA13}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {57284B7A-82A4-407A-B706-EBEA6BF8EA13}.Debug|Any CPU.Build.0 = Debug|Any CPU - {57284B7A-82A4-407A-B706-EBEA6BF8EA13}.Debug|Win32.ActiveCfg = Debug|Any CPU - {57284B7A-82A4-407A-B706-EBEA6BF8EA13}.Debug|Win32.Build.0 = Debug|Any CPU - {57284B7A-82A4-407A-B706-EBEA6BF8EA13}.Release|Any CPU.ActiveCfg = Release|Any CPU - {57284B7A-82A4-407A-B706-EBEA6BF8EA13}.Release|Any CPU.Build.0 = Release|Any CPU - {57284B7A-82A4-407A-B706-EBEA6BF8EA13}.Release|Win32.ActiveCfg = Release|Any CPU - {57284B7A-82A4-407A-B706-EBEA6BF8EA13}.Release|Win32.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {0DE77F55-ADE5-43C1-999A-0BC81153B039} = {5297623A-1A95-4F89-9AAE-DA634081EC86} - {93843402-842B-44B4-B303-AEE829BE0B43} = {077C2CEC-B687-4B53-86E9-C1A1BF5554E5} - {CA5C71DB-C5A8-4C27-BF83-8E6DAED9D6B5} = {BC4AD891-E87E-4F30-867C-FD8084A29E5D} - {9D0C63E2-B6FF-4A85-BD36-B3E5D7F27D06} = {5297623A-1A95-4F89-9AAE-DA634081EC86} - {57284B7A-82A4-407A-B706-EBEA6BF8EA13} = {BC4AD891-E87E-4F30-867C-FD8084A29E5D} - EndGlobalSection -EndGlobal +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 2013 +VisualStudioVersion = 12.0.31101.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "winsw", "Core\ServiceWrapper\winsw.csproj", "{0DE77F55-ADE5-43C1-999A-0BC81153B039}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "winswTests", "Test\winswTests\winswTests.csproj", "{93843402-842B-44B4-B303-AEE829BE0B43}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharedDirectoryMapper", "Plugins\SharedDirectoryMapper\SharedDirectoryMapper.csproj", "{CA5C71DB-C5A8-4C27-BF83-8E6DAED9D6B5}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{077C2CEC-B687-4B53-86E9-C1A1BF5554E5}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Plugins", "Plugins", "{BC4AD891-E87E-4F30-867C-FD8084A29E5D}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Core", "Core", "{5297623A-1A95-4F89-9AAE-DA634081EC86}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WinSWCore", "Core\WinSWCore\WinSWCore.csproj", "{9D0C63E2-B6FF-4A85-BD36-B3E5D7F27D06}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RunawayProcessKiller", "Plugins\RunawayProcessKiller\RunawayProcessKiller.csproj", "{57284B7A-82A4-407A-B706-EBEA6BF8EA13}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{AA414F46-B863-473A-A0E0-C2971B3396AE}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + ..\examples\sample-allOptions.xml = ..\examples\sample-allOptions.xml + ..\examples\sample-minimal.xml = ..\examples\sample-minimal.xml + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|Win32 = Debug|Win32 + Release|Any CPU = Release|Any CPU + Release|Win32 = Release|Win32 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {0DE77F55-ADE5-43C1-999A-0BC81153B039}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0DE77F55-ADE5-43C1-999A-0BC81153B039}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0DE77F55-ADE5-43C1-999A-0BC81153B039}.Debug|Win32.ActiveCfg = Debug|Any CPU + {0DE77F55-ADE5-43C1-999A-0BC81153B039}.Debug|Win32.Build.0 = Debug|Any CPU + {0DE77F55-ADE5-43C1-999A-0BC81153B039}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0DE77F55-ADE5-43C1-999A-0BC81153B039}.Release|Any CPU.Build.0 = Release|Any CPU + {0DE77F55-ADE5-43C1-999A-0BC81153B039}.Release|Win32.ActiveCfg = Release|Any CPU + {0DE77F55-ADE5-43C1-999A-0BC81153B039}.Release|Win32.Build.0 = Release|Any CPU + {93843402-842B-44B4-B303-AEE829BE0B43}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {93843402-842B-44B4-B303-AEE829BE0B43}.Debug|Any CPU.Build.0 = Debug|Any CPU + {93843402-842B-44B4-B303-AEE829BE0B43}.Debug|Win32.ActiveCfg = Debug|Any CPU + {93843402-842B-44B4-B303-AEE829BE0B43}.Debug|Win32.Build.0 = Debug|Any CPU + {93843402-842B-44B4-B303-AEE829BE0B43}.Release|Any CPU.ActiveCfg = Release|Any CPU + {93843402-842B-44B4-B303-AEE829BE0B43}.Release|Any CPU.Build.0 = Release|Any CPU + {93843402-842B-44B4-B303-AEE829BE0B43}.Release|Win32.ActiveCfg = Release|Any CPU + {93843402-842B-44B4-B303-AEE829BE0B43}.Release|Win32.Build.0 = Release|Any CPU + {CA5C71DB-C5A8-4C27-BF83-8E6DAED9D6B5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CA5C71DB-C5A8-4C27-BF83-8E6DAED9D6B5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CA5C71DB-C5A8-4C27-BF83-8E6DAED9D6B5}.Debug|Win32.ActiveCfg = Debug|Any CPU + {CA5C71DB-C5A8-4C27-BF83-8E6DAED9D6B5}.Debug|Win32.Build.0 = Debug|Any CPU + {CA5C71DB-C5A8-4C27-BF83-8E6DAED9D6B5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CA5C71DB-C5A8-4C27-BF83-8E6DAED9D6B5}.Release|Any CPU.Build.0 = Release|Any CPU + {CA5C71DB-C5A8-4C27-BF83-8E6DAED9D6B5}.Release|Win32.ActiveCfg = Release|Any CPU + {CA5C71DB-C5A8-4C27-BF83-8E6DAED9D6B5}.Release|Win32.Build.0 = Release|Any CPU + {9D0C63E2-B6FF-4A85-BD36-B3E5D7F27D06}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9D0C63E2-B6FF-4A85-BD36-B3E5D7F27D06}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9D0C63E2-B6FF-4A85-BD36-B3E5D7F27D06}.Debug|Win32.ActiveCfg = Debug|Any CPU + {9D0C63E2-B6FF-4A85-BD36-B3E5D7F27D06}.Debug|Win32.Build.0 = Debug|Any CPU + {9D0C63E2-B6FF-4A85-BD36-B3E5D7F27D06}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9D0C63E2-B6FF-4A85-BD36-B3E5D7F27D06}.Release|Any CPU.Build.0 = Release|Any CPU + {9D0C63E2-B6FF-4A85-BD36-B3E5D7F27D06}.Release|Win32.ActiveCfg = Release|Any CPU + {9D0C63E2-B6FF-4A85-BD36-B3E5D7F27D06}.Release|Win32.Build.0 = Release|Any CPU + {57284B7A-82A4-407A-B706-EBEA6BF8EA13}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {57284B7A-82A4-407A-B706-EBEA6BF8EA13}.Debug|Any CPU.Build.0 = Debug|Any CPU + {57284B7A-82A4-407A-B706-EBEA6BF8EA13}.Debug|Win32.ActiveCfg = Debug|Any CPU + {57284B7A-82A4-407A-B706-EBEA6BF8EA13}.Debug|Win32.Build.0 = Debug|Any CPU + {57284B7A-82A4-407A-B706-EBEA6BF8EA13}.Release|Any CPU.ActiveCfg = Release|Any CPU + {57284B7A-82A4-407A-B706-EBEA6BF8EA13}.Release|Any CPU.Build.0 = Release|Any CPU + {57284B7A-82A4-407A-B706-EBEA6BF8EA13}.Release|Win32.ActiveCfg = Release|Any CPU + {57284B7A-82A4-407A-B706-EBEA6BF8EA13}.Release|Win32.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {0DE77F55-ADE5-43C1-999A-0BC81153B039} = {5297623A-1A95-4F89-9AAE-DA634081EC86} + {93843402-842B-44B4-B303-AEE829BE0B43} = {077C2CEC-B687-4B53-86E9-C1A1BF5554E5} + {CA5C71DB-C5A8-4C27-BF83-8E6DAED9D6B5} = {BC4AD891-E87E-4F30-867C-FD8084A29E5D} + {9D0C63E2-B6FF-4A85-BD36-B3E5D7F27D06} = {5297623A-1A95-4F89-9AAE-DA634081EC86} + {57284B7A-82A4-407A-B706-EBEA6BF8EA13} = {BC4AD891-E87E-4F30-867C-FD8084A29E5D} + EndGlobalSection +EndGlobal