Merge shared directory mapping into core

pull/772/head
NextTurn 2020-09-13 00:00:00 +08:00 committed by Next Turn
parent 4b00bba644
commit 3c5d67e287
21 changed files with 201 additions and 277 deletions

View File

@ -77,13 +77,15 @@ namespace WinSW.Configuration
public virtual string ErrFilePattern => ".err.log";
// Environment
public virtual List<Download> Downloads => new List<Download>(0);
public virtual List<Download> Downloads => new(0);
public virtual Dictionary<string, string> EnvironmentVariables => new Dictionary<string, string>(0);
public virtual Dictionary<string, string> EnvironmentVariables => new(0);
// Misc
public virtual bool BeepOnShutdown => false;
public virtual List<SharedDirectoryMapperConfig> SharedDirectories => new(0);
// Extensions
public virtual XmlNode? ExtensionsConfiguration => null;
}

View File

@ -18,7 +18,7 @@ namespace WinSW
/// </summary>
public class XmlServiceConfig : ServiceConfig
{
protected readonly XmlDocument dom = new XmlDocument();
protected readonly XmlDocument dom = new();
private readonly XmlNode root;
private readonly Dictionary<string, string> environmentVariables;
@ -145,7 +145,7 @@ namespace WinSW
return value is null ? defaultValue : ParseTimeSpan(value);
}
private static readonly Dictionary<string, long> Suffix = new Dictionary<string, long>
private static readonly Dictionary<string, long> Suffix = new()
{
{ "ms", 1 },
{ "sec", 1000L },
@ -581,6 +581,31 @@ namespace WinSW
public bool AutoRefresh => this.SingleBoolElementOrDefault("autoRefresh", true);
public override List<SharedDirectoryMapperConfig> SharedDirectories
{
get
{
var mapNodes = this.root.SelectSingleNode("sharedDirectoryMapping")?.SelectNodes("map");
if (mapNodes is null)
{
return new();
}
var result = new List<SharedDirectoryMapperConfig>(mapNodes.Count);
for (int i = 0; i < mapNodes.Count; i++)
{
if (mapNodes[i] is XmlElement mapElement)
{
string label = XmlHelper.SingleAttribute<string>(mapElement, "label");
string uncPath = XmlHelper.SingleAttribute<string>(mapElement, "uncpath");
result.Add(new(label, uncPath));
}
}
return result;
}
}
private Dictionary<string, string> LoadEnvironmentVariables()
{
var nodeList = this.root.SelectNodes("env")!;

View File

@ -419,7 +419,7 @@ namespace WinSW
private async Task CopyStreamWithRotationAsync(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
object? fileLock = new object();
object? fileLock = new();
string? baseDirectory = Path.GetDirectoryName(this.BaseLogFileName)!;
string? baseFileName = Path.GetFileName(this.BaseLogFileName);

View File

@ -5,6 +5,7 @@
internal const string Advapi32 = "advapi32.dll";
internal const string CredUI = "credui.dll";
internal const string Kernel32 = "kernel32.dll";
internal const string Mpr = "mpr.dll";
internal const string NtDll = "ntdll.dll";
}
}

View File

@ -0,0 +1,31 @@
#pragma warning disable SA1310 // Field names should not contain underscore
using System.Runtime.InteropServices;
using static WinSW.Native.Libraries;
namespace WinSW.Native
{
internal static class NetworkApis
{
internal const uint RESOURCETYPE_DISK = 0x00000001;
[DllImport(Mpr, SetLastError = true, CharSet = CharSet.Unicode)]
internal static extern int WNetAddConnection2W(in NETRESOURCEW netResource, string? password = null, string? userName = null, uint flags = 0);
[DllImport(Mpr, SetLastError = true, CharSet = CharSet.Unicode)]
internal static extern int WNetCancelConnection2W(string name, uint flags = 0, bool force = false);
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
internal struct NETRESOURCEW
{
public uint Scope;
public uint Type;
public uint DisplayType;
public uint Usage;
public string LocalName;
public string RemoteName;
public string Comment;
public string Provider;
}
}
}

View File

@ -7,9 +7,9 @@ namespace WinSW.Native
{
internal static class ResourceApis
{
internal static readonly IntPtr VS_VERSION_INFO = new IntPtr(1);
internal static readonly IntPtr VS_VERSION_INFO = new(1);
internal static readonly IntPtr RT_VERSION = new IntPtr(16);
internal static readonly IntPtr RT_VERSION = new(16);
[DllImport(Libraries.Kernel32, SetLastError = true, CharSet = CharSet.Unicode)]
internal static extern IntPtr BeginUpdateResourceW(string fileName, bool deleteExistingResources);

View File

@ -0,0 +1,50 @@
using System.Collections.Generic;
using WinSW.Native;
using static WinSW.Native.NetworkApis;
namespace WinSW
{
public sealed class SharedDirectoryMapper
{
private readonly List<SharedDirectoryMapperConfig> entries;
public SharedDirectoryMapper(List<SharedDirectoryMapperConfig> entries)
{
this.entries = entries;
}
public void Map()
{
foreach (var config in this.entries)
{
string label = config.Label;
string uncPath = config.UncPath;
int error = WNetAddConnection2W(new()
{
Type = RESOURCETYPE_DISK,
LocalName = label,
RemoteName = uncPath,
});
if (error != 0)
{
Throw.Command.Win32Exception(error, $"Failed to map {label}.");
}
}
}
public void Unmap()
{
foreach (var config in this.entries)
{
string label = config.Label;
int error = WNetCancelConnection2W(label);
if (error != 0)
{
Throw.Command.Win32Exception(error, $"Failed to unmap {label}.");
}
}
}
}
}

View File

@ -0,0 +1,15 @@
namespace WinSW
{
public sealed class SharedDirectoryMapperConfig
{
public string Label { get; }
public string UncPath { get; }
public SharedDirectoryMapperConfig(string driveLabel, string directoryUncPath)
{
this.Label = driveLabel;
this.UncPath = directoryUncPath;
}
}
}

View File

@ -17,9 +17,9 @@ namespace WinSW
{
public sealed class WrapperService : ServiceBase, IEventLogger, IServiceEventLog
{
internal static readonly WrapperServiceEventLogProvider eventLogProvider = new WrapperServiceEventLogProvider();
internal static readonly WrapperServiceEventLogProvider EventLogProvider = new();
private static readonly int additionalStopTimeout = 1_000;
private static readonly int AdditionalStopTimeout = 1_000;
private static readonly ILog Log = LogManager.GetLogger(typeof(WrapperService));
@ -31,6 +31,8 @@ namespace WinSW
internal WinSWExtensionManager ExtensionManager { get; }
private SharedDirectoryMapper? sharedDirectoryMapper;
private bool shuttingdown;
/// <summary>
@ -51,7 +53,7 @@ namespace WinSW
this.ExtensionManager = new WinSWExtensionManager(config);
// Register the event log provider
eventLogProvider.Service = this;
EventLogProvider.Service = this;
if (config.Preshutdown)
{
@ -297,6 +299,13 @@ namespace WinSW
throw new AggregateException(exceptions);
}
var sharedDirectories = this.config.SharedDirectories;
if (sharedDirectories.Count > 0)
{
this.sharedDirectoryMapper = new(sharedDirectories);
this.sharedDirectoryMapper.Map();
}
var prestart = this.config.Prestart;
string? prestartExecutable = prestart.Executable;
if (prestartExecutable != null)
@ -306,7 +315,7 @@ namespace WinSW
using var process = this.StartProcess(prestartExecutable, prestart.Arguments, prestart.CreateLogHandler());
this.WaitForProcessToExit(process);
this.LogExited($"Pre-start process '{process.Format()}' exited with code {process.ExitCode}.", process.ExitCode);
process.StopDescendants(additionalStopTimeout);
process.StopDescendants(AdditionalStopTimeout);
}
catch (Exception e)
{
@ -335,7 +344,7 @@ namespace WinSW
using var process = StartProcessLocked();
this.WaitForProcessToExit(process);
this.LogExited($"Post-start process '{process.Format()}' exited with code {process.ExitCode}.", process.ExitCode);
process.StopDescendants(additionalStopTimeout);
process.StopDescendants(AdditionalStopTimeout);
this.startingProcess = null;
Process StartProcessLocked()
@ -367,7 +376,7 @@ namespace WinSW
using var process = StartProcessLocked(prestopExecutable, prestop.Arguments, prestop.CreateLogHandler());
this.WaitForProcessToExit(process);
this.LogExited($"Pre-stop process '{process.Format()}' exited with code {process.ExitCode}.", process.ExitCode);
process.StopDescendants(additionalStopTimeout);
process.StopDescendants(AdditionalStopTimeout);
this.stoppingProcess = null;
}
catch (Exception e)
@ -408,7 +417,7 @@ namespace WinSW
Log.Debug("WaitForProcessToExit " + this.process.Id + "+" + stopProcess.Id);
this.WaitForProcessToExit(stopProcess);
stopProcess.StopDescendants(additionalStopTimeout);
stopProcess.StopDescendants(AdditionalStopTimeout);
this.stoppingProcess = null;
this.WaitForProcessToExit(this.process);
@ -429,8 +438,8 @@ namespace WinSW
{
using var process = StartProcessLocked(poststopExecutable, poststop.Arguments, poststop.CreateLogHandler());
this.WaitForProcessToExit(process);
this.LogExited($"Post-Stop process '{process.Format()}' exited with code {process.ExitCode}.", process.ExitCode);
process.StopDescendants(additionalStopTimeout);
this.LogExited($"Post-stop process '{process.Format()}' exited with code {process.ExitCode}.", process.ExitCode);
process.StopDescendants(AdditionalStopTimeout);
this.stoppingProcess = null;
}
catch (Exception e)
@ -439,6 +448,15 @@ namespace WinSW
}
}
try
{
this.sharedDirectoryMapper?.Unmap();
}
catch (Exception e)
{
Log.Error(e);
}
// Stop extensions
this.ExtensionManager.FireBeforeWrapperStopped();
@ -513,8 +531,8 @@ namespace WinSW
process.StopDescendants(this.config.StopTimeoutInMs);
this.startingProcess?.StopTree(additionalStopTimeout);
this.stoppingProcess?.StopTree(additionalStopTimeout);
this.startingProcess?.StopTree(AdditionalStopTimeout);
this.stoppingProcess?.StopTree(AdditionalStopTimeout);
// if we finished orderly, report that to SCM.
// by not reporting unclean shutdown, we let Windows SCM to decide if it wants to

View File

@ -1,121 +0,0 @@
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.SharedDirectoryMapper.Native;
namespace WinSW.Plugins.SharedDirectoryMapper
{
public class SharedDirectoryMapper : AbstractWinSWExtension
{
private readonly List<SharedDirectoryMapperConfig> entries = new List<SharedDirectoryMapperConfig>();
public override string DisplayName => "Shared Directory Mapper";
private static readonly ILog Logger = LogManager.GetLogger(typeof(SharedDirectoryMapper));
public SharedDirectoryMapper()
{
}
public SharedDirectoryMapper(bool enableMapping, string directoryUNC, string driveLabel)
{
var config = new SharedDirectoryMapperConfig(enableMapping, driveLabel, directoryUNC);
this.entries.Add(config);
}
public override void Configure(XmlServiceConfig config, XmlNode node)
{
var mapNodes = XmlHelper.SingleNode(node, "mapping", false)!.SelectNodes("map");
if (mapNodes != null)
{
for (int i = 0; i < mapNodes.Count; i++)
{
if (mapNodes[i] is XmlElement mapElement)
{
this.entries.Add(SharedDirectoryMapperConfig.FromXml(mapElement));
}
}
}
}
public override void OnWrapperStarted()
{
foreach (var config in this.entries)
{
string label = config.Label;
string uncPath = config.UNCPath;
if (config.EnableMapping)
{
Logger.Info(this.DisplayName + ": Mapping shared directory " + uncPath + " to " + label);
int error = WNetAddConnection2(new NETRESOURCE
{
Type = RESOURCETYPE_DISK,
LocalName = label,
RemoteName = uncPath,
});
if (error != 0)
{
this.ThrowExtensionException(error, $"Mapping of {label} failed.");
}
}
else
{
Logger.Warn(this.DisplayName + ": Mapping of " + label + " is disabled");
}
}
}
public override void BeforeWrapperStopped()
{
foreach (var config in this.entries)
{
string label = config.Label;
if (config.EnableMapping)
{
int error = WNetCancelConnection2(label);
if (error != 0)
{
this.ThrowExtensionException(error, $"Unmapping of {label} failed.");
}
}
}
}
private void ThrowExtensionException(int error, string message)
{
var 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;
}
}
}
}

View File

@ -1,32 +0,0 @@
using System.Xml;
using WinSW.Util;
namespace WinSW.Plugins.SharedDirectoryMapper
{
/// <summary>
/// Stores configuration entries for SharedDirectoryMapper extension.
/// </summary>
public class SharedDirectoryMapperConfig
{
public bool EnableMapping { get; }
public string Label { get; }
public string UNCPath { get; }
public SharedDirectoryMapperConfig(bool enableMapping, string label, string uncPath)
{
this.EnableMapping = enableMapping;
this.Label = label;
this.UNCPath = uncPath;
}
public static SharedDirectoryMapperConfig FromXml(XmlElement node)
{
bool enableMapping = XmlHelper.SingleAttribute(node, "enabled", true);
string label = XmlHelper.SingleAttribute<string>(node, "label");
string uncPath = XmlHelper.SingleAttribute<string>(node, "uncpath");
return new SharedDirectoryMapperConfig(enableMapping, label, uncPath);
}
}
}

View File

@ -11,7 +11,7 @@ namespace WinSW.Tests
{
public class DownloadTests : IDisposable
{
private readonly HttpListener globalListener = new HttpListener();
private readonly HttpListener globalListener = new();
private readonly byte[] contents = { 0x57, 0x69, 0x6e, 0x53, 0x57 };

View File

@ -1,21 +0,0 @@
using System;
namespace WinSW.Tests.Extensions
{
/// <summary>
/// Base class for testing of WinSW Extensions.
/// </summary>
public class ExtensionTestBase
{
/// <summary>
/// Defines the name of the extension to be passed in the configuration.
/// This name should point to assembly in tests, because we do not merge extension DLLs for testing purposes.
/// </summary>
/// <param name="type">Type of the extension</param>
/// <returns>String for Type locator, which includes class and assembly names</returns>
public static string GetExtensionClassNameWithAssembly(Type type)
{
return type.ToString() + ", " + type.Assembly;
}
}
}

View File

@ -1,58 +0,0 @@
using WinSW.Extensions;
using WinSW.Plugins.SharedDirectoryMapper;
using Xunit;
namespace WinSW.Tests.Extensions
{
public class SharedDirectoryMapperConfigTest : ExtensionTestBase
{
private readonly XmlServiceConfig serviceConfig;
private readonly string testExtension = GetExtensionClassNameWithAssembly(typeof(SharedDirectoryMapper));
public SharedDirectoryMapperConfigTest()
{
string seedXml =
$@"<service>
<id>SERVICE_NAME</id>
<name>Jenkins Slave</name>
<description>This service runs a slave for Jenkins continuous integration system.</description>
<executable>C:\Program Files\Java\jre7\bin\java.exe</executable>
<arguments>-Xrs -jar \""%BASE%\slave.jar\"" -jnlpUrl ...</arguments>
<log mode=""roll""></log>
<extensions>
<extension enabled=""true"" className=""{this.testExtension}"" id=""mapNetworDirs"">
<mapping>
<map enabled=""false"" label=""N:"" uncpath=""\\UNC""/>
<map enabled=""false"" label=""M:"" uncpath=""\\UNC2""/>
</mapping>
</extension>
<extension enabled=""true"" className=""{this.testExtension}"" id=""mapNetworDirs2"">
<mapping>
<map enabled=""false"" label=""X:"" uncpath=""\\UNC""/>
<map enabled=""false"" label=""Y:"" uncpath=""\\UNC2""/>
</mapping>
</extension>
</extensions>
</service>";
this.serviceConfig = XmlServiceConfig.FromXml(seedXml);
}
[Fact]
public void LoadExtensions()
{
var manager = new WinSWExtensionManager(this.serviceConfig);
manager.LoadExtensions();
Assert.Equal(2, manager.Extensions.Count);
}
[Fact]
public void StartStopExtension()
{
var manager = new WinSWExtensionManager(this.serviceConfig);
manager.LoadExtensions();
manager.FireOnWrapperStarted();
manager.FireBeforeWrapperStopped();
}
}
}

View File

@ -420,5 +420,26 @@ $@"<service>
Assert.Equal(expected.StderrPath, actual.StderrPath);
}
}
[Fact]
public void SharedDirectoryMapping()
{
string seedXml =
@"<service>
<sharedDirectoryMapping>
<map label=""N:"" uncpath=""\\UNC"" />
<map label=""M:"" uncpath=""\\UNC2"" />
</sharedDirectoryMapping>
</service>";
var config = XmlServiceConfig.FromXml(seedXml);
var sharedDirectories = config.SharedDirectories;
Assert.Equal(2, sharedDirectories.Count);
Assert.Equal("N:", sharedDirectories[0].Label);
Assert.Equal(@"\\UNC", sharedDirectories[0].UncPath);
Assert.Equal("M:", sharedDirectories[1].Label);
Assert.Equal(@"\\UNC2", sharedDirectories[1].UncPath);
}
}
}

