From 4fd69398cef17a1db3524bbb10140ebd5ba7e4d0 Mon Sep 17 00:00:00 2001
From: Niko Ehrenfeuchter <nikolaus.ehrenfeuchter@unibas.ch>
Date: Tue, 9 Jan 2018 15:17:30 +0100
Subject: [PATCH] Create class library for XML wrappers.

Note that this only adds the new class library, it does not yet adjust
the existing service code to use it!

Refers to #21
---
 ATXSerializables/ATXSerializables.csproj    |  56 +++++
 ATXSerializables/DriveToCheck.cs            |  18 ++
 ATXSerializables/Properties/AssemblyInfo.cs |  36 ++++
 ATXSerializables/ServiceConfig.cs           | 225 +++++++++++++++++++
 ATXSerializables/ServiceStatus.cs           | 226 ++++++++++++++++++++
 ATXService.sln                              |   6 +
 6 files changed, 567 insertions(+)
 create mode 100644 ATXSerializables/ATXSerializables.csproj
 create mode 100644 ATXSerializables/DriveToCheck.cs
 create mode 100644 ATXSerializables/Properties/AssemblyInfo.cs
 create mode 100644 ATXSerializables/ServiceConfig.cs
 create mode 100644 ATXSerializables/ServiceStatus.cs

