Browse Source

Reorganize projects

NextTurn 5 years ago
parent
commit
bbb3cd578c
74 changed files with 2349 additions and 2437 deletions
  1. 0 3
      CHANGELOG.md
  2. 0 2
      README.md
  3. 0 0
      docs/self-restarting-service.md
  4. 5 5
      eng/build.yml
  5. 0 0
      samples/sample-runaway-process-killer.xml
  6. 0 0
      samples/sample-shared-directory-mapper.xml
  7. 0 3
      src/Core/ServiceWrapper/Properties/AssemblyInfo.cs
  8. 0 3
      src/Core/WinSWCore/AssemblyInfo.cs
  9. 0 180
      src/Plugins/RunawayProcessKiller/NativeMethods.cs
  10. 0 30
      src/Plugins/SharedDirectoryMapper/NativeMethods.cs
  11. 0 19
      src/Plugins/SharedDirectoryMapper/SharedDirectoryMapper.csproj
  12. 3 0
      src/WinSW.Core/AssemblyInfo.cs
  13. 0 0
      src/WinSW.Core/Configuration/DefaultSettings.cs
  14. 0 0
      src/WinSW.Core/Configuration/IWinSWConfiguration.cs
  15. 229 229
      src/WinSW.Core/Download.cs
  16. 0 0
      src/WinSW.Core/Extensions/AbstractWinSWExtension.cs
  17. 0 0
      src/WinSW.Core/Extensions/ExtensionException.cs
  18. 0 0
      src/WinSW.Core/Extensions/IWinSWExtension.cs
  19. 0 0
      src/WinSW.Core/Extensions/WinSWExtensionDescriptor.cs
  20. 0 0
      src/WinSW.Core/Extensions/WinSWExtensionManager.cs
  21. 587 587
      src/WinSW.Core/LogAppenders.cs
  22. 0 0
      src/WinSW.Core/Logging/IServiceEventLogProvider.cs
  23. 0 0
      src/WinSW.Core/Logging/ServiceEventLogAppender.cs
  24. 0 0
      src/WinSW.Core/Native/ConsoleApis.cs
  25. 0 0
      src/WinSW.Core/Native/CredentialApis.cs
  26. 0 0
      src/WinSW.Core/Native/Errors.cs
  27. 0 0
      src/WinSW.Core/Native/HandleApis.cs
  28. 0 0
      src/WinSW.Core/Native/Libraries.cs
  29. 0 0
      src/WinSW.Core/Native/ProcessApis.cs
  30. 0 0
      src/WinSW.Core/Native/Security.cs
  31. 0 0
      src/WinSW.Core/Native/SecurityApis.cs
  32. 0 0
      src/WinSW.Core/Native/Service.cs
  33. 0 0
      src/WinSW.Core/Native/ServiceApis.cs
  34. 0 0
      src/WinSW.Core/Native/Throw.cs
  35. 0 0
      src/WinSW.Core/NullableAttributes.cs
  36. 108 108
      src/WinSW.Core/PeriodicRollingCalendar.cs
  37. 730 730
      src/WinSW.Core/ServiceDescriptor.cs
  38. 0 0
      src/WinSW.Core/Util/FileHelper.cs
  39. 0 0
      src/WinSW.Core/Util/ProcessHelper.cs
  40. 0 0
      src/WinSW.Core/Util/SignalHelper.cs
  41. 0 0
      src/WinSW.Core/Util/XmlHelper.cs
  42. 0 2
      src/WinSW.Core/WinSW.Core.csproj
  43. 0 0
      src/WinSW.Core/WinSWException.cs
  44. 0 0
      src/WinSW.Core/WinSWSystem.cs
  45. 174 1
      src/WinSW.Plugins/RunawayProcessKillerExtension.cs
  46. 28 1
      src/WinSW.Plugins/SharedDirectoryMapper.cs
  47. 0 0
      src/WinSW.Plugins/SharedDirectoryMapperConfig.cs
  48. 1 3
      src/WinSW.Plugins/WinSW.Plugins.csproj
  49. 0 0
      src/WinSW.Tests/Attributes/ElevatedFactAttribute.cs
  50. 0 0
      src/WinSW.Tests/Configuration/ExamplesTest.cs
  51. 0 0
      src/WinSW.Tests/DownloadConfigTests.cs
  52. 0 0
      src/WinSW.Tests/DownloadTests.cs
  53. 0 0
      src/WinSW.Tests/Extensions/ExtensionTestBase.cs
  54. 0 0
      src/WinSW.Tests/Extensions/RunawayProcessKillerTest.cs
  55. 0 0
      src/WinSW.Tests/Extensions/SharedDirectoryMapperConfigTest.cs
  56. 0 0
      src/WinSW.Tests/Extensions/SharedDirectoryMapperTests.cs
  57. 0 0
      src/WinSW.Tests/MainTest.cs
  58. 423 423
      src/WinSW.Tests/ServiceDescriptorTests.cs
  59. 0 0
      src/WinSW.Tests/Util/CLITestHelper.cs
  60. 0 0
      src/WinSW.Tests/Util/ConfigXmlBuilder.cs
  61. 0 0
      src/WinSW.Tests/Util/DateTimeExtensions.cs
  62. 0 0
      src/WinSW.Tests/Util/FilesystemTestHelper.cs
  63. 0 0
      src/WinSW.Tests/Util/ServiceDescriptorAssert.cs
  64. 4 7
      src/WinSW.Tests/WinSW.Tests.csproj
  65. 48 0
      src/WinSW.sln
  66. 3 0
      src/WinSW/AssemblyInfo.cs
  67. 0 0
      src/WinSW/Logging/WrapperServiceEventLogProvider.cs
  68. 0 0
      src/WinSW/NullableAttributes.cs
  69. 0 0
      src/WinSW/Program.cs
  70. 0 0
      src/WinSW/ServiceControllerExtension.cs
  71. 0 0
      src/WinSW/UserException.cs
  72. 5 9
      src/WinSW/WinSW.csproj
  73. 1 1
      src/WinSW/WrapperService.cs
  74. 0 91
      src/winsw.sln

+ 0 - 3
CHANGELOG.md

@@ -1,3 +0,0 @@
-# Release notes
-
-This content has been moved to the [Releases](https://github.com/winsw/winsw/releases).

+ 0 - 2
README.md

@@ -64,8 +64,6 @@ User documentation:
 * Use-cases:
   * [Self-restarting services](docs/self-restarting-service.md)
   * [Deferred file operations](docs/deferred-file-operations.md)
-* Configuration Management:
-  * [Puppet Forge Module](docs/puppetWinSW.md)
 
 Developer documentation:
 

+ 0 - 0
docs/selfRestartingService.md → docs/self-restarting-service.md


+ 5 - 5
eng/build.yml

@@ -28,10 +28,10 @@ jobs:
         BuildConfiguration: Release
   steps:
   - script: |
-      dotnet build -c $(BuildConfiguration) src\winsw.sln -p:Version=$(BuildVersion)
-      dotnet publish -c $(BuildConfiguration) -f netcoreapp3.1 src\Core\ServiceWrapper\winsw.csproj -p:Version=$(BuildVersion)
-      dotnet publish -c $(BuildConfiguration) -f netcoreapp3.1 -r win-x64 src\Core\ServiceWrapper\winsw.csproj -p:PublishSingleFile=true -p:PublishTrimmed=true -p:Version=$(BuildVersion)
-      dotnet publish -c $(BuildConfiguration) -f netcoreapp3.1 -r win-x86 src\Core\ServiceWrapper\winsw.csproj -p:PublishSingleFile=true -p:PublishTrimmed=true -p:Version=$(BuildVersion)
+      dotnet build -c $(BuildConfiguration) src\WinSW.sln -p:Version=$(BuildVersion)
+      dotnet publish -c $(BuildConfiguration) -f netcoreapp3.1 src\WinSW\WinSW.csproj -p:Version=$(BuildVersion)
+      dotnet publish -c $(BuildConfiguration) -f netcoreapp3.1 -r win-x64 src\WinSW\WinSW.csproj -p:PublishSingleFile=true -p:PublishTrimmed=true -p:Version=$(BuildVersion)
+      dotnet publish -c $(BuildConfiguration) -f netcoreapp3.1 -r win-x86 src\WinSW\WinSW.csproj -p:PublishSingleFile=true -p:PublishTrimmed=true -p:Version=$(BuildVersion)
     displayName: Build
   - task: NuGetToolInstaller@1
     displayName: Install Nuget
@@ -44,7 +44,7 @@ jobs:
       packagesToPack: WinSW.nuspec
       versioningScheme: byEnvVar
       versionEnvVar: BuildVersion
-  - script: dotnet test -c $(BuildConfiguration) --no-build src\Test\winswTests\winswTests.csproj
+  - script: dotnet test -c $(BuildConfiguration) --no-build src\WinSW.Tests\WinSW.Tests.csproj
     displayName: Test
   - task: PublishBuildArtifacts@1
     displayName: Publish .NET 4.6.1

+ 0 - 0
src/Plugins/RunawayProcessKiller/sampleConfig.xml → samples/sample-runaway-process-killer.xml


+ 0 - 0
src/Plugins/SharedDirectoryMapper/sampleConfig.xml → samples/sample-shared-directory-mapper.xml


+ 0 - 3
src/Core/ServiceWrapper/Properties/AssemblyInfo.cs

@@ -1,3 +0,0 @@
-using System.Runtime.CompilerServices;
-
-[assembly: InternalsVisibleTo("winswTests")]

+ 0 - 3
src/Core/WinSWCore/AssemblyInfo.cs

@@ -1,3 +0,0 @@
-using System.Runtime.CompilerServices;
-
-[assembly: InternalsVisibleTo("WindowsService")]

+ 0 - 180
src/Plugins/RunawayProcessKiller/NativeMethods.cs

@@ -1,180 +0,0 @@
-using System;
-using System.Runtime.InteropServices;
-
-namespace WinSW.Plugins.RunawayProcessKiller
-{
-    public partial class RunawayProcessKillerExtension
-    {
-        internal static class NativeMethods
-        {
-            private const string Kernel32 = "kernel32.dll";
-            private const string NTDll = "ntdll.dll";
-
-            [DllImport(Kernel32)]
-            internal static extern int IsWow64Process(IntPtr hProcess, out int Wow64Process);
-
-            [DllImport(NTDll)]
-            internal static extern int NtQueryInformationProcess(
-                IntPtr ProcessHandle,
-                PROCESSINFOCLASS ProcessInformationClass,
-                out PROCESS_BASIC_INFORMATION32 ProcessInformation,
-                int ProcessInformationLength,
-                IntPtr ReturnLength = default);
-
-            [DllImport(NTDll)]
-            internal static extern int NtQueryInformationProcess(
-                IntPtr ProcessHandle,
-                PROCESSINFOCLASS ProcessInformationClass,
-                out PROCESS_BASIC_INFORMATION64 ProcessInformation,
-                int ProcessInformationLength,
-                IntPtr ReturnLength = default);
-
-            [DllImport(NTDll)]
-            internal static extern unsafe int NtReadVirtualMemory(
-                IntPtr ProcessHandle,
-                IntPtr BaseAddress,
-                void* Buffer,
-                IntPtr BufferSize,
-                IntPtr NumberOfBytesRead = default);
-
-            [DllImport(NTDll)]
-            internal static extern int NtWow64QueryInformationProcess64(
-                IntPtr ProcessHandle,
-                PROCESSINFOCLASS ProcessInformationClass,
-                out PROCESS_BASIC_INFORMATION64 ProcessInformation,
-                int ProcessInformationLength,
-                IntPtr ReturnLength = default);
-
-            [DllImport(NTDll)]
-            internal static extern unsafe int NtWow64ReadVirtualMemory64(
-                IntPtr ProcessHandle,
-                long BaseAddress,
-                void* Buffer,
-                long BufferSize,
-                long NumberOfBytesRead = default);
-
-            internal enum PROCESSINFOCLASS
-            {
-                ProcessBasicInformation = 0,
-            }
-
-            [StructLayout(LayoutKind.Sequential)]
-            internal readonly struct MEMORY_BASIC_INFORMATION
-            {
-                public readonly IntPtr BaseAddress;
-                private readonly IntPtr AllocationBase;
-                private readonly uint AllocationProtect;
-                public readonly IntPtr RegionSize;
-                private readonly uint State;
-                private readonly uint Protect;
-                private readonly uint Type;
-            }
-
-            [StructLayout(LayoutKind.Sequential)]
-            internal unsafe struct PROCESS_BASIC_INFORMATION32
-            {
-                private readonly int Reserved1;
-                public readonly int PebBaseAddress;
-                private fixed int Reserved2[2];
-                private readonly uint UniqueProcessId;
-                private readonly int Reserved3;
-            }
-
-            [StructLayout(LayoutKind.Sequential)]
-            internal unsafe struct PROCESS_BASIC_INFORMATION64
-            {
-                private readonly long Reserved1;
-                public readonly long PebBaseAddress;
-                private fixed long Reserved2[2];
-                private readonly ulong UniqueProcessId;
-                private readonly long Reserved3;
-            }
-
-            [StructLayout(LayoutKind.Sequential)]
-            internal unsafe struct PEB32
-            {
-                private fixed byte Reserved1[2];
-                private readonly byte BeingDebugged;
-                private fixed byte Reserved2[1];
-                private fixed int Reserved3[2];
-                private readonly int Ldr;
-                public readonly int ProcessParameters;
-                private fixed int Reserved4[3];
-                private readonly int AtlThunkSListPtr;
-                private readonly int Reserved5;
-                private readonly uint Reserved6;
-                private readonly int Reserved7;
-                private readonly uint Reserved8;
-                private readonly uint AtlThunkSListPtr32;
-                private fixed int Reserved9[45];
-                private fixed byte Reserved10[96];
-                private readonly int PostProcessInitRoutine;
-                private fixed byte Reserved11[128];
-                private fixed int Reserved12[1];
-                private readonly uint SessionId;
-            }
-
-            [StructLayout(LayoutKind.Sequential)]
-            internal unsafe struct PEB64
-            {
-                private fixed byte Reserved1[2];
-                private readonly byte BeingDebugged;
-                private fixed byte Reserved2[1];
-                private fixed long Reserved3[2];
-                private readonly long Ldr;
-                public readonly long ProcessParameters;
-                private fixed long Reserved4[3];
-                private readonly long AtlThunkSListPtr;
-                private readonly long Reserved5;
-                private readonly uint Reserved6;
-                private readonly long Reserved7;
-                private readonly uint Reserved8;
-                private readonly uint AtlThunkSListPtr32;
-                private fixed long Reserved9[45];
-                private fixed byte Reserved10[96];
-                private readonly long PostProcessInitRoutine;
-                private fixed byte Reserved11[128];
-                private fixed long Reserved12[1];
-                private readonly uint SessionId;
-            }
-
-            [StructLayout(LayoutKind.Sequential)]
-            internal unsafe struct RTL_USER_PROCESS_PARAMETERS32
-            {
-                private fixed byte Reserved1[16];
-                private fixed int Reserved2[10];
-                private readonly UNICODE_STRING32 ImagePathName;
-                private readonly UNICODE_STRING32 CommandLine;
-
-                internal readonly int Environment;
-            }
-
-            [StructLayout(LayoutKind.Sequential)]
-            internal unsafe struct RTL_USER_PROCESS_PARAMETERS64
-            {
-                private fixed byte Reserved1[16];
-                private fixed long Reserved2[10];
-                private readonly UNICODE_STRING64 ImagePathName;
-                private readonly UNICODE_STRING64 CommandLine;
-
-                internal readonly long Environment;
-            }
-
-            [StructLayout(LayoutKind.Sequential)]
-            internal readonly struct UNICODE_STRING32
-            {
-                private readonly ushort Length;
-                private readonly ushort MaximumLength;
-                private readonly int Buffer;
-            }
-
-            [StructLayout(LayoutKind.Sequential)]
-            internal readonly struct UNICODE_STRING64
-            {
-                private readonly ushort Length;
-                private readonly ushort MaximumLength;
-                private readonly long Buffer;
-            }
-        }
-    }
-}

+ 0 - 30
src/Plugins/SharedDirectoryMapper/NativeMethods.cs

@@ -1,30 +0,0 @@
-using System.Runtime.InteropServices;
-
-namespace WinSW.Plugins.SharedDirectoryMapper
-{
-    internal static class NativeMethods
-    {
-        internal const uint RESOURCETYPE_DISK = 0x00000001;
-
-        private const string MprLibraryName = "mpr.dll";
-
-        [DllImport(MprLibraryName, SetLastError = true, CharSet = CharSet.Unicode, EntryPoint = "WNetAddConnection2W")]
-        internal static extern int WNetAddConnection2(in NETRESOURCE netResource, string? password = null, string? userName = null, uint flags = 0);
-
-        [DllImport(MprLibraryName, SetLastError = true, CharSet = CharSet.Unicode, EntryPoint = "WNetCancelConnection2W")]
-        internal static extern int WNetCancelConnection2(string name, uint flags = 0, bool force = false);
-
-        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
-        internal struct NETRESOURCE
-        {
-            public uint Scope;
-            public uint Type;
-            public uint DisplayType;
-            public uint Usage;
-            public string LocalName;
-            public string RemoteName;
-            public string Comment;
-            public string Provider;
-        }
-    }
-}

+ 0 - 19
src/Plugins/SharedDirectoryMapper/SharedDirectoryMapper.csproj

@@ -1,19 +0,0 @@
-<Project Sdk="Microsoft.NET.Sdk">
-
-  <PropertyGroup>
-    <TargetFrameworks>net461;netcoreapp3.1</TargetFrameworks>
-    <LangVersion>latest</LangVersion>
-    <Nullable>enable</Nullable>
-
-    <RootNamespace>winsw.Plugins.SharedDirectoryMapper</RootNamespace>
-  </PropertyGroup>
-
-  <ItemGroup Condition="'$(TargetFramework)' != 'netcoreapp3.1'">
-    <PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.0" />
-  </ItemGroup>
-
-  <ItemGroup>
-    <ProjectReference Include="..\..\Core\WinSWCore\WinSWCore.csproj" />
-  </ItemGroup>
-
-</Project>

+ 3 - 0
src/WinSW.Core/AssemblyInfo.cs

@@ -0,0 +1,3 @@
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("WinSW")]