View File

@ -3,52 +3,45 @@ using System;
using System.IO;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using WinSW.Plugins.SharedDirectoryMapper;
using Xunit;
namespace WinSW.Tests.Extensions
{
// TODO: Assert.Throws<ExtensionException>
public class SharedDirectoryMapperTests
{
private static SharedDirectoryMapper CreateMapper(string driveLabel, string directoryUncPath)
{
return new(new(1)
{
new(driveLabel, directoryUncPath),
});
}
[ElevatedFact]
public void TestMap()
{
using var data = TestData.Create();
const string label = "W:";
var mapper = new SharedDirectoryMapper(true, $@"\\{Environment.MachineName}\{data.name}", label);
var mapper = CreateMapper(label, $@"\\{Environment.MachineName}\{data.name}");
mapper.OnWrapperStarted();
mapper.Map();
Assert.True(Directory.Exists($@"{label}\"));
mapper.BeforeWrapperStopped();
mapper.Unmap();
Assert.False(Directory.Exists($@"{label}\"));
}
[ElevatedFact]
public void TestDisableMapping()
{
using var data = TestData.Create();
const string label = "W:";
var mapper = new SharedDirectoryMapper(enableMapping: false, $@"\\{Environment.MachineName}\{data.name}", label);
mapper.OnWrapperStarted();
Assert.False(Directory.Exists($@"{label}\"));
mapper.BeforeWrapperStopped();
}
[ElevatedFact]
public void TestMap_PathEndsWithSlash_Throws()
{
using var data = TestData.Create();
const string label = "W:";
var mapper = new SharedDirectoryMapper(true, $@"\\{Environment.MachineName}\{data.name}\", label);
var mapper = CreateMapper(label, $@"\\{Environment.MachineName}\{data.name}\");
_ = Assert.ThrowsAny<Exception>(() => mapper.OnWrapperStarted());
_ = Assert.ThrowsAny<Exception>(() => mapper.Map());
Assert.False(Directory.Exists($@"{label}\"));
_ = Assert.ThrowsAny<Exception>(() => mapper.BeforeWrapperStopped());
_ = Assert.ThrowsAny<Exception>(() => mapper.Unmap());
}
[ElevatedFact]
@ -57,11 +50,11 @@ namespace WinSW.Tests.Extensions
using var data = TestData.Create();
const string label = "W";
var mapper = new SharedDirectoryMapper(true, $@"\\{Environment.MachineName}\{data.name}", label);
var mapper = CreateMapper(label, $@"\\{Environment.MachineName}\{data.name}");
_ = Assert.ThrowsAny<Exception>(() => mapper.OnWrapperStarted());
_ = Assert.ThrowsAny<Exception>(() => mapper.Map());
Assert.False(Directory.Exists($@"{label}\"));
_ = Assert.ThrowsAny<Exception>(() => mapper.BeforeWrapperStopped());
_ = Assert.ThrowsAny<Exception>(() => mapper.Unmap());
}
private readonly ref struct TestData

View File

@ -23,7 +23,7 @@ namespace WinSW.Tests.Util
public List<string> ExtensionXmls { get; } = new List<string>();
private readonly List<string> configEntries = new List<string>();
private readonly List<string> configEntries = new();
private readonly ITestOutputHelper output;

View File

@ -1099,7 +1099,7 @@ namespace WinSW
}
else
{
var eventLogAppender = new ServiceEventLogAppender(WrapperService.eventLogProvider)
var eventLogAppender = new ServiceEventLogAppender(WrapperService.EventLogProvider)
{
Name = "Wrapper event log",
Threshold = eventLogLevel,