diff --git a/.gitignore b/.gitignore index 52acab8..41c0c8a 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,6 @@ obj /winsw.csproj.user /winsw_cert.pfx *.user +/src/packages/NUnit.2.6.4 +/src/packages/ILMerge.MSBuild.Tasks.1.0.0.3 +/winsw_key.snk diff --git a/src/.nuget/packages.config b/src/.nuget/packages.config new file mode 100644 index 0000000..e1e92a3 --- /dev/null +++ b/src/.nuget/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/Core/ServiceWrapper/Main.cs b/src/Core/ServiceWrapper/Main.cs index bf29bbf..dd18771 100644 --- a/src/Core/ServiceWrapper/Main.cs +++ b/src/Core/ServiceWrapper/Main.cs @@ -14,13 +14,16 @@ using log4net.Core; using log4net.Layout; using log4net.Repository.Hierarchy; using Microsoft.Win32; +using winsw.Extensions; +using winsw.Util; using WMI; using ServiceType = WMI.ServiceType; +using winsw.Native; using System.Reflection; namespace winsw { - public class WrapperService : ServiceBase, EventLogger + public class WrapperService : ServiceBase, EventLogger, IEventWriter { private SERVICE_STATUS _wrapperServiceStatus; @@ -28,6 +31,8 @@ namespace winsw private readonly ServiceDescriptor _descriptor; private Dictionary _envs; + internal WinSWExtensionManager ExtensionManager { private set; get; } + private static readonly ILog Log = LogManager.GetLogger("WinSW"); /// @@ -52,6 +57,7 @@ namespace winsw { _descriptor = descriptor; ServiceName = _descriptor.Id; + ExtensionManager = new WinSWExtensionManager(_descriptor); CanShutdown = true; CanStop = true; CanPauseAndContinue = false; @@ -246,6 +252,22 @@ namespace winsw LogEvent("Starting " + _descriptor.Executable + ' ' + startarguments); WriteEvent("Starting " + _descriptor.Executable + ' ' + startarguments); + // Load and start extensions + ExtensionManager.LoadExtensions(this); + try + { + ExtensionManager.OnStart(this); + } + catch (ExtensionException ex) + { + LogEvent("Failed to start extension " + ex.ExtensionId + "\n" + ex.Message, EventLogEntryType.Error); + WriteEvent("Failed to start extension " + ex.ExtensionId, ex); + //TODO: Exit on error? + } + + LogEvent("Starting " + _descriptor.Executable + ' ' + startarguments); + WriteEvent("Starting " + _descriptor.Executable + ' ' + startarguments); + StartProcess(_process, startarguments, _descriptor.Executable); // send stdout and stderr to its respective output file. @@ -327,6 +349,17 @@ namespace winsw SignalShutdownComplete(); } + // Stop extensions + try + { + ExtensionManager.OnStop(this); + } + catch (ExtensionException ex) + { + LogEvent("Failed to stop extension " + ex.ExtensionId + "\n" + ex.Message, EventLogEntryType.Error); + WriteEvent("Failed to stop extension " + ex.ExtensionId, ex); + } + if (_systemShuttingdown && _descriptor.BeepOnShutdown) { Console.Beep(); diff --git a/src/Core/ServiceWrapper/packages.config b/src/Core/ServiceWrapper/packages.config index 7a2c004..682cb90 100644 --- a/src/Core/ServiceWrapper/packages.config +++ b/src/Core/ServiceWrapper/packages.config @@ -1,4 +1,6 @@  + + \ No newline at end of file diff --git a/src/Core/ServiceWrapper/winsw.csproj b/src/Core/ServiceWrapper/winsw.csproj index 0844ff4..c13d484 100644 --- a/src/Core/ServiceWrapper/winsw.csproj +++ b/src/Core/ServiceWrapper/winsw.csproj @@ -9,14 +9,14 @@ Exe Properties winsw - winsw + WindowsService v2.0 512 false true - ..\..\..\winsw_cert.pfx + $(SolutionDir)..\winsw_key.snk @@ -74,20 +74,11 @@ - - - - - Component - - - - @@ -100,7 +91,10 @@ - + + Designer + + @@ -114,6 +108,17 @@ true + + + {ca5c71db-c5a8-4c27-bf83-8e6daed9d6b5} + SharedDirectoryMapper + + + {9d0c63e2-b6ff-4a85-bd36-b3e5d7f27d06} + WinSWCore + + + - - - - @@ -133,4 +134,46 @@ + + + + + + + + + + + $(ProjectDir)$(OutDir)winsw.exe + $(AssemblyOriginatorKeyFile) + + + + $(OutputPath)sn-path.txt + + + + + + + + + + + + $([System.Text.RegularExpressions.Regex]::Replace('$(SNPath)', ';.*', '')) + + + + + + $(SolutionDir)packages\ilmerge.2.14.1208\tools + $(OutputPath)winsw_cert.pub + + + + + + + \ No newline at end of file diff --git a/src/Core/ServiceWrapper/Download.cs b/src/Core/WinSWCore/Download.cs similarity index 100% rename from src/Core/ServiceWrapper/Download.cs rename to src/Core/WinSWCore/Download.cs diff --git a/src/Core/ServiceWrapper/DynamicProxy.cs b/src/Core/WinSWCore/DynamicProxy.cs similarity index 100% rename from src/Core/ServiceWrapper/DynamicProxy.cs rename to src/Core/WinSWCore/DynamicProxy.cs diff --git a/src/Core/WinSWCore/Extensions/AbstractWinSWExtension.cs b/src/Core/WinSWCore/Extensions/AbstractWinSWExtension.cs new file mode 100644 index 0000000..acba93c --- /dev/null +++ b/src/Core/WinSWCore/Extensions/AbstractWinSWExtension.cs @@ -0,0 +1,27 @@ +using System; +using System.Xml; +using winsw.Util; + +namespace winsw.Extensions +{ + public abstract class AbstractWinSWExtension : IWinSWExtension + { + public abstract String DisplayName { get; } + public WinSWExtensionDescriptor Descriptor { get; set; } + + public virtual void Configure(ServiceDescriptor descriptor, XmlNode node, IEventWriter logger) + { + // Do nothing + } + + public virtual void OnStart(IEventWriter eventWriter) + { + // Do nothing + } + + public virtual void OnStop(IEventWriter eventWriter) + { + // Do nothing + } + } +} diff --git a/src/Core/WinSWCore/Extensions/ExtensionException.cs b/src/Core/WinSWCore/Extensions/ExtensionException.cs new file mode 100644 index 0000000..5ac682a --- /dev/null +++ b/src/Core/WinSWCore/Extensions/ExtensionException.cs @@ -0,0 +1,29 @@ +using System; + +namespace winsw.Extensions +{ + public class ExtensionException : WinSWException + { + public String ExtensionId { get; private set; } + + public ExtensionException(String extensionName, String message) + : base(message) + { + ExtensionId = extensionName; + } + + public ExtensionException(String extensionName, String message, Exception innerException) + : base(message, innerException) + { + ExtensionId = extensionName; + } + + public override string Message + { + get + { + return ExtensionId + ": " + base.Message; + } + } + } +} diff --git a/src/Core/WinSWCore/Extensions/ExtensionPointAttribute.cs b/src/Core/WinSWCore/Extensions/ExtensionPointAttribute.cs new file mode 100644 index 0000000..61a4e11 --- /dev/null +++ b/src/Core/WinSWCore/Extensions/ExtensionPointAttribute.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace winsw.Extensions +{ + /// + /// This attribute is used to identify extension points within the code + /// + /// + /// Each extension point implements its own entry type. + /// + class ExtensionPointAttribute + { + } +} diff --git a/src/Core/WinSWCore/Extensions/IWinSWExtension.cs b/src/Core/WinSWCore/Extensions/IWinSWExtension.cs new file mode 100644 index 0000000..c114381 --- /dev/null +++ b/src/Core/WinSWCore/Extensions/IWinSWExtension.cs @@ -0,0 +1,47 @@ +using System; +using System.Xml; +using winsw.Util; + +namespace winsw.Extensions +{ + /// + /// Interface for Win Service Wrapper Extension + /// + /// + /// All implementations should provide the default empty constructor. + /// The initialization will be performed by Init methods + /// + public interface IWinSWExtension + { + /// + /// Extension name to be displayed in logs + /// + String DisplayName { get; } + + /// + /// Extension descriptor + /// + WinSWExtensionDescriptor Descriptor { get; set; } + + /// + /// Init handler. Extension should load it's config during that step + /// + /// Service descriptor + /// Configuration node + void Configure(ServiceDescriptor descriptor, XmlNode node, IEventWriter logger); + + /// + /// Start handler. Called during startup of the service before the child process. + /// + /// Logger + /// Any error during execution + void OnStart(IEventWriter logger); + + /// + /// Stop handler. Called during stop of the service + /// + /// Logger + /// Any error during execution + void OnStop(IEventWriter logger); + } +} diff --git a/src/Core/WinSWCore/Extensions/WinSWExtensionDescriptor.cs b/src/Core/WinSWCore/Extensions/WinSWExtensionDescriptor.cs new file mode 100644 index 0000000..3d58c73 --- /dev/null +++ b/src/Core/WinSWCore/Extensions/WinSWExtensionDescriptor.cs @@ -0,0 +1,45 @@ +using System; +using System.Xml; +using winsw.Util; + +namespace winsw.Extensions +{ + /// + /// Describes WinSW extensions in + /// + /// + /// Any extension has its own descriptor instance. + /// + public class WinSWExtensionDescriptor + { + /// + /// Unique extension ID + /// + public String Id { get; private set; } + + /// + /// Exception is enabled + /// + public bool Enabled { get; private set; } + + /// + /// Extension classname + /// + public String ClassName { get; private set; } + + private WinSWExtensionDescriptor(string id, string className, bool enabled) + { + Id = id; + Enabled = enabled; + ClassName = className; + } + + public static WinSWExtensionDescriptor FromXml(XmlElement node) + { + bool enabled = XmlHelper.SingleAttribute(node, "enabled", true); + string className = XmlHelper.SingleAttribute(node, "className"); + string id = XmlHelper.SingleAttribute(node, "id"); + return new WinSWExtensionDescriptor(id, className, enabled); + } + } +} diff --git a/src/Core/WinSWCore/Extensions/WinSWExtensionManager.cs b/src/Core/WinSWCore/Extensions/WinSWExtensionManager.cs new file mode 100644 index 0000000..c305409 --- /dev/null +++ b/src/Core/WinSWCore/Extensions/WinSWExtensionManager.cs @@ -0,0 +1,123 @@ +using System; +using System.Collections.Generic; +using System.Xml; +using System.Reflection; +using System.Diagnostics; +using winsw.Util; + +namespace winsw.Extensions +{ + public class WinSWExtensionManager + { + public Dictionary Extensions { private set; get; } + public ServiceDescriptor ServiceDescriptor { private set; get; } + + public WinSWExtensionManager(ServiceDescriptor serviceDescriptor) + { + ServiceDescriptor = serviceDescriptor; + Extensions = new Dictionary(); + } + + /// + /// Starts all extensions + /// + /// Start failure + public void OnStart(IEventWriter logger) + { + foreach (var ext in Extensions) + { + ext.Value.OnStart(logger); + } + } + + /// + /// Stops all extensions + /// + /// Stop failure + public void OnStop(IEventWriter logger) + { + foreach (var ext in Extensions) + { + ext.Value.OnStop(logger); + } + } + + //TODO: Implement loading of external extensions. Current version supports internal hack + #region Extension load management + + public void LoadExtensions(IEventWriter logger) + { + var extensionIds = ServiceDescriptor.ExtensionIds; + foreach (String extensionId in extensionIds) + { + LoadExtension(extensionId, logger); + } + } + + /// + /// Loads extensions from the configuration file + /// + /// Extension ID + /// Logger + /// Loading failure + private void LoadExtension(string id, IEventWriter logger) + { + if (Extensions.ContainsKey(id)) + { + throw new ExtensionException(id, "Extension has been already loaded"); + } + + var extensionsConfig = ServiceDescriptor.ExtensionsConfiguration; + XmlElement configNode =(extensionsConfig != null) ? extensionsConfig.SelectSingleNode("extension[@id='"+id+"'][1]") as XmlElement : null; + if (configNode == null) + { + throw new ExtensionException(id, "Cannot get the configuration entry"); + } + + var descriptor = WinSWExtensionDescriptor.FromXml(configNode); + if (descriptor.Enabled) + { + IWinSWExtension extension = CreateExtensionInstance(descriptor.Id, descriptor.ClassName); + extension.Descriptor = descriptor; + extension.Configure(ServiceDescriptor, configNode, logger); + Extensions.Add(id, extension); + logger.LogEvent("Extension loaded: "+id, EventLogEntryType.Information); + } + else + { + logger.LogEvent("Extension is disabled: " + id, EventLogEntryType.Warning); + } + + } + + private IWinSWExtension CreateExtensionInstance(string id, string className) + { + ActivationContext ac = AppDomain.CurrentDomain.ActivationContext; + Assembly assembly = Assembly.GetCallingAssembly(); + Object created; + + try + { + Type t = Type.GetType(className); + if (t == null) + { + throw new ExtensionException(id, "Class "+className+" does not exist"); + } + created = Activator.CreateInstance(t); + } + catch (Exception ex) + { + throw new ExtensionException(id, "Cannot load the class by name: "+className, ex); + } + + var extension = created as IWinSWExtension; + if (extension == null) + { + throw new ExtensionException(id, "The loaded class is not a WinSW extension: " + className + ". Type is " + created.GetType()); + } + return extension; + } + + #endregion + } +} diff --git a/src/Core/ServiceWrapper/LogAppenders.cs b/src/Core/WinSWCore/LogAppenders.cs similarity index 100% rename from src/Core/ServiceWrapper/LogAppenders.cs rename to src/Core/WinSWCore/LogAppenders.cs diff --git a/src/Core/ServiceWrapper/Advapi32.cs b/src/Core/WinSWCore/Native/Advapi32.cs similarity index 96% rename from src/Core/ServiceWrapper/Advapi32.cs rename to src/Core/WinSWCore/Native/Advapi32.cs index ae6ea7a..cc2ed32 100755 --- a/src/Core/ServiceWrapper/Advapi32.cs +++ b/src/Core/WinSWCore/Native/Advapi32.cs @@ -6,9 +6,9 @@ using System.Text; // ReSharper disable InconsistentNaming -namespace winsw +namespace winsw.Native { - class ServiceManager : IDisposable + public class ServiceManager : IDisposable { private IntPtr _handle; @@ -39,7 +39,7 @@ namespace winsw } } - class Service : IDisposable + public class Service : IDisposable { internal IntPtr Handle; @@ -87,7 +87,7 @@ namespace winsw } } - static class LogonAsAService + public static class LogonAsAService { public static void AddLogonAsAServiceRight(string username) { @@ -251,7 +251,7 @@ namespace winsw /// Advapi32.dll wrapper for performing additional service related operations that are not /// available in WMI. /// - internal class Advapi32 + public class Advapi32 { [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)] [return: MarshalAs(UnmanagedType.Bool)] @@ -272,7 +272,7 @@ namespace winsw internal static extern bool CloseServiceHandle(IntPtr hSCObject); [DllImport("advapi32.DLL")] - internal static extern bool SetServiceStatus(IntPtr hServiceStatus, ref SERVICE_STATUS lpServiceStatus); + public static extern bool SetServiceStatus(IntPtr hServiceStatus, ref SERVICE_STATUS lpServiceStatus); [DllImport("advapi32.dll", PreserveSig = true)] internal static extern UInt32 LsaOpenPolicy(ref LSA_UNICODE_STRING SystemName, ref LSA_OBJECT_ATTRIBUTES ObjectAttributes, Int32 DesiredAccess, diff --git a/src/Core/ServiceWrapper/Kernel32.cs b/src/Core/WinSWCore/Native/Kernel32.cs similarity index 79% rename from src/Core/ServiceWrapper/Kernel32.cs rename to src/Core/WinSWCore/Native/Kernel32.cs index 29b20ae..3d57a9a 100755 --- a/src/Core/ServiceWrapper/Kernel32.cs +++ b/src/Core/WinSWCore/Native/Kernel32.cs @@ -1,18 +1,18 @@ using System; using System.Runtime.InteropServices; -namespace winsw +namespace winsw.Native { /// /// kernel32.dll P/Invoke wrappers /// - internal class Kernel32 + public class Kernel32 { [DllImport("Kernel32.dll", SetLastError = true)] - internal static extern int SetStdHandle(int device, IntPtr handle); + public static extern int SetStdHandle(int device, IntPtr handle); [DllImport("kernel32.dll", SetLastError = true)] - internal static extern bool CreateProcess(string lpApplicationName, + public static extern bool CreateProcess(string lpApplicationName, string lpCommandLine, IntPtr lpProcessAttributes, IntPtr lpThreadAttributes, bool bInheritHandles, uint dwCreationFlags, IntPtr lpEnvironment, string lpCurrentDirectory, @@ -20,11 +20,11 @@ namespace winsw out PROCESS_INFORMATION lpProcessInformation); [DllImport("kernel32.dll")] - internal static extern int GetLastError(); + public static extern int GetLastError(); } [StructLayout(LayoutKind.Sequential)] - internal struct PROCESS_INFORMATION + public struct PROCESS_INFORMATION { public IntPtr hProcess; public IntPtr hThread; @@ -33,7 +33,7 @@ namespace winsw } [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] - struct STARTUPINFO + public struct STARTUPINFO { public Int32 cb; public string lpReserved; diff --git a/src/Core/ServiceWrapper/PeriodicRollingCalendar.cs b/src/Core/WinSWCore/PeriodicRollingCalendar.cs similarity index 100% rename from src/Core/ServiceWrapper/PeriodicRollingCalendar.cs rename to src/Core/WinSWCore/PeriodicRollingCalendar.cs diff --git a/src/Core/WinSWCore/Properties/AssemblyInfo.cs b/src/Core/WinSWCore/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..5cb6f2a --- /dev/null +++ b/src/Core/WinSWCore/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("WinSWCore")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("WinSWCore")] +[assembly: AssemblyCopyright("Copyright © 2015")] +[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("8f845354-ba20-455d-82d1-9b6ec4e0e517")] + +// 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/src/Core/ServiceWrapper/ServiceDescriptor.cs b/src/Core/WinSWCore/ServiceDescriptor.cs similarity index 92% rename from src/Core/ServiceWrapper/ServiceDescriptor.cs rename to src/Core/WinSWCore/ServiceDescriptor.cs index 1263087..8d770fe 100755 --- a/src/Core/ServiceWrapper/ServiceDescriptor.cs +++ b/src/Core/WinSWCore/ServiceDescriptor.cs @@ -5,6 +5,8 @@ using System.Diagnostics; using System.IO; using System.Reflection; using System.Xml; +using winsw.Native; +using winsw.Util; using WMI; namespace winsw @@ -227,9 +229,7 @@ namespace winsw } } - /// - /// Optional working directory. - /// + public string WorkingDirectory { get { var wd = SingleElement("workingdirectory", true); @@ -237,6 +237,37 @@ namespace winsw } } + public List ExtensionIds + { + get + { + List res = new List(); + + XmlNode argumentNode = ExtensionsConfiguration; + XmlNodeList extensions = argumentNode != null ? argumentNode.SelectNodes("extension") : null; + if ( extensions != null) + { + foreach (XmlNode e in extensions) + { + XmlElement extension = (XmlElement)e; + String extensionId = XmlHelper.SingleAttribute(extension, "id"); + res.Add(extensionId); + } + } + + return res; + } + } + + public XmlNode ExtensionsConfiguration + { + get + { + XmlNode argumentNode = dom.SelectSingleNode("//extensions"); + return argumentNode; + } + } + /// /// Combines the contents of all the elements of the given name, /// or return null if no element exists. Handles whitespace quotation. diff --git a/src/Core/WinSWCore/Util/IEventWriter.cs b/src/Core/WinSWCore/Util/IEventWriter.cs new file mode 100644 index 0000000..7f49dfa --- /dev/null +++ b/src/Core/WinSWCore/Util/IEventWriter.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Diagnostics; + +namespace winsw.Util +{ + public interface IEventWriter + { + void LogEvent(String message); + void LogEvent(String message, EventLogEntryType type); + } +} diff --git a/src/Core/WinSWCore/Util/XmlHelper.cs b/src/Core/WinSWCore/Util/XmlHelper.cs new file mode 100644 index 0000000..687a8d2 --- /dev/null +++ b/src/Core/WinSWCore/Util/XmlHelper.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Xml; +using System.IO; + +namespace winsw.Util +{ + public class XmlHelper + { + /// + /// Retrieves a single string element + /// + /// Parent node + /// Element name + /// If otional, don't throw an exception if the elemen is missing + /// String value or null + /// The required element is missing + public static string SingleElement(XmlNode node, string tagName, Boolean optional) + { + var n = node.SelectSingleNode(tagName); + if (n == null && !optional) throw new InvalidDataException("<" + tagName + "> is missing in configuration XML"); + return n == null ? null : Environment.ExpandEnvironmentVariables(n.InnerText); + } + + /// + /// Retrieves a single node + /// + /// Parent node + /// Element name + /// If otional, don't throw an exception if the elemen is missing + /// String value or null + /// The required element is missing + public static XmlNode SingleNode(XmlNode node, string tagName, Boolean optional) + { + var n = node.SelectSingleNode(tagName); + if (n == null && !optional) throw new InvalidDataException("<" + tagName + "> is missing in configuration XML"); + return n; + } + + /// + /// Retrieves a single mandatory attribute + /// + /// Parent node + /// Attribute name + /// Attribute value + /// The required attribute is missing + public static TAttributeType SingleAttribute (XmlElement node, string attributeName) + { + if (!node.HasAttribute(attributeName)) + { + throw new InvalidDataException("Attribute <" + attributeName + "> is missing in configuration XML"); + } + + return SingleAttribute(node, attributeName, default(TAttributeType)); + } + + /// + /// Retrieves a single optional attribute + /// + /// Parent node + /// Attribute name + /// Default value + /// Attribute value (or default) + public static TAttributeType SingleAttribute(XmlElement node, string attributeName, TAttributeType defaultValue) + { + if (!node.HasAttribute(attributeName)) return defaultValue; + + string rawValue = node.GetAttribute(attributeName); + string substitutedValue = Environment.ExpandEnvironmentVariables(rawValue); + var value = (TAttributeType)Convert.ChangeType(substitutedValue, typeof(TAttributeType)); + return value; + } + } +} diff --git a/src/Core/WinSWCore/WinSWCore.csproj b/src/Core/WinSWCore/WinSWCore.csproj new file mode 100644 index 0000000..6ea87bb --- /dev/null +++ b/src/Core/WinSWCore/WinSWCore.csproj @@ -0,0 +1,89 @@ + + + + + Debug + AnyCPU + {9D0C63E2-B6FF-4A85-BD36-B3E5D7F27D06} + Library + Properties + winsw + WinSWCore + v2.0 + 512 + ..\..\ + true + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + true + + + ..\..\..\winsw_cert.pfx + + + + ..\..\packages\log4net.2.0.3\lib\net20-full\log4net.dll + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + \ No newline at end of file diff --git a/src/Core/WinSWCore/WinSWException.cs b/src/Core/WinSWCore/WinSWException.cs new file mode 100644 index 0000000..870660e --- /dev/null +++ b/src/Core/WinSWCore/WinSWException.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace winsw +{ + public class WinSWException : Exception + { + public WinSWException(String message) + : base(message) + { } + + public WinSWException(String message, Exception innerException) + : base(message, innerException) + { } + } +} diff --git a/src/Core/ServiceWrapper/Wmi.cs b/src/Core/WinSWCore/Wmi.cs similarity index 100% rename from src/Core/ServiceWrapper/Wmi.cs rename to src/Core/WinSWCore/Wmi.cs diff --git a/src/Core/ServiceWrapper/WmiSchema.cs b/src/Core/WinSWCore/WmiSchema.cs similarity index 100% rename from src/Core/ServiceWrapper/WmiSchema.cs rename to src/Core/WinSWCore/WmiSchema.cs diff --git a/src/Core/WinSWCore/packages.config b/src/Core/WinSWCore/packages.config new file mode 100644 index 0000000..7a2c004 --- /dev/null +++ b/src/Core/WinSWCore/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/Plugins/SharedDirectoryMapper/Properties/AssemblyInfo.cs b/src/Plugins/SharedDirectoryMapper/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..5d8fa29 --- /dev/null +++ b/src/Plugins/SharedDirectoryMapper/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("SharedDirectoryMapper")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("SharedDirectoryMapper")] +[assembly: AssemblyCopyright("Copyright © 2015")] +[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("d962c792-b900-4e60-8ae6-6c8d05b23a61")] + +// 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/src/Plugins/SharedDirectoryMapper/SharedDirectoryMapper.cs b/src/Plugins/SharedDirectoryMapper/SharedDirectoryMapper.cs new file mode 100644 index 0000000..97b36a9 --- /dev/null +++ b/src/Plugins/SharedDirectoryMapper/SharedDirectoryMapper.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Generic; +using System.Xml; +using System.Diagnostics; +using winsw.Extensions; +using winsw.Util; + +namespace winsw.Plugins.SharedDirectoryMapper +{ + public class SharedDirectoryMapper : AbstractWinSWExtension + { + private readonly SharedDirectoryMappingHelper _mapper = new SharedDirectoryMappingHelper(); + private readonly List _entries = new List(); + + public override String DisplayName { get { return "Shared Directory Mapper"; } } + + public SharedDirectoryMapper() + { + } + + public SharedDirectoryMapper(bool enableMapping, string directoryUNC, string driveLabel) + { + SharedDirectoryMapperConfig config = new SharedDirectoryMapperConfig(enableMapping, driveLabel, directoryUNC); + _entries.Add(config); + } + + public override void Configure(ServiceDescriptor descriptor, XmlNode node, IEventWriter logger) + { + var nodes = XmlHelper.SingleNode(node, "mapping", false).SelectNodes("map"); + if (nodes != null) + { + foreach (XmlNode mapNode in nodes) + { + var mapElement = mapNode as XmlElement; + if (mapElement != null) + { + var config = SharedDirectoryMapperConfig.FromXml(mapElement); + _entries.Add(config); + } + } + } + } + + public override void OnStart(IEventWriter eventWriter) + { + foreach (SharedDirectoryMapperConfig config in _entries) + { + if (config.EnableMapping) + { + eventWriter.LogEvent(DisplayName + ": Mapping shared directory " + config.UNCPath + " to " + config.Label, EventLogEntryType.Information); + try + { + _mapper.MapDirectory(config.Label, config.UNCPath); + } + catch (MapperException ex) + { + HandleMappingError(config, eventWriter, ex); + } + } + else + { + eventWriter.LogEvent(DisplayName + ": Mapping of " + config.Label + " is disabled", EventLogEntryType.Warning); + } + } + } + + public override void OnStop(IEventWriter eventWriter) + { + foreach (SharedDirectoryMapperConfig config in _entries) + { + if (config.EnableMapping) + { + try + { + _mapper.UnmapDirectory(config.Label); + } + catch (MapperException ex) + { + HandleMappingError(config, eventWriter, ex); + } + } + } + } + + private void HandleMappingError(SharedDirectoryMapperConfig config, IEventWriter eventWriter, MapperException ex) { + String prefix = "Mapping of " + config.Label+ " "; + eventWriter.LogEvent(prefix + "STDOUT: " + ex.Process.StandardOutput.ReadToEnd(), EventLogEntryType.Information); + eventWriter.LogEvent(prefix + "STDERR: " + ex.Process.StandardError.ReadToEnd(), EventLogEntryType.Information); + + throw new ExtensionException(Descriptor.Id, DisplayName + ": " + prefix + "failed", ex); + } + } +} diff --git a/src/Plugins/SharedDirectoryMapper/SharedDirectoryMapper.csproj b/src/Plugins/SharedDirectoryMapper/SharedDirectoryMapper.csproj new file mode 100644 index 0000000..6552879 --- /dev/null +++ b/src/Plugins/SharedDirectoryMapper/SharedDirectoryMapper.csproj @@ -0,0 +1,57 @@ + + + + + Debug + AnyCPU + {CA5C71DB-C5A8-4C27-BF83-8E6DAED9D6B5} + Library + Properties + winsw.Plugins.SharedDirectoryMapper + SharedDirectoryMapper + v2.0 + 512 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + {9d0c63e2-b6ff-4a85-bd36-b3e5d7f27d06} + WinSWCore + + + + + \ No newline at end of file diff --git a/src/Plugins/SharedDirectoryMapper/SharedDirectoryMapperConfig.cs b/src/Plugins/SharedDirectoryMapper/SharedDirectoryMapperConfig.cs new file mode 100644 index 0000000..59276e8 --- /dev/null +++ b/src/Plugins/SharedDirectoryMapper/SharedDirectoryMapperConfig.cs @@ -0,0 +1,31 @@ +using System; +using System.Xml; +using winsw.Util; + +namespace winsw.Plugins.SharedDirectoryMapper +{ + /// + /// Stores configuration entries for SharedDirectoryMapper extension. + /// + public class SharedDirectoryMapperConfig + { + public bool EnableMapping { get; set; } + public String Label { get; set; } + public String UNCPath { get; set; } + + public SharedDirectoryMapperConfig(bool enableMapping, string label, string uncPath) + { + EnableMapping = enableMapping; + Label = label; + UNCPath = uncPath; + } + + public static SharedDirectoryMapperConfig FromXml(XmlElement node) + { + bool enableMapping = XmlHelper.SingleAttribute(node, "enabled", true); + string label = XmlHelper.SingleAttribute(node, "label"); + string uncPath = XmlHelper.SingleAttribute(node, "uncpath"); + return new SharedDirectoryMapperConfig(enableMapping, label, uncPath); + } + } +} diff --git a/src/Plugins/SharedDirectoryMapper/SharedDirectoryMapperHelper.cs b/src/Plugins/SharedDirectoryMapper/SharedDirectoryMapperHelper.cs new file mode 100644 index 0000000..8016a93 --- /dev/null +++ b/src/Plugins/SharedDirectoryMapper/SharedDirectoryMapperHelper.cs @@ -0,0 +1,73 @@ +using System; +using System.Diagnostics; +using winsw.Util; + +namespace winsw.Plugins.SharedDirectoryMapper +{ + class SharedDirectoryMappingHelper + { + /// + /// Invokes a system command + /// + /// + /// Command to be executed + /// Command arguments + /// Operation failure + private void InvokeCommand(String command, String args) + { + Process p = new Process + { + StartInfo = + { + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardError = true, + RedirectStandardOutput = true, + FileName = command, + Arguments = args + } + }; + + p.Start(); + p.WaitForExit(); + if (p.ExitCode != 0) + { + throw new MapperException(p, command, args); + } + } + + /// + /// Maps the remote directory + /// + /// Disk label + /// UNC path to the directory + /// Operation failure + public void MapDirectory(String label, String uncPath) + { + InvokeCommand("net.exe", " use " + label + " " + uncPath); + } + + /// + /// Unmaps the label + /// + /// Disk label + /// Operation failure + public void UnmapDirectory(String label) + { + InvokeCommand("net.exe", " use /DELETE /YES " + label); + } + } + + class MapperException : WinSWException + { + public String Call { get; private set; } + public Process Process { get; private set; } + + public MapperException(Process process, string command, string args) + : base("Command " + command + " " + args + " failed with code " + process.ExitCode) + { + Call = command + " " + args; + Process = process; + } + } +} diff --git a/src/Plugins/SharedDirectoryMapper/sampleConfig.xml b/src/Plugins/SharedDirectoryMapper/sampleConfig.xml new file mode 100644 index 0000000..8cf5907 --- /dev/null +++ b/src/Plugins/SharedDirectoryMapper/sampleConfig.xml @@ -0,0 +1,18 @@ + + + SERVICE_NAME + Jenkins Slave + This service runs a slave for Jenkins continuous integration system. + C:\Program Files\Java\jre7\bin\java.exe + -Xrs -jar "%BASE%\slave.jar" -jnlpUrl ... + rotate + + + + + + + + + + \ No newline at end of file diff --git a/src/Test/winswTests/Extensions/WinSWExtensionManagerTest.cs b/src/Test/winswTests/Extensions/WinSWExtensionManagerTest.cs new file mode 100644 index 0000000..5ccd638 --- /dev/null +++ b/src/Test/winswTests/Extensions/WinSWExtensionManagerTest.cs @@ -0,0 +1,62 @@ +using winsw; +using NUnit.Framework; +using winsw.Extensions; +using winsw.Plugins.SharedDirectoryMapper; +using winswTests.util; + +namespace winswTests.extensions +{ + [TestFixture] + class WinSWExtensionManagerTest + { + ServiceDescriptor _testServiceDescriptor; + readonly TestLogger _logger = new TestLogger(); + + [SetUp] + public void SetUp() + { + string testExtension = typeof (SharedDirectoryMapper).ToString(); + string seedXml = "" + + " " + + " SERVICE_NAME " + + " Jenkins Slave " + + " This service runs a slave for Jenkins continuous integration system. " + + " C:\\Program Files\\Java\\jre7\\bin\\java.exe " + + " -Xrs -jar \\\"%BASE%\\slave.jar\\\" -jnlpUrl ... " + + " rotate " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + ""; + _testServiceDescriptor = ServiceDescriptor.FromXML(seedXml); + } + + [Test] + public void LoadExtensions() + { + WinSWExtensionManager manager = new WinSWExtensionManager(_testServiceDescriptor); + manager.LoadExtensions(_logger); + Assert.AreEqual(2, manager.Extensions.Count, "Two extensions should be loaded"); + } + + [Test] + public void StartStopExtension() + { + WinSWExtensionManager manager = new WinSWExtensionManager(_testServiceDescriptor); + manager.LoadExtensions(_logger); + manager.OnStart(_logger); + manager.OnStop(_logger); + } + } +} diff --git a/src/Test/winswTests/NunitTest.nunit b/src/Test/winswTests/NunitTest.nunit new file mode 100644 index 0000000..3eaf587 --- /dev/null +++ b/src/Test/winswTests/NunitTest.nunit @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/src/Test/winswTests/Util/TestLogger.cs b/src/Test/winswTests/Util/TestLogger.cs new file mode 100644 index 0000000..d73ffab --- /dev/null +++ b/src/Test/winswTests/Util/TestLogger.cs @@ -0,0 +1,19 @@ +using System; +using System.Diagnostics; +using winsw.Util; + +namespace winswTests.util +{ + class TestLogger : IEventWriter + { + public void LogEvent(String message) + { + Console.WriteLine(message); + } + + public void LogEvent(String message, EventLogEntryType type) + { + Console.WriteLine("[" + type + "]" + message); + } + } +} diff --git a/src/Test/winswTests/winswTests.csproj b/src/Test/winswTests/winswTests.csproj index 83bddf7..4818a3c 100644 --- a/src/Test/winswTests/winswTests.csproj +++ b/src/Test/winswTests/winswTests.csproj @@ -52,9 +52,11 @@ + + @@ -62,10 +64,19 @@ {0DE77F55-ADE5-43C1-999A-0BC81153B039} winsw + + {9d0c63e2-b6ff-4a85-bd36-b3e5d7f27d06} + WinSWCore + + + {ca5c71db-c5a8-4c27-bf83-8e6daed9d6b5} + SharedDirectoryMapper + + diff --git a/src/packages/repositories.config b/src/packages/repositories.config index 486e754..d72f1c5 100644 --- a/src/packages/repositories.config +++ b/src/packages/repositories.config @@ -1,5 +1,6 @@  + \ No newline at end of file diff --git a/src/winsw.sln b/src/winsw.sln index 15d09bd..95098b0 100644 --- a/src/winsw.sln +++ b/src/winsw.sln @@ -1,4 +1,3 @@ - Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 2013 VisualStudioVersion = 12.0.31101.0 @@ -11,25 +10,91 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{6BDF40 ProjectSection(SolutionItems) = preProject .nuget\NuGet.Config = .nuget\NuGet.Config .nuget\NuGet.exe = .nuget\NuGet.exe - .nuget\NuGet.targets = .nuget\NuGet.targets + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharedDirectoryMapper", "Plugins\SharedDirectoryMapper\SharedDirectoryMapper.csproj", "{CA5C71DB-C5A8-4C27-BF83-8E6DAED9D6B5}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{077C2CEC-B687-4B53-86E9-C1A1BF5554E5}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Plugins", "Plugins", "{BC4AD891-E87E-4F30-867C-FD8084A29E5D}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Core", "Core", "{5297623A-1A95-4F89-9AAE-DA634081EC86}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WinSWCore", "Core\WinSWCore\WinSWCore.csproj", "{9D0C63E2-B6FF-4A85-BD36-B3E5D7F27D06}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".build", ".build", "{D8806424-4640-440C-952D-37790B603C27}" + ProjectSection(SolutionItems) = preProject + Build.proj = Build.proj + .build\MSBuild.Community.Tasks.dll = .build\MSBuild.Community.Tasks.dll + .build\MSBuild.Community.Tasks.targets = .build\MSBuild.Community.Tasks.targets EndProjectSection EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|Mixed Platforms = Debug|Mixed Platforms Debug|Win32 = Debug|Win32 + Release|Any CPU = Release|Any CPU + Release|Mixed Platforms = Release|Mixed Platforms 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|Mixed Platforms.ActiveCfg = Release|Any CPU + {0DE77F55-ADE5-43C1-999A-0BC81153B039}.Debug|Mixed Platforms.Build.0 = Release|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|Mixed Platforms.ActiveCfg = Release|Any CPU + {0DE77F55-ADE5-43C1-999A-0BC81153B039}.Release|Mixed Platforms.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|Mixed Platforms.ActiveCfg = Debug|Any CPU + {93843402-842B-44B4-B303-AEE829BE0B43}.Debug|Mixed Platforms.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|Mixed Platforms.ActiveCfg = Release|Any CPU + {93843402-842B-44B4-B303-AEE829BE0B43}.Release|Mixed Platforms.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|Mixed Platforms.ActiveCfg = Debug|Any CPU + {CA5C71DB-C5A8-4C27-BF83-8E6DAED9D6B5}.Debug|Mixed Platforms.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|Mixed Platforms.ActiveCfg = Release|Any CPU + {CA5C71DB-C5A8-4C27-BF83-8E6DAED9D6B5}.Release|Mixed Platforms.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|Mixed Platforms.ActiveCfg = Debug|Any CPU + {9D0C63E2-B6FF-4A85-BD36-B3E5D7F27D06}.Debug|Mixed Platforms.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|Mixed Platforms.ActiveCfg = Release|Any CPU + {9D0C63E2-B6FF-4A85-BD36-B3E5D7F27D06}.Release|Mixed Platforms.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 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} + EndGlobalSection EndGlobal diff --git a/src/winsw.sln.DotSettings b/src/winsw.sln.DotSettings new file mode 100644 index 0000000..45d451a --- /dev/null +++ b/src/winsw.sln.DotSettings @@ -0,0 +1,3 @@ + + SW + UNC \ No newline at end of file