+ 0 - 0
src/Core/WinSWCore/Configuration/DefaultSettings.cs → src/WinSW.Core/Configuration/DefaultSettings.cs


+ 0 - 0
src/Core/WinSWCore/Configuration/IWinSWConfiguration.cs → src/WinSW.Core/Configuration/IWinSWConfiguration.cs


+ 229 - 229
src/Core/WinSWCore/Download.cs → src/WinSW.Core/Download.cs

@@ -1,229 +1,229 @@
-using System;
-using System.IO;
-using System.Net;
-using System.Text;
-using System.Threading.Tasks;
-using System.Xml;
-using log4net;
-using WinSW.Util;
-
-namespace WinSW
-{
-    /// <summary>
-    /// Specify the download activities prior to the launch.
-    /// This enables self-updating services.
-    /// </summary>
-    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})";
-
-#if NET461
-        static Download()
-        {
-            // If your app runs on .NET Framework 4.7 or later versions, but targets an earlier version
-            AppContext.SetSwitch("Switch.System.Net.DontEnableSystemDefaultTlsVersions", false);
-        }
-#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;
-        }
-
-        /// <summary>
-        /// Constructs the download setting sfrom the XML entry
-        /// </summary>
-        /// <param name="n">XML element</param>
-        /// <exception cref="InvalidDataException">The required attribute is missing or the configuration is invalid</exception>
-        internal Download(XmlElement n)
-        {
-            this.From = XmlHelper.SingleAttribute<string>(n, "from");
-            this.To = XmlHelper.SingleAttribute<string>(n, "to");
-
-            // All arguments below are optional
-            this.FailOnError = XmlHelper.SingleAttribute(n, "failOnError", false);
-            this.Proxy = XmlHelper.SingleAttribute<string>(n, "proxy", null);
-
-            this.Auth = XmlHelper.EnumAttribute(n, "auth", AuthType.None);
-            this.Username = XmlHelper.SingleAttribute<string>(n, "user", null);
-            this.Password = XmlHelper.SingleAttribute<string>(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;
-        }
-
-        /// <summary>
-        ///     Downloads the requested file and puts it to the specified target.
-        /// </summary>
-        /// <exception cref="WebException">
-        ///     Download failure. FailOnError flag should be processed outside.
-        /// </exception>
-        public async Task PerformAsync()
-        {
-            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
-            {
-                using (WebResponse response = await request.GetResponseAsync())
-                using (Stream responseStream = response.GetResponseStream())
-                using (FileStream tmpStream = new FileStream(tmpFilePath, FileMode.Create))
-                {
-                    if (supportsIfModifiedSince)
-                    {
-                        lastModified = ((HttpWebResponse)response).LastModified;
-                    }
-
-                    await responseStream.CopyToAsync(tmpStream);
-                }
-
-                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;
-                }
-            }
-        }
-    }
-
-    public class CustomProxyInformation
-    {
-        public string ServerAddress { get; }
-
-        public NetworkCredential? Credentials { get; }
-
-        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 + "@", null);
-            }
-            else
-            {
-                this.ServerAddress = proxy;
-            }
-        }
-    }
-}
+using System;
+using System.IO;
+using System.Net;
+using System.Text;
+using System.Threading.Tasks;
+using System.Xml;
+using log4net;
+using WinSW.Util;
+
+namespace WinSW
+{
+    /// <summary>
+    /// Specify the download activities prior to the launch.
+    /// This enables self-updating services.
+    /// </summary>
+    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})";
+
+#if NET461
+        static Download()
+        {
+            // If your app runs on .NET Framework 4.7 or later versions, but targets an earlier version
+            AppContext.SetSwitch("Switch.System.Net.DontEnableSystemDefaultTlsVersions", false);
+        }
+#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;
+        }
+
+        /// <summary>
+        /// Constructs the download setting sfrom the XML entry
+        /// </summary>
+        /// <param name="n">XML element</param>
+        /// <exception cref="InvalidDataException">The required attribute is missing or the configuration is invalid</exception>
+        internal Download(XmlElement n)
+        {
+            this.From = XmlHelper.SingleAttribute<string>(n, "from");
+            this.To = XmlHelper.SingleAttribute<string>(n, "to");
+
+            // All arguments below are optional
+            this.FailOnError = XmlHelper.SingleAttribute(n, "failOnError", false);
+            this.Proxy = XmlHelper.SingleAttribute<string>(n, "proxy", null);
+
+            this.Auth = XmlHelper.EnumAttribute(n, "auth", AuthType.None);
+            this.Username = XmlHelper.SingleAttribute<string>(n, "user", null);
+            this.Password = XmlHelper.SingleAttribute<string>(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;
+        }
+
+        /// <summary>
+        ///     Downloads the requested file and puts it to the specified target.
+        /// </summary>
+        /// <exception cref="WebException">
+        ///     Download failure. FailOnError flag should be processed outside.
+        /// </exception>
+        public async Task PerformAsync()
+        {
+            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
+            {
+                using (WebResponse response = await request.GetResponseAsync())
+                using (Stream responseStream = response.GetResponseStream())
+                using (FileStream tmpStream = new FileStream(tmpFilePath, FileMode.Create))
+                {
+                    if (supportsIfModifiedSince)
+                    {
+                        lastModified = ((HttpWebResponse)response).LastModified;
+                    }
+
+                    await responseStream.CopyToAsync(tmpStream);
+                }
+
+                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;
+                }
+            }
+        }
+    }
+
+    public class CustomProxyInformation
+    {
+        public string ServerAddress { get; }
+
+        public NetworkCredential? Credentials { get; }
+
+        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 + "@", null);
+            }
+            else
+            {
+                this.ServerAddress = proxy;
+            }
+        }
+    }
+}

+ 0 - 0
src/Core/WinSWCore/Extensions/AbstractWinSWExtension.cs → src/WinSW.Core/Extensions/AbstractWinSWExtension.cs


+ 0 - 0
src/Core/WinSWCore/Extensions/ExtensionException.cs → src/WinSW.Core/Extensions/ExtensionException.cs


+ 0 - 0
src/Core/WinSWCore/Extensions/IWinSWExtension.cs → src/WinSW.Core/Extensions/IWinSWExtension.cs


+ 0 - 0
src/Core/WinSWCore/Extensions/WinSWExtensionDescriptor.cs → src/WinSW.Core/Extensions/WinSWExtensionDescriptor.cs


+ 0 - 0
src/Core/WinSWCore/Extensions/WinSWExtensionManager.cs → src/WinSW.Core/Extensions/WinSWExtensionManager.cs


+ 587 - 587
src/Core/WinSWCore/LogAppenders.cs → src/WinSW.Core/LogAppenders.cs

