diff --git a/Libraries/Opc.Ua.PubSub/Transport/MqttClientProtocolConfiguration.cs b/Libraries/Opc.Ua.PubSub/Transport/MqttClientProtocolConfiguration.cs index 0e873b81c..bce34f691 100644 --- a/Libraries/Opc.Ua.PubSub/Transport/MqttClientProtocolConfiguration.cs +++ b/Libraries/Opc.Ua.PubSub/Transport/MqttClientProtocolConfiguration.cs @@ -144,11 +144,11 @@ public MqttTlsCertificates(ArrayOf keyValuePairs) internal ArrayOf KeyValuePairs { get; set; } - internal List X509Certificates + internal List X509Certificates { get { - var values = new List(); + var values = new List(); if (m_caCertificate != null) { values.Add(m_caCertificate); @@ -157,7 +157,6 @@ internal List X509Certificates { values.Add(m_clientCertificate); } - return values; } } diff --git a/Libraries/Opc.Ua.PubSub/Transport/MqttPubSubConnection.cs b/Libraries/Opc.Ua.PubSub/Transport/MqttPubSubConnection.cs index abfd374ef..9dc16dc51 100644 --- a/Libraries/Opc.Ua.PubSub/Transport/MqttPubSubConnection.cs +++ b/Libraries/Opc.Ua.PubSub/Transport/MqttPubSubConnection.cs @@ -802,15 +802,8 @@ private MqttClientOptions GetMqttClientOptions() var x509Certificate2s = new List(); if (mqttTlsOptions?.Certificates != null) { - foreach (X509Certificate x509cert in mqttTlsOptions?.Certificates - .X509Certificates) - { - if (x509cert is X509Certificate2 x509Certificate2) - { - x509Certificate2s.Add( - CertificateFactory.Create(x509Certificate2.RawData)); - } - } + x509Certificate2s.AddRange(mqttTlsOptions?.Certificates + .X509Certificates); } MqttClientOptionsBuilder mqttClientOptionsBuilder diff --git a/Tests/Opc.Ua.PubSub.Tests/Encoding/MessagesHelper.cs b/Tests/Opc.Ua.PubSub.Tests/Encoding/MessagesHelper.cs index 36ebe790e..bef2ec512 100644 --- a/Tests/Opc.Ua.PubSub.Tests/Encoding/MessagesHelper.cs +++ b/Tests/Opc.Ua.PubSub.Tests/Encoding/MessagesHelper.cs @@ -145,10 +145,8 @@ public static PubSubConnectionDataType GetConnection( { if (pubSubConfiguration != null) { -#pragma warning disable CS0618 // Type or member is obsolete return pubSubConfiguration.Connections - .Find(x => x.PublisherId.Value.Equals(publisherId)); -#pragma warning restore CS0618 // Type or member is obsolete + .Find(x => x.PublisherId.Equals(publisherId)); } return null; } diff --git a/Tests/Opc.Ua.PubSub.Tests/Transport/MqttPubSubConnectionTests.Mqtts.cs b/Tests/Opc.Ua.PubSub.Tests/Transport/MqttPubSubConnectionTests.Mqtts.cs new file mode 100644 index 000000000..8915489b1 --- /dev/null +++ b/Tests/Opc.Ua.PubSub.Tests/Transport/MqttPubSubConnectionTests.Mqtts.cs @@ -0,0 +1,393 @@ +/* ======================================================================== + * Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved. + * + * OPC Foundation MIT License 1.00 + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + * The complete license agreement can be found here: + * http://opcfoundation.org/License/MIT/1.00/ + * ======================================================================*/ + +using System.Collections.Generic; +using System.Threading; +#if !NET8_0_OR_GREATER +using MQTTnet.Client; +#endif +using NUnit.Framework; +using Opc.Ua.PubSub.Tests.Encoding; +using Opc.Ua.PubSub.Transport; +using Opc.Ua.Tests; +using PubSubEncoding = Opc.Ua.PubSub.Encoding; +using System.Linq; +using System.Diagnostics; +using System.Security.Cryptography.X509Certificates; +using MQTTnet; +using Opc.Ua.Security.Certificates; +using System; +using System.IO; +using System.Security.Cryptography; +using System.Text; + +namespace Opc.Ua.PubSub.Tests.Transport +{ + [TestFixture(Description = "Tests for Mqtt connections")] + public partial class MqttPubSubConnectionTests + { + internal const string MqttsUrlFormat = $"{Utils.UriSchemeMqtts}://{{0}}:8883"; + + [Test] + public void ClientCertificateHasPrivateKey() + { + using X509Certificate2 cert = CertificateBuilder.Create("CN=Subject").CreateForRSA(); + using TestCertificateDirectory certificateDirectory = new(); + certificateDirectory.CreateAssets(); + + ITelemetryContext telemetry = NUnitTelemetryContext.Create(); + var mqttTlsCertificates = new MqttTlsCertificates(caCertificatePath: null, certificateDirectory.ClientCertificatePfxPath); + var mqttTlsOptions = new MqttTlsOptions(certificates: mqttTlsCertificates); + + var mqttConfiguration = new MqttClientProtocolConfiguration( + version: EnumMqttProtocolVersion.V500, mqttTlsOptions: mqttTlsOptions); + + var uaPubSubApplication = UaPubSubApplication.Create(telemetry); + var pubSubConnectionDataType = new PubSubConnectionDataType + { + Address = new ExtensionObject(new NetworkAddressUrlDataType { Url = "mqtts://localhost:8883" }), + ConnectionProperties = mqttConfiguration.ConnectionProperties + }; + + var pubSubConnection = new MqttPubSubConnection(uaPubSubApplication, pubSubConnectionDataType, MessageMapping.Json, telemetry); + MqttClientOptions mqttClientOptions = pubSubConnection.PublisherMqttClientOptions; + MqttClientTlsOptions channelTlsOptions = mqttClientOptions.ChannelOptions.TlsOptions; + + Assert.That(channelTlsOptions.UseTls, Is.True); + X509CertificateCollection clientCertificates = channelTlsOptions.ClientCertificatesProvider.GetCertificates(); + Assert.That(clientCertificates.Count, Is.EqualTo(1)); + Assert.That((clientCertificates[0] as X509Certificate2)!.HasPrivateKey, Is.True, "Client certificate needs private key"); + } + +#if NET7_0_OR_GREATER + [Test(Description = "Validate mqtts local pub/sub connection with json data.")] +#if !CUSTOM_TESTS + [Ignore("A mosquitto tool should be installed local in order to run correctly.")] +#endif + public void ValidateMqttsLocalPubSubConnectionWithJson( + [ValueSource(nameof(s_validPublisherIds))] Variant publisherId, + [Values(0, 10000)] double metaDataUpdateTime) + { + using TestCertificateDirectory certificateDirectory = new(); + certificateDirectory.CreateAssets(); + + ITelemetryContext telemetry = NUnitTelemetryContext.Create(); + using Process process = RestartMosquitto($"-v -c \"{certificateDirectory.MosquittoConfigFilePath}\""); + + //Arrange + const ushort writerGroupId = 1; + + string mqttLocalBrokerUrl = Utils.Format( + MqttsUrlFormat, + "localhost"); + + var mqttTlsCertificates = new MqttTlsCertificates( + clientCertificatePath: certificateDirectory.ClientCertificatePfxPath); + var mqttTlsOptions = new MqttTlsOptions(certificates: mqttTlsCertificates); + + var mqttConfiguration = new MqttClientProtocolConfiguration( + version: EnumMqttProtocolVersion.V500, + mqttTlsOptions: mqttTlsOptions); + + const JsonNetworkMessageContentMask jsonNetworkMessageContentMask = + JsonNetworkMessageContentMask.NetworkMessageHeader | + JsonNetworkMessageContentMask.PublisherId | + JsonNetworkMessageContentMask.DataSetMessageHeader; + const JsonDataSetMessageContentMask jsonDataSetMessageContentMask + = JsonDataSetMessageContentMask.None; + + const DataSetFieldContentMask dataSetFieldContentMask = DataSetFieldContentMask.None; + + var dataSetMetaDataArray = new DataSetMetaDataType[] + { + MessagesHelper.CreateDataSetMetaData1("DataSet1"), + MessagesHelper.CreateDataSetMetaData2("DataSet2"), + MessagesHelper.CreateDataSetMetaData3("DataSet3"), + MessagesHelper.CreateDataSetMetaDataAllTypes("DataSet4") + }; + + PubSubConfigurationDataType publisherConfiguration = MessagesHelper + .CreatePublisherConfiguration( + Profiles.PubSubMqttJsonTransport, + mqttLocalBrokerUrl, + publisherId: publisherId, + writerGroupId: writerGroupId, + jsonNetworkMessageContentMask: jsonNetworkMessageContentMask, + jsonDataSetMessageContentMask: jsonDataSetMessageContentMask, + dataSetFieldContentMask: dataSetFieldContentMask, + dataSetMetaDataArray: dataSetMetaDataArray, + nameSpaceIndexForData: kNamespaceIndexAllTypes, + metaDataUpdateTime: metaDataUpdateTime); + Assert.That(publisherConfiguration, Is.Not.Null, "publisherConfiguration should not be null"); + + // Configure the mqtt publisher configuration with the MQTTbroker + PubSubConnectionDataType mqttPublisherConnection = MessagesHelper.GetConnection( + publisherConfiguration, + publisherId); + Assert.That(mqttPublisherConnection, Is.Not.Null, "The MQTT publisher connection is invalid."); + mqttPublisherConnection.ConnectionProperties = mqttConfiguration.ConnectionProperties; + Assert.That( + mqttPublisherConnection.ConnectionProperties.IsNull, + Is.False, + "The MQTT publisher connection properties are not valid."); + + // Create publisher application for multiple datasets + UaPubSubApplication publisherApplication = UaPubSubApplication.Create(publisherConfiguration, telemetry); + publisherApplication.OnValidateBrokerCertificate = certificateDirectory.ValidateBrokerCertificate; + MessagesHelper.LoadData(publisherApplication, kNamespaceIndexAllTypes); + + IUaPubSubConnection publisherConnection = publisherApplication.PubSubConnections[0]; + Assert.That(publisherConnection, Is.Not.Null, "Publisher first connection should not be null"); + + Assert.That( + publisherConfiguration.Connections[0], + Is.Not.Null, + "publisherConfiguration first connection should not be null"); + Assert.That( + publisherConfiguration.Connections[0].WriterGroups[0], + Is.Not.Null, + "publisherConfiguration first writer group of first connection should not be null"); + + IList networkMessages = publisherConnection.CreateNetworkMessages( + publisherConfiguration.Connections[0].WriterGroups[0], + new WriterGroupPublishState()); + Assert.That( + networkMessages, + Is.Not.Null, + "connection.CreateNetworkMessages shall not return null"); + Assert.That( + networkMessages, + Is.Not.Empty, + "connection.CreateNetworkMessages shall have at least one network message"); + + List uaNetworkMessages = MessagesHelper + .GetJsonUaDataNetworkMessages( + [.. networkMessages.Cast()]); + Assert.That( + uaNetworkMessages, + Is.Not.Null, + "Json ua-data entries are missing from configuration!"); + + List uaMetaDataNetworkMessages = + MessagesHelper.GetJsonUaMetaDataNetworkMessages( + [.. networkMessages.Cast()]); + Assert.That( + uaMetaDataNetworkMessages, + Is.Not.Null, + "Json ua-metadata entries are missing from configuration!"); + + const bool hasDataSetWriterId = + (jsonNetworkMessageContentMask & + JsonNetworkMessageContentMask.DataSetMessageHeader) != 0 && + (jsonDataSetMessageContentMask & + JsonDataSetMessageContentMask.DataSetWriterId) != 0; + + PubSubConfigurationDataType subscriberConfiguration = MessagesHelper + .CreateSubscriberConfiguration( + Profiles.PubSubMqttJsonTransport, + mqttLocalBrokerUrl, + publisherId: publisherId, + writerGroupId: writerGroupId, + setDataSetWriterId: hasDataSetWriterId, + jsonNetworkMessageContentMask: jsonNetworkMessageContentMask, + jsonDataSetMessageContentMask: jsonDataSetMessageContentMask, + dataSetFieldContentMask: dataSetFieldContentMask, + dataSetMetaDataArray: dataSetMetaDataArray, + nameSpaceIndexForData: kNamespaceIndexAllTypes); + Assert.That(subscriberConfiguration, Is.Not.Null, "subscriberConfiguration should not be null"); + + // Configure the mqtt subscriber configuration with the MQTTbroker + PubSubConnectionDataType mqttSubscriberConnection = MessagesHelper.GetConnection( + subscriberConfiguration, + publisherId); + Assert.That( + mqttSubscriberConnection, + Is.Not.Null, + "The MQTT subscriber connection is invalid."); + mqttSubscriberConnection.ConnectionProperties = mqttConfiguration.ConnectionProperties; + Assert.That( + mqttSubscriberConnection.ConnectionProperties.IsNull, + Is.False, + "The MQTT subscriber connection properties are not valid."); + + // Create subscriber application for multiple datasets + UaPubSubApplication subscriberApplication = UaPubSubApplication.Create(subscriberConfiguration, telemetry); + subscriberApplication.OnValidateBrokerCertificate = certificateDirectory.ValidateBrokerCertificate; + Assert.That(subscriberApplication, Is.Not.Null, "subscriberApplication should not be null"); + Assert.That( + subscriberApplication.PubSubConnections[0], + Is.Not.Null, + "subscriberConfiguration first connection should not be null"); + + List dataSetReaders = subscriberApplication + .PubSubConnections[0] + .GetOperationalDataSetReaders(); + Assert.That(dataSetReaders, Is.Not.Null, "dataSetReaders should not be null"); + IUaPubSubConnection subscriberConnection = subscriberApplication.PubSubConnections[0]; + Assert.That( + subscriberConnection, + Is.Not.Null, + "Subscriber first connection should not be null"); + + //Act + // it will signal if the mqtt message was received from local ip + m_uaDataShutdownEvent = new ManualResetEvent(false); + // it will signal if the mqtt metadata message was received from local ip + m_uaMetaDataShutdownEvent = new ManualResetEvent(false); + // it will signal if the changed configuration message was received on local ip + m_uaConfigurationUpdateEvent = new ManualResetEvent(false); + + m_isDeltaFrame = false; + subscriberApplication.DataReceived += UaPubSubApplication_DataReceived; + subscriberApplication.MetaDataReceived += UaPubSubApplication_MetaDataReceived; + subscriberApplication.ConfigurationUpdating + += UaPubSubApplication_ConfigurationUpdating; + subscriberConnection.Start(); + + publisherConnection.Start(); + + //Assert + if (!m_uaDataShutdownEvent.WaitOne(kEstimatedPublishingTime)) + { + Assert.Fail("The JSON message was not received"); + } + if (!m_uaMetaDataShutdownEvent.WaitOne(kEstimatedPublishingTime)) + { + Assert.Fail("The JSON metadata message was not received"); + } + + subscriberConnection.Stop(); + publisherConnection.Stop(); + } +#endif + + /// + /// Creates a temp directory with client and server certificate files and a mqtts mosquitto config. + /// Deletes the directory on dispose. + /// + private sealed class TestCertificateDirectory : IDisposable + { + private readonly string m_path; + private readonly X509Certificate2 m_clientCert; + private readonly X509Certificate2 m_serverCert; + + public TestCertificateDirectory() + { + m_path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + + m_clientCert = CertificateBuilder.Create("CN=Client").CreateForRSA(); + m_serverCert = CertificateBuilder.Create("CN=Server").CreateForRSA(); + } + + public void CreateAssets() + { + Directory.CreateDirectory(m_path); + + ClientCertificatePfxPath = CombinePath("client.pfx"); + string clientCertificateDerPath = CombinePath("client.der"); + string clientCertificateCrtPath = CombinePath("client.crt"); + string serverCertificateDerPath = CombinePath("server.der"); + string serverCertificateKeyPath = CombinePath("server.key"); + + File.WriteAllBytes(ClientCertificatePfxPath, m_clientCert.Export(X509ContentType.Pfx)); + File.WriteAllBytes(clientCertificateDerPath, m_clientCert.Export(X509ContentType.Cert)); +#if NET7_0_OR_GREATER + string clientCertificatePem = m_clientCert.ExportCertificatePem(); + File.WriteAllText(clientCertificateCrtPath, clientCertificatePem); + + ServerCertificateCertPath = CombinePath("server.crt"); + + string serverCertificatePem = m_serverCert.ExportCertificatePem(); + + AsymmetricAlgorithm key = m_serverCert.GetRSAPrivateKey(); + string privKeyPem = key.ExportPkcs8PrivateKeyPem(); + + File.WriteAllText(serverCertificateKeyPath, privKeyPem); + File.WriteAllText(ServerCertificateCertPath, serverCertificatePem); +#endif + File.WriteAllBytes(serverCertificateDerPath, m_serverCert.Export(X509ContentType.Cert)); + + string mosquittoTlsConfig = CreateMosquittoTlsConfig(clientCertificateCrtPath, + serverCertificateKeyPath, ServerCertificateCertPath); + MosquittoConfigFilePath = CombinePath("mosquitto.conf"); + + File.WriteAllText(MosquittoConfigFilePath, mosquittoTlsConfig); + } + + public string MosquittoConfigFilePath { get; private set; } + public string ClientCertificatePfxPath { get; private set; } + public string ServerCertificateCertPath { get; private set; } + + private string CombinePath(string fileName) + { + return Path.Combine(m_path, fileName); + } + + private static string CreateMosquittoTlsConfig(string caFile, string keyFile, string certFile) + { + return new StringBuilder() + .AppendLine("listener 8883") + .Append("cafile ").AppendLine(caFile) + .Append("keyfile ").AppendLine(keyFile) + .Append("certfile ").AppendLine(certFile) + .AppendLine("require_certificate true") + .AppendLine("allow_anonymous true") + .AppendLine("log_type all") + .AppendLine("log_dest stderr") + .AppendLine("connection_messages true") + .ToString(); + } + + public void Dispose() + { +#pragma warning disable RCS1075 // Avoid empty catch clause that catches System.Exception + try + { + Directory.Delete(m_path, true); + m_clientCert?.Dispose(); + m_serverCert?.Dispose(); + } + catch (Exception) + { + } +#pragma warning restore RCS1075 // Avoid empty catch clause that catches System.Exception + } + + internal bool ValidateBrokerCertificate(X509Certificate2 brokerCertificate) + { + return string.Equals(brokerCertificate.Thumbprint, m_serverCert.Thumbprint, StringComparison.OrdinalIgnoreCase); + } + + public static implicit operator string(TestCertificateDirectory dir) + { + return dir?.m_path ?? string.Empty; + } + } + } +} diff --git a/Tests/Opc.Ua.PubSub.Tests/Transport/MqttPubSubConnectionTests.cs b/Tests/Opc.Ua.PubSub.Tests/Transport/MqttPubSubConnectionTests.cs index 9b8be8f28..abf42701d 100644 --- a/Tests/Opc.Ua.PubSub.Tests/Transport/MqttPubSubConnectionTests.cs +++ b/Tests/Opc.Ua.PubSub.Tests/Transport/MqttPubSubConnectionTests.cs @@ -44,7 +44,7 @@ namespace Opc.Ua.PubSub.Tests.Transport { [TestFixture(Description = "Tests for Mqtt connections")] - public class MqttPubSubConnectionTests + public partial class MqttPubSubConnectionTests { private const ushort kNamespaceIndexAllTypes = 3; @@ -57,7 +57,7 @@ public class MqttPubSubConnectionTests private const int kEstimatedPublishingTime = 60000; internal const string DefaultBrokerProcessName = "mosquitto"; - internal const string MqttUrlFormat = "{0}://{1}:1883"; + internal const string MqttUrlFormat = $"{Utils.UriSchemeMqtt}://{{0}}:1883"; private static readonly Variant[] s_validPublisherIds = [ @@ -90,14 +90,13 @@ public void ValidateMqttLocalPubSubConnectionWithUadp( [ValueSource(nameof(s_validPublisherIds))] Variant publisherId) { ITelemetryContext telemetry = NUnitTelemetryContext.Create(); - RestartMosquitto(); + using Process _ = RestartMosquitto(); //Arrange const ushort writerGroupId = 1; string mqttLocalBrokerUrl = Utils.Format( MqttUrlFormat, - Utils.UriSchemeMqtt, "localhost"); var mqttConfiguration = new MqttClientProtocolConfiguration( @@ -252,13 +251,12 @@ public void ValidateMqttLocalPubSubConnectionWithDeltaUadp( [Values(1, 2, 3, 4)] int keyFrameCount) { ITelemetryContext telemetry = NUnitTelemetryContext.Create(); - RestartMosquitto(); + using Process _ = RestartMosquitto(); //Arrange const ushort writerGroupId = 1; string mqttLocalBrokerUrl = Utils.Format( - MqttUrlFormat, Utils.UriSchemeMqtt, "localhost"); @@ -445,14 +443,13 @@ public void ValidateMqttLocalPubSubConnectionWithJson( [Values(0, 10000)] double metaDataUpdateTime) { ITelemetryContext telemetry = NUnitTelemetryContext.Create(); - RestartMosquitto(); + using Process _ = RestartMosquitto(); //Arrange const ushort writerGroupId = 1; string mqttLocalBrokerUrl = Utils.Format( MqttUrlFormat, - Utils.UriSchemeMqtt, "localhost"); var mqttConfiguration = new MqttClientProtocolConfiguration( @@ -636,14 +633,13 @@ public void ValidateMqttLocalPubSubConnectionWithDeltaJson( [Values(2, 3, 4)] int keyFrameCount) { ITelemetryContext telemetry = NUnitTelemetryContext.Create(); - RestartMosquitto(); + using Process _ = RestartMosquitto(); //Arrange const ushort writerGroupId = 1; string mqttLocalBrokerUrl = Utils.Format( MqttUrlFormat, - Utils.UriSchemeMqtt, "localhost"); var mqttConfiguration = new MqttClientProtocolConfiguration( @@ -895,7 +891,7 @@ private void UaPubSubApplication_ConfigurationUpdating( /// /// Start/stop local mosquitto /// - private static void RestartMosquitto(string arguments = "") + private static Process RestartMosquitto(string arguments = "") { try { @@ -903,32 +899,52 @@ private static void RestartMosquitto(string arguments = "") if (processes.Length > 0) { Process mosquittoProcess = processes[0]; - mosquittoProcess.Kill(); + try + { + mosquittoProcess.Kill(); +#if NET472 || NET48 + mosquittoProcess.WaitForExit(10); +#else + mosquittoProcess.WaitForExit(TimeSpan.FromSeconds(10)); +#endif + } + finally + { + mosquittoProcess?.Dispose(); + } } - using var process = new Process(); + var process = new Process(); string programFilesPath = Environment.Is64BitOperatingSystem ? Environment.GetEnvironmentVariable("ProgramW6432") : Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86); - process.StartInfo = new ProcessStartInfo( - Path.Combine( - programFilesPath, - Path.Combine(DefaultBrokerProcessName, $"{DefaultBrokerProcessName}.exe"))) + process.StartInfo = new ProcessStartInfo { - WindowStyle = ProcessWindowStyle.Hidden, - //startInfo.CreateNoWindow = true; - //startInfo.RedirectStandardOutput = true; - //startInfo.UseShellExecute = true; - //startInfo.Verb = "runas"; + FileName = Path.Combine( + programFilesPath, + Path.Combine(DefaultBrokerProcessName, $"{DefaultBrokerProcessName}.exe")), + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardError = true, Arguments = arguments }; + process.Start(); + process.ErrorDataReceived += ErrorHandler; + process.BeginErrorReadLine(); + return process; } catch (Exception) { Assert.Fail("The mosquitto could not be restarted!"); } + return null; + } + + private static void ErrorHandler(object sender, DataReceivedEventArgs e) + { + Debug.WriteLine($"MOSQUITTO {e.Data}"); } } }