From c49d50381ef445fb19a36ec1d9405ffc6e4a3d32 Mon Sep 17 00:00:00 2001 From: Next Turn <45985406+nxtn@users.noreply.github.com> Date: Wed, 30 Dec 2020 17:14:40 +0800 Subject: [PATCH] Rework Shared Directory Mapper (#765) --- src/WinSW.Plugins/SharedDirectoryMapper.cs | 88 +++++++---- .../SharedDirectoryMapperConfig.cs | 10 +- .../SharedDirectoryMapperHelper.cs | 71 --------- ....cs => SharedDirectoryMapperConfigTest.cs} | 2 +- .../Extensions/SharedDirectoryMapperTests.cs | 148 ++++++++++++++++++ 5 files changed, 212 insertions(+), 107 deletions(-) delete mode 100644 src/WinSW.Plugins/SharedDirectoryMapperHelper.cs rename src/WinSW.Tests/Extensions/{SharedDirectoryMapperTest.cs => SharedDirectoryMapperConfigTest.cs} (98%) create mode 100644 src/WinSW.Tests/Extensions/SharedDirectoryMapperTests.cs diff --git a/src/WinSW.Plugins/SharedDirectoryMapper.cs b/src/WinSW.Plugins/SharedDirectoryMapper.cs index 045f6ae..02f1f0e 100644 --- a/src/WinSW.Plugins/SharedDirectoryMapper.cs +++ b/src/WinSW.Plugins/SharedDirectoryMapper.cs @@ -1,17 +1,19 @@ using System.Collections.Generic; +using System.ComponentModel; using System.IO; +using System.Runtime.InteropServices; using System.Xml; using log4net; using WinSW.Configuration; using WinSW.Extensions; using WinSW.Util; +using static WinSW.Plugins.SharedDirectoryMapper.Native; namespace WinSW.Plugins { public class SharedDirectoryMapper : AbstractWinSWExtension { - private readonly SharedDirectoryMappingHelper _mapper = new(); - private readonly List _entries = new(); + private readonly List entries = new(); public override string DisplayName => "Shared Directory Mapper"; @@ -24,28 +26,27 @@ namespace WinSW.Plugins public SharedDirectoryMapper(bool enableMapping, string directoryUNC, string driveLabel) { var config = new SharedDirectoryMapperConfig(enableMapping, driveLabel, directoryUNC); - this._entries.Add(config); + this.entries.Add(config); } - public override void Configure(IServiceConfig descriptor, XmlNode node) + public override void Configure(IServiceConfig service, XmlNode extension) { - var mapNodes = XmlHelper.SingleNode(node, "mapping", false)!.SelectNodes("map"); + var mapNodes = XmlHelper.SingleNode(extension, "mapping", false)!.SelectNodes("map"); if (mapNodes != null) { for (int i = 0; i < mapNodes.Count; i++) { if (mapNodes[i] is XmlElement mapElement) { - var config = SharedDirectoryMapperConfig.FromXml(mapElement); - this._entries.Add(config); + this.entries.Add(SharedDirectoryMapperConfig.FromXml(mapElement)); } } } } - public override void Configure(IServiceConfig descriptor, YamlExtensionConfig config) + public override void Configure(IServiceConfig service, YamlExtensionConfig extension) { - var dict = config.GetSettings(); + var dict = extension.GetSettings(); object mappingNode = dict["mapping"]; @@ -56,57 +57,84 @@ namespace WinSW.Plugins foreach (object map in mappings) { - var mapConfig = SharedDirectoryMapperConfig.FromYaml(map); - this._entries.Add(mapConfig); + this.entries.Add(SharedDirectoryMapperConfig.FromYaml(map)); } } public override void OnWrapperStarted() { - foreach (var config in this._entries) + foreach (var config in this.entries) { + string label = config.Label; + string uncPath = config.UNCPath; if (config.EnableMapping) { - Logger.Info(this.DisplayName + ": Mapping shared directory " + config.UNCPath + " to " + config.Label); - try + Logger.Info(this.DisplayName + ": Mapping shared directory " + uncPath + " to " + label); + + int error = WNetAddConnection2(new() { - this._mapper.MapDirectory(config.Label, config.UNCPath); - } - catch (MapperException ex) + Type = RESOURCETYPE_DISK, + LocalName = label, + RemoteName = uncPath, + }); + if (error != 0) { - this.HandleMappingError(config, ex); + this.ThrowExtensionException(error, $"Mapping of {label} failed."); } } else { - Logger.Warn(this.DisplayName + ": Mapping of " + config.Label + " is disabled"); + Logger.Warn(this.DisplayName + ": Mapping of " + label + " is disabled"); } } } public override void BeforeWrapperStopped() { - foreach (var config in this._entries) + foreach (var config in this.entries) { + string label = config.Label; if (config.EnableMapping) { - try + int error = WNetCancelConnection2(label); + if (error != 0) { - this._mapper.UnmapDirectory(config.Label); - } - catch (MapperException ex) - { - this.HandleMappingError(config, ex); + this.ThrowExtensionException(error, $"Unmapping of {label} failed."); } } } } - private void HandleMappingError(SharedDirectoryMapperConfig config, MapperException ex) + private void ThrowExtensionException(int error, string message) { - Logger.Error("Mapping of " + config.Label + " failed. STDOUT: " + ex.Process.StandardOutput.ReadToEnd() - + " \r\nSTDERR: " + ex.Process.StandardError.ReadToEnd(), ex); - throw new ExtensionException(this.Descriptor.Id, this.DisplayName + ": Mapping of " + config.Label + "failed", ex); + 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; + } } } } diff --git a/src/WinSW.Plugins/SharedDirectoryMapperConfig.cs b/src/WinSW.Plugins/SharedDirectoryMapperConfig.cs index 089a4e1..84b47a8 100644 --- a/src/WinSW.Plugins/SharedDirectoryMapperConfig.cs +++ b/src/WinSW.Plugins/SharedDirectoryMapperConfig.cs @@ -1,8 +1,8 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.IO; using System.Xml; using WinSW.Util; -using static System.Environment; namespace WinSW.Plugins { @@ -38,11 +38,11 @@ namespace WinSW.Plugins throw new InvalidDataException("SharedDirectoryMapperConfig config error"); } - string enableMappingConfig = ExpandEnvironmentVariables((string)dict["enabled"]); + string enableMappingConfig = Environment.ExpandEnvironmentVariables((string)dict["enabled"]); bool enableMapping = ConfigHelper.YamlBoolParse(enableMappingConfig); - string label = ExpandEnvironmentVariables((string)dict["label"]); - string uncPath = ExpandEnvironmentVariables((string)dict["uncPath"]); + string label = Environment.ExpandEnvironmentVariables((string)dict["label"]); + string uncPath = Environment.ExpandEnvironmentVariables((string)dict["uncPath"]); return new SharedDirectoryMapperConfig(enableMapping, label, uncPath); } diff --git a/src/WinSW.Plugins/SharedDirectoryMapperHelper.cs b/src/WinSW.Plugins/SharedDirectoryMapperHelper.cs deleted file mode 100644 index 65c0874..0000000 --- a/src/WinSW.Plugins/SharedDirectoryMapperHelper.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System.Diagnostics; - -namespace WinSW.Plugins -{ - class SharedDirectoryMappingHelper - { - /// - /// Invokes a system command - /// - /// - /// Command to be executed - /// Command arguments - /// Operation failure - private void InvokeCommand(string command, string args) - { - var 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) - { - this.InvokeCommand("net.exe", " use " + label + " " + uncPath); - } - - /// - /// Unmaps the label - /// - /// Disk label - /// Operation failure - public void UnmapDirectory(string label) - { - this.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) - { - this.Call = command + " " + args; - this.Process = process; - } - } -} diff --git a/src/WinSW.Tests/Extensions/SharedDirectoryMapperTest.cs b/src/WinSW.Tests/Extensions/SharedDirectoryMapperConfigTest.cs similarity index 98% rename from src/WinSW.Tests/Extensions/SharedDirectoryMapperTest.cs rename to src/WinSW.Tests/Extensions/SharedDirectoryMapperConfigTest.cs index 18c79cd..0afc8ec 100644 --- a/src/WinSW.Tests/Extensions/SharedDirectoryMapperTest.cs +++ b/src/WinSW.Tests/Extensions/SharedDirectoryMapperConfigTest.cs @@ -7,7 +7,7 @@ using WinSW.Plugins; namespace winswTests.Extensions { [TestFixture] - class SharedDirectoryMapperTest : ExtensionTestBase + class SharedDirectoryMapperConfigTest : ExtensionTestBase { IServiceConfig _testServiceDescriptor; IServiceConfig _testServiceDescriptorYaml; diff --git a/src/WinSW.Tests/Extensions/SharedDirectoryMapperTests.cs b/src/WinSW.Tests/Extensions/SharedDirectoryMapperTests.cs new file mode 100644 index 0000000..d1ced3e --- /dev/null +++ b/src/WinSW.Tests/Extensions/SharedDirectoryMapperTests.cs @@ -0,0 +1,148 @@ +#if NET +using System; +using System.IO; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using NUnit.Framework; +using WinSW.Plugins; + +namespace winswTests.Extensions +{ + // TODO: Throws.TypeOf() + [TestFixture] + public class SharedDirectoryMapperTests + { + [Test] + public void TestMap() + { + using var data = TestData.Create(); + + const string label = "W:"; + var mapper = new SharedDirectoryMapper(true, $@"\\{Environment.MachineName}\{data.name}", label); + + mapper.OnWrapperStarted(); + Assert.That($@"{label}\", Does.Exist); + mapper.BeforeWrapperStopped(); + Assert.That($@"{label}\", Does.Not.Exist); + } + + [Test] + 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.That($@"{label}\", Does.Not.Exist); + mapper.BeforeWrapperStopped(); + } + + [Test] + public void TestMap_PathEndsWithSlash_Throws() + { + using var data = TestData.Create(); + + const string label = "W:"; + var mapper = new SharedDirectoryMapper(true, $@"\\{Environment.MachineName}\{data.name}\", label); + + Assert.That(() => mapper.OnWrapperStarted(), Throws.Exception); + Assert.That($@"{label}\", Does.Not.Exist); + Assert.That(() => mapper.BeforeWrapperStopped(), Throws.Exception); + } + + [Test] + public void TestMap_LabelDoesNotEndWithColon_Throws() + { + using var data = TestData.Create(); + + const string label = "W"; + var mapper = new SharedDirectoryMapper(true, $@"\\{Environment.MachineName}\{data.name}", label); + + Assert.That(() => mapper.OnWrapperStarted(), Throws.Exception); + Assert.That($@"{label}\", Does.Not.Exist); + Assert.That(() => mapper.BeforeWrapperStopped(), Throws.Exception); + } + + private readonly ref struct TestData + { + internal readonly string name; + internal readonly string path; + + private TestData(string name, string path) + { + this.name = name; + this.path = path; + } + + internal static TestData Create([CallerMemberName] string name = null) + { + string path = Path.Combine(Path.GetTempPath(), name); + _ = Directory.CreateDirectory(path); + + try + { + var shareInfo = new NativeMethods.SHARE_INFO_2 + { + netname = name, + type = NativeMethods.STYPE_DISKTREE | NativeMethods.STYPE_TEMPORARY, + max_uses = unchecked((uint)-1), + path = path, + }; + + uint error = NativeMethods.NetShareAdd(null, 2, shareInfo, out _); + Assert.That(error, Is.Zero); + + return new TestData(name, path); + } + catch + { + Directory.Delete(path); + throw; + } + } + + public void Dispose() + { + try + { + uint error = NativeMethods.NetShareDel(null, this.name); + Assert.That(error, Is.Zero); + } + finally + { + Directory.Delete(this.path); + } + } + } + + private static class NativeMethods + { + internal const uint STYPE_DISKTREE = 0; + internal const uint STYPE_TEMPORARY = 0x40000000; + + private const string Netapi32LibraryName = "netapi32.dll"; + + [DllImport(Netapi32LibraryName, CharSet = CharSet.Unicode)] + internal static extern uint NetShareAdd(string servername, uint level, in SHARE_INFO_2 buf, out uint parm_err); + + [DllImport(Netapi32LibraryName, CharSet = CharSet.Unicode)] + internal static extern uint NetShareDel(string servername, string netname, uint reserved = 0); + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + internal struct SHARE_INFO_2 + { + public string netname; + public uint type; + public string remark; + public uint permissions; + public uint max_uses; + public uint current_uses; + public string path; + public string passwd; + } + } + } +} +#endif