@@ -1,587 +1,587 @@
-using System;
-using System.Diagnostics;
-using System.IO;
-using System.IO.Compression;
-using System.Threading;
-using WinSW.Util;
-
-namespace WinSW
-{
-    public interface IEventLogger
-    {
-        void LogEvent(string message);
-
-        void LogEvent(string message, EventLogEntryType type);
-    }
-
-    /// <summary>
-    /// Abstraction for handling log.
-    /// </summary>
-    public abstract class LogHandler
-    {
-        public abstract void Log(StreamReader outputReader, StreamReader errorReader);
-
-        /// <summary>
-        /// Error and information about logging should be reported here.
-        /// </summary>
-#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.
-
-        /// <summary>
-        /// Convenience method to copy stuff from StreamReader to StreamWriter
-        /// </summary>
-        protected void CopyStream(StreamReader reader, StreamWriter writer)
-        {
-            string? line;
-            while ((line = reader.ReadLine()) != null)
-            {
-                writer.WriteLine(line);
-            }
-
-            reader.Dispose();
-            writer.Dispose();
-        }
-
-        /// <summary>
-        /// File replacement.
-        /// </summary>
-        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);
-            }
-        }
-    }
-
-    /// <summary>
-    /// Base class for file-based loggers
-    /// </summary>
-    public abstract class AbstractFileLogAppender : LogHandler
-    {
-        protected string BaseLogFileName { get; }
-
-        protected bool OutFileDisabled { get; }
-
-        protected bool ErrFileDisabled { get; }
-
-        protected string OutFilePattern { get; }
-
-        protected string ErrFilePattern { get; }
-
-        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; }
-
-        public string OutputLogFileName { get; }
-
-        public string ErrorLogFileName { get; }
-
-        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)
-        {
-        }
-    }
-
-    /// <summary>
-    /// LogHandler that throws away output
-    /// </summary>
-    public class IgnoreLogAppender : LogHandler
-    {
-        public override void Log(StreamReader outputReader, StreamReader errorReader)
-        {
-            outputReader.Dispose();
-            errorReader.Dispose();
-        }
-    }
-
-    public class TimeBasedRollingLogAppender : AbstractFileLogAppender
-    {
-        public string Pattern { get; }
-
-        public int Period { get; }
-
-        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();
-        }
-
-        /// <summary>
-        /// Works like the CopyStream method but does a log rotation based on time.
-        /// </summary>
-        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 const int BytesPerKB = 1024;
-        public const int BytesPerMB = 1024 * BytesPerKB;
-        public const int DefaultSizeThreshold = 10 * BytesPerMB; // roll every 10MB.
-        public const int DefaultFilesToKeep = 8;
-
-        public int SizeThreshold { get; }
-
-        public int FilesToKeep { get; }
-
-        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.SizeThreshold = 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();
-        }
-
-        /// <summary>
-        /// Works like the CopyStream method but does a log rotation.
-        /// </summary>
-        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 = (line.Length + Environment.NewLine.Length) * sizeof(char);
-                if (fileLength + lengthToWrite > this.SizeThreshold)
-                {
-                    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();
-        }
-    }
-
-    /// <summary>
-    /// Roll log when a service is newly started.
-    /// </summary>
-    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 const int BytesPerKB = 1024;
-
-        public int SizeThreshold { get; }
-
-        public string FilePattern { get; }
-
-        public TimeSpan? AutoRollAtTime { get; }
-
-        public int? ZipOlderThanNumDays { get; }
-
-        public string ZipDateFormat { get; }
-
-        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.SizeThreshold = 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 = (line.Length + Environment.NewLine.Length) * sizeof(char);
-                    if (fileLength + lengthToWrite > this.SizeThreshold)
-                    {
-                        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}");
-            }
-        }
-
-        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();
-            }
-        }
-
-        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;
+using System.IO;
+using System.IO.Compression;
+using System.Threading;
+using WinSW.Util;
+
+namespace WinSW
+{
+    public interface IEventLogger
+    {
+        void LogEvent(string message);
+
+        void LogEvent(string message, EventLogEntryType type);
+    }
+
+    /// <summary>
+    /// Abstraction for handling log.
+    /// </summary>
+    public abstract class LogHandler
+    {
+        public abstract void Log(StreamReader outputReader, StreamReader errorReader);
+
+        /// <summary>
+        /// Error and information about logging should be reported here.
+        /// </summary>
+#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.
+
+        /// <summary>
+        /// Convenience method to copy stuff from StreamReader to StreamWriter
+        /// </summary>
+        protected void CopyStream(StreamReader reader, StreamWriter writer)
+        {
+            string? line;
+            while ((line = reader.ReadLine()) != null)
+            {
+                writer.WriteLine(line);
+            }
+
+            reader.Dispose();
+            writer.Dispose();
+        }
+
+        /// <summary>
+        /// File replacement.
+        /// </summary>
+        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);
+            }
+        }
+    }
+
+    /// <summary>
+    /// Base class for file-based loggers
+    /// </summary>
+    public abstract class AbstractFileLogAppender : LogHandler
+    {
+        protected string BaseLogFileName { get; }
+
+        protected bool OutFileDisabled { get; }
+
+        protected bool ErrFileDisabled { get; }
+
+        protected string OutFilePattern { get; }
+
+        protected string ErrFilePattern { get; }
+
+        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; }
+
+        public string OutputLogFileName { get; }
+
+        public string ErrorLogFileName { get; }
+
+        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)
+        {
+        }
+    }
+
+    /// <summary>
+    /// LogHandler that throws away output
+    /// </summary>
+    public class IgnoreLogAppender : LogHandler
+    {
+        public override void Log(StreamReader outputReader, StreamReader errorReader)
+        {
+            outputReader.Dispose();
+            errorReader.Dispose();
+        }
+    }
+
+    public class TimeBasedRollingLogAppender : AbstractFileLogAppender
+    {
+        public string Pattern { get; }
+
+        public int Period { get; }
+
+        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();
+        }
+
+        /// <summary>
+        /// Works like the CopyStream method but does a log rotation based on time.
+        /// </summary>
+        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 const int BytesPerKB = 1024;
+        public const int BytesPerMB = 1024 * BytesPerKB;
+        public const int DefaultSizeThreshold = 10 * BytesPerMB; // roll every 10MB.
+        public const int DefaultFilesToKeep = 8;
+
+        public int SizeThreshold { get; }
+
+        public int FilesToKeep { get; }
+
+        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.SizeThreshold = 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();
+        }
+
+        /// <summary>
+        /// Works like the CopyStream method but does a log rotation.
+        /// </summary>
+        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 = (line.Length + Environment.NewLine.Length) * sizeof(char);
+                if (fileLength + lengthToWrite > this.SizeThreshold)
+                {
+                    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();
+        }
+    }
+
+    /// <summary>
+    /// Roll log when a service is newly started.
+    /// </summary>
+    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 const int BytesPerKB = 1024;
+
+        public int SizeThreshold { get; }
+
+        public string FilePattern { get; }
+
+        public TimeSpan? AutoRollAtTime { get; }
+
+        public int? ZipOlderThanNumDays { get; }
+
+        public string ZipDateFormat { get; }
+
+        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.SizeThreshold = 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 = (line.Length + Environment.NewLine.Length) * sizeof(char);
+                    if (fileLength + lengthToWrite > this.SizeThreshold)
+                    {
+                        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}");
+            }
+        }
+
+        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();
+            }
+        }
+
+        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;
+        }
+    }
+}

+ 0 - 0
src/Core/WinSWCore/Logging/IServiceEventLogProvider.cs → src/WinSW.Core/Logging/IServiceEventLogProvider.cs


+ 0 - 0
src/Core/WinSWCore/Logging/ServiceEventLogAppender.cs → src/WinSW.Core/Logging/ServiceEventLogAppender.cs


+ 0 - 0
src/Core/WinSWCore/Native/ConsoleApis.cs → src/WinSW.Core/Native/ConsoleApis.cs


+ 0 - 0
src/Core/WinSWCore/Native/CredentialApis.cs → src/WinSW.Core/Native/CredentialApis.cs


+ 0 - 0
src/Core/WinSWCore/Native/Errors.cs → src/WinSW.Core/Native/Errors.cs


+ 0 - 0
src/Core/WinSWCore/Native/HandleApis.cs → src/WinSW.Core/Native/HandleApis.cs


+ 0 - 0
src/Core/WinSWCore/Native/Libraries.cs → src/WinSW.Core/Native/Libraries.cs


+ 0 - 0
src/Core/WinSWCore/Native/ProcessApis.cs → src/WinSW.Core/Native/ProcessApis.cs


+ 0 - 0
src/Core/WinSWCore/Native/Security.cs → src/WinSW.Core/Native/Security.cs


+ 0 - 0
src/Core/WinSWCore/Native/SecurityApis.cs → src/WinSW.Core/Native/SecurityApis.cs


+ 0 - 0
src/Core/WinSWCore/Native/Service.cs → src/WinSW.Core/Native/Service.cs


+ 0 - 0
src/Core/WinSWCore/Native/ServiceApis.cs → src/WinSW.Core/Native/ServiceApis.cs


+ 0 - 0
src/Core/WinSWCore/Native/Throw.cs → src/WinSW.Core/Native/Throw.cs


+ 0 - 0
src/Core/WinSWCore/NullableAttributes.cs → src/WinSW.Core/NullableAttributes.cs


+ 108 - 108
src/Core/WinSWCore/PeriodicRollingCalendar.cs → src/WinSW.Core/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.Periodicity = this.DeterminePeriodicityType();
-            this.nextRoll = this.NextTriggeringTime(this.currentRoll, this.period);
-        }
-
-        public enum PeriodicityType
-        {
-            ERRONEOUS,
-            TOP_OF_MILLISECOND,
-            TOP_OF_SECOND,
-            TOP_OF_MINUTE,
-            TOP_OF_HOUR,
-            TOP_OF_DAY
-        }
-
-        private static readonly PeriodicityType[] ValidOrderedList =
-        {
-            PeriodicityType.TOP_OF_MILLISECOND, PeriodicityType.TOP_OF_SECOND, PeriodicityType.TOP_OF_MINUTE, PeriodicityType.TOP_OF_HOUR, PeriodicityType.TOP_OF_DAY
-        };
-
-        private PeriodicityType DeterminePeriodicityType()
-        {
-            PeriodicRollingCalendar periodicRollingCalendar = new PeriodicRollingCalendar(this.format, this.period);
-            DateTime epoch = new DateTime(1970, 1, 1);
-
-            foreach (PeriodicityType i in ValidOrderedList)
-            {
-                string r0 = epoch.ToString(this.format);
-                periodicRollingCalendar.Periodicity = i;
-
-                DateTime next = periodicRollingCalendar.NextTriggeringTime(epoch, 1);
-                string r1 = next.ToString(this.format);
-
-                if (r0 != r1)
-                {
-                    return i;
-                }
-            }
-
-            return PeriodicityType.ERRONEOUS;
-        }
-
-        private DateTime NextTriggeringTime(DateTime input, long increment) => this.Periodicity switch
-        {
-            PeriodicityType.TOP_OF_MILLISECOND =>
-                new DateTime(input.Year, input.Month, input.Day, input.Hour, input.Minute, input.Second, input.Millisecond)
-                    .AddMilliseconds(increment),
-
-            PeriodicityType.TOP_OF_SECOND =>
-                new DateTime(input.Year, input.Month, input.Day, input.Hour, input.Minute, input.Second)
-                    .AddSeconds(increment),
-
-            PeriodicityType.TOP_OF_MINUTE =>
-                new DateTime(input.Year, input.Month, input.Day, input.Hour, input.Minute, 0)
-                    .AddMinutes(increment),
-
-            PeriodicityType.TOP_OF_HOUR =>
-                new DateTime(input.Year, input.Month, input.Day, input.Hour, 0, 0)
-                    .AddHours(increment),
-
-            PeriodicityType.TOP_OF_DAY =>
-                new DateTime(input.Year, input.Month, input.Day)
-                    .AddDays(increment),
-
-            _ => throw new Exception("invalid periodicity type: " + this.Periodicity),
-        };
-
-        public PeriodicityType Periodicity { 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.Periodicity = this.DeterminePeriodicityType();
+            this.nextRoll = this.NextTriggeringTime(this.currentRoll, this.period);
+        }
+
+        public enum PeriodicityType
+        {
+            ERRONEOUS,
+            TOP_OF_MILLISECOND,
+            TOP_OF_SECOND,
+            TOP_OF_MINUTE,
+            TOP_OF_HOUR,
+            TOP_OF_DAY
+        }
+
+        private static readonly PeriodicityType[] ValidOrderedList =
+        {
+            PeriodicityType.TOP_OF_MILLISECOND, PeriodicityType.TOP_OF_SECOND, PeriodicityType.TOP_OF_MINUTE, PeriodicityType.TOP_OF_HOUR, PeriodicityType.TOP_OF_DAY
+        };
+
+        private PeriodicityType DeterminePeriodicityType()
+        {
+            PeriodicRollingCalendar periodicRollingCalendar = new PeriodicRollingCalendar(this.format, this.period);
+            DateTime epoch = new DateTime(1970, 1, 1);
+
+            foreach (PeriodicityType i in ValidOrderedList)
+            {
+                string r0 = epoch.ToString(this.format);
+                periodicRollingCalendar.Periodicity = i;
+
+                DateTime next = periodicRollingCalendar.NextTriggeringTime(epoch, 1);
+                string r1 = next.ToString(this.format);
+
+                if (r0 != r1)
+                {
+                    return i;
+                }
+            }
+
+            return PeriodicityType.ERRONEOUS;
+        }
+
+        private DateTime NextTriggeringTime(DateTime input, long increment) => this.Periodicity switch
+        {
+            PeriodicityType.TOP_OF_MILLISECOND =>
+                new DateTime(input.Year, input.Month, input.Day, input.Hour, input.Minute, input.Second, input.Millisecond)
+                    .AddMilliseconds(increment),
+
+            PeriodicityType.TOP_OF_SECOND =>
+                new DateTime(input.Year, input.Month, input.Day, input.Hour, input.Minute, input.Second)
+                    .AddSeconds(increment),
+
+            PeriodicityType.TOP_OF_MINUTE =>
+                new DateTime(input.Year, input.Month, input.Day, input.Hour, input.Minute, 0)
+                    .AddMinutes(increment),
+
+            PeriodicityType.TOP_OF_HOUR =>
+                new DateTime(input.Year, input.Month, input.Day, input.Hour, 0, 0)
+                    .AddHours(increment),
+
+            PeriodicityType.TOP_OF_DAY =>
+                new DateTime(input.Year, input.Month, input.Day)
+                    .AddDays(increment),
+
+            _ => throw new Exception("invalid periodicity type: " + this.Periodicity),
+        };
+
+        public PeriodicityType Periodicity { 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);
+    }
+}

+ 730 - 730
src/Core/WinSWCore/ServiceDescriptor.cs → src/WinSW.Core/ServiceDescriptor.cs

