diff --git a/CHANGELOG.TXT b/CHANGELOG.TXT index d5e5fe45c..9753ae021 100644 --- a/CHANGELOG.TXT +++ b/CHANGELOG.TXT @@ -24,6 +24,31 @@ Fixes: #1064: "Esc" button does does not close some dialogs #1044: Dragging (grabbing) the program window requires 2 clicks +1.76.15 (2019-03-09): + +Fixes: +------ +#1303: Exception on first connection with new SQL server database +#1304: Resolved several issues with importing multiple RDP Manager v2.7 files + +Features/Enhancements: +---------------------- +Importing multiple files now only causes 1 save event, rather than 1 per file imported. + + +1.76.14 (2019-02-08): + +Features/Enhancements: +---------------------- +#222: Allow FIPS to be enabled + + +1.76.13 (2018-12-22): + +Changes: +-------- +#222: Pre-Release Test build for running on systems with FIPS Enabled + 1.76.12 (2018-11-08): diff --git a/mRemoteNGTests/App/ImportTests.cs b/mRemoteNGTests/App/ImportTests.cs new file mode 100644 index 000000000..22628db3f --- /dev/null +++ b/mRemoteNGTests/App/ImportTests.cs @@ -0,0 +1,47 @@ +using System.IO; +using mRemoteNG.App; +using mRemoteNG.Config.Putty; +using mRemoteNG.Connection; +using mRemoteNG.Container; +using mRemoteNGTests.Properties; +using mRemoteNGTests.TestHelpers; +using NUnit.Framework; + +namespace mRemoteNGTests.App +{ + public class ImportTests + { + [Test] + public void ErrorHandlerCalledWhenUnsupportedFileExtensionFound() + { + using (FileTestHelpers.DisposableTempFile(out var file, ".blah")) + { + var conService = new ConnectionsService(PuttySessionsManager.Instance); + var container = new ContainerInfo(); + var exceptionOccurred = false; + + Import.HeadlessFileImport(new []{file}, container, conService, s => exceptionOccurred = true); + + Assert.That(exceptionOccurred); + } + } + + [Test] + public void AnErrorInOneFileDoNotPreventOtherFilesFromProcessing() + { + using (FileTestHelpers.DisposableTempFile(out var badFile, ".blah")) + using (FileTestHelpers.DisposableTempFile(out var rdpFile, ".rdp")) + { + File.AppendAllText(rdpFile, Resources.test_remotedesktopconnection_rdp); + var conService = new ConnectionsService(PuttySessionsManager.Instance); + var container = new ContainerInfo(); + var exceptionCount = 0; + + Import.HeadlessFileImport(new[] { badFile, rdpFile }, container, conService, s => exceptionCount++); + + Assert.That(exceptionCount, Is.EqualTo(1)); + Assert.That(container.Children, Has.One.Items); + } + } + } +} diff --git a/mRemoteNGTests/Config/Serializers/MiscSerializers/RemoteDesktopConnectionManager27DeserializerTests.cs b/mRemoteNGTests/Config/Serializers/MiscSerializers/RemoteDesktopConnectionManager27DeserializerTests.cs index 20185a5f1..2157fadf3 100644 --- a/mRemoteNGTests/Config/Serializers/MiscSerializers/RemoteDesktopConnectionManager27DeserializerTests.cs +++ b/mRemoteNGTests/Config/Serializers/MiscSerializers/RemoteDesktopConnectionManager27DeserializerTests.cs @@ -1,20 +1,21 @@ -using System.IO; +using System; +using System.Collections.Generic; +using System.IO; using System.Linq; using mRemoteNG.Config.Serializers; +using mRemoteNG.Connection; using mRemoteNG.Connection.Protocol; using mRemoteNG.Connection.Protocol.RDP; using mRemoteNG.Container; -using mRemoteNG.Tree; using mRemoteNGTests.Properties; using NUnit.Framework; namespace mRemoteNGTests.Config.Serializers.MiscSerializers { - public class RemoteDesktopConnectionManager27DeserializerTests + public class RemoteDesktopConnectionManager27DeserializerTests { private string _connectionFileContents; private RemoteDesktopConnectionManagerDeserializer _deserializer; - private ConnectionTreeModel _connectionTreeModel; private const string ExpectedName = "server1_displayname"; private const string ExpectedHostname = "server1"; private const string ExpectedDescription = "Comment text here"; @@ -44,262 +45,62 @@ namespace mRemoteNGTests.Config.Serializers.MiscSerializers { _connectionFileContents = Resources.test_rdcman_v2_7_schema3; _deserializer = new RemoteDesktopConnectionManagerDeserializer(); - _connectionTreeModel = _deserializer.Deserialize(_connectionFileContents); } [Test] public void ConnectionTreeModelHasARootNode() { - var numberOfRootNodes = _connectionTreeModel.RootNodes.Count; + var connectionTreeModel = _deserializer.Deserialize(_connectionFileContents); + var numberOfRootNodes = connectionTreeModel.RootNodes.Count; Assert.That(numberOfRootNodes, Is.GreaterThan(0)); } [Test] public void RootNodeHasContents() { - var rootNodeContents = _connectionTreeModel.RootNodes.First().Children; + var connectionTreeModel = _deserializer.Deserialize(_connectionFileContents); + var rootNodeContents = connectionTreeModel.RootNodes.First().Children; Assert.That(rootNodeContents, Is.Not.Empty); } [Test] public void AllSubRootFoldersImported() { - var rootNode = _connectionTreeModel.RootNodes.First(); + var connectionTreeModel = _deserializer.Deserialize(_connectionFileContents); + var rootNode = connectionTreeModel.RootNodes.First(); var importedRdcmanRootNode = rootNode.Children.OfType().First(); var rootNodeContents = importedRdcmanRootNode.Children.Count(node => node.Name == "Group1" || node.Name == "Group2"); Assert.That(rootNodeContents, Is.EqualTo(2)); } - [Test] - public void ConnectionDisplayNameImported() + [TestCaseSource(nameof(ExpectedPropertyValues))] + public void PropertiesWithValuesAreCorrectlyImported(Func propSelector, object expectedValue) { - var rootNode = _connectionTreeModel.RootNodes.First(); - var importedRdcmanRootNode = rootNode.Children.OfType().First(); - var group1 = importedRdcmanRootNode.Children.OfType().First(node => node.Name == "Group1"); - var connection = group1.Children.First(); - Assert.That(connection.Name, Is.EqualTo(ExpectedName)); + var connectionTreeModel = _deserializer.Deserialize(_connectionFileContents); + + var connection = connectionTreeModel + .GetRecursiveChildList() + .OfType() + .First(node => node.Name == "Group1") + .Children + .First(); + + Assert.That(propSelector(connection), Is.EqualTo(expectedValue)); } - [Test] - public void ConnectionHostnameImported() + [TestCaseSource(nameof(NullPropertyValues))] + public void PropertiesWithoutValuesAreIgnored(Func propSelector) { - var rootNode = _connectionTreeModel.RootNodes.First(); - var importedRdcmanRootNode = rootNode.Children.OfType().First(); - var group1 = importedRdcmanRootNode.Children.OfType().First(node => node.Name == "Group1"); - var connection = group1.Children.First(); - Assert.That(connection.Hostname, Is.EqualTo(ExpectedHostname)); - } + var connectionTreeModel = _deserializer.Deserialize(Resources.test_rdcman_v2_7_schema3_null_values); - [Test] - public void ConnectionDescriptionImported() - { - var rootNode = _connectionTreeModel.RootNodes.First(); - var importedRdcmanRootNode = rootNode.Children.OfType().First(); - var group1 = importedRdcmanRootNode.Children.OfType().First(node => node.Name == "Group1"); - var connection = group1.Children.First(); - Assert.That(connection.Description, Is.EqualTo(ExpectedDescription)); - } + var importedConnection = connectionTreeModel + .GetRecursiveChildList() + .OfType() + .First(node => node.Name == "Group1") + .Children + .First(); - [Test] - public void ConnectionUsernameImported() - { - var rootNode = _connectionTreeModel.RootNodes.First(); - var importedRdcmanRootNode = rootNode.Children.OfType().First(); - var group1 = importedRdcmanRootNode.Children.OfType().First(node => node.Name == "Group1"); - var connection = group1.Children.First(); - Assert.That(connection.Username, Is.EqualTo(ExpectedUsername)); - } - - [Test] - public void ConnectionDomainImported() - { - var rootNode = _connectionTreeModel.RootNodes.First(); - var importedRdcmanRootNode = rootNode.Children.OfType().First(); - var group1 = importedRdcmanRootNode.Children.OfType().First(node => node.Name == "Group1"); - var connection = group1.Children.First(); - Assert.That(connection.Domain, Is.EqualTo(ExpectedDomain)); - } - - // Since password is encrypted with a machine key, cant test decryption on another machine - //[Test] - //public void ConnectionPasswordImported() - //{ - // var rootNode = _connectionTreeModel.RootNodes.First(); - // var importedRdcmanRootNode = rootNode.Children.OfType().First(); - // var group1 = importedRdcmanRootNode.Children.OfType().First(node => node.Name == "Group1"); - // var connection = group1.Children.First(); - // Assert.That(connection.Password, Is.EqualTo(ExpectedPassword)); - //} - - [Test] - public void ConnectionProtocolSetToRdp() - { - var rootNode = _connectionTreeModel.RootNodes.First(); - var importedRdcmanRootNode = rootNode.Children.OfType().First(); - var group1 = importedRdcmanRootNode.Children.OfType().First(node => node.Name == "Group1"); - var connection = group1.Children.First(); - Assert.That(connection.Protocol, Is.EqualTo(ProtocolType.RDP)); - } - - [Test] - public void ConnectionUseConsoleSessionImported() - { - var rootNode = _connectionTreeModel.RootNodes.First(); - var importedRdcmanRootNode = rootNode.Children.OfType().First(); - var group1 = importedRdcmanRootNode.Children.OfType().First(node => node.Name == "Group1"); - var connection = group1.Children.First(); - Assert.That(connection.UseConsoleSession, Is.EqualTo(ExpectedUseConsoleSession)); - } - - [Test] - public void ConnectionPortImported() - { - var rootNode = _connectionTreeModel.RootNodes.First(); - var importedRdcmanRootNode = rootNode.Children.OfType().First(); - var group1 = importedRdcmanRootNode.Children.OfType().First(node => node.Name == "Group1"); - var connection = group1.Children.First(); - Assert.That(connection.Port, Is.EqualTo(ExpectedPort)); - } - - [Test] - public void ConnectionGatewayUsageMethodImported() - { - var rootNode = _connectionTreeModel.RootNodes.First(); - var importedRdcmanRootNode = rootNode.Children.OfType().First(); - var group1 = importedRdcmanRootNode.Children.OfType().First(node => node.Name == "Group1"); - var connection = group1.Children.First(); - Assert.That(connection.RDGatewayUsageMethod, Is.EqualTo(ExpectedGatewayUsageMethod)); - } - - [Test] - public void ConnectionGatewayHostnameImported() - { - var rootNode = _connectionTreeModel.RootNodes.First(); - var importedRdcmanRootNode = rootNode.Children.OfType().First(); - var group1 = importedRdcmanRootNode.Children.OfType().First(node => node.Name == "Group1"); - var connection = group1.Children.First(); - Assert.That(connection.RDGatewayHostname, Is.EqualTo(ExpectedGatewayHostname)); - } - - [Test] - public void ConnectionGatewayUsernameImported() - { - var rootNode = _connectionTreeModel.RootNodes.First(); - var importedRdcmanRootNode = rootNode.Children.OfType().First(); - var group1 = importedRdcmanRootNode.Children.OfType().First(node => node.Name == "Group1"); - var connection = group1.Children.First(); - Assert.That(connection.RDGatewayUsername, Is.EqualTo(ExpectedGatewayUsername)); - } - - // Since password is encrypted with a machine key, cant test decryption on another machine - //[Test] - //public void ConnectionGatewayPasswordImported() - //{ - // var rootNode = _connectionTreeModel.RootNodes.First(); - // var importedRdcmanRootNode = rootNode.Children.OfType().First(); - // var group1 = importedRdcmanRootNode.Children.OfType().First(node => node.Name == "Group1"); - // var connection = group1.Children.First(); - // Assert.That(connection.RDGatewayPassword, Is.EqualTo(ExpectedGatewayPassword)); - //} - - [Test] - public void ConnectionGatewayDomainImported() - { - var rootNode = _connectionTreeModel.RootNodes.First(); - var importedRdcmanRootNode = rootNode.Children.OfType().First(); - var group1 = importedRdcmanRootNode.Children.OfType().First(node => node.Name == "Group1"); - var connection = group1.Children.First(); - Assert.That(connection.RDGatewayDomain, Is.EqualTo(ExpectedGatewayDomain)); - } - - [Test] - public void ConnectionResolutionImported() - { - var rootNode = _connectionTreeModel.RootNodes.First(); - var importedRdcmanRootNode = rootNode.Children.OfType().First(); - var group1 = importedRdcmanRootNode.Children.OfType().First(node => node.Name == "Group1"); - var connection = group1.Children.First(); - Assert.That(connection.Resolution, Is.EqualTo(ExpectedRdpResolution)); - } - - [Test] - public void ConnectionColorDepthImported() - { - var rootNode = _connectionTreeModel.RootNodes.First(); - var importedRdcmanRootNode = rootNode.Children.OfType().First(); - var group1 = importedRdcmanRootNode.Children.OfType().First(node => node.Name == "Group1"); - var connection = group1.Children.First(); - Assert.That(connection.Colors, Is.EqualTo(ExpectedRdpColorDepth)); - } - - [Test] - public void ConnectionAudioRedirectionImported() - { - var rootNode = _connectionTreeModel.RootNodes.First(); - var importedRdcmanRootNode = rootNode.Children.OfType().First(); - var group1 = importedRdcmanRootNode.Children.OfType().First(node => node.Name == "Group1"); - var connection = group1.Children.First(); - Assert.That(connection.RedirectSound, Is.EqualTo(ExpectedAudioRedirection)); - } - - [Test] - public void ConnectionKeyRedirectionImported() - { - var rootNode = _connectionTreeModel.RootNodes.First(); - var importedRdcmanRootNode = rootNode.Children.OfType().First(); - var group1 = importedRdcmanRootNode.Children.OfType().First(node => node.Name == "Group1"); - var connection = group1.Children.First(); - Assert.That(connection.RedirectKeys, Is.EqualTo(ExpectedKeyRedirection)); - } - - [Test] - public void ConnectionDriveRedirectionImported() - { - var rootNode = _connectionTreeModel.RootNodes.First(); - var importedRdcmanRootNode = rootNode.Children.OfType().First(); - var group1 = importedRdcmanRootNode.Children.OfType().First(node => node.Name == "Group1"); - var connection = group1.Children.First(); - Assert.That(connection.RedirectDiskDrives, Is.EqualTo(ExpectedDriveRedirection)); - } - - [Test] - public void ConnectionPortRedirectionImported() - { - var rootNode = _connectionTreeModel.RootNodes.First(); - var importedRdcmanRootNode = rootNode.Children.OfType().First(); - var group1 = importedRdcmanRootNode.Children.OfType().First(node => node.Name == "Group1"); - var connection = group1.Children.First(); - Assert.That(connection.RedirectPorts, Is.EqualTo(ExpectedPortRedirection)); - } - - [Test] - public void ConnectionPrinterRedirectionImported() - { - var rootNode = _connectionTreeModel.RootNodes.First(); - var importedRdcmanRootNode = rootNode.Children.OfType().First(); - var group1 = importedRdcmanRootNode.Children.OfType().First(node => node.Name == "Group1"); - var connection = group1.Children.First(); - Assert.That(connection.RedirectPrinters, Is.EqualTo(ExpectedPrinterRedirection)); - } - - [Test] - public void ConnectionSmartcardRedirectionImported() - { - var rootNode = _connectionTreeModel.RootNodes.First(); - var importedRdcmanRootNode = rootNode.Children.OfType().First(); - var group1 = importedRdcmanRootNode.Children.OfType().First(node => node.Name == "Group1"); - var connection = group1.Children.First(); - Assert.That(connection.RedirectSmartCards, Is.EqualTo(ExpectedSmartcardRedirection)); - } - - [Test] - public void ConnectionauthenticationLevelImported() - { - var rootNode = _connectionTreeModel.RootNodes.First(); - var importedRdcmanRootNode = rootNode.Children.OfType().First(); - var group1 = importedRdcmanRootNode.Children.OfType().First(node => node.Name == "Group1"); - var connection = group1.Children.First(); - Assert.That(connection.RDPAuthenticationLevel, Is.EqualTo(ExpectedAuthLevel)); + Assert.That(propSelector(importedConnection), Is.EqualTo(propSelector(new ConnectionInfo()))); } [Test] @@ -322,5 +123,61 @@ namespace mRemoteNGTests.Config.Serializers.MiscSerializers var badFileContents = Resources.test_rdcman_noversion; Assert.That(() => _deserializer.Deserialize(badFileContents), Throws.TypeOf()); } + + private static IEnumerable ExpectedPropertyValues() + { + return new[] + { + new TestCaseData((Func)(con => con.Name), ExpectedName).SetName(nameof(ConnectionInfo.Name)), + new TestCaseData((Func)(con => con.Hostname), ExpectedHostname).SetName(nameof(ConnectionInfo.Hostname)), + new TestCaseData((Func)(con => con.Description), ExpectedDescription).SetName(nameof(ConnectionInfo.Description)), + new TestCaseData((Func)(con => con.Username), ExpectedUsername).SetName(nameof(ConnectionInfo.Username)), + new TestCaseData((Func)(con => con.Domain), ExpectedDomain).SetName(nameof(ConnectionInfo.Domain)), + new TestCaseData((Func)(con => con.Protocol), ProtocolType.RDP).SetName(nameof(ConnectionInfo.Protocol)), + new TestCaseData((Func)(con => con.UseConsoleSession), ExpectedUseConsoleSession).SetName(nameof(ConnectionInfo.UseConsoleSession)), + new TestCaseData((Func)(con => con.Port), ExpectedPort).SetName(nameof(ConnectionInfo.Port)), + new TestCaseData((Func)(con => con.RDGatewayUsageMethod), ExpectedGatewayUsageMethod).SetName(nameof(ConnectionInfo.RDGatewayUsageMethod)), + new TestCaseData((Func)(con => con.RDGatewayHostname), ExpectedGatewayHostname).SetName(nameof(ConnectionInfo.RDGatewayHostname)), + new TestCaseData((Func)(con => con.RDGatewayUsername), ExpectedGatewayUsername).SetName(nameof(ConnectionInfo.RDGatewayUsername)), + new TestCaseData((Func)(con => con.RDGatewayDomain), ExpectedGatewayDomain).SetName(nameof(ConnectionInfo.RDGatewayDomain)), + new TestCaseData((Func)(con => con.Resolution), ExpectedRdpResolution).SetName(nameof(ConnectionInfo.Resolution)), + new TestCaseData((Func)(con => con.Colors), ExpectedRdpColorDepth).SetName(nameof(ConnectionInfo.Colors)), + new TestCaseData((Func)(con => con.RedirectSound), ExpectedAudioRedirection).SetName(nameof(ConnectionInfo.RedirectSound)), + new TestCaseData((Func)(con => con.RedirectKeys), ExpectedKeyRedirection).SetName(nameof(ConnectionInfo.RedirectKeys)), + new TestCaseData((Func)(con => con.RDPAuthenticationLevel), ExpectedAuthLevel).SetName(nameof(ConnectionInfo.RDPAuthenticationLevel)), + new TestCaseData((Func)(con => con.RedirectSmartCards), ExpectedSmartcardRedirection).SetName(nameof(ConnectionInfo.RedirectSmartCards)), + new TestCaseData((Func)(con => con.RedirectPrinters), ExpectedPrinterRedirection).SetName(nameof(ConnectionInfo.RedirectPrinters)), + new TestCaseData((Func)(con => con.RedirectPorts), ExpectedPortRedirection).SetName(nameof(ConnectionInfo.RedirectPorts)), + new TestCaseData((Func)(con => con.RedirectDiskDrives), ExpectedDriveRedirection).SetName(nameof(ConnectionInfo.RedirectDiskDrives)), + }; + } + + private static IEnumerable NullPropertyValues() + { + return new[] + { + new TestCaseData((Func)(con => con.Name)).SetName(nameof(ConnectionInfo.Name)), + new TestCaseData((Func)(con => con.Hostname)).SetName(nameof(ConnectionInfo.Hostname)), + new TestCaseData((Func)(con => con.Description)).SetName(nameof(ConnectionInfo.Description)), + new TestCaseData((Func)(con => con.Username)).SetName(nameof(ConnectionInfo.Username)), + new TestCaseData((Func)(con => con.Domain)).SetName(nameof(ConnectionInfo.Domain)), + new TestCaseData((Func)(con => con.Protocol)).SetName(nameof(ConnectionInfo.Protocol)), + new TestCaseData((Func)(con => con.UseConsoleSession)).SetName(nameof(ConnectionInfo.UseConsoleSession)), + new TestCaseData((Func)(con => con.Port)).SetName(nameof(ConnectionInfo.Port)), + new TestCaseData((Func)(con => con.RDGatewayUsageMethod)).SetName(nameof(ConnectionInfo.RDGatewayUsageMethod)), + new TestCaseData((Func)(con => con.RDGatewayHostname)).SetName(nameof(ConnectionInfo.RDGatewayHostname)), + new TestCaseData((Func)(con => con.RDGatewayUsername)).SetName(nameof(ConnectionInfo.RDGatewayUsername)), + new TestCaseData((Func)(con => con.RDGatewayDomain)).SetName(nameof(ConnectionInfo.RDGatewayDomain)), + new TestCaseData((Func)(con => con.Resolution)).SetName(nameof(ConnectionInfo.Resolution)), + new TestCaseData((Func)(con => con.Colors)).SetName(nameof(ConnectionInfo.Colors)), + new TestCaseData((Func)(con => con.RedirectSound)).SetName(nameof(ConnectionInfo.RedirectSound)), + new TestCaseData((Func)(con => con.RedirectKeys)).SetName(nameof(ConnectionInfo.RedirectKeys)), + new TestCaseData((Func)(con => con.RDPAuthenticationLevel)).SetName(nameof(ConnectionInfo.RDPAuthenticationLevel)), + new TestCaseData((Func)(con => con.RedirectSmartCards)).SetName(nameof(ConnectionInfo.RedirectSmartCards)), + new TestCaseData((Func)(con => con.RedirectPrinters)).SetName(nameof(ConnectionInfo.RedirectPrinters)), + new TestCaseData((Func)(con => con.RedirectPorts)).SetName(nameof(ConnectionInfo.RedirectPorts)), + new TestCaseData((Func)(con => con.RedirectDiskDrives)).SetName(nameof(ConnectionInfo.RedirectDiskDrives)), + }; + } } } \ No newline at end of file diff --git a/mRemoteNGTests/Properties/Resources.Designer.cs b/mRemoteNGTests/Properties/Resources.Designer.cs index 48157bac3..2d858f11c 100644 --- a/mRemoteNGTests/Properties/Resources.Designer.cs +++ b/mRemoteNGTests/Properties/Resources.Designer.cs @@ -298,6 +298,32 @@ namespace mRemoteNGTests.Properties { } } + /// + /// Looks up a localized string similar to <?xml version="1.0" encoding="utf-8"?> + ///<RDCMan programVersion="2.7" schemaVersion="3"> + /// <file> + /// <credentialsProfiles /> + /// <properties> + /// <expanded>True</expanded> + /// <name>test_RDCMan_connections</name> + /// </properties> + /// <smartGroup> + /// <properties> + /// <expanded>False</expanded> + /// <name>AllServers</name> + /// </properties> + /// <ruleGroup operator="All"> + /// <rule> + /// <property>DisplayName</property> + /// <operator>Matches</operator> + /// [rest of string was truncated]";. + /// + internal static string test_rdcman_v2_7_schema3_null_values { + get { + return ResourceManager.GetString("test_rdcman_v2_7_schema3_null_values", resourceCulture); + } + } + /// /// Looks up a localized string similar to screen mode id:i:1 ///use multimon:i:0 diff --git a/mRemoteNGTests/Properties/Resources.resx b/mRemoteNGTests/Properties/Resources.resx index 93d4db1fa..d527187c1 100644 --- a/mRemoteNGTests/Properties/Resources.resx +++ b/mRemoteNGTests/Properties/Resources.resx @@ -175,6 +175,9 @@ ..\Resources\test_RDCMan_v2_7_schema3.rdg;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 + + ..\Resources\test_rdcman_v2_7_schema3_null_values.rdg;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 + ..\Resources\test_remotedesktopconnection.rdp;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16 diff --git a/mRemoteNGTests/Resources/test_rdcman_v2_7_schema3_null_values.rdg b/mRemoteNGTests/Resources/test_rdcman_v2_7_schema3_null_values.rdg new file mode 100644 index 000000000..2a8c7ba3c --- /dev/null +++ b/mRemoteNGTests/Resources/test_rdcman_v2_7_schema3_null_values.rdg @@ -0,0 +1,95 @@ + + + + + + True + test_RDCMan_connections + + + + False + AllServers + + + + DisplayName + Matches + server + + + + + + True + Group1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/mRemoteNGTests/TestHelpers/FileTestHelpers.cs b/mRemoteNGTests/TestHelpers/FileTestHelpers.cs index d1b9c7eba..9e34a5eaf 100644 --- a/mRemoteNGTests/TestHelpers/FileTestHelpers.cs +++ b/mRemoteNGTests/TestHelpers/FileTestHelpers.cs @@ -1,4 +1,5 @@ using System.IO; +using mRemoteNG.Tools; namespace mRemoteNGTests.TestHelpers { @@ -18,9 +19,17 @@ namespace mRemoteNGTests.TestHelpers File.Delete(file); } - public static string NewTempFilePath() + public static void DeleteDirectory(string directory) + { + if (Directory.Exists(directory)) + Directory.Delete(directory, true); + } + + public static string NewTempFilePath(string extension = "") { var newPath = Path.Combine(GetTestSpecificTempDirectory(), Path.GetRandomFileName()); + if (!string.IsNullOrWhiteSpace(extension)) + newPath = newPath + extension; var folderPath = Path.GetDirectoryName(newPath); if (!Directory.Exists(folderPath)) Directory.CreateDirectory(folderPath); @@ -35,5 +44,15 @@ namespace mRemoteNGTests.TestHelpers { return Path.Combine(Path.GetTempPath(), "mRemoteNGTests", Path.GetRandomFileName()); } + + public static DisposableAction DisposableTempFile(out string filePath, string extension = "") + { + var file = NewTempFilePath(extension); + filePath = file; + File.AppendAllText(file, ""); + return new DisposableAction( + () => {}, + () => DeleteDirectory(Path.GetDirectoryName(file))); + } } } \ No newline at end of file diff --git a/mRemoteNGTests/Tools/DisposableActionTests.cs b/mRemoteNGTests/Tools/DisposableActionTests.cs new file mode 100644 index 000000000..8fab6f801 --- /dev/null +++ b/mRemoteNGTests/Tools/DisposableActionTests.cs @@ -0,0 +1,42 @@ +using mRemoteNG.Tools; +using NUnit.Framework; + +namespace mRemoteNGTests.Tools +{ + public class DisposableActionTests + { + [Test] + public void InitializerActionRunsWhenObjectIsCreated() + { + var initializerRan = false; + new DisposableAction(() => initializerRan = true, () => { }); + + Assert.That(initializerRan); + } + + [Test] + public void DisposalActionRunsWhenDisposeIsCalled() + { + var disposeActionRan = false; + var action = new DisposableAction(() => {}, () => disposeActionRan = true); + + Assert.That(disposeActionRan, Is.False); + action.Dispose(); + Assert.That(disposeActionRan, Is.True); + } + + [Test] + public void DisposeActionOnlyExecutedOnceWhenCallingDisposeMultipleTimes() + { + var invokeCount = 0; + var action = new DisposableAction(() => { }, () => invokeCount++); + + action.Dispose(); + action.Dispose(); + action.Dispose(); + action.Dispose(); + action.Dispose(); + Assert.That(invokeCount, Is.EqualTo(1)); + } + } +} diff --git a/mRemoteNGTests/mRemoteNGTests.csproj b/mRemoteNGTests/mRemoteNGTests.csproj index e26765560..55310f852 100644 --- a/mRemoteNGTests/mRemoteNGTests.csproj +++ b/mRemoteNGTests/mRemoteNGTests.csproj @@ -114,6 +114,7 @@ + @@ -182,6 +183,7 @@ + @@ -274,6 +276,7 @@ + diff --git a/mRemoteV1/App/Import.cs b/mRemoteV1/App/Import.cs index 0a837cf7a..d1832f90b 100644 --- a/mRemoteV1/App/Import.cs +++ b/mRemoteV1/App/Import.cs @@ -3,13 +3,14 @@ using System.Collections.Generic; using System.IO; using System.Windows.Forms; using mRemoteNG.Config.Import; +using mRemoteNG.Connection; using mRemoteNG.Connection.Protocol; using mRemoteNG.Container; using mRemoteNG.Tools; namespace mRemoteNG.App { - public static class Import + public static class Import { public static void ImportFromFile(ContainerInfo importDestinationContainer) { @@ -35,24 +36,12 @@ namespace mRemoteNG.App if (openFileDialog.ShowDialog() != DialogResult.OK) return; - foreach (var fileName in openFileDialog.FileNames) - { - try - { - var importer = BuildConnectionImporterFromFileExtension(fileName); - importer.Import(fileName, importDestinationContainer); - } - catch (Exception ex) - { - MessageBox.Show(string.Format(Language.strImportFileFailedContent, fileName), - Language.strImportFileFailedMainInstruction, - MessageBoxButtons.OK, MessageBoxIcon.Exclamation, - MessageBoxDefaultButton.Button1); - Runtime.MessageCollector.AddExceptionMessage("Unable to import file.", ex); - } - } - - Runtime.ConnectionsService.SaveConnectionsAsync(); + HeadlessFileImport( + openFileDialog.FileNames, + importDestinationContainer, + Runtime.ConnectionsService, + fileName => MessageBox.Show(string.Format(Language.strImportFileFailedContent, fileName), Language.strImportFileFailedMainInstruction, + MessageBoxButtons.OK, MessageBoxIcon.Exclamation, MessageBoxDefaultButton.Button1)); } } catch (Exception ex) @@ -61,14 +50,40 @@ namespace mRemoteNG.App } } + public static void HeadlessFileImport( + IEnumerable filePaths, + ContainerInfo importDestinationContainer, + ConnectionsService connectionsService, + Action exceptionAction = null) + { + using (connectionsService.BatchedSavingContext()) + { + foreach (var fileName in filePaths) + { + try + { + var importer = BuildConnectionImporterFromFileExtension(fileName); + importer.Import(fileName, importDestinationContainer); + } + catch (Exception ex) + { + exceptionAction?.Invoke(fileName); + Runtime.MessageCollector.AddExceptionMessage($"Error occurred while importing file '{fileName}'.", ex); + } + } + } + } + public static void ImportFromActiveDirectory(string ldapPath, ContainerInfo importDestinationContainer, bool importSubOu) { try { - ActiveDirectoryImporter.Import(ldapPath, importDestinationContainer, importSubOu); - Runtime.ConnectionsService.SaveConnectionsAsync(); + using (Runtime.ConnectionsService.BatchedSavingContext()) + { + ActiveDirectoryImporter.Import(ldapPath, importDestinationContainer, importSubOu); + } } catch (Exception ex) { @@ -82,9 +97,11 @@ namespace mRemoteNG.App { try { - var importer = new PortScanImporter(protocol); - importer.Import(hosts, importDestinationContainer); - Runtime.ConnectionsService.SaveConnectionsAsync(); + using (Runtime.ConnectionsService.BatchedSavingContext()) + { + var importer = new PortScanImporter(protocol); + importer.Import(hosts, importDestinationContainer); + } } catch (Exception ex) { diff --git a/mRemoteV1/Config/Connections/SqlConnectionsLoader.cs b/mRemoteV1/Config/Connections/SqlConnectionsLoader.cs index ecab5b284..9ebe970e9 100644 --- a/mRemoteV1/Config/Connections/SqlConnectionsLoader.cs +++ b/mRemoteV1/Config/Connections/SqlConnectionsLoader.cs @@ -1,23 +1,23 @@ using System; +using System.Collections.Generic; +using System.Linq; +using System.Security; using mRemoteNG.Config.DatabaseConnectors; using mRemoteNG.Config.DataProviders; using mRemoteNG.Config.Serializers; using mRemoteNG.Config.Serializers.MsSql; using mRemoteNG.Config.Serializers.Versioning; using mRemoteNG.Container; -using mRemoteNG.Tools; -using mRemoteNG.Tree; -using mRemoteNG.Tree.Root; -using System.Collections.Generic; -using System.Linq; -using System.Security; using mRemoteNG.Security; using mRemoteNG.Security.Authentication; using mRemoteNG.Security.SymmetricEncryption; +using mRemoteNG.Tools; +using mRemoteNG.Tree; +using mRemoteNG.Tree.Root; namespace mRemoteNG.Config.Connections { - public class SqlConnectionsLoader : IConnectionsLoader + public class SqlConnectionsLoader : IConnectionsLoader { private readonly IDeserializer> _localConnectionPropertiesDeserializer; @@ -44,7 +44,8 @@ namespace mRemoteNG.Config.Connections var databaseVersionVerifier = new SqlDatabaseVersionVerifier(connector); var cryptoProvider = new LegacyRijndaelCryptographyProvider(); - var metaData = metaDataRetriever.GetDatabaseMetaData(connector); + var metaData = metaDataRetriever.GetDatabaseMetaData(connector) ?? + HandleFirstRun(metaDataRetriever, connector); var decryptionKey = GetDecryptionKey(metaData); if (!decryptionKey.Any()) @@ -91,5 +92,11 @@ namespace mRemoteNG.Config.Connections container.IsExpanded = x.LocalProperties.Expanded; }); } + + private SqlConnectionListMetaData HandleFirstRun(SqlDatabaseMetaDataRetriever metaDataRetriever, SqlDatabaseConnector connector) + { + metaDataRetriever.WriteDatabaseMetaData(new RootNodeInfo(RootNodeType.Connection), connector); + return metaDataRetriever.GetDatabaseMetaData(connector); + } } } \ No newline at end of file diff --git a/mRemoteV1/Config/Connections/SqlConnectionsSaver.cs b/mRemoteV1/Config/Connections/SqlConnectionsSaver.cs index 94ed8cbef..1786e3ca2 100644 --- a/mRemoteV1/Config/Connections/SqlConnectionsSaver.cs +++ b/mRemoteV1/Config/Connections/SqlConnectionsSaver.cs @@ -1,10 +1,16 @@ -using mRemoteNG.App; +using System; +using System.Collections.Generic; +using System.Data.SqlClient; +using System.Globalization; +using System.Linq; +using mRemoteNG.App; using mRemoteNG.App.Info; using mRemoteNG.Config.DatabaseConnectors; using mRemoteNG.Config.DataProviders; using mRemoteNG.Config.Serializers; using mRemoteNG.Config.Serializers.MsSql; using mRemoteNG.Config.Serializers.Versioning; +using mRemoteNG.Connection; using mRemoteNG.Container; using mRemoteNG.Messages; using mRemoteNG.Security; @@ -12,19 +18,11 @@ using mRemoteNG.Security.SymmetricEncryption; using mRemoteNG.Tools; using mRemoteNG.Tree; using mRemoteNG.Tree.Root; -using System; -using System.Collections.Generic; -using System.Data.SqlClient; -using System.Globalization; -using System.Linq; -using System.Security; -using mRemoteNG.Connection; namespace mRemoteNG.Config.Connections { - public class SqlConnectionsSaver : ISaver + public class SqlConnectionsSaver : ISaver { - private SecureString _password = Runtime.EncryptionKey; private readonly SaveFilter _saveFilter; private readonly ISerializer, string> _localPropertiesSerializer; private readonly IDataProvider _dataProvider; @@ -75,7 +73,7 @@ namespace mRemoteNG.Config.Connections return; } - UpdateRootNodeTable(rootTreeNode, sqlConnector); + metaDataRetriever.WriteDatabaseMetaData(rootTreeNode, sqlConnector); UpdateConnectionsTable(rootTreeNode, sqlConnector); UpdateUpdatesTable(sqlConnector); } @@ -121,17 +119,17 @@ namespace mRemoteNG.Config.Connections { if (rootTreeNode.Password) { - _password = rootTreeNode.PasswordString.ConvertToSecureString(); - strProtected = cryptographyProvider.Encrypt("ThisIsProtected", _password); + var password = rootTreeNode.PasswordString.ConvertToSecureString(); + strProtected = cryptographyProvider.Encrypt("ThisIsProtected", password); } else { - strProtected = cryptographyProvider.Encrypt("ThisIsNotProtected", _password); + strProtected = cryptographyProvider.Encrypt("ThisIsNotProtected", Runtime.EncryptionKey); } } else { - strProtected = cryptographyProvider.Encrypt("ThisIsNotProtected", _password); + strProtected = cryptographyProvider.Encrypt("ThisIsNotProtected", Runtime.EncryptionKey); } var sqlQuery = new SqlCommand("DELETE FROM tblRoot", sqlDatabaseConnector.SqlConnection); diff --git a/mRemoteV1/Config/Serializers/ConnectionSerializers/MsSql/SqlDatabaseMetaDataRetriever.cs b/mRemoteV1/Config/Serializers/ConnectionSerializers/MsSql/SqlDatabaseMetaDataRetriever.cs index 2bfa7ef66..16c34352b 100644 --- a/mRemoteV1/Config/Serializers/ConnectionSerializers/MsSql/SqlDatabaseMetaDataRetriever.cs +++ b/mRemoteV1/Config/Serializers/ConnectionSerializers/MsSql/SqlDatabaseMetaDataRetriever.cs @@ -2,12 +2,17 @@ using System.Data.SqlClient; using System.Globalization; using mRemoteNG.App; +using mRemoteNG.App.Info; using mRemoteNG.Config.DatabaseConnectors; using mRemoteNG.Messages; +using mRemoteNG.Security; +using mRemoteNG.Security.SymmetricEncryption; +using mRemoteNG.Tools; +using mRemoteNG.Tree.Root; namespace mRemoteNG.Config.Serializers.MsSql { - public class SqlDatabaseMetaDataRetriever + public class SqlDatabaseMetaDataRetriever { public SqlConnectionListMetaData GetDatabaseMetaData(SqlDatabaseConnector sqlDatabaseConnector) { @@ -46,5 +51,45 @@ namespace mRemoteNG.Config.Serializers.MsSql return metaData; } + + public void WriteDatabaseMetaData(RootNodeInfo rootTreeNode, SqlDatabaseConnector sqlDatabaseConnector) + { + var cryptographyProvider = new LegacyRijndaelCryptographyProvider(); + string strProtected; + if (rootTreeNode != null) + { + if (rootTreeNode.Password) + { + var password = rootTreeNode.PasswordString.ConvertToSecureString(); + strProtected = cryptographyProvider.Encrypt("ThisIsProtected", password); + } + else + { + strProtected = cryptographyProvider.Encrypt("ThisIsNotProtected", Runtime.EncryptionKey); + } + } + else + { + strProtected = cryptographyProvider.Encrypt("ThisIsNotProtected", Runtime.EncryptionKey); + } + + var sqlQuery = new SqlCommand("DELETE FROM tblRoot", sqlDatabaseConnector.SqlConnection); + sqlQuery.ExecuteNonQuery(); + + if (rootTreeNode != null) + { + sqlQuery = + new SqlCommand( + "INSERT INTO tblRoot (Name, Export, Protected, ConfVersion) VALUES(\'" + + MiscTools.PrepareValueForDB(rootTreeNode.Name) + "\', 0, \'" + strProtected + "\'," + + ConnectionsFileInfo.ConnectionFileVersion.ToString(CultureInfo.InvariantCulture) + ")", + sqlDatabaseConnector.SqlConnection); + sqlQuery.ExecuteNonQuery(); + } + else + { + Runtime.MessageCollector.AddMessage(MessageClass.ErrorMsg, $"UpdateRootNodeTable: rootTreeNode was null. Could not insert!"); + } + } } } \ No newline at end of file diff --git a/mRemoteV1/Config/Serializers/MiscSerializers/RemoteDesktopConnectionManagerDeserializer.cs b/mRemoteV1/Config/Serializers/MiscSerializers/RemoteDesktopConnectionManagerDeserializer.cs index 53c1bc371..451eb2ce6 100644 --- a/mRemoteV1/Config/Serializers/MiscSerializers/RemoteDesktopConnectionManagerDeserializer.cs +++ b/mRemoteV1/Config/Serializers/MiscSerializers/RemoteDesktopConnectionManagerDeserializer.cs @@ -13,7 +13,7 @@ using mRemoteNG.Tree.Root; namespace mRemoteNG.Config.Serializers { - public class RemoteDesktopConnectionManagerDeserializer : IDeserializer + public class RemoteDesktopConnectionManagerDeserializer : IDeserializer { private static int _schemaVersion; /* 1 = RDCMan v2.2 3 = RDCMan v2.7 */ @@ -40,11 +40,15 @@ namespace mRemoteNG.Config.Serializers private static void VerifySchemaVersion(XmlNode rdcManNode) { - _schemaVersion = Convert.ToInt32(rdcManNode?.Attributes?["schemaVersion"].Value); - if (_schemaVersion != 1 && _schemaVersion != 3) + if (!int.TryParse(rdcManNode?.Attributes?["schemaVersion"]?.Value, out var version)) + throw new FileFormatException("Could not find schema version attribute."); + + if (version != 1 && version != 3) { - throw (new FileFormatException($"Unsupported schema version ({_schemaVersion}).")); + throw new FileFormatException($"Unsupported schema version ({version})."); } + + _schemaVersion = version; } private static void VerifyFileVersion(XmlNode rdcManNode) @@ -114,10 +118,9 @@ namespace mRemoteNG.Config.Serializers // Program Version 2.7 wraps these properties containerPropertiesNode = containerPropertiesNode.SelectSingleNode("./properties"); } - newContainer.Name = containerPropertiesNode?.SelectSingleNode("./name")?.InnerText ?? Language.strNewFolder; - newContainer.IsExpanded = - bool.Parse(containerPropertiesNode?.SelectSingleNode("./expanded")?.InnerText ?? "false"); + if (bool.TryParse(containerPropertiesNode?.SelectSingleNode("./expanded")?.InnerText, out var expanded)) + newContainer.IsExpanded = expanded; parentContainer.AddChild(newContainer); return newContainer; } @@ -132,13 +135,19 @@ namespace mRemoteNG.Config.Serializers { var connectionInfo = new ConnectionInfo {Protocol = ProtocolType.RDP}; - var propertiesNode = xmlNode.SelectSingleNode("./properties"); if (_schemaVersion == 1) - propertiesNode = xmlNode; // Version 2.2 defines the container name at the root instead + propertiesNode = xmlNode; // Version 2.2 defines the container name at the root instead + connectionInfo.Hostname = propertiesNode?.SelectSingleNode("./name")?.InnerText ?? ""; - connectionInfo.Name = - propertiesNode?.SelectSingleNode("./displayName")?.InnerText ?? connectionInfo.Hostname; + + var connectionDisplayName = propertiesNode?.SelectSingleNode("./displayName")?.InnerText; + connectionInfo.Name = !string.IsNullOrWhiteSpace(connectionDisplayName) + ? connectionDisplayName + : string.IsNullOrWhiteSpace(connectionInfo.Hostname) + ? connectionInfo.Name + : connectionInfo.Hostname; + connectionInfo.Description = propertiesNode?.SelectSingleNode("./comment")?.InnerText ?? string.Empty; var logonCredentialsNode = xmlNode.SelectSingleNode("./logonCredentials"); @@ -170,11 +179,12 @@ namespace mRemoteNG.Config.Serializers var connectionSettingsNode = xmlNode.SelectSingleNode("./connectionSettings"); if (connectionSettingsNode?.Attributes?["inherit"]?.Value == "None") { - connectionInfo.UseConsoleSession = - bool.Parse(connectionSettingsNode.SelectSingleNode("./connectToConsole")?.InnerText ?? "false"); + if (bool.TryParse(connectionSettingsNode.SelectSingleNode("./connectToConsole")?.InnerText, out var useConsole)) + connectionInfo.UseConsoleSession = useConsole; // ./startProgram // ./workingDir - connectionInfo.Port = Convert.ToInt32(connectionSettingsNode.SelectSingleNode("./port")?.InnerText); + if (int.TryParse(connectionSettingsNode.SelectSingleNode("./port")?.InnerText, out var port)) + connectionInfo.Port = port; } else { @@ -214,17 +224,10 @@ namespace mRemoteNG.Config.Serializers var remoteDesktopNode = xmlNode.SelectSingleNode("./remoteDesktop"); if (remoteDesktopNode?.Attributes?["inherit"]?.Value == "None") { - var resolutionString = remoteDesktopNode.SelectSingleNode("./size")?.InnerText.Replace(" ", ""); - try - { - connectionInfo.Resolution = - (RdpProtocol.RDPResolutions)Enum.Parse(typeof(RdpProtocol.RDPResolutions), - "Res" + resolutionString); - } - catch (ArgumentException) - { - connectionInfo.Resolution = RdpProtocol.RDPResolutions.FitToWindow; - } + connectionInfo.Resolution = + Enum.TryParse(remoteDesktopNode.SelectSingleNode("./size")?.InnerText.Replace(" ", ""), true, out var rdpResolution) + ? rdpResolution + : RdpProtocol.RDPResolutions.FitToWindow; if (remoteDesktopNode.SelectSingleNode("./sameSizeAsClientArea")?.InnerText == "True") { @@ -236,10 +239,8 @@ namespace mRemoteNG.Config.Serializers connectionInfo.Resolution = RdpProtocol.RDPResolutions.Fullscreen; } - var colorDepth = remoteDesktopNode.SelectSingleNode("./colorDepth")?.InnerText; - if (colorDepth != null) - connectionInfo.Colors = - (RdpProtocol.RDPColors)Enum.Parse(typeof(RdpProtocol.RDPColors), colorDepth); + if (Enum.TryParse(remoteDesktopNode.SelectSingleNode("./colorDepth")?.InnerText, true, out var rdpColors)) + connectionInfo.Colors = rdpColors; } else { @@ -288,16 +289,20 @@ namespace mRemoteNG.Config.Serializers } // ./redirectClipboard - connectionInfo.RedirectDiskDrives = - bool.Parse(localResourcesNode?.SelectSingleNode("./redirectDrives")?.InnerText ?? "false"); - connectionInfo.RedirectPorts = - bool.Parse(localResourcesNode?.SelectSingleNode("./redirectPorts")?.InnerText ?? "false"); - connectionInfo.RedirectPrinters = - bool.Parse(localResourcesNode?.SelectSingleNode("./redirectPrinters")?.InnerText ?? "false"); - connectionInfo.RedirectSmartCards = - bool.Parse(localResourcesNode?.SelectSingleNode("./redirectSmartCards")?.InnerText ?? "false"); - connectionInfo.RedirectClipboard = - bool.Parse(localResourcesNode?.SelectSingleNode("./redirectClipboard")?.InnerText ?? "false"); + if (bool.TryParse(localResourcesNode?.SelectSingleNode("./redirectDrives")?.InnerText, out var redirectDisks)) + connectionInfo.RedirectDiskDrives = redirectDisks; + + if (bool.TryParse(localResourcesNode?.SelectSingleNode("./redirectPorts")?.InnerText, out var redirectPorts)) + connectionInfo.RedirectPorts = redirectPorts; + + if (bool.TryParse(localResourcesNode?.SelectSingleNode("./redirectPrinters")?.InnerText, out var redirectPrinters)) + connectionInfo.RedirectPrinters = redirectPrinters; + + if (bool.TryParse(localResourcesNode?.SelectSingleNode("./redirectSmartCards")?.InnerText, out var redirectSmartCards)) + connectionInfo.RedirectSmartCards = redirectSmartCards; + + if (bool.TryParse(localResourcesNode?.SelectSingleNode("./redirectClipboard")?.InnerText, out var redirectClipboard)) + connectionInfo.RedirectClipboard = redirectClipboard; } else { diff --git a/mRemoteV1/Connection/ConnectionsService.cs b/mRemoteV1/Connection/ConnectionsService.cs index fc6382a24..aeb8352ca 100644 --- a/mRemoteV1/Connection/ConnectionsService.cs +++ b/mRemoteV1/Connection/ConnectionsService.cs @@ -1,5 +1,10 @@ -using mRemoteNG.App; +using System; +using System.IO; +using System.Threading; +using System.Windows.Forms; +using mRemoteNG.App; using mRemoteNG.App.Info; +using mRemoteNG.Config; using mRemoteNG.Config.Connections; using mRemoteNG.Config.Connections.Multiuser; using mRemoteNG.Config.DataProviders; @@ -12,15 +17,10 @@ using mRemoteNG.Tools; using mRemoteNG.Tree; using mRemoteNG.Tree.Root; using mRemoteNG.UI; -using System; -using System.IO; -using System.Threading; -using System.Windows.Forms; -using mRemoteNG.Config; namespace mRemoteNG.Connection { - public class ConnectionsService + public class ConnectionsService { private static readonly object SaveLock = new object(); private readonly PuttySessionsManager _puttySessionsManager; @@ -179,6 +179,19 @@ namespace mRemoteNG.Connection SaveConnections(); } + /// + /// All calls to or + /// will be deferred until the returned is disposed. + /// Once disposed, this will immediately executes a single + /// or if one has been requested. + /// Place this call in a 'using' block to represent a batched saving context. + /// + /// + public DisposableAction BatchedSavingContext() + { + return new DisposableAction(BeginBatchingSaves, EndBatchingSaves); + } + /// /// Saves the currently loaded with /// no . diff --git a/mRemoteV1/Tools/DisposableAction.cs b/mRemoteV1/Tools/DisposableAction.cs new file mode 100644 index 000000000..19f2f86b2 --- /dev/null +++ b/mRemoteV1/Tools/DisposableAction.cs @@ -0,0 +1,45 @@ +using System; + +namespace mRemoteNG.Tools +{ + /// + /// Represents an action that will be executed when the + /// method is called. Useful for creating Using blocks around logical start/end + /// actions. + /// + public class DisposableAction : IDisposable + { + private bool _isDisposed; + private readonly Action _disposeAction; + + /// + /// + /// + /// + /// An that should be performed immediately + /// when this object is initialized. It should return quickly. + /// + /// + /// An to be executed when this object is disposed. + /// + public DisposableAction(Action initializeAction, Action disposeAction) + { + initializeAction(); + _disposeAction = disposeAction; + } + + public void Dispose() + { + Dispose(true); + } + + protected virtual void Dispose(bool disposing) + { + if (!disposing || _isDisposed) + return; + + _isDisposed = true; + _disposeAction(); + } + } +} diff --git a/mRemoteV1/mRemoteV1.csproj b/mRemoteV1/mRemoteV1.csproj index 7b55bc964..d59dab423 100644 --- a/mRemoteV1/mRemoteV1.csproj +++ b/mRemoteV1/mRemoteV1.csproj @@ -326,6 +326,7 @@ +