diff --git a/ATXSerializables/ATXSerializables.csproj b/ATXSerializables/ATXSerializables.csproj
new file mode 100644
index 0000000..0b6ec7c
--- /dev/null
+++ b/ATXSerializables/ATXSerializables.csproj
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
+  <PropertyGroup>
+    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+    <ProjectGuid>{166D65D5-EE10-4364-8AA3-4D86BA5CE244}</ProjectGuid>
+    <OutputType>Library</OutputType>
+    <AppDesignerFolder>Properties</AppDesignerFolder>
+    <RootNamespace>ATXSerializables</RootNamespace>
+    <AssemblyName>ATXSerializables</AssemblyName>
+    <TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
+    <FileAlignment>512</FileAlignment>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+    <DebugSymbols>true</DebugSymbols>
+    <DebugType>full</DebugType>
+    <Optimize>false</Optimize>
+    <OutputPath>bin\Debug\</OutputPath>
+    <DefineConstants>DEBUG;TRACE</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+    <DebugType>pdbonly</DebugType>
+    <Optimize>true</Optimize>
+    <OutputPath>bin\Release\</OutputPath>
+    <DefineConstants>TRACE</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+  </PropertyGroup>
+  <ItemGroup>
+    <Reference Include="System" />
+    <Reference Include="System.Configuration" />
+    <Reference Include="System.Core" />
+    <Reference Include="System.Xml.Linq" />
+    <Reference Include="System.Data.DataSetExtensions" />
+    <Reference Include="Microsoft.CSharp" />
+    <Reference Include="System.Data" />
+    <Reference Include="System.Xml" />
+  </ItemGroup>
+  <ItemGroup>
+    <Compile Include="DriveToCheck.cs" />
+    <Compile Include="ServiceConfig.cs" />
+    <Compile Include="Properties\AssemblyInfo.cs" />
+    <Compile Include="ServiceStatus.cs" />
+  </ItemGroup>
+  <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
+  <!-- To modify your build process, add your task inside one of the targets below and uncomment it. 
+       Other similar extension points exist, see Microsoft.Common.targets.
+  <Target Name="BeforeBuild">
+  </Target>
+  <Target Name="AfterBuild">
+  </Target>
+  -->
+</Project>
\ No newline at end of file
diff --git a/ATXSerializables/DriveToCheck.cs b/ATXSerializables/DriveToCheck.cs
new file mode 100644
index 0000000..7d52e74
--- /dev/null
+++ b/ATXSerializables/DriveToCheck.cs
@@ -0,0 +1,18 @@
+using System.Xml.Serialization;
+
+namespace ATXSerializables
+{
+    /// <summary>
+    /// Helper class for the nested SpaceMonitoring sections.
+    /// </summary>
+    public class DriveToCheck
+    {
+        [XmlElement("DriveName")]
+        public string DriveName { get; set; }
+
+        // the value is to be compared to System.IO.DriveInfo.TotalFreeSpace
+        // hence we use the same type (long) to avoid unnecessary casts later:
+        [XmlElement("SpaceThreshold")]
+        public long SpaceThreshold { get; set; }
+    }
+}
\ No newline at end of file
diff --git a/ATXSerializables/Properties/AssemblyInfo.cs b/ATXSerializables/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000..3a2d27e
--- /dev/null
+++ b/ATXSerializables/Properties/AssemblyInfo.cs
@@ -0,0 +1,36 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following 
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("ATXSerializables")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("Universitaet Basel")]
+[assembly: AssemblyProduct("ATXSerializables")]
+[assembly: AssemblyCopyright("Copyright © Universitaet Basel 2018")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible 
+// to COM components.  If you need to access a type in this assembly from 
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("c1ed2607-6acb-4202-9354-65d19c5a06d1")]
+
+// Version information for an assembly consists of the following four values:
+//
+//      Major Version
+//      Minor Version 
+//      Build Number
+//      Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers 
+// by using the '*' as shown below:
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
diff --git a/ATXSerializables/ServiceConfig.cs b/ATXSerializables/ServiceConfig.cs
new file mode 100644
index 0000000..512e426
--- /dev/null
+++ b/ATXSerializables/ServiceConfig.cs
@@ -0,0 +1,225 @@
+using System;
+using System.Collections.Generic;
+using System.Configuration;
+using System.IO;
+using System.Xml.Serialization;
+
+namespace ATXSerializables
+{
+    /// <summary>
+    /// configuration class based on xml
+    /// </summary>
+    [Serializable]
+    public class ServiceConfig
+    {
+        [XmlIgnore] public string ValidationWarnings;
+
+        public ServiceConfig() {
+            ValidationWarnings = "";
+            // set values for the optional XML elements:
+            SmtpHost = "";
+            SmtpPort = 25;
+            SmtpUserCredential = "";
+            SmtpPasswortCredential = "";
+            EmailPrefix = "";
+            AdminEmailAdress = "";
+            AdminDebugEmailAdress = "";
+            GraceNotificationDelta = 720;
+
+            InterPacketGap = 0;
+
+            EnforceInheritedACLs = true;
+        }
+
+        #region required configuration parameters
+
+        /// <summary>
+        /// A human friendly name for the host, to be used in emails etc.
+        /// </summary>
+        public string HostAlias { get; set; }
+
+        /// <summary>
+        /// A human friendly name for the target, to be used in emails etc.
+        /// </summary>
+        public string DestinationAlias { get; set; }
+
+        /// <summary>
+        /// The base drive for the spooling directories (incoming and managed).
+        /// </summary>
+        public string SourceDrive { get; set; }
+
+        /// <summary>
+        /// The name of a directory on SourceDrive that is monitored for new files.
+        /// </summary>
+        public string IncomingDirectory { get; set; }
+
+        /// <summary>
+        /// The name of a marker file to be placed in all **sub**directories
+        /// inside the IncomingDirectory.
+        /// </summary>
+        public string MarkerFile { get; set; }
+
+        /// <summary>
+        /// A directory on SourceDrive to hold the three subdirectories "DONE",
+        /// "PROCESSING" and "UNMATCHED" used during and after transfers.
+        /// </summary>
+        public string ManagedDirectory { get; set; }
+
+        /// <summary>
+        /// Target path to transfer files to. Usually a UNC location.
+        /// </summary>
+        public string DestinationDirectory { get; set; }
+
+        /// <summary>
+        /// The name of a subdirectory in the DestinationDirectory to be used
+        /// to keep the temporary data of running transfers.
+        /// </summary>
+        public string TmpTransferDir { get; set; }
+
+        public string EmailFrom { get; set; }
+
+        public int ServiceTimer { get; set; }
+
+        public int MaxCpuUsage { get; set; }
+        public int MinAvailableMemory { get; set; }
+        public int AdminNotificationDelta { get; set; }
+        public int StorageNotificationDelta { get; set; }
+
+        /// <summary>
+        /// GracePeriod: number of days after data in the "DONE" location expires,
+        /// which will trigger a summary email to the admin address.
+        /// </summary>
+        public int GracePeriod { get; set; }
+
+        public bool SendAdminNotification { get; set; }
+        public bool SendTransferNotification { get; set; }
+        public bool Debug { get; set; }
+
+        [XmlArray]
+        [XmlArrayItem(ElementName = "DriveToCheck")]
+        public List<DriveToCheck> SpaceMonitoring { get; set; }
+
+        [XmlArray]
+        [XmlArrayItem(ElementName = "ProcessName")]
+        public List<string> BlacklistedProcesses { get; set; }
+
+        #endregion
+
+
+        #region optional configuration parameters
+
+        public string SmtpHost { get; set; }
+        public string SmtpUserCredential { get; set; }
+        public string SmtpPasswortCredential { get; set; }
+        public int SmtpPort { get; set; }
+
+        public string EmailPrefix { get; set; }
+        public string AdminEmailAdress { get; set; }
+        public string AdminDebugEmailAdress { get; set; }
+
+        public int GraceNotificationDelta { get; set; }
+
+        public int InterPacketGap { get; set; }
+
+        /// <summary>
+        /// EnforceInheritedACLs: whether to enforce ACL inheritance when moving files and
+        /// directories, see https://support.microsoft.com/en-us/help/320246 for more details.
+        /// </summary>
+        public bool EnforceInheritedACLs { get; set; }
+
+        #endregion
+
+
+        public static void Serialize(string file, ServiceConfig c) {
+            // the config is never meant to be written by us, therefore:
+            throw new SettingsPropertyIsReadOnlyException("The config file must not be written by the service!");
+        }
+
+        public static ServiceConfig Deserialize(string file) {
+            var xs = new XmlSerializer(typeof(ServiceConfig));
+            var reader = File.OpenText(file);
+            var config = (ServiceConfig) xs.Deserialize(reader);
+            reader.Close();
+            ValidateConfiguration(config);
+            return config;
+        }
+
+        private static void ValidateConfiguration(ServiceConfig c) {
+            if (string.IsNullOrEmpty(c.SourceDrive) ||
+                string.IsNullOrEmpty(c.IncomingDirectory) ||
+                string.IsNullOrEmpty(c.ManagedDirectory))
+                throw new ConfigurationErrorsException("mandatory parameter missing!");
+
+            if (c.SourceDrive.Substring(1) != @":\")
+                throw new ConfigurationErrorsException("SourceDrive must be a drive " +
+                                                       @"letter followed by a colon and a backslash, e.g. 'D:\'!");
+
+            // make sure SourceDrive is a local (fixed) disk:
+            var driveInfo = new DriveInfo(c.SourceDrive);
+            if (driveInfo.DriveType != DriveType.Fixed)
+                throw new ConfigurationErrorsException("SourceDrive (" + c.SourceDrive +
+                                                       ") must be a local (fixed) drive, OS reports '" +
+                                                       driveInfo.DriveType + "')!");
+
+
+            // spooling directories: IncomingDirectory + ManagedDirectory
+            if (c.IncomingDirectory.StartsWith(@"\"))
+                throw new ConfigurationErrorsException("IncomingDirectory must not start with a backslash!");
+            if (c.ManagedDirectory.StartsWith(@"\"))
+                throw new ConfigurationErrorsException("ManagedDirectory must not start with a backslash!");
+
+            if (!Directory.Exists(c.DestinationDirectory))
+                throw new ConfigurationErrorsException("can't find destination: " + c.DestinationDirectory);
+
+            var tmpTransferPath = Path.Combine(c.DestinationDirectory, c.TmpTransferDir);
+            if (!Directory.Exists(tmpTransferPath))
+                throw new ConfigurationErrorsException("temporary transfer dir doesn't exist: " + tmpTransferPath);
+
+            if (c.ServiceTimer < 1000)
+                throw new ConfigurationErrorsException("ServiceTimer must not be smaller than 1000 ms!");
+
+
+            // NON-CRITICAL stuff just adds messages to ValidationWarnings:
+            // DestinationDirectory
+            if (!c.DestinationDirectory.StartsWith(@"\\"))
+                c.ValidationWarnings += " - <DestinationDirectory> is not a UNC path!\n";
+        }
+
+        public string Summary() {
+            var msg =
+                "HostAlias: " + HostAlias + "\n" +
+                "SourceDrive: " + SourceDrive + "\n" +
+                "IncomingDirectory: " + IncomingDirectory + "\n" +
+                "MarkerFile: " + MarkerFile + "\n" +
+                "ManagedDirectory: " + ManagedDirectory + "\n" +
+                "GracePeriod: " + GracePeriod + "\n" +
+                "DestinationDirectory: " + DestinationDirectory + "\n" +
+                "TmpTransferDir: " + TmpTransferDir + "\n" +
+                "EnforceInheritedACLs: " + EnforceInheritedACLs + "\n" +
+                "ServiceTimer: " + ServiceTimer + "\n" +
+                "InterPacketGap: " + InterPacketGap + "\n" +
+                "MaxCpuUsage: " + MaxCpuUsage + "\n" +
+                "MinAvailableMemory: " + MinAvailableMemory + "\n";
+            foreach (var processName in BlacklistedProcesses) {
+                msg += "BlacklistedProcess: " + processName + "\n";
+            }
+            foreach (var driveToCheck in SpaceMonitoring) {
+                msg += "Drive to check free space: " + driveToCheck.DriveName +
+                       " (threshold: " + driveToCheck.SpaceThreshold + ")" + "\n";
+            }
+            if (string.IsNullOrEmpty(SmtpHost)) {
+                msg += "SmtpHost: ====== Not configured, disabling email! ======" + "\n";
+            } else {
+                msg +=
+                    "SmtpHost: " + SmtpHost + "\n" +
+                    "EmailFrom: " + EmailFrom + "\n" +
+                    "AdminEmailAdress: " + AdminEmailAdress + "\n" +
+                    "AdminDebugEmailAdress: " + AdminDebugEmailAdress + "\n" +
+                    "StorageNotificationDelta: " + StorageNotificationDelta + "\n" +
+                    "AdminNotificationDelta: " + AdminNotificationDelta + "\n" +
+                    "GraceNotificationDelta: " + GraceNotificationDelta + "\n";
+            }
+            return msg;
+        }
+    }
+}
diff --git a/ATXSerializables/ServiceStatus.cs b/ATXSerializables/ServiceStatus.cs
new file mode 100644
index 0000000..5bdc52f
--- /dev/null
+++ b/ATXSerializables/ServiceStatus.cs
@@ -0,0 +1,226 @@
+using System;
+using System.IO;
+using System.Xml.Serialization;
+
+namespace ATXSerializables
+{
+    [Serializable]
+    public class ServiceStatus
+    {
+        [XmlIgnore] string _storageFile; // remember where we came from
+        [XmlIgnore] private ServiceConfig _config;
+        [XmlIgnore] public string ValidationWarnings;
+
+        private DateTime _lastStatusUpdate;
+        private DateTime _lastStorageNotification;
+        private DateTime _lastAdminNotification;
+        private DateTime _lastGraceNotification;
+
+        private string _limitReason;
+        string _currentTransferSrc;
+        string _currentTargetTmp;
+
+        bool _transferInProgress;
+        private bool _serviceSuspended;
+        private bool _cleanShutdown;
+
+        private long _currentTransferSize;
+
+        [XmlElement("LastStatusUpdate", DataType = "dateTime")]
+        public DateTime LastStatusUpdate {
+            get { return _lastStatusUpdate; }
+            set { _lastStatusUpdate = value; }
+        }
+
+        [XmlElement("LastStorageNotification", DataType = "dateTime")]
+        public DateTime LastStorageNotification {
+            get { return _lastStorageNotification; }
+            set {
+                _lastStorageNotification = value;
+                Serialize();
+            }
+        }
+
+        [XmlElement("LastAdminNotification", DataType = "dateTime")]
+        public DateTime LastAdminNotification {
+            get { return _lastAdminNotification; }
+            set {
+                _lastAdminNotification = value;
+                Serialize();
+            }
+        }
+
+        [XmlElement("LastGraceNotification", DataType = "dateTime")]
+        public DateTime LastGraceNotification {
+            get { return _lastGraceNotification; }
+            set {
+                _lastGraceNotification = value;
+                Serialize();
+            }
+        }
+
+        public string LimitReason {
+            get { return _limitReason; }
+            set {
+                _limitReason = value;
+                // log("LimitReason was updated (" + value + "), calling Serialize()...");
+                Serialize();
+            }
+        }
+
+        public string CurrentTransferSrc {
+            get { return _currentTransferSrc; }
+            set {
+                _currentTransferSrc = value;
+                // log("CurrentTransferSrc was updated (" + value + "), calling Serialize()...");
+                Serialize();
+            }
+        }
+
+        public string CurrentTargetTmp {
+            get { return _currentTargetTmp; }
+            set {
+                _currentTargetTmp = value;
+                // log("CurrentTargetTmp was updated (" + value + "), calling Serialize()...");
+                Serialize();
+            }
+        }
+
+        public bool ServiceSuspended {
+            get { return _serviceSuspended; }
+            set {
+                _serviceSuspended = value;
+                // log("ServiceSuspended was updated (" + value + "), calling Serialize()...");
+                Serialize();
+            }
+        }
+
+        public bool TransferInProgress {
+            get { return _transferInProgress; }
+            set {
+                _transferInProgress = value;
+                // log("FilecopyFinished was updated (" + value + "), calling Serialize()...");
+                Serialize();
+            }
+        }
+
+        /// <summary>
+        /// Indicates whether the service was cleanly shut down (false while the service is running).
+        /// </summary>
+        public bool CleanShutdown {
+            get { return _cleanShutdown; }
+            set {
+                _cleanShutdown = value;
+                Serialize();
+            }
+        }
+
+        public long CurrentTransferSize {
+            get { return _currentTransferSize; }
+            set {
+                _currentTransferSize = value;
+                // log("CurrentTransferSize was updated (" + value + "), calling Serialize()...");
+                Serialize();
+            }
+        }
+
+        public ServiceStatus() {
+            _currentTransferSrc = "";
+            _currentTargetTmp = "";
+            _transferInProgress = false;
+        }
+
+        public void Serialize() {
+            /* During de-serialization, the setter methods get called as well but
+             * we should not serialize until the deserialization has completed.
+             * As the storage file name will only be set after this, it is sufficient
+             * to test for this (plus, we can't serialize anyway without it).
+             */
+            if (_storageFile == null) {
+                // log("File name for XML serialization is not set, doing nothing.");
+                return;
+            }
+            // update the timestamp:
+            LastStatusUpdate = DateTime.Now;
+            try {
+                var xs = new XmlSerializer(GetType());
+                var writer = File.CreateText(_storageFile);
+                xs.Serialize(writer, this);
+                writer.Flush();
+                writer.Close();
+            }
+            catch (Exception ex) {
+                log("Error in Serialize(): " + ex.Message);
+            }
+            log("Finished serializing " + _storageFile);
+        }
+
+        static void log(string message) {
+            // use Console.WriteLine until proper logging is there (running as a system
+            // service means those messages will disappear):
+            Console.WriteLine(message);
+            /*
+            using (var sw = File.AppendText(@"C:\Tools\AutoTx\console.log")) {
+                sw.WriteLine(message);
+            }
+             */
+        }
+
+        public static ServiceStatus Deserialize(string file, ServiceConfig config) {
+            ServiceStatus status;
+
+            var xs = new XmlSerializer(typeof(ServiceStatus));
+            try {
+                var reader = File.OpenText(file);
+                status = (ServiceStatus) xs.Deserialize(reader);
+                reader.Close();
+            }
+            catch (Exception) {
+                // if reading the status XML fails, we return an empty (new) one
+                status = new ServiceStatus();
+            }
+            status._config = config;
+            ValidateStatus(status);
+            // now set the storage filename:
+            status._storageFile = file;
+            return status;
+        }
+
+        private static void ValidateStatus(ServiceStatus s) {
+            // CurrentTransferSrc
+            if (s.CurrentTransferSrc.Length > 0
+                && !Directory.Exists(s.CurrentTransferSrc)) {
+                s.ValidationWarnings += " - found non-existing source path of an unfinished " +
+                                        "transfer: " + s.CurrentTransferSrc + "\n";
+                s.CurrentTransferSrc = "";
+            }
+
+            // CurrentTargetTmp
+            var currentTargetTmpPath = Path.Combine(s._config.DestinationDirectory,
+                s._config.TmpTransferDir,
+                s.CurrentTargetTmp);
+            if (s.CurrentTargetTmp.Length > 0
+                && !Directory.Exists(currentTargetTmpPath)) {
+                s.ValidationWarnings += " - found non-existing temporary path of an " +
+                                        "unfinished transfer: " + currentTargetTmpPath + "\n";
+                s.CurrentTargetTmp = "";
+            }
+        }
+
+        public string Summary() {
+            return
+                "CurrentTransferSrc: " + CurrentTransferSrc + "\n" +
+                "CurrentTargetTmp: " + CurrentTargetTmp + "\n" +
+                "TransferInProgress: " + TransferInProgress + "\n" +
+                "CurrentTransferSize: " + CurrentTransferSize + "\n" +
+                "LastStatusUpdate: " +
+                LastStatusUpdate.ToString("yyyy-MM-dd HH:mm:ss") + "\n" +
+                "LastStorageNotification: " +
+                LastStorageNotification.ToString("yyyy-MM-dd HH:mm:ss") + "\n" +
+                "LastAdminNotification: " +
+                LastAdminNotification.ToString("yyyy-MM-dd HH:mm:ss") + "\n" +
+                "LastGraceNotification: " +
+                LastGraceNotification.ToString("yyyy-MM-dd HH:mm:ss") + "\n";
+        }
+    }
+}
diff --git a/ATXService.sln b/ATXService.sln
index d949344..0c405b0 100644
--- a/ATXService.sln
+++ b/ATXService.sln
@@ -5,6 +5,8 @@ VisualStudioVersion = 12.0.40629.0
 MinimumVisualStudioVersion = 10.0.40219.1
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ATXProject", "AutoTx\ATXProject.csproj", "{5CB67E3A-E63A-4791-B90B-8CEF0027AEAD}"
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ATXSerializables", "ATXSerializables\ATXSerializables.csproj", "{166D65D5-EE10-4364-8AA3-4D86BA5CE244}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
@@ -15,6 +17,10 @@ Global
 		{5CB67E3A-E63A-4791-B90B-8CEF0027AEAD}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{5CB67E3A-E63A-4791-B90B-8CEF0027AEAD}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{5CB67E3A-E63A-4791-B90B-8CEF0027AEAD}.Release|Any CPU.Build.0 = Release|Any CPU
+		{166D65D5-EE10-4364-8AA3-4D86BA5CE244}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{166D65D5-EE10-4364-8AA3-4D86BA5CE244}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{166D65D5-EE10-4364-8AA3-4D86BA5CE244}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{166D65D5-EE10-4364-8AA3-4D86BA5CE244}.Release|Any CPU.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE
-- 
GitLab