@@ -1,730 +1,730 @@
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.IO;
-using System.ServiceProcess;
-using System.Text;
-using System.Xml;
-using WinSW.Configuration;
-using WinSW.Native;
-using WinSW.Util;
-
-namespace WinSW
-{
-    /// <summary>
-    /// In-memory representation of the configuration file.
-    /// </summary>
-    public class ServiceDescriptor : IWinSWConfiguration
-    {
-        protected readonly XmlDocument dom = new XmlDocument();
-
-        private readonly Dictionary<string, string> environmentVariables;
-
-        public static DefaultWinSWSettings Defaults { get; } = new DefaultWinSWSettings();
-
-        /// <summary>
-        /// Where did we find the configuration file?
-        ///
-        /// This string is "c:\abc\def\ghi" when the configuration XML is "c:\abc\def\ghi.xml"
-        /// </summary>
-        public string BasePath { get; set; }
-
-        /// <summary>
-        /// The file name portion of the configuration file.
-        ///
-        /// In the above example, this would be "ghi".
-        /// </summary>
-        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();
-        }
-
-        /// <summary>
-        /// Loads descriptor from existing DOM
-        /// </summary>
-#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<string, long> Suffix = new Dictionary<string, long>
-        {
-            { "ms",     1 },
-            { "sec",    1000L },
-            { "secs",   1000L },
-            { "min",    1000L * 60L },
-            { "mins",   1000L * 60L },
-            { "hr",     1000L * 60L * 60L },
-            { "hrs",    1000L * 60L * 60L },
-            { "hour",   1000L * 60L * 60L },
-            { "hours",  1000L * 60L * 60L },
-            { "day",    1000L * 60L * 60L * 24L },
-            { "days",   1000L * 60L * 60L * 24L }
-        };
-
-        /// <summary>
-        /// Path to the executable.
-        /// </summary>
-        public string Executable => this.SingleElement("executable");
-
-        public bool HideWindow => this.SingleBoolElement("hidewindow", Defaults.HideWindow);
-
-        /// <summary>
-        /// Optionally specify a different Path to an executable to shutdown the service.
-        /// </summary>
-        public string? StopExecutable => this.SingleElement("stopexecutable", true);
-
-        /// <summary>
-        /// <c>arguments</c> or multiple optional <c>argument</c> elements which overrule the arguments element.
-        /// </summary>
-        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);
-            }
-        }
-
-        /// <summary>
-        /// <c>startarguments</c> or multiple optional <c>startargument</c> elements.
-        /// </summary>
-        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);
-            }
-        }
-
-        /// <summary>
-        /// <c>stoparguments</c> or multiple optional <c>stopargument</c> elements.
-        /// </summary>
-        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<string> ExtensionIds
-        {
-            get
-            {
-                XmlNode? argumentNode = this.ExtensionsConfiguration;
-                XmlNodeList? extensions = argumentNode?.SelectNodes("extension");
-                if (extensions is null)
-                {
-                    return new List<string>(0);
-                }
-
-                List<string> result = new List<string>(extensions.Count);
-                for (int i = 0; i < extensions.Count; i++)
-                {
-                    result.Add(XmlHelper.SingleAttribute<string>((XmlElement)extensions[i], "id"));
-                }
-
-                return result;
-            }
-        }
-
-        public XmlNode? ExtensionsConfiguration => this.dom.SelectSingleNode("//extensions");
-
-        /// <summary>
-        /// Combines the contents of all the elements of the given name,
-        /// or return null if no element exists. Handles whitespace quotation.
-        /// </summary>
-        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();
-        }
-
-        /// <summary>
-        /// LogDirectory is the service wrapper executable directory or the optionally specified logpath element.
-        /// </summary>
-        public string LogDirectory
-        {
-            get
-            {
-                XmlNode? loggingNode = this.dom.SelectSingleNode("//logpath");
-
-                return loggingNode is null
-                    ? Defaults.LogDirectory
-                    : Environment.ExpandEnvironmentVariables(loggingNode.InnerText);
-            }
-        }
-
-        public string 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);
-                }
-            }
-        }
-
-        /// <summary>
-        /// Optionally specified depend services that must start before this service starts.
-        /// </summary>
-        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");
-
-        /// <summary>
-        /// Start mode of the Service
-        /// </summary>
-        public ServiceStartMode StartMode
-        {
-            get
-            {
-                string? p = this.SingleElement("startmode", true);
-                if (p is null)
-                {
-                    return Defaults.StartMode;
-                }
-
-                try
-                {
-                    return (ServiceStartMode)Enum.Parse(typeof(ServiceStartMode), p, true);
-                }
-                catch
-                {
-                    Console.WriteLine("Start mode in XML must be one of the following:");
-                    foreach (string sm in Enum.GetNames(typeof(ServiceStartMode)))
-                    {
-                        Console.WriteLine(sm);
-                    }
-
-                    throw;
-                }
-            }
-        }
-
-        /// <summary>
-        /// 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.
-        /// </summary>
-        public bool DelayedAutoStart => this.dom.SelectSingleNode("//delayedAutoStart") != null;
-
-        /// <summary>
-        /// 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
-        /// </summary>
-        public bool BeepOnShutdown => this.dom.SelectSingleNode("//beeponshutdown") != null;
-
-        /// <summary>
-        /// 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)
-        /// </summary>
-        public TimeSpan WaitHint => this.SingleTimeSpanElement(this.dom, "waithint", Defaults.WaitHint);
-
-        /// <summary>
-        /// The time before the service should make its next call to the SetServiceStatus function
-        /// with an incremented checkPoint value (default 1 sec).
-        /// Do not wait longer than the wait hint. A good interval is one-tenth of the wait hint but not less than 1 second and not more than 10 seconds.
-        /// </summary>
-        public TimeSpan SleepTime => this.SingleTimeSpanElement(this.dom, "sleeptime", Defaults.SleepTime);
-
-        /// <summary>
-        /// True if the service can interact with the desktop.
-        /// </summary>
-        public bool Interactive => this.dom.SelectSingleNode("//interactive") != null;
-
-        /// <summary>
-        /// Environment variable overrides
-        /// </summary>
-        public Dictionary<string, string> EnvironmentVariables => new Dictionary<string, string>(this.environmentVariables);
-
-        /// <summary>
-        /// List of downloads to be performed by the wrapper before starting
-        /// a service.
-        /// </summary>
-        public List<Download> Downloads
-        {
-            get
-            {
-                XmlNodeList? nodeList = this.dom.SelectNodes("//download");
-                if (nodeList is null)
-                {
-                    return Defaults.Downloads;
-                }
-
-                List<Download> result = new List<Download>(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;
-        }
-
-        public string? ServiceAccountPrompt => this.GetServiceAccountPart("prompt")?.ToLowerInvariant();
-
-        protected string? AllowServiceLogon => this.GetServiceAccountPart("allowservicelogon");
-
-        public string? ServiceAccountPassword => this.GetServiceAccountPart("password");
-
-        public string? ServiceAccountUserName => this.GetServiceAccountPart("username");
-
-        public bool HasServiceAccount()
-        {
-            return this.dom.SelectSingleNode("//serviceaccount") != null;
-        }
-
-        public bool AllowServiceAcountLogonRight
-        {
-            get
-            {
-                if (this.AllowServiceLogon != null)
-                {
-                    if (bool.TryParse(this.AllowServiceLogon, out bool parsedvalue))
-                    {
-                        return parsedvalue;
-                    }
-                }
-
-                return false;
-            }
-        }
-
-        /// <summary>
-        /// Time to wait for the service to gracefully shutdown the executable before we forcibly kill it
-        /// </summary>
-        public TimeSpan StopTimeout => this.SingleTimeSpanElement(this.dom, "stoptimeout", Defaults.StopTimeout);
-
-        /// <summary>
-        /// Desired process priority or null if not specified.
-        /// </summary>
-        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<string, string> LoadEnvironmentVariables()
-        {
-            XmlNodeList nodeList = this.dom.SelectNodes("//env");
-            Dictionary<string, string> environment = new Dictionary<string, string>(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.ServiceProcess;
+using System.Text;
+using System.Xml;
+using WinSW.Configuration;
+using WinSW.Native;
+using WinSW.Util;
+
+namespace WinSW
+{
+    /// <summary>
+    /// In-memory representation of the configuration file.
+    /// </summary>
+    public class ServiceDescriptor : IWinSWConfiguration
+    {
+        protected readonly XmlDocument dom = new XmlDocument();
+
+        private readonly Dictionary<string, string> environmentVariables;
+
+        public static DefaultWinSWSettings Defaults { get; } = new DefaultWinSWSettings();
+
+        /// <summary>
+        /// Where did we find the configuration file?
+        ///
+        /// This string is "c:\abc\def\ghi" when the configuration XML is "c:\abc\def\ghi.xml"
+        /// </summary>
+        public string BasePath { get; set; }
+
+        /// <summary>
+        /// The file name portion of the configuration file.
+        ///
+        /// In the above example, this would be "ghi".
+        /// </summary>
+        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();
+        }
+
+        /// <summary>
+        /// Loads descriptor from existing DOM
+        /// </summary>
+#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<string, long> Suffix = new Dictionary<string, long>
+        {
+            { "ms",     1 },
+            { "sec",    1000L },
+            { "secs",   1000L },
+            { "min",    1000L * 60L },
+            { "mins",   1000L * 60L },
+            { "hr",     1000L * 60L * 60L },
+            { "hrs",    1000L * 60L * 60L },
+            { "hour",   1000L * 60L * 60L },
+            { "hours",  1000L * 60L * 60L },
+            { "day",    1000L * 60L * 60L * 24L },
+            { "days",   1000L * 60L * 60L * 24L }
+        };
+
+        /// <summary>
+        /// Path to the executable.
+        /// </summary>
+        public string Executable => this.SingleElement("executable");
+
+        public bool HideWindow => this.SingleBoolElement("hidewindow", Defaults.HideWindow);
+
+        /// <summary>
+        /// Optionally specify a different Path to an executable to shutdown the service.
+        /// </summary>
+        public string? StopExecutable => this.SingleElement("stopexecutable", true);
+
+        /// <summary>
+        /// <c>arguments</c> or multiple optional <c>argument</c> elements which overrule the arguments element.
+        /// </summary>
+        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);
+            }
+        }
+
+        /// <summary>
+        /// <c>startarguments</c> or multiple optional <c>startargument</c> elements.
+        /// </summary>
+        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);
+            }
+        }
+
+        /// <summary>
+        /// <c>stoparguments</c> or multiple optional <c>stopargument</c> elements.
+        /// </summary>
+        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<string> ExtensionIds
+        {
+            get
+            {
+                XmlNode? argumentNode = this.ExtensionsConfiguration;
+                XmlNodeList? extensions = argumentNode?.SelectNodes("extension");
+                if (extensions is null)
+                {
+                    return new List<string>(0);
+                }
+
+                List<string> result = new List<string>(extensions.Count);
+                for (int i = 0; i < extensions.Count; i++)
+                {
+                    result.Add(XmlHelper.SingleAttribute<string>((XmlElement)extensions[i], "id"));
+                }
+
+                return result;
+            }
+        }
+
+        public XmlNode? ExtensionsConfiguration => this.dom.SelectSingleNode("//extensions");
+
+        /// <summary>
+        /// Combines the contents of all the elements of the given name,
+        /// or return null if no element exists. Handles whitespace quotation.
+        /// </summary>
+        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();
+        }
+
+        /// <summary>
+        /// LogDirectory is the service wrapper executable directory or the optionally specified logpath element.
+        /// </summary>
+        public string LogDirectory
+        {
+            get
+            {
+                XmlNode? loggingNode = this.dom.SelectSingleNode("//logpath");
+
+                return loggingNode is null
+                    ? Defaults.LogDirectory
+                    : Environment.ExpandEnvironmentVariables(loggingNode.InnerText);
+            }
+        }
+
+        public string 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);
+                }
+            }
+        }
+
+        /// <summary>
+        /// Optionally specified depend services that must start before this service starts.
+        /// </summary>
+        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");
+
+        /// <summary>
+        /// Start mode of the Service
+        /// </summary>
+        public ServiceStartMode StartMode
+        {
+            get
+            {
+                string? p = this.SingleElement("startmode", true);
+                if (p is null)
+                {
+                    return Defaults.StartMode;
+                }
+
+                try
+                {
+                    return (ServiceStartMode)Enum.Parse(typeof(ServiceStartMode), p, true);
+                }
+                catch
+                {
+                    Console.WriteLine("Start mode in XML must be one of the following:");
+                    foreach (string sm in Enum.GetNames(typeof(ServiceStartMode)))
+                    {
+                        Console.WriteLine(sm);
+                    }
+
+                    throw;
+                }
+            }
+        }
+
+        /// <summary>
+        /// 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.
+        /// </summary>
+        public bool DelayedAutoStart => this.dom.SelectSingleNode("//delayedAutoStart") != null;
+
+        /// <summary>
+        /// 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
+        /// </summary>
+        public bool BeepOnShutdown => this.dom.SelectSingleNode("//beeponshutdown") != null;
+
+        /// <summary>
+        /// 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)
+        /// </summary>
+        public TimeSpan WaitHint => this.SingleTimeSpanElement(this.dom, "waithint", Defaults.WaitHint);
+
+        /// <summary>
+        /// The time before the service should make its next call to the SetServiceStatus function
+        /// with an incremented checkPoint value (default 1 sec).
+        /// Do not wait longer than the wait hint. A good interval is one-tenth of the wait hint but not less than 1 second and not more than 10 seconds.
+        /// </summary>
+        public TimeSpan SleepTime => this.SingleTimeSpanElement(this.dom, "sleeptime", Defaults.SleepTime);
+
+        /// <summary>
+        /// True if the service can interact with the desktop.
+        /// </summary>
+        public bool Interactive => this.dom.SelectSingleNode("//interactive") != null;
+
+        /// <summary>
+        /// Environment variable overrides
+        /// </summary>
+        public Dictionary<string, string> EnvironmentVariables => new Dictionary<string, string>(this.environmentVariables);
+
+        /// <summary>
+        /// List of downloads to be performed by the wrapper before starting
+        /// a service.
+        /// </summary>
+        public List<Download> Downloads
+        {
+            get
+            {
+                XmlNodeList? nodeList = this.dom.SelectNodes("//download");
+                if (nodeList is null)
+                {
+                    return Defaults.Downloads;
+                }
+
+                List<Download> result = new List<Download>(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;
+        }
+
+        public string? ServiceAccountPrompt => this.GetServiceAccountPart("prompt")?.ToLowerInvariant();
+
+        protected string? AllowServiceLogon => this.GetServiceAccountPart("allowservicelogon");
+
+        public string? ServiceAccountPassword => this.GetServiceAccountPart("password");
+
+        public string? ServiceAccountUserName => this.GetServiceAccountPart("username");
+
+        public bool HasServiceAccount()
+        {
+            return this.dom.SelectSingleNode("//serviceaccount") != null;
+        }
+
+        public bool AllowServiceAcountLogonRight
+        {
+            get
+            {
+                if (this.AllowServiceLogon != null)
+                {
+                    if (bool.TryParse(this.AllowServiceLogon, out bool parsedvalue))
+                    {
+                        return parsedvalue;
+                    }
+                }
+
+                return false;
+            }
+        }
+
+        /// <summary>
+        /// Time to wait for the service to gracefully shutdown the executable before we forcibly kill it
+        /// </summary>
+        public TimeSpan StopTimeout => this.SingleTimeSpanElement(this.dom, "stoptimeout", Defaults.StopTimeout);
+
+        /// <summary>
+        /// Desired process priority or null if not specified.
+        /// </summary>
+        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<string, string> LoadEnvironmentVariables()
+        {
+            XmlNodeList nodeList = this.dom.SelectNodes("//env");
+            Dictionary<string, string> environment = new Dictionary<string, string>(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;
+        }
+    }
+}

+ 0 - 0
src/Core/WinSWCore/Util/FileHelper.cs → src/WinSW.Core/Util/FileHelper.cs


+ 0 - 0
src/Core/WinSWCore/Util/ProcessHelper.cs → src/WinSW.Core/Util/ProcessHelper.cs


+ 0 - 0
src/Core/WinSWCore/Util/SignalHelper.cs → src/WinSW.Core/Util/SignalHelper.cs


+ 0 - 0
src/Core/WinSWCore/Util/XmlHelper.cs → src/WinSW.Core/Util/XmlHelper.cs


+ 0 - 2
src/Core/WinSWCore/WinSWCore.csproj → src/WinSW.Core/WinSW.Core.csproj

@@ -5,8 +5,6 @@
     <LangVersion>latest</LangVersion>
     <Nullable>enable</Nullable>
     <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
-
-    <RootNamespace>winsw</RootNamespace>
   </PropertyGroup>
 
   <ItemGroup>

+ 0 - 0
src/Core/WinSWCore/WinSWException.cs → src/WinSW.Core/WinSWException.cs


+ 0 - 0
src/Core/WinSWCore/WinSWSystem.cs → src/WinSW.Core/WinSWSystem.cs


+ 174 - 1
src/Plugins/RunawayProcessKiller/RunawayProcessKillerExtension.cs → src/WinSW.Plugins/RunawayProcessKillerExtension.cs

@@ -1,12 +1,13 @@
 using System;
 using System.Diagnostics;
 using System.IO;
+using System.Runtime.InteropServices;
 using System.Text;
 using System.Xml;
 using log4net;
 using WinSW.Extensions;
 using WinSW.Util;
-using static WinSW.Plugins.RunawayProcessKiller.RunawayProcessKillerExtension.NativeMethods;
+using static WinSW.Plugins.RunawayProcessKiller.RunawayProcessKillerExtension.Native;
 
 namespace WinSW.Plugins.RunawayProcessKiller
 {
@@ -287,5 +288,177 @@ namespace WinSW.Plugins.RunawayProcessKiller
                 Logger.Error("Cannot update the PID file " + this.Pidfile, ex);
             }
         }
