diff --git a/.azurepipelines/expected-packages.txt b/.azurepipelines/expected-packages.txt index 78da758690..ef913cc1b3 100644 --- a/.azurepipelines/expected-packages.txt +++ b/.azurepipelines/expected-packages.txt @@ -15,10 +15,12 @@ # package ids with this file in the same pull request that changes them. OPCFoundation.NetStandard.Opc.Ua.Bindings.Https +OPCFoundation.NetStandard.Opc.Ua.Bindings.Quic OPCFoundation.NetStandard.Opc.Ua.Client OPCFoundation.NetStandard.Opc.Ua.Client.ComplexTypes OPCFoundation.NetStandard.Opc.Ua.Configuration OPCFoundation.NetStandard.Opc.Ua.Core +OPCFoundation.NetStandard.Opc.Ua.Core.Channels OPCFoundation.NetStandard.Opc.Ua.Core.Diagnostics OPCFoundation.NetStandard.Opc.Ua.Core.Schema OPCFoundation.NetStandard.Opc.Ua.Core.Types diff --git a/docs/DataChannels.md b/docs/DataChannels.md index 95addfd2fb..d09baba4f8 100644 --- a/docs/DataChannels.md +++ b/docs/DataChannels.md @@ -36,14 +36,32 @@ it, and a peer that does use it never speaks first: it may not transmit a frame because an unrecognized `MessageType` is a protocol error that closes the SecureChannel, taking every Session, Subscription and Service call with it. -Consumers opt in by suppressing the experimental diagnostic: +Consumers opt in by referencing the package and suppressing the experimental diagnostic: ```xml + + + $(NoWarn);DataChannels ``` +## Where the code lives + +The engine ships in `OPCFoundation.NetStandard.Opc.Ua.Core.Channels` rather than in +`Opc.Ua.Core`. It is a consumer of the message-extension seam in Core: it registers as the +owner of the `STR` MessageType, and the SecureChannel decrypts, verifies and +sequence-checks every chunk before the engine sees it. Core itself carries no data channel +vocabulary โ€” `ISecureChannelMessageExtension` and `ISecureChannelMessageHost` name only "a +MessageType that is neither a Service call nor part of establishing the SecureChannel". + +Two things stay in Core because they are the SecureChannel's own concerns rather than the +feature's: `SequenceNumberBudget`, which tracks the sequence space every MessageType draws +on, and `UaSCSecureChannelRegistry`, which maps a SecureChannel identifier to the channel +that owns it. The `opc.quic` transport remains its own package, +`OPCFoundation.NetStandard.Opc.Ua.Bindings.Quic`. + ## Server side ```csharp @@ -253,8 +271,8 @@ second. The sample measured 0.5 Mbit/s before the fix and 1.3 Gbit/s after it, a The suite is 284 tests on `net10.0` and 212 tests on `net48` over `tests/Opc.Ua.Core.DataChannels.Tests`, covering -`Stack/DataChannels/**`, `Stack/Tcp/UaSCBinaryChannel.DataChannels.cs`, -`Server/InlineServerDataChannelTransport.cs` and `Opc.Ua.Bindings.Quic`. Coverage was +`Opc.Ua.Core.Channels`, `Stack/Tcp/UaSCBinaryChannel.MessageExtensions.cs` +and `Opc.Ua.Bindings.Quic`. Coverage was **80.1%** at 255 tests, having fallen from 87.6% while the test count rose, because wiring the previously-uncalled obligations added a good deal of production code โ€” the server-side Service dispatch, the stream mapping, the diff --git a/docs/README.md b/docs/README.md index 16a4cb90d1..2ba0ebedf3 100644 --- a/docs/README.md +++ b/docs/README.md @@ -27,7 +27,7 @@ Here is a list of available documentation for different topics: * Pluggable [Identity Providers](IdentityProviders.md) โ€” interfaces (`IClientIdentityProvider`, `IUserTokenAuthenticator`, `IAccessTokenProvider`, `ITokenIssuer`, `IIdentityClaims`) plus the OPC 10000-6 ยง6.5.2.2 `IssuerEndpointUrl` JSON parser for OAuth2 / OIDC / Entra / JWT flows. * Support for [ECC Certificates](EccProfiles.md). * Working with [ComplexTypes](ComplexTypes.md) - Custom structures and enumerations. -* [Data Channels](DataChannels.md) - **Experimental.** Inline streaming of opaque bytes over a SecureChannel: the `STR` frame, per-channel flow control and scheduling, and the `OpenDataChannel` / `ModifyDataChannel` / `CloseDataChannel` Service Set. Identifiers are provisional and the errata is not endorsed by the OPC Foundation. +* [Data Channels](DataChannels.md) - **Experimental.** Inline streaming of opaque bytes over a SecureChannel: the `STR` frame, per-channel flow control and scheduling, and the `OpenDataChannel` / `ModifyDataChannel` / `CloseDataChannel` Service Set. Ships as `OPCFoundation.NetStandard.Opc.Ua.Core.Channels`, attached to the SecureChannel through the message-extension seam in `Opc.Ua.Core`. Identifiers are provisional and the errata is not endorsed by the OPC Foundation. * Client-based [NodeSet Export](NodeSetExport.md) - Export server address space to NodeSet2 XML. * Source generated [DataTypes] - How to annotate POCO classes and let the source generator generate the `IEncodeable` implementation. * Runtime [Schema Generation](SchemaGeneration.md) - Produce XSD, OPC Binary (BSD) and JSON Schema (Part 6 Annex C, compact + verbose) for generated encodeable types and dynamically added complex types via the injectable `ISchemaProvider`; schemas are built as object models in code (trimmable, NativeAOT compatible). diff --git a/samples/ConsoleDataChannelStreaming/ConsoleDataChannelStreaming.csproj b/samples/ConsoleDataChannelStreaming/ConsoleDataChannelStreaming.csproj index 435078bee1..2f4c4ff181 100644 --- a/samples/ConsoleDataChannelStreaming/ConsoleDataChannelStreaming.csproj +++ b/samples/ConsoleDataChannelStreaming/ConsoleDataChannelStreaming.csproj @@ -20,6 +20,7 @@ + diff --git a/samples/ConsoleDataChannelStreaming/StreamingHarness.cs b/samples/ConsoleDataChannelStreaming/StreamingHarness.cs index aadbc8a43f..4dea07f9fc 100644 --- a/samples/ConsoleDataChannelStreaming/StreamingHarness.cs +++ b/samples/ConsoleDataChannelStreaming/StreamingHarness.cs @@ -365,7 +365,7 @@ namespace ConsoleDataChannelStreaming } else { - clientManager = clientChannel.EnableDataChannels( + clientManager = clientChannel.SecureChannel!.EnableDataChannels( isServer: false, telemetry, maxDataChannels: 16, @@ -789,7 +789,7 @@ namespace ConsoleDataChannelStreaming out uint maxFrameSize, out bool isReliable) { - if (!UaSCDataChannelSecureChannelRegistry.TryGet( + if (!UaSCSecureChannelRegistry.TryGet( secureChannelContext.SecureChannelId, out UaSCUaBinaryChannel? channel) || channel == null) diff --git a/src/Opc.Ua.Bindings.Quic/Opc.Ua.Bindings.Quic.csproj b/src/Opc.Ua.Bindings.Quic/Opc.Ua.Bindings.Quic.csproj index 0c21ca8919..f48ce9fd6e 100644 --- a/src/Opc.Ua.Bindings.Quic/Opc.Ua.Bindings.Quic.csproj +++ b/src/Opc.Ua.Bindings.Quic/Opc.Ua.Bindings.Quic.csproj @@ -23,6 +23,7 @@ + diff --git a/src/Opc.Ua.Bindings.Quic/QuicMultiplexedTransport.cs b/src/Opc.Ua.Bindings.Quic/QuicMultiplexedTransport.cs index fcd47a3e29..8ee1bd9325 100644 --- a/src/Opc.Ua.Bindings.Quic/QuicMultiplexedTransport.cs +++ b/src/Opc.Ua.Bindings.Quic/QuicMultiplexedTransport.cs @@ -129,7 +129,7 @@ namespace Opc.Ua.Bindings public TransportChannelFeatures Features => TransportChannelFeatures.Reconnect | TransportChannelFeatures.ReverseConnect | - TransportChannelFeatures.DataChannels | + TransportChannelFeatures.MessageExtensions | TransportChannelFeatures.MultiplexedStreams; /// diff --git a/src/Opc.Ua.Client/Session/ManagedSession.Services.cs b/src/Opc.Ua.Client/Session/ManagedSession.Services.cs index 2c4dbd69e6..8de69e38fe 100644 --- a/src/Opc.Ua.Client/Session/ManagedSession.Services.cs +++ b/src/Opc.Ua.Client/Session/ManagedSession.Services.cs @@ -610,8 +610,8 @@ namespace Opc.Ua.Client /// #if NET8_0_OR_GREATER - [Experimental(DataChannelConstants.ExperimentalDiagnosticId, - UrlFormat = DataChannelConstants.ExperimentalUrlFormat)] + [Experimental(DataChannelFeature.ExperimentalDiagnosticId, + UrlFormat = DataChannelFeature.ExperimentalUrlFormat)] #endif public async ValueTask OpenDataChannelAsync( RequestHeader? requestHeader, @@ -635,8 +635,8 @@ namespace Opc.Ua.Client /// #if NET8_0_OR_GREATER - [Experimental(DataChannelConstants.ExperimentalDiagnosticId, - UrlFormat = DataChannelConstants.ExperimentalUrlFormat)] + [Experimental(DataChannelFeature.ExperimentalDiagnosticId, + UrlFormat = DataChannelFeature.ExperimentalUrlFormat)] #endif public async ValueTask ModifyDataChannelAsync( RequestHeader? requestHeader, @@ -656,8 +656,8 @@ namespace Opc.Ua.Client /// #if NET8_0_OR_GREATER - [Experimental(DataChannelConstants.ExperimentalDiagnosticId, - UrlFormat = DataChannelConstants.ExperimentalUrlFormat)] + [Experimental(DataChannelFeature.ExperimentalDiagnosticId, + UrlFormat = DataChannelFeature.ExperimentalUrlFormat)] #endif public async ValueTask CloseDataChannelAsync( RequestHeader? requestHeader, diff --git a/src/Opc.Ua.Core.Channels/ChannelsEventIds.cs b/src/Opc.Ua.Core.Channels/ChannelsEventIds.cs new file mode 100644 index 0000000000..2013f8112d --- /dev/null +++ b/src/Opc.Ua.Core.Channels/ChannelsEventIds.cs @@ -0,0 +1,46 @@ +/* ======================================================================== + * Copyright (c) 2005-2026 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/ + * ======================================================================*/ + +namespace Opc.Ua.Bindings +{ + /// + /// The base EventIds each source-generated logging class in this assembly + /// counts from. + /// + /// + /// The values continue the block these messages used while the engine + /// lived in Opc.Ua.Core, so an operator's existing log filters keep + /// working across the move. + /// + internal static class ChannelsEventIds + { + public const int DataChannel = 600; + public const int DataChannelManager = 620; + } +} diff --git a/src/Opc.Ua.Core/Stack/DataChannels/DataChannel.cs b/src/Opc.Ua.Core.Channels/DataChannels/DataChannel.cs similarity index 100% rename from src/Opc.Ua.Core/Stack/DataChannels/DataChannel.cs rename to src/Opc.Ua.Core.Channels/DataChannels/DataChannel.cs diff --git a/src/Opc.Ua.Core/Stack/DataChannels/DataChannelConstants.cs b/src/Opc.Ua.Core.Channels/DataChannels/DataChannelConstants.cs similarity index 97% rename from src/Opc.Ua.Core/Stack/DataChannels/DataChannelConstants.cs rename to src/Opc.Ua.Core.Channels/DataChannels/DataChannelConstants.cs index 3e18de533f..7fb71cedc9 100644 --- a/src/Opc.Ua.Core/Stack/DataChannels/DataChannelConstants.cs +++ b/src/Opc.Ua.Core.Channels/DataChannels/DataChannelConstants.cs @@ -45,14 +45,13 @@ namespace Opc.Ua.Bindings /// The diagnostic id suppressed by a consumer that opts in to the /// experimental data channel surface. /// - public const string ExperimentalDiagnosticId = "DataChannels"; + public const string ExperimentalDiagnosticId = DataChannelFeature.ExperimentalDiagnosticId; /// /// The documentation url reported alongside /// . /// - public const string ExperimentalUrlFormat = - "https://github.com/OPCFoundation/UA-.NETStandard/blob/master/docs/DataChannels.md"; + public const string ExperimentalUrlFormat = DataChannelFeature.ExperimentalUrlFormat; /// /// The connection control channel (Part 6 errata 5.6). Never diff --git a/src/Opc.Ua.Core/Stack/DataChannels/DataChannelExtension.cs b/src/Opc.Ua.Core.Channels/DataChannels/DataChannelExtension.cs similarity index 94% rename from src/Opc.Ua.Core/Stack/DataChannels/DataChannelExtension.cs rename to src/Opc.Ua.Core.Channels/DataChannels/DataChannelExtension.cs index 19d6e52eeb..9fae0a7301 100644 --- a/src/Opc.Ua.Core/Stack/DataChannels/DataChannelExtension.cs +++ b/src/Opc.Ua.Core.Channels/DataChannels/DataChannelExtension.cs @@ -65,7 +65,6 @@ namespace Opc.Ua.Bindings { m_host = host ?? throw new ArgumentNullException(nameof(host)); m_isSource = isServer; - m_budget = new DataChannelSequenceBudget(host.TimeProvider); // Cached so a frame does not allocate a closure on the send path. m_onSecuring = ReserveSequenceNumber; @@ -88,14 +87,7 @@ namespace Opc.Ua.Bindings /// under the current token. STR, MSG, OPN and CLO chunks all draw on /// the same space. /// - public DataChannelSequenceBudget SequenceBudget - { - get - { - m_budget.ObserveConsumed(m_host.SequenceNumbersIssued); - return m_budget; - } - } + public SequenceNumberBudget SequenceBudget => m_host.SequenceBudget; /// /// Raised when a frame violated the framing rules badly enough to cost @@ -145,7 +137,7 @@ namespace Opc.Ua.Bindings ArraySegment body = message.Body; if (!DataChannelFrameCodec.TryDecode( - new ReadOnlyMemory(body.GetArray(), body.Offset, body.Count), + new ReadOnlyMemory(body.Array!, body.Offset, body.Count), m_host.ReceiveBufferSize, out DataChannelFrame frame, out DataChannelFrameError error)) @@ -179,7 +171,8 @@ namespace Opc.Ua.Bindings /// public void OnSecurityTokenActivated() { - m_budget.OnTokenActivated(); + // The channel rebases the budget itself, because every MessageType + // it carries draws on the same space. } /// @@ -243,9 +236,7 @@ namespace Opc.Ua.Bindings /// private void ReserveSequenceNumber() { - m_budget.ObserveConsumed(m_host.SequenceNumbersIssued); - - if (!m_budget.TryConsume()) + if (!m_host.SequenceBudget.TryConsume()) { throw ServiceResultException.Create( StatusCodes.BadSecureChannelTokenUnknown, @@ -254,7 +245,6 @@ namespace Opc.Ua.Bindings } private readonly ISecureChannelMessageHost m_host; - private readonly DataChannelSequenceBudget m_budget; private readonly Action m_onSecuring; private readonly bool m_isSource; } diff --git a/src/Opc.Ua.Core/Stack/DataChannels/DataChannelLog.cs b/src/Opc.Ua.Core.Channels/DataChannels/DataChannelLog.cs similarity index 84% rename from src/Opc.Ua.Core/Stack/DataChannels/DataChannelLog.cs rename to src/Opc.Ua.Core.Channels/DataChannels/DataChannelLog.cs index a456d2b0de..9424cb24e0 100644 --- a/src/Opc.Ua.Core/Stack/DataChannels/DataChannelLog.cs +++ b/src/Opc.Ua.Core.Channels/DataChannels/DataChannelLog.cs @@ -37,23 +37,23 @@ namespace Opc.Ua.Bindings /// internal static partial class DataChannelLog { - [LoggerMessage(EventId = CoreEventIds.DataChannelManager + 0, Level = LogLevel.Warning, + [LoggerMessage(EventId = ChannelsEventIds.DataChannelManager + 0, Level = LogLevel.Warning, Message = "DataChannel: A frame named ChannelId {ChannelId}, which is not open on this SecureChannel.")] public static partial void DataChannelManagerUnknownChannel( this ILogger logger, uint channelId); - [LoggerMessage(EventId = CoreEventIds.DataChannelManager + 1, Level = LogLevel.Error, + [LoggerMessage(EventId = ChannelsEventIds.DataChannelManager + 1, Level = LogLevel.Error, Message = "DataChannel: A connection level credit grant would overflow the window.")] public static partial void DataChannelManagerCreditOverflow(this ILogger logger); - [LoggerMessage(EventId = CoreEventIds.DataChannelManager + 2, Level = LogLevel.Error, + [LoggerMessage(EventId = ChannelsEventIds.DataChannelManager + 2, Level = LogLevel.Error, Message = "DataChannel: The scheduler round faulted.")] public static partial void DataChannelManagerSchedulerFault( this ILogger logger, Exception exception); - [LoggerMessage(EventId = CoreEventIds.DataChannel + 0, Level = LogLevel.Debug, + [LoggerMessage(EventId = ChannelsEventIds.DataChannel + 0, Level = LogLevel.Debug, Message = "DataChannel {ChannelId}: entered {State} with {Status}.")] public static partial void DataChannelStateChanged( this ILogger logger, @@ -61,14 +61,14 @@ namespace Opc.Ua.Bindings DataChannelState state, StatusCode status); - [LoggerMessage(EventId = CoreEventIds.DataChannel + 1, Level = LogLevel.Warning, + [LoggerMessage(EventId = ChannelsEventIds.DataChannel + 1, Level = LogLevel.Warning, Message = "DataChannel {ChannelId}: rejected a frame, resetting with {Status}.")] public static partial void DataChannelFrameRejected( this ILogger logger, uint channelId, StatusCode status); - [LoggerMessage(EventId = CoreEventIds.DataChannel + 2, Level = LogLevel.Warning, + [LoggerMessage(EventId = ChannelsEventIds.DataChannel + 2, Level = LogLevel.Warning, Message = "DataChannel: a STR frame arrived but the data channel feature is not enabled.")] public static partial void DataChannelFeatureDisabled(this ILogger logger); } diff --git a/src/Opc.Ua.Core/Stack/DataChannels/DataChannelManager.cs b/src/Opc.Ua.Core.Channels/DataChannels/DataChannelManager.cs similarity index 100% rename from src/Opc.Ua.Core/Stack/DataChannels/DataChannelManager.cs rename to src/Opc.Ua.Core.Channels/DataChannels/DataChannelManager.cs diff --git a/src/Opc.Ua.Core/Stack/DataChannels/DataChannelMessage.cs b/src/Opc.Ua.Core.Channels/DataChannels/DataChannelMessage.cs similarity index 100% rename from src/Opc.Ua.Core/Stack/DataChannels/DataChannelMessage.cs rename to src/Opc.Ua.Core.Channels/DataChannels/DataChannelMessage.cs diff --git a/src/Opc.Ua.Core/Stack/DataChannels/DataChannelSettings.cs b/src/Opc.Ua.Core.Channels/DataChannels/DataChannelSettings.cs similarity index 100% rename from src/Opc.Ua.Core/Stack/DataChannels/DataChannelSettings.cs rename to src/Opc.Ua.Core.Channels/DataChannels/DataChannelSettings.cs diff --git a/src/Opc.Ua.Core/Stack/DataChannels/FlowControl/DataChannelCredit.cs b/src/Opc.Ua.Core.Channels/DataChannels/FlowControl/DataChannelCredit.cs similarity index 100% rename from src/Opc.Ua.Core/Stack/DataChannels/FlowControl/DataChannelCredit.cs rename to src/Opc.Ua.Core.Channels/DataChannels/FlowControl/DataChannelCredit.cs diff --git a/src/Opc.Ua.Core/Stack/DataChannels/Framing/DataChannelFrame.cs b/src/Opc.Ua.Core.Channels/DataChannels/Framing/DataChannelFrame.cs similarity index 100% rename from src/Opc.Ua.Core/Stack/DataChannels/Framing/DataChannelFrame.cs rename to src/Opc.Ua.Core.Channels/DataChannels/Framing/DataChannelFrame.cs diff --git a/src/Opc.Ua.Core/Stack/DataChannels/Framing/DataChannelFrameCodec.cs b/src/Opc.Ua.Core.Channels/DataChannels/Framing/DataChannelFrameCodec.cs similarity index 100% rename from src/Opc.Ua.Core/Stack/DataChannels/Framing/DataChannelFrameCodec.cs rename to src/Opc.Ua.Core.Channels/DataChannels/Framing/DataChannelFrameCodec.cs diff --git a/src/Opc.Ua.Core/Stack/DataChannels/Framing/DataChannelFrameError.cs b/src/Opc.Ua.Core.Channels/DataChannels/Framing/DataChannelFrameError.cs similarity index 100% rename from src/Opc.Ua.Core/Stack/DataChannels/Framing/DataChannelFrameError.cs rename to src/Opc.Ua.Core.Channels/DataChannels/Framing/DataChannelFrameError.cs diff --git a/src/Opc.Ua.Core/Stack/DataChannels/Framing/DataChannelFrameType.cs b/src/Opc.Ua.Core.Channels/DataChannels/Framing/DataChannelFrameType.cs similarity index 100% rename from src/Opc.Ua.Core/Stack/DataChannels/Framing/DataChannelFrameType.cs rename to src/Opc.Ua.Core.Channels/DataChannels/Framing/DataChannelFrameType.cs diff --git a/src/Opc.Ua.Core/Stack/DataChannels/IDataChannelTransport.cs b/src/Opc.Ua.Core.Channels/DataChannels/IDataChannelTransport.cs similarity index 100% rename from src/Opc.Ua.Core/Stack/DataChannels/IDataChannelTransport.cs rename to src/Opc.Ua.Core.Channels/DataChannels/IDataChannelTransport.cs diff --git a/src/Opc.Ua.Core/Stack/DataChannels/Model/DataChannelModel.cs b/src/Opc.Ua.Core.Channels/DataChannels/Model/DataChannelModel.cs similarity index 100% rename from src/Opc.Ua.Core/Stack/DataChannels/Model/DataChannelModel.cs rename to src/Opc.Ua.Core.Channels/DataChannels/Model/DataChannelModel.cs diff --git a/src/Opc.Ua.Core/Stack/DataChannels/Scheduling/DataChannelSendQueue.cs b/src/Opc.Ua.Core.Channels/DataChannels/Scheduling/DataChannelSendQueue.cs similarity index 100% rename from src/Opc.Ua.Core/Stack/DataChannels/Scheduling/DataChannelSendQueue.cs rename to src/Opc.Ua.Core.Channels/DataChannels/Scheduling/DataChannelSendQueue.cs diff --git a/src/Opc.Ua.Core/Stack/DataChannels/Sequencing/DataChannelReceiveWindow.cs b/src/Opc.Ua.Core.Channels/DataChannels/Sequencing/DataChannelReceiveWindow.cs similarity index 100% rename from src/Opc.Ua.Core/Stack/DataChannels/Sequencing/DataChannelReceiveWindow.cs rename to src/Opc.Ua.Core.Channels/DataChannels/Sequencing/DataChannelReceiveWindow.cs diff --git a/src/Opc.Ua.Core/Stack/DataChannels/Sequencing/DataChannelSequence.cs b/src/Opc.Ua.Core.Channels/DataChannels/Sequencing/DataChannelSequence.cs similarity index 100% rename from src/Opc.Ua.Core/Stack/DataChannels/Sequencing/DataChannelSequence.cs rename to src/Opc.Ua.Core.Channels/DataChannels/Sequencing/DataChannelSequence.cs diff --git a/src/Opc.Ua.Core/Stack/DataChannels/Services/DataChannelNegotiator.cs b/src/Opc.Ua.Core.Channels/DataChannels/Services/DataChannelNegotiator.cs similarity index 100% rename from src/Opc.Ua.Core/Stack/DataChannels/Services/DataChannelNegotiator.cs rename to src/Opc.Ua.Core.Channels/DataChannels/Services/DataChannelNegotiator.cs diff --git a/src/Opc.Ua.Core/Stack/DataChannels/Services/DataChannelServiceHandler.cs b/src/Opc.Ua.Core.Channels/DataChannels/Services/DataChannelServiceHandler.cs similarity index 100% rename from src/Opc.Ua.Core/Stack/DataChannels/Services/DataChannelServiceHandler.cs rename to src/Opc.Ua.Core.Channels/DataChannels/Services/DataChannelServiceHandler.cs diff --git a/src/Opc.Ua.Core/Stack/DataChannels/Services/DataChannelSourceRegistry.cs b/src/Opc.Ua.Core.Channels/DataChannels/Services/DataChannelSourceRegistry.cs similarity index 100% rename from src/Opc.Ua.Core/Stack/DataChannels/Services/DataChannelSourceRegistry.cs rename to src/Opc.Ua.Core.Channels/DataChannels/Services/DataChannelSourceRegistry.cs diff --git a/src/Opc.Ua.Core/Stack/DataChannels/Services/QuicPeerBinding.cs b/src/Opc.Ua.Core.Channels/DataChannels/Services/QuicPeerBinding.cs similarity index 100% rename from src/Opc.Ua.Core/Stack/DataChannels/Services/QuicPeerBinding.cs rename to src/Opc.Ua.Core.Channels/DataChannels/Services/QuicPeerBinding.cs diff --git a/src/Opc.Ua.Core.Channels/IServerDataChannelTransport.cs b/src/Opc.Ua.Core.Channels/IServerDataChannelTransport.cs new file mode 100644 index 0000000000..68cdefa0bf --- /dev/null +++ b/src/Opc.Ua.Core.Channels/IServerDataChannelTransport.cs @@ -0,0 +1,109 @@ +/* ======================================================================== + * Copyright (c) 2005-2026 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.Threading; +using System.Threading.Tasks; + +namespace Opc.Ua.Bindings +{ + /// + /// The transport a Server binds an accepted OpenDataChannel to. + /// + /// + /// A data channel rides a transport that already exists, so the Service + /// Set needs a way to reach it from the SecureChannel a request arrived + /// on. Inline framing and the outer-protocol transports answer this + /// differently: inline multiplexes on the ChannelId in the frame header, + /// while opc.quic binds each channel to its own stream. + /// + public interface IServerDataChannelTransport + { + /// + /// Resolves the data channel engine for a SecureChannel, if this + /// transport carries it. + /// + /// The SecureChannel the request + /// arrived on. + /// What the Server advertises. + /// Telemetry context. + /// The engine that carries the channels. + /// The largest payload a frame may carry + /// on this transport. + /// False when frames can be genuinely lost + /// in flight. + bool TryGetManager( + SecureChannelContext secureChannelContext, + DataChannelServerCapabilities capabilities, + ITelemetryContext telemetry, + out DataChannelManager manager, + out uint maxFrameSize, + out bool isReliable); + + /// + /// Opens the transport stream for a direction this peer initiates, and + /// returns the identifier to carry in revisedTransportChannelId. + /// + /// The SecureChannel. + /// The data channel. + /// The negotiated direction. + /// Cancellation token. + ValueTask AllocateServerStreamAsync( + SecureChannelContext secureChannelContext, + uint channelId, + DataChannelDirection direction, + CancellationToken ct); + + /// + /// Binds a data channel to the transport stream the Client named. + /// + /// + /// The identifier is taken straight from the requester, so an + /// implementation validates it before it binds anything and refuses + /// rather than echoing a value it has not checked. + /// + /// The SecureChannel. + /// The data channel. + /// The identifier from transportChannelId. + /// The negotiated direction. + /// Cancellation token. + ValueTask BindClientStreamAsync( + SecureChannelContext secureChannelContext, + uint channelId, + ulong streamId, + DataChannelDirection direction, + CancellationToken ct); + + /// + /// Tears down every data channel on a SecureChannel. + /// + /// The SecureChannel. + /// Why the channels cannot continue. + void AbortSecureChannel(SecureChannelContext secureChannelContext, StatusCode reason); + } +} diff --git a/src/Opc.Ua.Server/Server/InlineServerDataChannelTransport.cs b/src/Opc.Ua.Core.Channels/InlineServerDataChannelTransport.cs similarity index 95% rename from src/Opc.Ua.Server/Server/InlineServerDataChannelTransport.cs rename to src/Opc.Ua.Core.Channels/InlineServerDataChannelTransport.cs index 4e0bc67f69..b5d069c54d 100644 --- a/src/Opc.Ua.Server/Server/InlineServerDataChannelTransport.cs +++ b/src/Opc.Ua.Core.Channels/InlineServerDataChannelTransport.cs @@ -30,9 +30,9 @@ using System; using System.Threading; using System.Threading.Tasks; -using Opc.Ua.Bindings; -namespace Opc.Ua.Server + +namespace Opc.Ua.Bindings { /// /// Carries data channels over the SecureChannel that already exists, @@ -81,7 +81,7 @@ namespace Opc.Ua.Server return false; } - if (!UaSCDataChannelSecureChannelRegistry.TryGet( + if (!UaSCSecureChannelRegistry.TryGet( secureChannelContext.SecureChannelId, out UaSCUaBinaryChannel? channel) || channel == null) @@ -95,7 +95,7 @@ namespace Opc.Ua.Server capabilities.MaxDataChannels, capabilities.MaxCreditPerChannel); - int body = channel.MaxDataChannelBodySize; + int body = channel.GetMaxDataChannelBodySize(); maxFrameSize = body <= 0 ? 0 : (uint)body; // Inline framing rides the SecureChannel, which is ordered and @@ -132,12 +132,12 @@ namespace Opc.Ua.Server public void AbortSecureChannel(SecureChannelContext secureChannelContext, StatusCode reason) { if (secureChannelContext != null && - UaSCDataChannelSecureChannelRegistry.TryGet( + UaSCSecureChannelRegistry.TryGet( secureChannelContext.SecureChannelId, out UaSCUaBinaryChannel? channel) && channel != null) { - channel.DataChannels?.AbortAll(reason); + channel.GetDataChannels()?.AbortAll(reason); } } diff --git a/src/Opc.Ua.Core.Channels/Properties/AssemblyInfo.cs b/src/Opc.Ua.Core.Channels/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..7798c9bd57 --- /dev/null +++ b/src/Opc.Ua.Core.Channels/Properties/AssemblyInfo.cs @@ -0,0 +1,32 @@ +/* ======================================================================== + * Copyright (c) 2005-2026 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; + +[assembly: CLSCompliant(false)] diff --git a/src/Opc.Ua.Core.Channels/UaSCDataChannelExtensions.cs b/src/Opc.Ua.Core.Channels/UaSCDataChannelExtensions.cs new file mode 100644 index 0000000000..1a39843ed7 --- /dev/null +++ b/src/Opc.Ua.Core.Channels/UaSCDataChannelExtensions.cs @@ -0,0 +1,161 @@ +/* ======================================================================== + * Copyright (c) 2005-2026 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; + +namespace Opc.Ua.Bindings +{ + /// + /// Attaches inline data channel framing to a UASC binary channel. + /// + /// + /// A frame is one STR MessageChunk, written to the connection exactly as a + /// MSG chunk is. The channel itself knows nothing of the frame format; it + /// carries the MessageType on behalf of the extension registered here. + /// + public static class UaSCDataChannelExtensions + { + /// + /// Enables the data channel feature on a SecureChannel. + /// + /// + /// Experimental. Until this is called the STR dispatch is inert and an + /// incoming frame closes the SecureChannel, which is what the + /// interoperability rule of the Part 6 errata 5.16 requires of a peer + /// that does not implement this specification. + /// + /// The SecureChannel that carries the frames. + /// True on the server side, which allocates + /// ChannelIds. + /// Telemetry context. + /// The most channels kept open at once + /// on this SecureChannel. + /// The largest window granted to one + /// channel. + /// is null. + public static DataChannelManager EnableDataChannels( + this UaSCUaBinaryChannel channel, + bool isServer, + ITelemetryContext telemetry, + ushort maxDataChannels = 16, + uint maxCreditPerChannel = 1024 * 1024) + { + if (channel == null) + { + throw new ArgumentNullException(nameof(channel)); + } + + if (channel.TryGetDataChannelExtension(out DataChannelExtension? existing) && + existing != null) + { + return existing.Manager; + } + + var extension = new DataChannelExtension( + channel, + isServer, + telemetry, + maxDataChannels, + maxCreditPerChannel); + + var registered = (DataChannelExtension)channel.RegisterMessageExtension(extension); + + if (!ReferenceEquals(registered, extension)) + { + _ = extension.Manager.DisposeAsync().AsTask(); + } + + return registered.Manager; + } + + /// + /// Returns the data channels multiplexed onto a SecureChannel, or + /// null when the feature has not been enabled on it. + /// + /// The SecureChannel. + /// is null. + public static DataChannelManager? GetDataChannels(this UaSCUaBinaryChannel channel) + { + return channel.TryGetDataChannelExtension(out DataChannelExtension? extension) + ? extension?.Manager + : null; + } + + /// + /// Returns the extension that owns the STR MessageType on a + /// SecureChannel, if one is registered. + /// + /// The SecureChannel. + /// The extension. + /// is null. + public static bool TryGetDataChannelExtension( + this UaSCUaBinaryChannel channel, + out DataChannelExtension? extension) + { + if (channel == null) + { + throw new ArgumentNullException(nameof(channel)); + } + + if (channel.TryGetMessageExtension( + TcpMessageType.Stream, + out ISecureChannelMessageExtension? registered) && + registered is DataChannelExtension dataChannels) + { + extension = dataChannels; + return true; + } + + extension = null; + return false; + } + + /// + /// The largest secured body a data channel frame may occupy on a + /// SecureChannel. + /// + /// The SecureChannel. + /// is null. + public static int GetMaxDataChannelBodySize(this UaSCUaBinaryChannel channel) + { + if (channel == null) + { + throw new ArgumentNullException(nameof(channel)); + } + + ISecureChannelMessageHost host = channel; + + return DataChannelFrameCodec.MaxPayload( + DataChannelFramingMode.Inline, + host.SendBufferSize, + host.SymmetricSignatureSize + 2, + withDeadline: true); + } + } +} diff --git a/src/Opc.Ua.Core/EventIds.cs b/src/Opc.Ua.Core/EventIds.cs index d5b57a1e1d..ea4eeb052e 100644 --- a/src/Opc.Ua.Core/EventIds.cs +++ b/src/Opc.Ua.Core/EventIds.cs @@ -103,7 +103,5 @@ namespace Opc.Ua public const int X509CertificateStore = 570; public const int X509CrlHelper = 580; public const int CryptoProvider = 590; - public const int DataChannel = 600; - public const int DataChannelManager = 620; } } diff --git a/src/Opc.Ua.Core/Stack/DataChannelFeature.cs b/src/Opc.Ua.Core/Stack/DataChannelFeature.cs new file mode 100644 index 0000000000..eb180c3908 --- /dev/null +++ b/src/Opc.Ua.Core/Stack/DataChannelFeature.cs @@ -0,0 +1,58 @@ +/* ======================================================================== + * Copyright (c) 2005-2026 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/ + * ======================================================================*/ + +namespace Opc.Ua +{ + /// + /// Marks the DataChannel Service Set as experimental. + /// + /// + /// The Services themselves are generated into the standard type surface + /// and are reachable from any Client, while the engine that carries the + /// frames ships separately in + /// OPCFoundation.NetStandard.Opc.Ua.Core.Channels. The marker + /// therefore lives here, where both the Services and their callers can see + /// it: every identifier the feature uses is provisional and will change if + /// and when the OPC Foundation assigns final values. + /// + public static class DataChannelFeature + { + /// + /// The diagnostic id reported for the experimental DataChannel API. + /// + public const string ExperimentalDiagnosticId = "DataChannels"; + + /// + /// The documentation url reported alongside + /// . + /// + public const string ExperimentalUrlFormat = + "https://github.com/OPCFoundation/UA-.NETStandard/blob/master/docs/DataChannels.md"; + } +} diff --git a/src/Opc.Ua.Core/Stack/Tcp/ISecureChannelMessageExtension.cs b/src/Opc.Ua.Core/Stack/Tcp/ISecureChannelMessageExtension.cs index efb90aa0a4..70913dd14f 100644 --- a/src/Opc.Ua.Core/Stack/Tcp/ISecureChannelMessageExtension.cs +++ b/src/Opc.Ua.Core/Stack/Tcp/ISecureChannelMessageExtension.cs @@ -159,17 +159,17 @@ namespace Opc.Ua.Bindings TimeProvider TimeProvider { get; } /// - /// How many SequenceNumbers this channel has issued under the + /// How much of the SequenceNumber space remains under the /// SecurityToken currently in force. /// /// - /// The SequenceNumber space is per token and is shared by every - /// MessageType the channel carries, so an extension that emits a large - /// number of messages has to watch it. Read this inside the callback - /// passed to to observe it consistently - /// with the message being sent. + /// The space is per token and is shared by every MessageType the + /// channel carries, so an extension that emits a large number of + /// messages has to draw on this rather than assume it is alone. + /// Consume it inside the callback passed to , + /// which is where the reservation and the send are atomic. /// - long SequenceNumbersIssued { get; } + SequenceNumberBudget SequenceBudget { get; } /// /// Reports a fault whose blast radius is the whole SecureChannel. diff --git a/src/Opc.Ua.Core/Stack/DataChannels/Sequencing/DataChannelSequenceBudget.cs b/src/Opc.Ua.Core/Stack/Tcp/SequenceNumberBudget.cs similarity index 91% rename from src/Opc.Ua.Core/Stack/DataChannels/Sequencing/DataChannelSequenceBudget.cs rename to src/Opc.Ua.Core/Stack/Tcp/SequenceNumberBudget.cs index 75d6723a99..249220b916 100644 --- a/src/Opc.Ua.Core/Stack/DataChannels/Sequencing/DataChannelSequenceBudget.cs +++ b/src/Opc.Ua.Core/Stack/Tcp/SequenceNumberBudget.cs @@ -64,7 +64,7 @@ namespace Opc.Ua.Bindings /// than emitting a chunk that would reuse a value. /// /// - public sealed class DataChannelSequenceBudget + public sealed class SequenceNumberBudget { /// /// Creates a budget. @@ -73,7 +73,7 @@ namespace Opc.Ua.Bindings /// emission rate. /// The number of SequenceNumbers usable /// under one token. Defaults to the 32 bit space. - public DataChannelSequenceBudget( + public SequenceNumberBudget( TimeProvider? timeProvider = null, uint capacity = uint.MaxValue) { @@ -142,14 +142,13 @@ namespace Opc.Ua.Bindings { get { - double perMinute = ChunksPerSecond * - DataChannelConstants.SequenceNumberRenewalSeconds; + double perMinute = ChunksPerSecond * SequenceNumberRenewalSeconds; long rateBased = perMinute >= long.MaxValue ? long.MaxValue : (long)perMinute; - return Math.Min(DataChannelConstants.SequenceNumberRenewalHeadroom, rateBased); + return Math.Min(SequenceNumberRenewalHeadroom, rateBased); } } @@ -230,5 +229,18 @@ namespace Opc.Ua.Bindings private readonly TimeProvider m_timeProvider; private long m_consumed; private long m_tokenStarted; + + /// + /// The SequenceNumber headroom below which a channel initiates token + /// renewal ahead of the normal lifetime schedule. + /// + private const uint SequenceNumberRenewalHeadroom = 1u << 30; + + /// + /// The window, in seconds, of expected chunk emission that also + /// triggers renewal when it exceeds the remaining SequenceNumber + /// space. + /// + private const int SequenceNumberRenewalSeconds = 60; } } diff --git a/src/Opc.Ua.Core/Stack/Tcp/TcpListenerChannel.cs b/src/Opc.Ua.Core/Stack/Tcp/TcpListenerChannel.cs index c82c93575b..eaa6cabf25 100644 --- a/src/Opc.Ua.Core/Stack/Tcp/TcpListenerChannel.cs +++ b/src/Opc.Ua.Core/Stack/Tcp/TcpListenerChannel.cs @@ -150,7 +150,7 @@ namespace Opc.Ua.Bindings /// protected override void Dispose(bool disposing) { - UaSCDataChannelSecureChannelRegistry.Unbind(GlobalChannelId, this); + UaSCSecureChannelRegistry.Unbind(GlobalChannelId, this); base.Dispose(disposing); } @@ -268,7 +268,7 @@ namespace Opc.Ua.Bindings boundTransport.OnSecureChannelAttached(GlobalChannelId); } - UaSCDataChannelSecureChannelRegistry.Bind(GlobalChannelId, this); + UaSCSecureChannelRegistry.Bind(GlobalChannelId, this); m_logger.TcpListenChannelLog0( ChannelName, diff --git a/src/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.DataChannels.cs b/src/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.DataChannels.cs deleted file mode 100644 index 0c26a0e17d..0000000000 --- a/src/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.DataChannels.cs +++ /dev/null @@ -1,180 +0,0 @@ -/* ======================================================================== - * Copyright (c) 2005-2026 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.Concurrent; - -namespace Opc.Ua.Bindings -{ - /// - /// Inline data channel framing over the UASC binary channel, expressed as - /// an extension that owns the STR MessageType. - /// - public partial class UaSCUaBinaryChannel - { - /// - /// The data channels multiplexed onto this SecureChannel, or null - /// when the feature is not enabled. - /// - /// - /// Experimental. Until is called the - /// STR dispatch is inert and an incoming frame closes the - /// SecureChannel, which is what the interoperability rule of the - /// Part 6 errata 5.16 requires of a peer that does not implement this - /// specification. - /// - public DataChannelManager? DataChannels => m_dataChannels?.Manager; - - /// - /// Enables the data channel feature on this SecureChannel. - /// - /// True on the server side, which - /// allocates ChannelIds. - /// Telemetry context. - /// The most channels kept open at - /// once on this SecureChannel. - /// The largest window granted - /// to one channel. - public DataChannelManager EnableDataChannels( - bool isServer, - ITelemetryContext telemetry, - ushort maxDataChannels = 16, - uint maxCreditPerChannel = 1024 * 1024) - { - DataChannelExtension? existing = m_dataChannels; - - if (existing != null) - { - return existing.Manager; - } - - var extension = new DataChannelExtension( - this, - isServer, - telemetry, - maxDataChannels, - maxCreditPerChannel); - - var registered = (DataChannelExtension)RegisterMessageExtension(extension); - - if (ReferenceEquals(registered, extension)) - { - m_isDataChannelSource = isServer; - } - else - { - _ = extension.Manager.DisposeAsync().AsTask(); - } - - m_dataChannels = registered; - return registered.Manager; - } - - /// - /// Tracks how much of the SecureChannel SequenceNumber space - /// remains under the current SecurityToken. STR, MSG, OPN and CLO - /// chunks all consume the same sender SequenceNumber space. - /// - public DataChannelSequenceBudget SequenceBudget - { - get - { - m_sequenceBudget.ObserveConsumed(SequenceNumbersIssuedUnderCurrentToken); - return m_sequenceBudget; - } - } - - /// - /// True when the SequenceNumber space remaining under the current - /// token has fallen below the renewal threshold, so the owning - /// channel should initiate OpenSecureChannel with - /// RenewalRequest ahead of the normal lifetime based renewal. - /// - public bool IsSequenceRenewalDue => SequenceBudget.ShouldRenew; - - /// - /// The largest secured body a data channel frame may occupy on - /// this SecureChannel. - /// - /// - /// Public because a Server has to advertise this ceiling when it - /// negotiates a channel, and the negotiation happens in the Server - /// assembly rather than here. - /// - public int MaxDataChannelBodySize - => DataChannelFrameCodec.MaxPayload( - DataChannelFramingMode.Inline, - SendBufferSize, - SymmetricSignatureSize + 2, - withDeadline: true); - - private readonly DataChannelSequenceBudget m_sequenceBudget = new(); - private DataChannelExtension? m_dataChannels; - private bool m_isDataChannelSource; - } - - /// - /// Maps server-side SecureChannel identifiers to their UASC channels so - /// the DataChannel Service Set can bind an accepted OpenDataChannel to the - /// transport that will carry its STR frames. - /// - public static class UaSCDataChannelSecureChannelRegistry - { - /// - /// Finds the server-side UASC channel that owns a SecureChannel. - /// - /// The SecureChannel identifier. - /// The channel that owns it. - public static bool TryGet( - string secureChannelId, - out UaSCUaBinaryChannel? channel) - { - return s_channels.TryGetValue(secureChannelId, out channel); - } - - internal static void Bind(string secureChannelId, UaSCUaBinaryChannel channel) - { - if (!string.IsNullOrEmpty(secureChannelId)) - { - s_channels[secureChannelId] = channel; - } - } - - internal static void Unbind(string secureChannelId, UaSCUaBinaryChannel channel) - { - if (!string.IsNullOrEmpty(secureChannelId) && - s_channels.TryGetValue(secureChannelId, out UaSCUaBinaryChannel? current) && - ReferenceEquals(current, channel)) - { - s_channels.TryRemove(secureChannelId, out _); - } - } - - private static readonly ConcurrentDictionary s_channels = new(); - } -} diff --git a/src/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.MessageExtensions.cs b/src/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.MessageExtensions.cs index 2ae3e1b808..95cd05c753 100644 --- a/src/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.MessageExtensions.cs +++ b/src/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.MessageExtensions.cs @@ -56,7 +56,29 @@ namespace Opc.Ua.Bindings TimeProvider ISecureChannelMessageHost.TimeProvider => TimeProvider; /// - long ISecureChannelMessageHost.SequenceNumbersIssued => SequenceNumbersIssuedUnderCurrentToken; + SequenceNumberBudget ISecureChannelMessageHost.SequenceBudget => SequenceBudget; + + /// + /// Tracks how much of the SecureChannel SequenceNumber space remains + /// under the current SecurityToken. Every MessageType the channel + /// carries draws on the same space. + /// + public SequenceNumberBudget SequenceBudget + { + get + { + m_sequenceBudget.ObserveConsumed(SequenceNumbersIssuedUnderCurrentToken); + return m_sequenceBudget; + } + } + + /// + /// True when the SequenceNumber space remaining under the current + /// token has fallen below the renewal threshold, so the owning channel + /// should initiate OpenSecureChannel with RenewalRequest ahead of the + /// normal lifetime based renewal. + /// + public bool IsSequenceRenewalDue => SequenceBudget.ShouldRenew; /// /// Registers the extension that owns a MessageType on this channel. @@ -244,6 +266,8 @@ namespace Opc.Ua.Bindings ref m_sequenceNumberBaseline, Interlocked.Read(ref m_sequenceNumber)); + m_sequenceBudget.OnTokenActivated(); + if (m_messageExtensions.IsEmpty) { return; @@ -305,6 +329,7 @@ namespace Opc.Ua.Bindings private readonly ConcurrentDictionary m_messageExtensions = new(); + private readonly SequenceNumberBudget m_sequenceBudget = new(); private long m_sequenceNumberBaseline; } } diff --git a/src/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Symmetric.cs b/src/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Symmetric.cs index 8efec94d93..aaa7305632 100644 --- a/src/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Symmetric.cs +++ b/src/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Symmetric.cs @@ -199,11 +199,10 @@ namespace Opc.Ua.Bindings CurrentToken = token; RenewedToken = null; - // The SequenceNumber space is per token, so a new token - // restores the budget an extension stalls against + // The SequenceNumber space is per token, so a new token restores + // the budget every MessageType on the channel draws on // (Part 6 errata 5.1.1). NotifySecurityTokenActivated(); - m_sequenceBudget.OnTokenActivated(); TokenActivatedCallback?.Invoke(token, PreviousToken); diff --git a/src/Opc.Ua.Core/Stack/Tcp/UaSCBinaryClientChannel.cs b/src/Opc.Ua.Core/Stack/Tcp/UaSCBinaryClientChannel.cs index ec99595cfc..0951d57b6d 100644 --- a/src/Opc.Ua.Core/Stack/Tcp/UaSCBinaryClientChannel.cs +++ b/src/Opc.Ua.Core/Stack/Tcp/UaSCBinaryClientChannel.cs @@ -1483,7 +1483,7 @@ namespace Opc.Ua.Bindings if (m_logger.IsEnabled(LogLevel.Information)) { - DataChannelSequenceBudget budget = SequenceBudget; + SequenceNumberBudget budget = SequenceBudget; m_logger.UaSCClientLog37( ChannelId, diff --git a/src/Opc.Ua.Core/Stack/Tcp/UaSCBinaryTransportChannel.cs b/src/Opc.Ua.Core/Stack/Tcp/UaSCBinaryTransportChannel.cs index a8f55983f6..d6e1174b1d 100644 --- a/src/Opc.Ua.Core/Stack/Tcp/UaSCBinaryTransportChannel.cs +++ b/src/Opc.Ua.Core/Stack/Tcp/UaSCBinaryTransportChannel.cs @@ -150,29 +150,18 @@ namespace Opc.Ua.Bindings /// public TransportChannelFeatures SupportedFeatures => (m_channel?.Transport?.Features ?? TransportChannelFeatures.None) | - TransportChannelFeatures.DataChannels; + TransportChannelFeatures.MessageExtensions; /// - /// Enables OPC UA data channels on the current SecureChannel. + /// The SecureChannel behind this transport channel, or null + /// while it is not connected. /// - public DataChannelManager EnableDataChannels( - bool isServer, - ITelemetryContext telemetry, - ushort maxDataChannels = 16, - uint maxCreditPerChannel = 1024 * 1024) - { - UaSCUaBinaryClientChannel channel = m_channel ?? throw BadNotConnected(); - return channel.EnableDataChannels( - isServer, - telemetry, - maxDataChannels, - maxCreditPerChannel); - } - - /// - /// The data channels multiplexed onto the current SecureChannel, if enabled. - /// - public DataChannelManager? DataChannels => m_channel?.DataChannels; + /// + /// Exposed so a message extension can attach itself to the channel a + /// Client is already using. It is the channel, not this wrapper, that + /// carries the MessageType an extension owns. + /// + public UaSCUaBinaryClientChannel? SecureChannel => m_channel; /// public EndpointDescription EndpointDescription diff --git a/src/Opc.Ua.Core/Stack/Tcp/UaSCSecureChannelRegistry.cs b/src/Opc.Ua.Core/Stack/Tcp/UaSCSecureChannelRegistry.cs new file mode 100644 index 0000000000..7088dafc3c --- /dev/null +++ b/src/Opc.Ua.Core/Stack/Tcp/UaSCSecureChannelRegistry.cs @@ -0,0 +1,95 @@ +/* ======================================================================== + * Copyright (c) 2005-2026 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.Concurrent; + +namespace Opc.Ua.Bindings +{ + /// + /// Maps server-side SecureChannel identifiers to the UASC channels that + /// own them. + /// + /// + /// A Service request carries a SecureChannel identifier rather than the + /// channel object, so anything that has to reach the transport behind a + /// request - a message extension binding itself to the connection the + /// request arrived on, for example - needs this lookup. + /// + /// TODO: This is process-global rather than scoped to the listener that + /// owns the channels, which means two servers in one process share it. + /// Scoping it to the listener requires threading a registry instance + /// through the transport bindings and is left as separate work. + /// + /// + public static class UaSCSecureChannelRegistry + { + /// + /// Finds the server-side UASC channel that owns a SecureChannel. + /// + /// The SecureChannel identifier. + /// The channel that owns it. + public static bool TryGet( + string secureChannelId, + out UaSCUaBinaryChannel? channel) + { + return s_channels.TryGetValue(secureChannelId, out channel); + } + + /// + /// Records the channel that owns a SecureChannel. + /// + /// The SecureChannel identifier. + /// The channel that owns it. + public static void Bind(string secureChannelId, UaSCUaBinaryChannel channel) + { + if (!string.IsNullOrEmpty(secureChannelId)) + { + s_channels[secureChannelId] = channel; + } + } + + /// + /// Releases the record for a SecureChannel, if the channel given still + /// owns it. + /// + /// The SecureChannel identifier. + /// The channel that owned it. + public static void Unbind(string secureChannelId, UaSCUaBinaryChannel channel) + { + if (!string.IsNullOrEmpty(secureChannelId) && + s_channels.TryGetValue(secureChannelId, out UaSCUaBinaryChannel? current) && + ReferenceEquals(current, channel)) + { + s_channels.TryRemove(secureChannelId, out _); + } + } + + private static readonly ConcurrentDictionary s_channels = new(); + } +} diff --git a/src/Opc.Ua.Core/Stack/Transport/ITransportChannel.cs b/src/Opc.Ua.Core/Stack/Transport/ITransportChannel.cs index 360d5b4989..ce689e6a6c 100644 --- a/src/Opc.Ua.Core/Stack/Transport/ITransportChannel.cs +++ b/src/Opc.Ua.Core/Stack/Transport/ITransportChannel.cs @@ -201,10 +201,10 @@ namespace Opc.Ua ReverseConnect = 0x0040, /// - /// The channel can multiplex OPC UA data channels onto the - /// SecureChannel. Experimental. + /// The channel can carry MessageTypes owned by a registered + /// extension, for example OPC UA data channels. Experimental. /// - DataChannels = 0x0080, + MessageExtensions = 0x0080, /// /// The transport under the channel provides independently diff --git a/src/Opc.Ua.Redundancy.Client/RedundantClientSession.cs b/src/Opc.Ua.Redundancy.Client/RedundantClientSession.cs index fafd7906d6..dce50c7e66 100644 --- a/src/Opc.Ua.Redundancy.Client/RedundantClientSession.cs +++ b/src/Opc.Ua.Redundancy.Client/RedundantClientSession.cs @@ -1237,8 +1237,8 @@ namespace Opc.Ua.Redundancy.Client /// #if NET8_0_OR_GREATER [System.Diagnostics.CodeAnalysis.Experimental( - Opc.Ua.Bindings.DataChannelConstants.ExperimentalDiagnosticId, - UrlFormat = Opc.Ua.Bindings.DataChannelConstants.ExperimentalUrlFormat)] + Opc.Ua.DataChannelFeature.ExperimentalDiagnosticId, + UrlFormat = Opc.Ua.DataChannelFeature.ExperimentalUrlFormat)] #endif public async ValueTask OpenDataChannelAsync( RequestHeader? requestHeader, @@ -1262,8 +1262,8 @@ namespace Opc.Ua.Redundancy.Client /// #if NET8_0_OR_GREATER [System.Diagnostics.CodeAnalysis.Experimental( - Opc.Ua.Bindings.DataChannelConstants.ExperimentalDiagnosticId, - UrlFormat = Opc.Ua.Bindings.DataChannelConstants.ExperimentalUrlFormat)] + Opc.Ua.DataChannelFeature.ExperimentalDiagnosticId, + UrlFormat = Opc.Ua.DataChannelFeature.ExperimentalUrlFormat)] #endif public async ValueTask ModifyDataChannelAsync( RequestHeader? requestHeader, @@ -1283,8 +1283,8 @@ namespace Opc.Ua.Redundancy.Client /// #if NET8_0_OR_GREATER [System.Diagnostics.CodeAnalysis.Experimental( - Opc.Ua.Bindings.DataChannelConstants.ExperimentalDiagnosticId, - UrlFormat = Opc.Ua.Bindings.DataChannelConstants.ExperimentalUrlFormat)] + Opc.Ua.DataChannelFeature.ExperimentalDiagnosticId, + UrlFormat = Opc.Ua.DataChannelFeature.ExperimentalUrlFormat)] #endif public async ValueTask CloseDataChannelAsync( RequestHeader? requestHeader, diff --git a/src/Opc.Ua.Server/Opc.Ua.Server.csproj b/src/Opc.Ua.Server/Opc.Ua.Server.csproj index 785fa43496..53486b6272 100644 --- a/src/Opc.Ua.Server/Opc.Ua.Server.csproj +++ b/src/Opc.Ua.Server/Opc.Ua.Server.csproj @@ -35,6 +35,7 @@ + diff --git a/src/Opc.Ua.Server/Server/StandardServer.DataChannels.cs b/src/Opc.Ua.Server/Server/StandardServer.DataChannels.cs index cb3333a28d..161a839893 100644 --- a/src/Opc.Ua.Server/Server/StandardServer.DataChannels.cs +++ b/src/Opc.Ua.Server/Server/StandardServer.DataChannels.cs @@ -16,35 +16,6 @@ using Opc.Ua.Bindings; namespace Opc.Ua.Server { - /// - /// Optional transport-side binding for the DataChannel Service Set. - /// - public interface IServerDataChannelTransport - { - bool TryGetManager( - SecureChannelContext secureChannelContext, - DataChannelServerCapabilities capabilities, - ITelemetryContext telemetry, - out DataChannelManager manager, - out uint maxFrameSize, - out bool isReliable); - - ValueTask AllocateServerStreamAsync( - SecureChannelContext secureChannelContext, - uint channelId, - DataChannelDirection direction, - CancellationToken ct); - - ValueTask BindClientStreamAsync( - SecureChannelContext secureChannelContext, - uint channelId, - ulong streamId, - DataChannelDirection direction, - CancellationToken ct); - - void AbortSecureChannel(SecureChannelContext secureChannelContext, StatusCode reason); - } - public partial class StandardServer { public DataChannelSourceRegistry DataChannelSources { get; } = new(); diff --git a/tests/Opc.Ua.Core.DataChannels.Tests/DataChannelConformanceTests.cs b/tests/Opc.Ua.Core.DataChannels.Tests/DataChannelConformanceTests.cs index e539563b5f..e345eb2b0c 100644 --- a/tests/Opc.Ua.Core.DataChannels.Tests/DataChannelConformanceTests.cs +++ b/tests/Opc.Ua.Core.DataChannels.Tests/DataChannelConformanceTests.cs @@ -126,14 +126,14 @@ namespace Opc.Ua.Core.DataChannels.Tests channel.AttachTransport(transport); channel.Activate(SecureChannelId, TokenId); DataChannel sink = OpenSink(channel); - await PumpSchedulerRoundAsync(channel.DataChannels!).ConfigureAwait(false); + await PumpSchedulerRoundAsync(channel.GetDataChannels()!).ConfigureAwait(false); transport.ClearChunks(); byte[] chunk = SpecVectors.Load("inline_data_first"); chunk[SpecVectors.InlinePrefix + 4] = frameType; Assert.That(channel.DispatchStream(chunk), Is.False); - await PumpSchedulerRoundAsync(channel.DataChannels!).ConfigureAwait(false); + await PumpSchedulerRoundAsync(channel.GetDataChannels()!).ConfigureAwait(false); DataChannelFrame reset = await WaitForOutboundFrameAsync(transport, DataChannelFrameType.Reset) .ConfigureAwait(false); @@ -151,7 +151,7 @@ namespace Opc.Ua.Core.DataChannels.Tests Assert.That(expectedDecodeError.ToStatusCode(), Is.EqualTo(expectedStatus)); }); - await channel.DataChannels!.DisposeAsync().ConfigureAwait(false); + await channel.GetDataChannels()!.DisposeAsync().ConfigureAwait(false); } [Test] diff --git a/tests/Opc.Ua.Core.DataChannels.Tests/DataChannelSequenceBudgetTests.cs b/tests/Opc.Ua.Core.DataChannels.Tests/DataChannelSequenceBudgetTests.cs index d215cae66e..c29add8eea 100644 --- a/tests/Opc.Ua.Core.DataChannels.Tests/DataChannelSequenceBudgetTests.cs +++ b/tests/Opc.Ua.Core.DataChannels.Tests/DataChannelSequenceBudgetTests.cs @@ -13,13 +13,13 @@ namespace Opc.Ua.Core.DataChannels.Tests { [TestFixture] [Category("DataChannels")] - public sealed class DataChannelSequenceBudgetTests + public sealed class SequenceNumberBudgetTests { [Test] public void ObservedServiceChunksContributeToRenewalAndStallBudget() { var clock = new FakeTimeProvider(DateTimeOffset.UtcNow); - var budget = new DataChannelSequenceBudget(clock, capacity: 5); + var budget = new SequenceNumberBudget(clock, capacity: 5); budget.ObserveConsumed(4); clock.Advance(TimeSpan.FromSeconds(1)); @@ -45,7 +45,7 @@ namespace Opc.Ua.Core.DataChannels.Tests [Test] public void TokenActivationResetsObservedAndConsumedBudget() { - var budget = new DataChannelSequenceBudget(capacity: 3); + var budget = new SequenceNumberBudget(capacity: 3); budget.ObserveSequenceNumber(2); Assert.That(budget.MustStall, Is.True); diff --git a/tests/Opc.Ua.Core.DataChannels.Tests/DataChannelSequencingTests.cs b/tests/Opc.Ua.Core.DataChannels.Tests/DataChannelSequencingTests.cs index 908f2598fc..a916978d14 100644 --- a/tests/Opc.Ua.Core.DataChannels.Tests/DataChannelSequencingTests.cs +++ b/tests/Opc.Ua.Core.DataChannels.Tests/DataChannelSequencingTests.cs @@ -261,7 +261,7 @@ namespace Opc.Ua.Core.DataChannels.Tests [Test] public void DcF034RenewalIsDueBeforeTheSpaceIsExhausted() { - var budget = new DataChannelSequenceBudget(capacity: 1000); + var budget = new SequenceNumberBudget(capacity: 1000); Assert.That(budget.ShouldRenew, Is.False, "a fresh token has the whole space"); @@ -281,7 +281,7 @@ namespace Opc.Ua.Core.DataChannels.Tests [Test] public void DcF034ASenderStallsRatherThanReusingASequenceNumber() { - var budget = new DataChannelSequenceBudget(capacity: 4); + var budget = new SequenceNumberBudget(capacity: 4); for (int ii = 0; ii < 4; ii++) { @@ -304,7 +304,7 @@ namespace Opc.Ua.Core.DataChannels.Tests [Test] public void ANewTokenRestoresTheWholeSpace() { - var budget = new DataChannelSequenceBudget(capacity: 4); + var budget = new SequenceNumberBudget(capacity: 4); for (int ii = 0; ii < 4; ii++) { @@ -329,7 +329,7 @@ namespace Opc.Ua.Core.DataChannels.Tests var clock = new Microsoft.Extensions.Time.Testing.FakeTimeProvider( new DateTimeOffset(2026, 1, 1, 0, 0, 0, TimeSpan.Zero)); - var budget = new DataChannelSequenceBudget(clock); + var budget = new SequenceNumberBudget(clock); // Ten chunks over ten seconds is one per second, so one // minute of traffic is sixty values: far below the fixed @@ -359,7 +359,7 @@ namespace Opc.Ua.Core.DataChannels.Tests var clock = new Microsoft.Extensions.Time.Testing.FakeTimeProvider( new DateTimeOffset(2026, 1, 1, 0, 0, 0, TimeSpan.Zero)); - var budget = new DataChannelSequenceBudget(clock); + var budget = new SequenceNumberBudget(clock); // A hundred million chunks in one second is a rate whose // minute of traffic exceeds the 32 bit space entirely, so the diff --git a/tests/Opc.Ua.Core.DataChannels.Tests/InlineServerDataChannelTransportTests.cs b/tests/Opc.Ua.Core.DataChannels.Tests/InlineServerDataChannelTransportTests.cs index 07e6442ad1..4cddbcded2 100644 --- a/tests/Opc.Ua.Core.DataChannels.Tests/InlineServerDataChannelTransportTests.cs +++ b/tests/Opc.Ua.Core.DataChannels.Tests/InlineServerDataChannelTransportTests.cs @@ -62,7 +62,7 @@ namespace Opc.Ua.Core.DataChannels.Tests channel.Activate(0x0000A17C, 7); const string secureChannelId = "inline-transport-1"; - UaSCDataChannelSecureChannelRegistry.Bind(secureChannelId, channel); + UaSCSecureChannelRegistry.Bind(secureChannelId, channel); try { @@ -84,7 +84,7 @@ namespace Opc.Ua.Core.DataChannels.Tests { Assert.That(maxFrameSize, Is.GreaterThan(0u)); Assert.That(isReliable, Is.True); - Assert.That(manager, Is.SameAs(channel.DataChannels)); + Assert.That(manager, Is.SameAs(channel.GetDataChannels())); }); DataChannel source = manager.Register( @@ -112,7 +112,7 @@ namespace Opc.Ua.Core.DataChannels.Tests } finally { - UaSCDataChannelSecureChannelRegistry.Unbind(secureChannelId, channel); + UaSCSecureChannelRegistry.Unbind(secureChannelId, channel); } } @@ -140,7 +140,7 @@ namespace Opc.Ua.Core.DataChannels.Tests ITelemetryContext telemetry = NUnitTelemetryContext.Create(); using InlineTestChannel channel = InlineTestChannel.Create("inline-profile", telemetry); const string secureChannelId = "inline-transport-2"; - UaSCDataChannelSecureChannelRegistry.Bind(secureChannelId, channel); + UaSCSecureChannelRegistry.Bind(secureChannelId, channel); try { @@ -159,7 +159,7 @@ namespace Opc.Ua.Core.DataChannels.Tests } finally { - UaSCDataChannelSecureChannelRegistry.Unbind(secureChannelId, channel); + UaSCSecureChannelRegistry.Unbind(secureChannelId, channel); } } diff --git a/tests/Opc.Ua.Core.DataChannels.Tests/Opc.Ua.Core.DataChannels.Tests.csproj b/tests/Opc.Ua.Core.DataChannels.Tests/Opc.Ua.Core.DataChannels.Tests.csproj index 3da3c4dae0..6643096cc6 100644 --- a/tests/Opc.Ua.Core.DataChannels.Tests/Opc.Ua.Core.DataChannels.Tests.csproj +++ b/tests/Opc.Ua.Core.DataChannels.Tests/Opc.Ua.Core.DataChannels.Tests.csproj @@ -42,6 +42,7 @@ + diff --git a/tests/Opc.Ua.Core.DataChannels.Tests/UaSCDataChannelDispatchTests.cs b/tests/Opc.Ua.Core.DataChannels.Tests/UaSCDataChannelDispatchTests.cs index 9391954b82..d2a25b7fde 100644 --- a/tests/Opc.Ua.Core.DataChannels.Tests/UaSCDataChannelDispatchTests.cs +++ b/tests/Opc.Ua.Core.DataChannels.Tests/UaSCDataChannelDispatchTests.cs @@ -432,9 +432,11 @@ namespace Opc.Ua.Core.DataChannels.Tests private static async Task DisposeDataChannelsAsync(TestChannel channel) { - if (channel.DataChannels != null) + DataChannelManager? manager = channel.GetDataChannels(); + + if (manager != null) { - await channel.DataChannels.DisposeAsync().ConfigureAwait(false); + await manager.DisposeAsync().ConfigureAwait(false); } }