diff --git a/Applications/ConsoleReferenceClient/UAClient.cs b/Applications/ConsoleReferenceClient/UAClient.cs index 5d29f2eb5f..5c273cdc86 100644 --- a/Applications/ConsoleReferenceClient/UAClient.cs +++ b/Applications/ConsoleReferenceClient/UAClient.cs @@ -178,11 +178,7 @@ public async Task ConnectAsync(string serverUrl, bool useSecurity = true, EndpointConfiguration endpointConfiguration = EndpointConfiguration.Create(m_configuration); ConfiguredEndpoint endpoint = new ConfiguredEndpoint(null, endpointDescription, endpointConfiguration); -#if NET6_0_OR_GREATER - var sessionFactory = TraceableSessionFactory.Instance; -#else var sessionFactory = DefaultSessionFactory.Instance; -#endif // Create the session var session = await sessionFactory.CreateAsync( diff --git a/Libraries/Opc.Ua.Client/ISession.cs b/Libraries/Opc.Ua.Client/ISession.cs index f7b1f6f87d..b0c3718994 100644 --- a/Libraries/Opc.Ua.Client/ISession.cs +++ b/Libraries/Opc.Ua.Client/ISession.cs @@ -63,7 +63,7 @@ namespace Opc.Ua.Client /// /// Manages a session with a server. /// - public interface ISession : ISessionClient, IDisposable + public interface ISession : ISessionClient { #region Events /// diff --git a/Libraries/Opc.Ua.Client/Session.cs b/Libraries/Opc.Ua.Client/Session.cs index 506719008c..333d86a255 100644 --- a/Libraries/Opc.Ua.Client/Session.cs +++ b/Libraries/Opc.Ua.Client/Session.cs @@ -1873,8 +1873,8 @@ public async Task> LoadDataTypeSystem(NodeId { if (StatusCode.IsNotBad(errors[ii].StatusCode)) { - namespaces[((NodeId)referenceNodeIds[ii])] = (string)nameSpaceValues[ii]; - } + namespaces[((NodeId)referenceNodeIds[ii])] = (string)nameSpaceValues[ii]; + } else { Utils.LogWarning("Failed to load namespace {0}: {1}", namespaceNodeIds[ii], errors[ii]); @@ -5646,13 +5646,13 @@ private void ProcessPublishResponse( // Validate publish time and reject old values. if (notificationMessage.PublishTime.AddMilliseconds(subscription.CurrentPublishingInterval * subscription.CurrentLifetimeCount) < DateTime.UtcNow) { - Utils.LogWarning("PublishTime {0} in publish response is too old for SubscriptionId {1}.", notificationMessage.PublishTime.ToLocalTime(), subscription.Id); + Utils.LogTrace("PublishTime {0} in publish response is too old for SubscriptionId {1}.", notificationMessage.PublishTime.ToLocalTime(), subscription.Id); } // Validate publish time and reject old values. if (notificationMessage.PublishTime > DateTime.UtcNow.AddMilliseconds(subscription.CurrentPublishingInterval * subscription.CurrentLifetimeCount)) { - Utils.LogWarning("PublishTime {0} in publish response is newer than actual time for SubscriptionId {1}.", notificationMessage.PublishTime.ToLocalTime(), subscription.Id); + Utils.LogTrace("PublishTime {0} in publish response is newer than actual time for SubscriptionId {1}.", notificationMessage.PublishTime.ToLocalTime(), subscription.Id); } // update subscription cache. diff --git a/Libraries/Opc.Ua.Configuration/ApplicationInstance.cs b/Libraries/Opc.Ua.Configuration/ApplicationInstance.cs index 7f8ddc1f91..f4b3eed7fe 100644 --- a/Libraries/Opc.Ua.Configuration/ApplicationInstance.cs +++ b/Libraries/Opc.Ua.Configuration/ApplicationInstance.cs @@ -522,7 +522,7 @@ public async Task CheckApplicationInstanceCertificate( else { var message = new StringBuilder(); - message.AppendLine("Thumbprint was explicitly specified in the configuration. "); + message.AppendLine("Thumbprint was explicitly specified in the configuration."); message.AppendLine("Cannot generate a new certificate."); throw ServiceResultException.Create(StatusCodes.BadConfigurationError, message.ToString()); } diff --git a/Stack/Opc.Ua.Bindings.Https/Stack/Https/HttpsTransportChannel.cs b/Stack/Opc.Ua.Bindings.Https/Stack/Https/HttpsTransportChannel.cs index 241f59d9c1..4ef389e0d8 100644 --- a/Stack/Opc.Ua.Bindings.Https/Stack/Https/HttpsTransportChannel.cs +++ b/Stack/Opc.Ua.Bindings.Https/Stack/Https/HttpsTransportChannel.cs @@ -235,6 +235,13 @@ public void Close() m_client?.Dispose(); } + /// + public Task CloseAsync(CancellationToken ct) + { + Close(); + return Task.CompletedTask; + } + /// /// The async result class for the Https transport. /// diff --git a/Stack/Opc.Ua.Core/Stack/Client/ClientBase.cs b/Stack/Opc.Ua.Core/Stack/Client/ClientBase.cs index aee835c628..05ff2ab329 100644 --- a/Stack/Opc.Ua.Core/Stack/Client/ClientBase.cs +++ b/Stack/Opc.Ua.Core/Stack/Client/ClientBase.cs @@ -278,16 +278,16 @@ public virtual StatusCode Close() /// /// Closes the channel using async call. /// - public virtual Task CloseAsync(CancellationToken ct = default) + public async virtual Task CloseAsync(CancellationToken ct = default) { if (m_channel != null) { - m_channel.Close(); + await m_channel.CloseAsync(ct).ConfigureAwait(false); m_channel = null; } m_authenticationToken = null; - return Task.FromResult(StatusCodes.Good); + return StatusCodes.Good; } /// diff --git a/Stack/Opc.Ua.Core/Stack/Client/UaChannelBase.cs b/Stack/Opc.Ua.Core/Stack/Client/UaChannelBase.cs index 4fb0135a4a..1442a4738f 100644 --- a/Stack/Opc.Ua.Core/Stack/Client/UaChannelBase.cs +++ b/Stack/Opc.Ua.Core/Stack/Client/UaChannelBase.cs @@ -356,6 +356,20 @@ public void Close() CloseChannel(); } + /// + /// Closes any existing secure channel. + /// + public async Task CloseAsync(CancellationToken ct) + { + if (m_uaBypassChannel != null) + { + await m_uaBypassChannel.CloseAsync(ct).ConfigureAwait(false); + return; + } + + CloseChannel(); + } + /// /// Begins an asynchronous operation to close the secure channel. /// diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryClientChannel.cs b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryClientChannel.cs index b200126a49..5b2412ce2f 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryClientChannel.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryClientChannel.cs @@ -294,6 +294,47 @@ public void Close(int timeout) Shutdown(StatusCodes.BadConnectionClosed); } + /// + /// Closes a connection with the server (async). + /// + public async Task CloseAsync(int timeout) + { + WriteOperation operation = InternalClose(timeout); + + // wait for the close to succeed. + if (operation != null) + { + try + { + await operation.EndAsync(timeout, false).ConfigureAwait(false); + } + catch (ServiceResultException e) + { + switch (e.StatusCode) + { + case StatusCodes.BadRequestInterrupted: + case StatusCodes.BadSecureChannelClosed: + { + break; + } + + default: + { + Utils.LogWarning(e, "ChannelId {0}: Could not gracefully close the channel. Reason={1}", ChannelId, e.Result.StatusCode); + break; + } + } + } + catch (Exception e) + { + Utils.LogError(e, "ChannelId {0}: Could not gracefully close the channel.", ChannelId); + } + } + + // shutdown. + Shutdown(StatusCodes.BadConnectionClosed); + } + /// /// Sends a request to the server. /// diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryTransportChannel.cs b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryTransportChannel.cs index 854228a6a2..992d0772c4 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryTransportChannel.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryTransportChannel.cs @@ -23,6 +23,8 @@ namespace Opc.Ua.Bindings /// public class UaSCUaBinaryTransportChannel : ITransportChannel, IMessageSocketChannel { + private const int kChannelCloseDefault = 1_000; + #region Constructors /// /// Create a transport channel from a message socket factory. @@ -223,7 +225,7 @@ public void Reconnect(ITransportWaitingConnection connection) { try { - channel.Close(1000); + channel.Close(kChannelCloseDefault); } catch (Exception e) { @@ -277,13 +279,34 @@ public void Close() { if (m_channel != null) { - m_channel.Close(1000); + m_channel.Close(kChannelCloseDefault); m_channel = null; } } } } + /// + /// Closes the secure channel (async). + /// + /// Thrown if any communication error occurs. + public async Task CloseAsync(CancellationToken ct) + { + UaSCUaBinaryClientChannel channel = null; + lock (m_lock) + { + if (m_channel != null) + { + channel = m_channel; + m_channel = null; + } + } + if (channel != null) + { + await channel.CloseAsync(kChannelCloseDefault, ct).ConfigureAwait(false); + } + } + /// /// Begins an asynchronous operation to close the secure channel. /// diff --git a/Stack/Opc.Ua.Core/Stack/Transport/ITransportChannel.cs b/Stack/Opc.Ua.Core/Stack/Transport/ITransportChannel.cs index b23893d0a8..cc02a3503a 100644 --- a/Stack/Opc.Ua.Core/Stack/Transport/ITransportChannel.cs +++ b/Stack/Opc.Ua.Core/Stack/Transport/ITransportChannel.cs @@ -138,6 +138,12 @@ IAsyncResult BeginOpen( /// Thrown if any communication error occurs. void Close(); + /// + /// Closes the secure channel (async). + /// + /// Thrown if any communication error occurs. + Task CloseAsync(CancellationToken ct); + /// /// Begins an asynchronous operation to close the secure channel. /// diff --git a/Tests/Opc.Ua.Client.Tests/ClientFixture.cs b/Tests/Opc.Ua.Client.Tests/ClientFixture.cs index 952263d77d..18082c1136 100644 --- a/Tests/Opc.Ua.Client.Tests/ClientFixture.cs +++ b/Tests/Opc.Ua.Client.Tests/ClientFixture.cs @@ -321,7 +321,9 @@ public async Task GetEndpoints(Uri url) using (var client = DiscoveryClient.Create(url, endpointConfiguration)) { - return await client.GetEndpointsAsync(null).ConfigureAwait(false); + var result = await client.GetEndpointsAsync(null).ConfigureAwait(false); + await client.CloseAsync().ConfigureAwait(false); + return result; } } diff --git a/Tests/Opc.Ua.Client.Tests/ClientTest.cs b/Tests/Opc.Ua.Client.Tests/ClientTest.cs index 35e036d458..c01733c31d 100644 --- a/Tests/Opc.Ua.Client.Tests/ClientTest.cs +++ b/Tests/Opc.Ua.Client.Tests/ClientTest.cs @@ -136,7 +136,10 @@ public async Task GetEndpointsAsync() using (var client = DiscoveryClient.Create(ServerUrl, endpointConfiguration)) { - Endpoints = await client.GetEndpointsAsync(null).ConfigureAwait(false); + Endpoints = await client.GetEndpointsAsync(null, CancellationToken.None).ConfigureAwait(false); + var statusCode = await client.CloseAsync(CancellationToken.None).ConfigureAwait(false); + Assert.AreEqual((StatusCode)StatusCodes.Good, statusCode); + TestContext.Out.WriteLine("Endpoints:"); foreach (var endpoint in Endpoints) { @@ -177,6 +180,9 @@ public async Task FindServersAsync() using (var client = DiscoveryClient.Create(ServerUrl, endpointConfiguration)) { var servers = await client.FindServersAsync(null).ConfigureAwait(false); + var statusCode = await client.CloseAsync(CancellationToken.None).ConfigureAwait(false); + Assert.AreEqual((StatusCode)StatusCodes.Good, statusCode); + foreach (var server in servers) { TestContext.Out.WriteLine("{0}", server.ApplicationName); @@ -202,6 +208,9 @@ public async Task FindServersOnNetworkAsync() try { var response = await client.FindServersOnNetworkAsync(null, 0, 100, null, CancellationToken.None).ConfigureAwait(false); + var statusCode = await client.CloseAsync(CancellationToken.None).ConfigureAwait(false); + Assert.AreEqual((StatusCode)StatusCodes.Good, statusCode); + foreach (ServerOnNetwork server in response.Servers) { TestContext.Out.WriteLine("{0}", server.ServerName); @@ -336,11 +345,35 @@ public async Task ConnectAndCloseAsync(string securityPolicy) var session = await ClientFixture.ConnectAsync(ServerUrl, securityPolicy, Endpoints).ConfigureAwait(false); Assert.NotNull(session); Session.SessionClosing += Session_Closing; - var result = await session.CloseAsync(5_000, closeChannel).ConfigureAwait(false); + var result = await session.CloseAsync(5_000, closeChannel, CancellationToken.None).ConfigureAwait(false); Assert.NotNull(result); session.Dispose(); } + [Test, Order(202)] + public async Task ConnectAndCloseAsyncReadAfterClose() + { + var securityPolicy = SecurityPolicies.Basic256Sha256; + using (var session = await ClientFixture.ConnectAsync(ServerUrl, securityPolicy, Endpoints).ConfigureAwait(false)) + { + Assert.NotNull(session); + Session.SessionClosing += Session_Closing; + + var nodeId = new NodeId(Opc.Ua.VariableIds.ServerStatusType_BuildInfo); + var node = await session.ReadNodeAsync(nodeId, CancellationToken.None).ConfigureAwait(false); + var value = await session.ReadValueAsync(nodeId, CancellationToken.None).ConfigureAwait(false); + + // keep channel open + var result = await session.CloseAsync(1_000, false).ConfigureAwait(false); + Assert.AreEqual((StatusCode)StatusCodes.Good, result); + + await Task.Delay(5_000).ConfigureAwait(false); + + var sre = Assert.ThrowsAsync(async () => await session.ReadNodeAsync(nodeId, CancellationToken.None).ConfigureAwait(false)); + Assert.AreEqual((StatusCode)StatusCodes.BadSessionIdInvalid, sre.StatusCode); + } + } + [Theory, Order(210)] public async Task ConnectAndReconnectAsync(bool reconnectAbort, bool useMaxReconnectPeriod) {