+
+        internal static class Native
+        {
+            private const string Kernel32 = "kernel32.dll";
+            private const string NTDll = "ntdll.dll";
+
+            [DllImport(Kernel32)]
+            internal static extern int IsWow64Process(IntPtr hProcess, out int Wow64Process);
+
+            [DllImport(NTDll)]
+            internal static extern int NtQueryInformationProcess(
+                IntPtr ProcessHandle,
+                PROCESSINFOCLASS ProcessInformationClass,
+                out PROCESS_BASIC_INFORMATION32 ProcessInformation,
+                int ProcessInformationLength,
+                IntPtr ReturnLength = default);
+
+            [DllImport(NTDll)]
+            internal static extern int NtQueryInformationProcess(
+                IntPtr ProcessHandle,
+                PROCESSINFOCLASS ProcessInformationClass,
+                out PROCESS_BASIC_INFORMATION64 ProcessInformation,
+                int ProcessInformationLength,
+                IntPtr ReturnLength = default);
+
+            [DllImport(NTDll)]
+            internal static extern unsafe int NtReadVirtualMemory(
+                IntPtr ProcessHandle,
+                IntPtr BaseAddress,
+                void* Buffer,
+                IntPtr BufferSize,
+                IntPtr NumberOfBytesRead = default);
+
+            [DllImport(NTDll)]
+            internal static extern int NtWow64QueryInformationProcess64(
+                IntPtr ProcessHandle,
+                PROCESSINFOCLASS ProcessInformationClass,
+                out PROCESS_BASIC_INFORMATION64 ProcessInformation,
+                int ProcessInformationLength,
+                IntPtr ReturnLength = default);
+
+            [DllImport(NTDll)]
+            internal static extern unsafe int NtWow64ReadVirtualMemory64(
+                IntPtr ProcessHandle,
+                long BaseAddress,
+                void* Buffer,
+                long BufferSize,
+                long NumberOfBytesRead = default);
+
+            internal enum PROCESSINFOCLASS
+            {
+                ProcessBasicInformation = 0,
+            }
+
+            [StructLayout(LayoutKind.Sequential)]
+            internal readonly struct MEMORY_BASIC_INFORMATION
+            {
+                public readonly IntPtr BaseAddress;
+                private readonly IntPtr AllocationBase;
+                private readonly uint AllocationProtect;
+                public readonly IntPtr RegionSize;
+                private readonly uint State;
+                private readonly uint Protect;
+                private readonly uint Type;
+            }
+
+            [StructLayout(LayoutKind.Sequential)]
+            internal unsafe struct PROCESS_BASIC_INFORMATION32
+            {
+                private readonly int Reserved1;
+                public readonly int PebBaseAddress;
+                private fixed int Reserved2[2];
+                private readonly uint UniqueProcessId;
+                private readonly int Reserved3;
+            }
+
+            [StructLayout(LayoutKind.Sequential)]
+            internal unsafe struct PROCESS_BASIC_INFORMATION64
+            {
+                private readonly long Reserved1;
+                public readonly long PebBaseAddress;
+                private fixed long Reserved2[2];
+                private readonly ulong UniqueProcessId;
+                private readonly long Reserved3;
+            }
+
+            [StructLayout(LayoutKind.Sequential)]
+            internal unsafe struct PEB32
+            {
+                private fixed byte Reserved1[2];
+                private readonly byte BeingDebugged;
+                private fixed byte Reserved2[1];
+                private fixed int Reserved3[2];
+                private readonly int Ldr;
+                public readonly int ProcessParameters;
+                private fixed int Reserved4[3];
+                private readonly int AtlThunkSListPtr;
+                private readonly int Reserved5;
+                private readonly uint Reserved6;
+                private readonly int Reserved7;
+                private readonly uint Reserved8;
+                private readonly uint AtlThunkSListPtr32;
+                private fixed int Reserved9[45];
+                private fixed byte Reserved10[96];
+                private readonly int PostProcessInitRoutine;
+                private fixed byte Reserved11[128];
+                private fixed int Reserved12[1];
+                private readonly uint SessionId;
+            }
+
+            [StructLayout(LayoutKind.Sequential)]
+            internal unsafe struct PEB64
+            {
+                private fixed byte Reserved1[2];
+                private readonly byte BeingDebugged;
+                private fixed byte Reserved2[1];
+                private fixed long Reserved3[2];
+                private readonly long Ldr;
+                public readonly long ProcessParameters;
+                private fixed long Reserved4[3];
+                private readonly long AtlThunkSListPtr;
+                private readonly long Reserved5;
+                private readonly uint Reserved6;
+                private readonly long Reserved7;
+                private readonly uint Reserved8;
+                private readonly uint AtlThunkSListPtr32;
+                private fixed long Reserved9[45];
+                private fixed byte Reserved10[96];
+                private readonly long PostProcessInitRoutine;
+                private fixed byte Reserved11[128];
+                private fixed long Reserved12[1];
+                private readonly uint SessionId;
+            }
+
+            [StructLayout(LayoutKind.Sequential)]
+            internal unsafe struct RTL_USER_PROCESS_PARAMETERS32
+            {
+                private fixed byte Reserved1[16];
+                private fixed int Reserved2[10];
+                private readonly UNICODE_STRING32 ImagePathName;
+                private readonly UNICODE_STRING32 CommandLine;
+
+                internal readonly int Environment;
+            }
+
+            [StructLayout(LayoutKind.Sequential)]
+            internal unsafe struct RTL_USER_PROCESS_PARAMETERS64
+            {
+                private fixed byte Reserved1[16];
+                private fixed long Reserved2[10];
+                private readonly UNICODE_STRING64 ImagePathName;
+                private readonly UNICODE_STRING64 CommandLine;
+
+                internal readonly long Environment;
+            }
+
+            [StructLayout(LayoutKind.Sequential)]
+            internal readonly struct UNICODE_STRING32
+            {
+                private readonly ushort Length;
+                private readonly ushort MaximumLength;
+                private readonly int Buffer;
+            }
+
+            [StructLayout(LayoutKind.Sequential)]
+            internal readonly struct UNICODE_STRING64
+            {
+                private readonly ushort Length;
+                private readonly ushort MaximumLength;
+                private readonly long Buffer;
+            }
+        }
     }
 }

+ 28 - 1
src/Plugins/SharedDirectoryMapper/SharedDirectoryMapper.cs → src/WinSW.Plugins/SharedDirectoryMapper.cs

@@ -1,10 +1,11 @@
 using System.Collections.Generic;
 using System.ComponentModel;
+using System.Runtime.InteropServices;
 using System.Xml;
 using log4net;
 using WinSW.Extensions;
 using WinSW.Util;
-using static WinSW.Plugins.SharedDirectoryMapper.NativeMethods;
+using static WinSW.Plugins.SharedDirectoryMapper.SharedDirectoryMapper.Native;
 
 namespace WinSW.Plugins.SharedDirectoryMapper
 {
@@ -91,5 +92,31 @@ namespace WinSW.Plugins.SharedDirectoryMapper
             Win32Exception inner = new Win32Exception(error);
             throw new ExtensionException(this.Descriptor.Id, $"{this.DisplayName}: {message} {inner.Message}", inner);
         }
+
+        internal static class Native
+        {
+            internal const uint RESOURCETYPE_DISK = 0x00000001;
+
+            private const string MprLibraryName = "mpr.dll";
+
+            [DllImport(MprLibraryName, SetLastError = true, CharSet = CharSet.Unicode, EntryPoint = "WNetAddConnection2W")]
+            internal static extern int WNetAddConnection2(in NETRESOURCE netResource, string? password = null, string? userName = null, uint flags = 0);
+
+            [DllImport(MprLibraryName, SetLastError = true, CharSet = CharSet.Unicode, EntryPoint = "WNetCancelConnection2W")]
+            internal static extern int WNetCancelConnection2(string name, uint flags = 0, bool force = false);
+
+            [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
+            internal struct NETRESOURCE
+            {
+                public uint Scope;
+                public uint Type;
+                public uint DisplayType;
+                public uint Usage;
+                public string LocalName;
+                public string RemoteName;
+                public string Comment;
+                public string Provider;
+            }
+        }
     }
 }

+ 0 - 0
src/Plugins/SharedDirectoryMapper/SharedDirectoryMapperConfig.cs → src/WinSW.Plugins/SharedDirectoryMapperConfig.cs


+ 1 - 3
src/Plugins/RunawayProcessKiller/RunawayProcessKiller.csproj → src/WinSW.Plugins/WinSW.Plugins.csproj

@@ -5,8 +5,6 @@
     <LangVersion>latest</LangVersion>
     <Nullable>enable</Nullable>
     <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
-
-    <RootNamespace>winsw.Plugins.RunawayProcessKiller</RootNamespace>
   </PropertyGroup>
 
   <ItemGroup Condition="'$(TargetFramework)' != 'netcoreapp3.1'">
@@ -14,7 +12,7 @@
   </ItemGroup>
 
   <ItemGroup>
-    <ProjectReference Include="..\..\Core\WinSWCore\WinSWCore.csproj" />
+    <ProjectReference Include="..\WinSW.Core\WinSW.Core.csproj" />
   </ItemGroup>
 
 </Project>

+ 0 - 0
src/Test/winswTests/Attributes/ElevatedFactAttribute.cs → src/WinSW.Tests/Attributes/ElevatedFactAttribute.cs


+ 0 - 0
src/Test/winswTests/Configuration/ExamplesTest.cs → src/WinSW.Tests/Configuration/ExamplesTest.cs


+ 0 - 0
src/Test/winswTests/DownloadConfigTests.cs → src/WinSW.Tests/DownloadConfigTests.cs


+ 0 - 0
src/Test/winswTests/DownloadTests.cs → src/WinSW.Tests/DownloadTests.cs


+ 0 - 0
src/Test/winswTests/Extensions/ExtensionTestBase.cs → src/WinSW.Tests/Extensions/ExtensionTestBase.cs


+ 0 - 0
src/Test/winswTests/Extensions/RunawayProcessKillerTest.cs → src/WinSW.Tests/Extensions/RunawayProcessKillerTest.cs


+ 0 - 0
src/Test/winswTests/Extensions/SharedDirectoryMapperConfigTest.cs → src/WinSW.Tests/Extensions/SharedDirectoryMapperConfigTest.cs


+ 0 - 0
src/Test/winswTests/Extensions/SharedDirectoryMapperTests.cs → src/WinSW.Tests/Extensions/SharedDirectoryMapperTests.cs


+ 0 - 0
src/Test/winswTests/MainTest.cs → src/WinSW.Tests/MainTest.cs


+ 423 - 423
src/Test/winswTests/ServiceDescriptorTests.cs → src/WinSW.Tests/ServiceDescriptorTests.cs

@@ -1,423 +1,423 @@
-using System;
-using System.Diagnostics;
-using System.ServiceProcess;
-using WinSW.Tests.Util;
-using Xunit;
-
-namespace WinSW.Tests
-{
-    public class ServiceDescriptorTests
-    {
-        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";
-
-        private ServiceDescriptor extendedServiceDescriptor;
-
-        public ServiceDescriptorTests()
-        {
-            string seedXml =
-$@"<service>
-  <id>service.exe</id>
-  <name>Service</name>
-  <description>The service.</description>
-  <executable>node.exe</executable>
-  <arguments>My Arguments</arguments>
-  <log mode=""roll""></log>
-  <serviceaccount>
-    <username>{Domain}\{Username}</username>
-    <password>{Password}</password>
-    <allowservicelogon>{AllowServiceAccountLogonRight}</allowservicelogon>
-  </serviceaccount>
-  <workingdirectory>{ExpectedWorkingDirectory}</workingdirectory>
-  <logpath>C:\logs</logpath>
-</service>";
-            this.extendedServiceDescriptor = ServiceDescriptor.FromXml(seedXml);
-        }
-
-        [Fact]
-        public void DefaultStartMode()
-        {
-            Assert.Equal(ServiceStartMode.Automatic, this.extendedServiceDescriptor.StartMode);
-        }
-
-        [Fact]
-        public void IncorrectStartMode()
-        {
-            string seedXml =
-$@"<service>
-  <id>service.exe</id>
-  <name>Service</name>
-  <description>The service.</description>
-  <executable>node.exe</executable>
-  <arguments>My Arguments</arguments>
-  <startmode>roll</startmode>
-  <log mode=""roll""></log>
-  <serviceaccount>
-    <username>{Domain}\{Username}</username>
-    <password>{Password}</password>
-    <allowservicelogon>{AllowServiceAccountLogonRight}</allowservicelogon>
-  </serviceaccount>
-  <workingdirectory>{ExpectedWorkingDirectory}</workingdirectory>
-  <logpath>C:\logs</logpath>
-</service>";
-
-            this.extendedServiceDescriptor = ServiceDescriptor.FromXml(seedXml);
-            Assert.Throws<ArgumentException>(() => this.extendedServiceDescriptor.StartMode);
-        }
-
-        [Fact]
-        public void ChangedStartMode()
-        {
-            string seedXml =
-$@"<service>
-  <id>service.exe</id>
-  <name>Service</name>
-  <description>The service.</description>
-  <executable>node.exe</executable>
-  <arguments>My Arguments</arguments>
-  <startmode>manual</startmode>
-  <log mode=""roll""></log>
-  <serviceaccount>
-    <username>{Domain}\{Username}</username>
-    <password>{Password}</password>
-    <allowservicelogon>{AllowServiceAccountLogonRight}</allowservicelogon>
-  </serviceaccount>
-  <workingdirectory>{ExpectedWorkingDirectory}</workingdirectory>
-  <logpath>C:\logs</logpath>
-</service>";
-
-            this.extendedServiceDescriptor = ServiceDescriptor.FromXml(seedXml);
-            Assert.Equal(ServiceStartMode.Manual, this.extendedServiceDescriptor.StartMode);
-        }
-
-        [Fact]
-        public void VerifyWorkingDirectory()
-        {
-            Debug.WriteLine("_extendedServiceDescriptor.WorkingDirectory :: " + this.extendedServiceDescriptor.WorkingDirectory);
-            Assert.Equal(ExpectedWorkingDirectory, this.extendedServiceDescriptor.WorkingDirectory);
-        }
-
-        [Fact]
-        public void VerifyServiceLogonRight()
-        {
-            Assert.True(this.extendedServiceDescriptor.AllowServiceAcountLogonRight);
-        }
-
-        [Fact]
-        public void VerifyUsername()
-        {
-            Debug.WriteLine("_extendedServiceDescriptor.WorkingDirectory :: " + this.extendedServiceDescriptor.WorkingDirectory);
-            Assert.Equal(Domain + "\\" + Username, this.extendedServiceDescriptor.ServiceAccountUserName);
-        }
-
-        [Fact]
-        public void VerifyPassword()
-        {
-            Debug.WriteLine("_extendedServiceDescriptor.WorkingDirectory :: " + this.extendedServiceDescriptor.WorkingDirectory);
-            Assert.Equal(Password, this.extendedServiceDescriptor.ServiceAccountPassword);
-        }
-
-        [Fact]
-        public void Priority()
-        {
-            var sd = ServiceDescriptor.FromXml("<service><id>test</id><priority>normal</priority></service>");
-            Assert.Equal(ProcessPriorityClass.Normal, sd.Priority);
-
-            sd = ServiceDescriptor.FromXml("<service><id>test</id><priority>idle</priority></service>");
-            Assert.Equal(ProcessPriorityClass.Idle, sd.Priority);
-
-            sd = ServiceDescriptor.FromXml("<service><id>test</id></service>");
-            Assert.Equal(ProcessPriorityClass.Normal, sd.Priority);
-        }
-
-        [Fact]
-        public void CanParseStopTimeout()
-        {
-            const string seedXml = "<service>"
-                                   + "<stoptimeout>60sec</stoptimeout>"
-                                   + "</service>";
-            var serviceDescriptor = ServiceDescriptor.FromXml(seedXml);
-
-            Assert.Equal(TimeSpan.FromSeconds(60), serviceDescriptor.StopTimeout);
-        }
-
-        [Fact]
-        public void CanParseStopTimeoutFromMinutes()
-        {
-            const string seedXml = "<service>"
-                                   + "<stoptimeout>10min</stoptimeout>"
-                                   + "</service>";
-            var serviceDescriptor = ServiceDescriptor.FromXml(seedXml);
-
-            Assert.Equal(TimeSpan.FromMinutes(10), serviceDescriptor.StopTimeout);
-        }
-
-        [Fact]
-        public void CanParseLogname()
-        {
-            const string seedXml = "<service>"
-                                   + "<logname>MyTestApp</logname>"
-                                   + "</service>";
-            var serviceDescriptor = ServiceDescriptor.FromXml(seedXml);
-
-            Assert.Equal("MyTestApp", serviceDescriptor.LogName);
-        }
-
-        [Fact]
-        public void CanParseOutfileDisabled()
-        {
-            const string seedXml = "<service>"
-                                   + "<outfiledisabled>true</outfiledisabled>"
-                                   + "</service>";
-            var serviceDescriptor = ServiceDescriptor.FromXml(seedXml);
-
-            Assert.True(serviceDescriptor.OutFileDisabled);
-        }
-
-        [Fact]
-        public void CanParseErrfileDisabled()
-        {
-            const string seedXml = "<service>"
-                                   + "<errfiledisabled>true</errfiledisabled>"
-                                   + "</service>";
-            var serviceDescriptor = ServiceDescriptor.FromXml(seedXml);
-
-            Assert.True(serviceDescriptor.ErrFileDisabled);
-        }
-
-        [Fact]
-        public void CanParseOutfilePattern()
-        {
-            const string seedXml = "<service>"
-                                   + "<outfilepattern>.out.test.log</outfilepattern>"
-                                   + "</service>";
-            var serviceDescriptor = ServiceDescriptor.FromXml(seedXml);
-
-            Assert.Equal(".out.test.log", serviceDescriptor.OutFilePattern);
-        }
-
-        [Fact]
-        public void CanParseErrfilePattern()
-        {
-            const string seedXml = "<service>"
-                                   + "<errfilepattern>.err.test.log</errfilepattern>"
-                                   + "</service>";
-            var serviceDescriptor = ServiceDescriptor.FromXml(seedXml);
-
-            Assert.Equal(".err.test.log", serviceDescriptor.ErrFilePattern);
-        }
-
-        [Fact]
-        public void LogModeRollBySize()
-        {
-            const string seedXml = "<service>"
-                                   + "<logpath>c:\\</logpath>"
-                                   + "<log mode=\"roll-by-size\">"
-                                   + "<sizeThreshold>112</sizeThreshold>"
-                                   + "<keepFiles>113</keepFiles>"
-                                   + "</log>"
-                                   + "</service>";
-
-            var serviceDescriptor = ServiceDescriptor.FromXml(seedXml);
-            serviceDescriptor.BaseName = "service";
-
-            var logHandler = serviceDescriptor.LogHandler as SizeBasedRollingLogAppender;
-            Assert.NotNull(logHandler);
-            Assert.Equal(112 * 1024, logHandler.SizeThreshold);
-            Assert.Equal(113, logHandler.FilesToKeep);
-        }
-
-        [Fact]
-        public void LogModeRollByTime()
-        {
-            const string seedXml = "<service>"
-                                   + "<logpath>c:\\</logpath>"
-                                   + "<log mode=\"roll-by-time\">"
-                                   + "<period>7</period>"
-                                   + "<pattern>log pattern</pattern>"
-                                   + "</log>"
-                                   + "</service>";
-
-            var serviceDescriptor = ServiceDescriptor.FromXml(seedXml);
-            serviceDescriptor.BaseName = "service";
-
-            var logHandler = serviceDescriptor.LogHandler as TimeBasedRollingLogAppender;
-            Assert.NotNull(logHandler);
-            Assert.Equal(7, logHandler.Period);
-            Assert.Equal("log pattern", logHandler.Pattern);
-        }
-
-        [Fact]
-        public void LogModeRollBySizeTime()
-        {
-            const string seedXml = "<service>"
-                                   + "<logpath>c:\\</logpath>"
-                                   + "<log mode=\"roll-by-size-time\">"
-                                   + "<sizeThreshold>10240</sizeThreshold>"
-                                   + "<pattern>yyyy-MM-dd</pattern>"
-                                   + "<autoRollAtTime>00:00:00</autoRollAtTime>"
-                                   + "</log>"
-                                   + "</service>";
-
-            var serviceDescriptor = ServiceDescriptor.FromXml(seedXml);
-            serviceDescriptor.BaseName = "service";
-
-            var logHandler = serviceDescriptor.LogHandler as RollingSizeTimeLogAppender;
-            Assert.NotNull(logHandler);
-            Assert.Equal(10240 * 1024, logHandler.SizeThreshold);
-            Assert.Equal("yyyy-MM-dd", logHandler.FilePattern);
-            Assert.Equal((TimeSpan?)new TimeSpan(0, 0, 0), logHandler.AutoRollAtTime);
-        }
-
-        [Fact]
-        public void VerifyServiceLogonRightGraceful()
-        {
-            const string seedXml = "<service>"
-                                   + "<serviceaccount>"
-                                   + "<domain>" + Domain + "</domain>"
-                                   + "<user>" + Username + "</user>"
-                                   + "<password>" + Password + "</password>"
-                                   + "<allowservicelogon>true1</allowservicelogon>"
-                                   + "</serviceaccount>"
-                                   + "</service>";
-            var serviceDescriptor = ServiceDescriptor.FromXml(seedXml);
-            Assert.False(serviceDescriptor.AllowServiceAcountLogonRight);
-        }
-
-        [Fact]
-        public void VerifyServiceLogonRightOmitted()
-        {
-            const string seedXml = "<service>"
-                                   + "<serviceaccount>"
-                                   + "<domain>" + Domain + "</domain>"
-                                   + "<user>" + Username + "</user>"
-                                   + "<password>" + Password + "</password>"
-                                   + "</serviceaccount>"
-                                   + "</service>";
-            var serviceDescriptor = ServiceDescriptor.FromXml(seedXml);
-            Assert.False(serviceDescriptor.AllowServiceAcountLogonRight);
-        }
-
-        [Fact]
-        public void VerifyWaitHint_FullXML()
-        {
-            var sd = ConfigXmlBuilder.Create()
-                .WithTag("waithint", "20 min")
-                .ToServiceDescriptor(true);
-            Assert.Equal(TimeSpan.FromMinutes(20), sd.WaitHint);
-        }
-
-        /// <summary>
-        /// Test for https://github.com/kohsuke/winsw/issues/159
-        /// </summary>
-        [Fact]
-        public void VerifyWaitHint_XMLWithoutVersion()
-        {
-            var sd = ConfigXmlBuilder.Create(printXMLVersion: false)
-                .WithTag("waithint", "21 min")
-                .ToServiceDescriptor(true);
-            Assert.Equal(TimeSpan.FromMinutes(21), sd.WaitHint);
-        }
-
-        [Fact]
-        public void VerifyWaitHint_XMLWithoutComment()
-        {
-            var sd = ConfigXmlBuilder.Create(xmlComment: null)
-                .WithTag("waithint", "22 min")
-                .ToServiceDescriptor(true);
-            Assert.Equal(TimeSpan.FromMinutes(22), sd.WaitHint);
-        }
-
-        [Fact]
-        public void VerifyWaitHint_XMLWithoutVersionAndComment()
-        {
-            var sd = ConfigXmlBuilder.Create(xmlComment: null, printXMLVersion: false)
-                .WithTag("waithint", "23 min")
-                .ToServiceDescriptor(true);
-            Assert.Equal(TimeSpan.FromMinutes(23), sd.WaitHint);
-        }
-
-        [Fact]
-        public void VerifySleepTime()
-        {
-            var sd = ConfigXmlBuilder.Create().WithTag("sleeptime", "3 hrs").ToServiceDescriptor(true);
-            Assert.Equal(TimeSpan.FromHours(3), sd.SleepTime);
-        }
-
-        [Fact]
-        public void VerifyResetFailureAfter()
-        {
-            var sd = ConfigXmlBuilder.Create().WithTag("resetfailure", "75 sec").ToServiceDescriptor(true);
-            Assert.Equal(TimeSpan.FromSeconds(75), sd.ResetFailureAfter);
-        }
-
-        [Fact]
-        public void VerifyStopTimeout()
-        {
-            var sd = ConfigXmlBuilder.Create().WithTag("stoptimeout", "35 secs").ToServiceDescriptor(true);
-            Assert.Equal(TimeSpan.FromSeconds(35), sd.StopTimeout);
-        }
-
-        /// <summary>
-        /// https://github.com/kohsuke/winsw/issues/178
-        /// </summary>
-        [Fact]
-        public void Arguments_LegacyParam()
-        {
-            var sd = ConfigXmlBuilder.Create().WithTag("arguments", "arg").ToServiceDescriptor(true);
-            Assert.Equal("arg", sd.Arguments);
-        }
-
-        [Fact]
-        public void Arguments_NewParam_Single()
-        {
-            var sd = ConfigXmlBuilder.Create()
-                .WithTag("argument", "--arg1=2")
-                .ToServiceDescriptor(true);
-            Assert.Equal(" --arg1=2", sd.Arguments);
-        }
-
-        [Fact]
-        public void Arguments_NewParam_MultipleArgs()
-        {
-            var sd = ConfigXmlBuilder.Create()
-                .WithTag("argument", "--arg1=2")
-                .WithTag("argument", "--arg2=123")
-                .WithTag("argument", "--arg3=null")
-                .ToServiceDescriptor(true);
-            Assert.Equal(" --arg1=2 --arg2=123 --arg3=null", sd.Arguments);
-        }
-
-        /// <summary>
-        /// Ensures that the new single-argument field has a higher priority.
-        /// </summary>
-        [Fact]
-        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.Equal(" --arg2=123 --arg3=null", sd.Arguments);
-        }
-
-        [Theory]
-        [InlineData(true)]
-        [InlineData(false)]
-        public void DelayedStart_RoundTrip(bool enabled)
-        {
-            var bldr = ConfigXmlBuilder.Create();
-            if (enabled)
-            {
-                bldr = bldr.WithDelayedAutoStart();
-            }
-
-            var sd = bldr.ToServiceDescriptor();
-            Assert.Equal(enabled, sd.DelayedAutoStart);
-        }
-    }
-}
+using System;
+using System.Diagnostics;
+using System.ServiceProcess;
+using WinSW.Tests.Util;
+using Xunit;
+
+namespace WinSW.Tests
+{
+    public class ServiceDescriptorTests
+    {
+        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";
+
+        private ServiceDescriptor extendedServiceDescriptor;
+
+        public ServiceDescriptorTests()
+        {
+            string seedXml =
+$@"<service>
+  <id>service.exe</id>
+  <name>Service</name>
+  <description>The service.</description>
+  <executable>node.exe</executable>
+  <arguments>My Arguments</arguments>
+  <log mode=""roll""></log>
+  <serviceaccount>
+    <username>{Domain}\{Username}</username>
+    <password>{Password}</password>
+    <allowservicelogon>{AllowServiceAccountLogonRight}</allowservicelogon>
+  </serviceaccount>
+  <workingdirectory>{ExpectedWorkingDirectory}</workingdirectory>
+  <logpath>C:\logs</logpath>
+</service>";
+            this.extendedServiceDescriptor = ServiceDescriptor.FromXml(seedXml);
+        }
+
+        [Fact]
+        public void DefaultStartMode()
+        {
+            Assert.Equal(ServiceStartMode.Automatic, this.extendedServiceDescriptor.StartMode);
+        }
+
+        [Fact]
+        public void IncorrectStartMode()
+        {
+            string seedXml =
+$@"<service>
+  <id>service.exe</id>
+  <name>Service</name>
+  <description>The service.</description>
+  <executable>node.exe</executable>
+  <arguments>My Arguments</arguments>
+  <startmode>roll</startmode>
+  <log mode=""roll""></log>
+  <serviceaccount>
+    <username>{Domain}\{Username}</username>
+    <password>{Password}</password>
+    <allowservicelogon>{AllowServiceAccountLogonRight}</allowservicelogon>
+  </serviceaccount>
+  <workingdirectory>{ExpectedWorkingDirectory}</workingdirectory>
+  <logpath>C:\logs</logpath>
+</service>";
+
+            this.extendedServiceDescriptor = ServiceDescriptor.FromXml(seedXml);
+            Assert.Throws<ArgumentException>(() => this.extendedServiceDescriptor.StartMode);
+        }
+
+        [Fact]
+        public void ChangedStartMode()
+        {
+            string seedXml =
+$@"<service>
+  <id>service.exe</id>
+  <name>Service</name>
+  <description>The service.</description>
+  <executable>node.exe</executable>
+  <arguments>My Arguments</arguments>
+  <startmode>manual</startmode>
+  <log mode=""roll""></log>
+  <serviceaccount>
+    <username>{Domain}\{Username}</username>
+    <password>{Password}</password>
+    <allowservicelogon>{AllowServiceAccountLogonRight}</allowservicelogon>
+  </serviceaccount>
+  <workingdirectory>{ExpectedWorkingDirectory}</workingdirectory>
+  <logpath>C:\logs</logpath>
+</service>";
+
+            this.extendedServiceDescriptor = ServiceDescriptor.FromXml(seedXml);
+            Assert.Equal(ServiceStartMode.Manual, this.extendedServiceDescriptor.StartMode);
+        }
+
+        [Fact]
+        public void VerifyWorkingDirectory()
+        {
+            Debug.WriteLine("_extendedServiceDescriptor.WorkingDirectory :: " + this.extendedServiceDescriptor.WorkingDirectory);
+            Assert.Equal(ExpectedWorkingDirectory, this.extendedServiceDescriptor.WorkingDirectory);
+        }
+
+        [Fact]
+        public void VerifyServiceLogonRight()
+        {
+            Assert.True(this.extendedServiceDescriptor.AllowServiceAcountLogonRight);
+        }
+
+        [Fact]
+        public void VerifyUsername()
+        {
+            Debug.WriteLine("_extendedServiceDescriptor.WorkingDirectory :: " + this.extendedServiceDescriptor.WorkingDirectory);
+            Assert.Equal(Domain + "\\" + Username, this.extendedServiceDescriptor.ServiceAccountUserName);
+        }
+
+        [Fact]
+        public void VerifyPassword()
+        {
+            Debug.WriteLine("_extendedServiceDescriptor.WorkingDirectory :: " + this.extendedServiceDescriptor.WorkingDirectory);
+            Assert.Equal(Password, this.extendedServiceDescriptor.ServiceAccountPassword);
+        }
+
+        [Fact]
+        public void Priority()
+        {
+            var sd = ServiceDescriptor.FromXml("<service><id>test</id><priority>normal</priority></service>");
+            Assert.Equal(ProcessPriorityClass.Normal, sd.Priority);
+
+            sd = ServiceDescriptor.FromXml("<service><id>test</id><priority>idle</priority></service>");
+            Assert.Equal(ProcessPriorityClass.Idle, sd.Priority);
+
+            sd = ServiceDescriptor.FromXml("<service><id>test</id></service>");
+            Assert.Equal(ProcessPriorityClass.Normal, sd.Priority);
+        }
+
+        [Fact]
+        public void CanParseStopTimeout()
+        {
+            const string seedXml = "<service>"
+                                   + "<stoptimeout>60sec</stoptimeout>"
+                                   + "</service>";
+            var serviceDescriptor = ServiceDescriptor.FromXml(seedXml);
+
+            Assert.Equal(TimeSpan.FromSeconds(60), serviceDescriptor.StopTimeout);
+        }
+
+        [Fact]
+        public void CanParseStopTimeoutFromMinutes()
+        {
+            const string seedXml = "<service>"
+                                   + "<stoptimeout>10min</stoptimeout>"
+                                   + "</service>";
+            var serviceDescriptor = ServiceDescriptor.FromXml(seedXml);
+
+            Assert.Equal(TimeSpan.FromMinutes(10), serviceDescriptor.StopTimeout);
+        }
+
+        [Fact]
+        public void CanParseLogname()
+        {
+            const string seedXml = "<service>"
+                                   + "<logname>MyTestApp</logname>"
+                                   + "</service>";
+            var serviceDescriptor = ServiceDescriptor.FromXml(seedXml);
+
+            Assert.Equal("MyTestApp", serviceDescriptor.LogName);
+        }
+
+        [Fact]
+        public void CanParseOutfileDisabled()
+        {
+            const string seedXml = "<service>"
+                                   + "<outfiledisabled>true</outfiledisabled>"
+                                   + "</service>";
+            var serviceDescriptor = ServiceDescriptor.FromXml(seedXml);
+
+            Assert.True(serviceDescriptor.OutFileDisabled);
+        }
+
+        [Fact]
+        public void CanParseErrfileDisabled()
+        {
+            const string seedXml = "<service>"
+                                   + "<errfiledisabled>true</errfiledisabled>"
+                                   + "</service>";
+            var serviceDescriptor = ServiceDescriptor.FromXml(seedXml);
+
+            Assert.True(serviceDescriptor.ErrFileDisabled);
+        }
+
+        [Fact]
+        public void CanParseOutfilePattern()
+        {
+            const string seedXml = "<service>"
+                                   + "<outfilepattern>.out.test.log</outfilepattern>"
+                                   + "</service>";
+            var serviceDescriptor = ServiceDescriptor.FromXml(seedXml);
+
+            Assert.Equal(".out.test.log", serviceDescriptor.OutFilePattern);
+        }
+
+        [Fact]
+        public void CanParseErrfilePattern()
+        {
+            const string seedXml = "<service>"
+                                   + "<errfilepattern>.err.test.log</errfilepattern>"
+                                   + "</service>";
+            var serviceDescriptor = ServiceDescriptor.FromXml(seedXml);
+
+            Assert.Equal(".err.test.log", serviceDescriptor.ErrFilePattern);
+        }
+
+        [Fact]
+        public void LogModeRollBySize()
+        {
+            const string seedXml = "<service>"
+                                   + "<logpath>c:\\</logpath>"
+                                   + "<log mode=\"roll-by-size\">"
+                                   + "<sizeThreshold>112</sizeThreshold>"
+                                   + "<keepFiles>113</keepFiles>"
+                                   + "</log>"
+                                   + "</service>";
+
+            var serviceDescriptor = ServiceDescriptor.FromXml(seedXml);
+            serviceDescriptor.BaseName = "service";
+
+            var logHandler = serviceDescriptor.LogHandler as SizeBasedRollingLogAppender;
+            Assert.NotNull(logHandler);
+            Assert.Equal(112 * 1024, logHandler.SizeThreshold);
+            Assert.Equal(113, logHandler.FilesToKeep);
+        }
+
+        [Fact]
+        public void LogModeRollByTime()
+        {
+            const string seedXml = "<service>"
+                                   + "<logpath>c:\\</logpath>"
+                                   + "<log mode=\"roll-by-time\">"
+                                   + "<period>7</period>"
+                                   + "<pattern>log pattern</pattern>"
+                                   + "</log>"
+                                   + "</service>";
+
+            var serviceDescriptor = ServiceDescriptor.FromXml(seedXml);
+            serviceDescriptor.BaseName = "service";
+
+            var logHandler = serviceDescriptor.LogHandler as TimeBasedRollingLogAppender;
+            Assert.NotNull(logHandler);
+            Assert.Equal(7, logHandler.Period);
+            Assert.Equal("log pattern", logHandler.Pattern);
+        }
+
+        [Fact]
+        public void LogModeRollBySizeTime()
+        {
+            const string seedXml = "<service>"
+                                   + "<logpath>c:\\</logpath>"
+                                   + "<log mode=\"roll-by-size-time\">"
+                                   + "<sizeThreshold>10240</sizeThreshold>"
+                                   + "<pattern>yyyy-MM-dd</pattern>"
+                                   + "<autoRollAtTime>00:00:00</autoRollAtTime>"
+                                   + "</log>"
+                                   + "</service>";
+
+            var serviceDescriptor = ServiceDescriptor.FromXml(seedXml);
+            serviceDescriptor.BaseName = "service";
+
+            var logHandler = serviceDescriptor.LogHandler as RollingSizeTimeLogAppender;
+            Assert.NotNull(logHandler);
+            Assert.Equal(10240 * 1024, logHandler.SizeThreshold);
+            Assert.Equal("yyyy-MM-dd", logHandler.FilePattern);
+            Assert.Equal((TimeSpan?)new TimeSpan(0, 0, 0), logHandler.AutoRollAtTime);
+        }
+
+        [Fact]
+        public void VerifyServiceLogonRightGraceful()
+        {
+            const string seedXml = "<service>"
+                                   + "<serviceaccount>"
+                                   + "<domain>" + Domain + "</domain>"
+                                   + "<user>" + Username + "</user>"
+                                   + "<password>" + Password + "</password>"
+                                   + "<allowservicelogon>true1</allowservicelogon>"
+                                   + "</serviceaccount>"
+                                   + "</service>";
+            var serviceDescriptor = ServiceDescriptor.FromXml(seedXml);
+            Assert.False(serviceDescriptor.AllowServiceAcountLogonRight);
+        }
+
+        [Fact]
+        public void VerifyServiceLogonRightOmitted()
+        {
+            const string seedXml = "<service>"
+                                   + "<serviceaccount>"
+                                   + "<domain>" + Domain + "</domain>"
+                                   + "<user>" + Username + "</user>"
+                                   + "<password>" + Password + "</password>"
+                                   + "</serviceaccount>"
+                                   + "</service>";
+            var serviceDescriptor = ServiceDescriptor.FromXml(seedXml);
+            Assert.False(serviceDescriptor.AllowServiceAcountLogonRight);
+        }
+
+        [Fact]
+        public void VerifyWaitHint_FullXML()
+        {
+            var sd = ConfigXmlBuilder.Create()
+                .WithTag("waithint", "20 min")
+                .ToServiceDescriptor(true);
+            Assert.Equal(TimeSpan.FromMinutes(20), sd.WaitHint);
+        }
+
+        /// <summary>
+        /// Test for https://github.com/kohsuke/winsw/issues/159
+        /// </summary>
+        [Fact]
+        public void VerifyWaitHint_XMLWithoutVersion()
+        {
+            var sd = ConfigXmlBuilder.Create(printXMLVersion: false)
+                .WithTag("waithint", "21 min")
+                .ToServiceDescriptor(true);
+            Assert.Equal(TimeSpan.FromMinutes(21), sd.WaitHint);
+        }
+
+        [Fact]
+        public void VerifyWaitHint_XMLWithoutComment()
+        {
+            var sd = ConfigXmlBuilder.Create(xmlComment: null)
+                .WithTag("waithint", "22 min")
+                .ToServiceDescriptor(true);
+            Assert.Equal(TimeSpan.FromMinutes(22), sd.WaitHint);
+        }
+
+        [Fact]
+        public void VerifyWaitHint_XMLWithoutVersionAndComment()
+        {
+            var sd = ConfigXmlBuilder.Create(xmlComment: null, printXMLVersion: false)
+                .WithTag("waithint", "23 min")
+                .ToServiceDescriptor(true);
+            Assert.Equal(TimeSpan.FromMinutes(23), sd.WaitHint);
+        }
+
+        [Fact]
+        public void VerifySleepTime()
+        {
+            var sd = ConfigXmlBuilder.Create().WithTag("sleeptime", "3 hrs").ToServiceDescriptor(true);
+            Assert.Equal(TimeSpan.FromHours(3), sd.SleepTime);
+        }
+
+        [Fact]
+        public void VerifyResetFailureAfter()
+        {
+            var sd = ConfigXmlBuilder.Create().WithTag("resetfailure", "75 sec").ToServiceDescriptor(true);
+            Assert.Equal(TimeSpan.FromSeconds(75), sd.ResetFailureAfter);
+        }
+
+        [Fact]
+        public void VerifyStopTimeout()
+        {
+            var sd = ConfigXmlBuilder.Create().WithTag("stoptimeout", "35 secs").ToServiceDescriptor(true);
+            Assert.Equal(TimeSpan.FromSeconds(35), sd.StopTimeout);
+        }
+
+        /// <summary>
+        /// https://github.com/kohsuke/winsw/issues/178
+        /// </summary>
+        [Fact]
+        public void Arguments_LegacyParam()
+        {
+            var sd = ConfigXmlBuilder.Create().WithTag("arguments", "arg").ToServiceDescriptor(true);
+            Assert.Equal("arg", sd.Arguments);
+        }
+
+        [Fact]
+        public void Arguments_NewParam_Single()
+        {
+            var sd = ConfigXmlBuilder.Create()
+                .WithTag("argument", "--arg1=2")
+                .ToServiceDescriptor(true);
+            Assert.Equal(" --arg1=2", sd.Arguments);
+        }
+
+        [Fact]
+        public void Arguments_NewParam_MultipleArgs()
+        {
+            var sd = ConfigXmlBuilder.Create()
+                .WithTag("argument", "--arg1=2")
+                .WithTag("argument", "--arg2=123")
+                .WithTag("argument", "--arg3=null")
+                .ToServiceDescriptor(true);
+            Assert.Equal(" --arg1=2 --arg2=123 --arg3=null", sd.Arguments);
+        }
+
+        /// <summary>
+        /// Ensures that the new single-argument field has a higher priority.
+        /// </summary>
+        [Fact]
+        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.Equal(" --arg2=123 --arg3=null", sd.Arguments);
+        }
+
+        [Theory]
+        [InlineData(true)]
+        [InlineData(false)]
+        public void DelayedStart_RoundTrip(bool enabled)
+        {
+            var bldr = ConfigXmlBuilder.Create();
+            if (enabled)
+            {
+                bldr = bldr.WithDelayedAutoStart();
+            }
+
+            var sd = bldr.ToServiceDescriptor();
+            Assert.Equal(enabled, sd.DelayedAutoStart);
+        }
+    }
+}

+ 0 - 0
src/Test/winswTests/Util/CLITestHelper.cs → src/WinSW.Tests/Util/CLITestHelper.cs


+ 0 - 0
src/Test/winswTests/Util/ConfigXmlBuilder.cs → src/WinSW.Tests/Util/ConfigXmlBuilder.cs


+ 0 - 0
src/Test/winswTests/Util/DateTimeExtensions.cs → src/WinSW.Tests/Util/DateTimeExtensions.cs


+ 0 - 0
src/Test/winswTests/Util/FilesystemTestHelper.cs → src/WinSW.Tests/Util/FilesystemTestHelper.cs


+ 0 - 0
src/Test/winswTests/Util/ServiceDescriptorAssert.cs → src/WinSW.Tests/Util/ServiceDescriptorAssert.cs


+ 4 - 7
src/Test/winswTests/winswTests.csproj → src/WinSW.Tests/WinSW.Tests.csproj

@@ -1,10 +1,8 @@
-<Project Sdk="Microsoft.NET.Sdk">
+<Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
     <TargetFrameworks>net461;netcoreapp3.1</TargetFrameworks>
     <LangVersion>latest</LangVersion>
-
-    <RootNamespace>winswTests</RootNamespace>
   </PropertyGroup>
 
   <ItemGroup>
@@ -26,10 +24,9 @@
   </ItemGroup>
 
   <ItemGroup>
-    <ProjectReference Include="..\..\Core\ServiceWrapper\winsw.csproj" />
-    <ProjectReference Include="..\..\Core\WinSWCore\WinSWCore.csproj" />
-    <ProjectReference Include="..\..\Plugins\RunawayProcessKiller\RunawayProcessKiller.csproj" />
-    <ProjectReference Include="..\..\Plugins\SharedDirectoryMapper\SharedDirectoryMapper.csproj" />
+    <ProjectReference Include="..\WinSW\WinSW.csproj" />
+    <ProjectReference Include="..\WinSW.Core\WinSW.Core.csproj" />
+    <ProjectReference Include="..\WinSW.Plugins\WinSW.Plugins.csproj" />
   </ItemGroup>
 
 </Project>

+ 48 - 0
src/WinSW.sln

@@ -0,0 +1,48 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 16
+VisualStudioVersion = 16.0.30128.74
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WinSW", "WinSW\WinSW.csproj", "{87BD4253-6957-4D83-B75D-5C4C2F114694}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WinSW.Core", "WinSW.Core\WinSW.Core.csproj", "{560E6F6A-18DD-4245-AD1A-D02A9AB9CDD7}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WinSW.Plugins", "WinSW.Plugins\WinSW.Plugins.csproj", "{B50AD8AA-54BD-4FD9-95A9-8AADC84DD46E}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WinSW.Tests", "WinSW.Tests\WinSW.Tests.csproj", "{691DE22D-4565-4E3C-9CC7-918A0098219E}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{4975DCF4-C32C-43ED-A731-8FCC1F7E6746}"
+	ProjectSection(SolutionItems) = preProject
+		.editorconfig = .editorconfig
+	EndProjectSection
+EndProject
+Global
+	GlobalSection(SolutionConfigurationPlatforms) = preSolution
+		Debug|Any CPU = Debug|Any CPU
+		Release|Any CPU = Release|Any CPU
+	EndGlobalSection
+	GlobalSection(ProjectConfigurationPlatforms) = postSolution
+		{87BD4253-6957-4D83-B75D-5C4C2F114694}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{87BD4253-6957-4D83-B75D-5C4C2F114694}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{87BD4253-6957-4D83-B75D-5C4C2F114694}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{87BD4253-6957-4D83-B75D-5C4C2F114694}.Release|Any CPU.Build.0 = Release|Any CPU
+		{560E6F6A-18DD-4245-AD1A-D02A9AB9CDD7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{560E6F6A-18DD-4245-AD1A-D02A9AB9CDD7}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{560E6F6A-18DD-4245-AD1A-D02A9AB9CDD7}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{560E6F6A-18DD-4245-AD1A-D02A9AB9CDD7}.Release|Any CPU.Build.0 = Release|Any CPU
+		{B50AD8AA-54BD-4FD9-95A9-8AADC84DD46E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{B50AD8AA-54BD-4FD9-95A9-8AADC84DD46E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{B50AD8AA-54BD-4FD9-95A9-8AADC84DD46E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{B50AD8AA-54BD-4FD9-95A9-8AADC84DD46E}.Release|Any CPU.Build.0 = Release|Any CPU
+		{691DE22D-4565-4E3C-9CC7-918A0098219E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{691DE22D-4565-4E3C-9CC7-918A0098219E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{691DE22D-4565-4E3C-9CC7-918A0098219E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{691DE22D-4565-4E3C-9CC7-918A0098219E}.Release|Any CPU.Build.0 = Release|Any CPU
+	EndGlobalSection
+	GlobalSection(SolutionProperties) = preSolution
+		HideSolutionNode = FALSE
+	EndGlobalSection
+	GlobalSection(ExtensibilityGlobals) = postSolution
+		SolutionGuid = {A508CEB3-957E-4B4D-9365-60E5A35EA009}
+	EndGlobalSection
+EndGlobal

+ 3 - 0
src/WinSW/AssemblyInfo.cs

@@ -0,0 +1,3 @@
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("WinSW.Tests")]

+ 0 - 0
src/Core/ServiceWrapper/Logging/WrapperServiceEventLogProvider.cs → src/WinSW/Logging/WrapperServiceEventLogProvider.cs


+ 0 - 0
src/Core/ServiceWrapper/NullableAttributes.cs → src/WinSW/NullableAttributes.cs


+ 0 - 0
src/Core/ServiceWrapper/Program.cs → src/WinSW/Program.cs


+ 0 - 0
src/Core/ServiceWrapper/ServiceControllerExtension.cs → src/WinSW/ServiceControllerExtension.cs


+ 0 - 0
src/Core/ServiceWrapper/UserException.cs → src/WinSW/UserException.cs


+ 5 - 9
src/Core/ServiceWrapper/winsw.csproj → src/WinSW/WinSW.csproj

@@ -1,4 +1,4 @@
-<Project Sdk="Microsoft.NET.Sdk">
+<Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
     <OutputType>Exe</OutputType>
@@ -12,8 +12,6 @@
     <Company>CloudBees, Inc.</Company>
     <Product>Windows Service Wrapper</Product>
     <Copyright>Copyright 2008-2016 Oleg Nenashev, CloudBees, Inc. and other contributors</Copyright>
-    <RootNamespace>winsw</RootNamespace>
-    <AssemblyName>WindowsService</AssemblyName>
   </PropertyGroup>
 
   <PropertyGroup Condition="'$(TargetFramework)' != 'netcoreapp3.1'">
@@ -31,9 +29,8 @@
   </ItemGroup>
 
   <ItemGroup>
-    <ProjectReference Include="..\WinSWCore\WinSWCore.csproj" />
-    <ProjectReference Include="..\..\Plugins\RunawayProcessKiller\RunawayProcessKiller.csproj" />
-    <ProjectReference Include="..\..\Plugins\SharedDirectoryMapper\SharedDirectoryMapper.csproj" />
+    <ProjectReference Include="..\WinSW.Core\WinSW.Core.csproj" />
+    <ProjectReference Include="..\WinSW.Plugins\WinSW.Plugins.csproj" />
   </ItemGroup>
 
   <Target Name="PublishCoreZip" AfterTargets="Publish" Condition="'$(TargetFramework)' == 'netcoreapp3.1' and '$(RuntimeIdentifier)' == ''">
@@ -67,9 +64,8 @@
 
     <PropertyGroup>
       <InputAssemblies>"$(OutDir)$(TargetFileName)"</InputAssemblies>
-      <InputAssemblies>$(InputAssemblies) "$(OutDir)WinSWCore.dll"</InputAssemblies>
-      <InputAssemblies>$(InputAssemblies) "$(OutDir)SharedDirectoryMapper.dll"</InputAssemblies>
-      <InputAssemblies>$(InputAssemblies) "$(OutDir)RunawayProcessKiller.dll"</InputAssemblies>
+      <InputAssemblies>$(InputAssemblies) "$(OutDir)WinSW.Core.dll"</InputAssemblies>
+      <InputAssemblies>$(InputAssemblies) "$(OutDir)WinSW.Plugins.dll"</InputAssemblies>
       <InputAssemblies>$(InputAssemblies) "$(OutDir)log4net.dll"</InputAssemblies>
       <OutputAssembly>"$(ArtifactsDir)WinSW.$(IdentifierSuffix).exe"</OutputAssembly>
     </PropertyGroup>

+ 1 - 1
src/Core/ServiceWrapper/WrapperService.cs → src/WinSW/WrapperService.cs

@@ -1,4 +1,4 @@
-using System;
+using System;
 using System.Collections.Generic;
 using System.Diagnostics;
 using System.IO;

+ 0 - 91
src/winsw.sln

@@ -1,91 +0,0 @@
-
-Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 16
-VisualStudioVersion = 16.0.30128.74
-MinimumVisualStudioVersion = 10.0.40219.1
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "winsw", "Core\ServiceWrapper\winsw.csproj", "{0DE77F55-ADE5-43C1-999A-0BC81153B039}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "winswTests", "Test\winswTests\winswTests.csproj", "{93843402-842B-44B4-B303-AEE829BE0B43}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "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("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WinSWCore", "Core\WinSWCore\WinSWCore.csproj", "{9D0C63E2-B6FF-4A85-BD36-B3E5D7F27D06}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "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
-		..\samples\sample-complete.xml = ..\samples\sample-complete.xml
-		..\samples\sample-minimal.xml = ..\samples\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
-	GlobalSection(ExtensibilityGlobals) = postSolution
-		SolutionGuid = {F9F8AFEA-196D-4041-8441-FABC3B730F9E}
-	EndGlobalSection
-EndGlobal