Skip to content
43 changes: 40 additions & 3 deletions src/Opc.Ua.Core/Stack/Client/Channels/ClientChannelManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -743,14 +743,51 @@ private async ValueTask ReconnectLeaseAsync(
entry = await SwapFaultedEntryAsync(lease, ct).ConfigureAwait(false);
}

Task<bool> reconnectTask = entry.RequestReconnectAsync(budget, ct);
Task<bool> reconnectTask;
try
{
reconnectTask = entry.RequestReconnectAsync(budget, ct);
}
catch (ServiceResultException sre) when (IsTerminalReconnectRace(entry, sre, ct))
{
entry = await SwapFaultedEntryAsync(lease, ct).ConfigureAwait(false);
reconnectTask = entry.RequestReconnectAsync(budget, ct);
}

if (throwOnReconnectFailure)
{
await AwaitReconnectResultAsync(reconnectTask).ConfigureAwait(false);
try
{
await AwaitReconnectResultAsync(reconnectTask).ConfigureAwait(false);
}
catch (ServiceResultException sre) when (IsTerminalReconnectRace(entry, sre, ct))
{
entry = await SwapFaultedEntryAsync(lease, ct).ConfigureAwait(false);
await AwaitReconnectResultAsync(entry.RequestReconnectAsync(budget, ct))
.ConfigureAwait(false);
}
return;
}

_ = await reconnectTask.ConfigureAwait(false);
try
{
_ = await reconnectTask.ConfigureAwait(false);
}
catch (ServiceResultException sre) when (IsTerminalReconnectRace(entry, sre, ct))
{
entry = await SwapFaultedEntryAsync(lease, ct).ConfigureAwait(false);
_ = await entry.RequestReconnectAsync(budget, ct).ConfigureAwait(false);
}
}

private static bool IsTerminalReconnectRace(
ChannelEntry entry,
ServiceResultException sre,
CancellationToken ct)
{
return !ct.IsCancellationRequested &&
sre.StatusCode == StatusCodes.BadSecureChannelClosed &&
entry.State is ChannelState.Closed or ChannelState.Faulted;
}

private async ValueTask<ChannelEntry> SwapFaultedEntryAsync(
Expand Down
91 changes: 70 additions & 21 deletions src/Opc.Ua.Core/Stack/Client/Channels/Internal/ChannelEntry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -139,11 +139,24 @@ public async Task OpenInitialAsync(
ITransportChannel channel = await CreateTransportChannelAsync(
clientCertificate, clientCertificateChain, ct)
.ConfigureAwait(false);
bool entryClosed;
lock (m_lock)
{
m_underlying = channel;
m_clientCertificateVersion = clientCertificateVersion;
m_activeMetricRecorded = true;
entryClosed = m_state is ChannelState.Closed or ChannelState.Faulted;
if (!entryClosed)
{
m_underlying = channel;
m_clientCertificateVersion = clientCertificateVersion;
m_activeMetricRecorded = true;
}
}
if (entryClosed)
{
await CloseTransportBestEffortAsync(channel).ConfigureAwait(false);
throw ServiceResultException.Create(
StatusCodes.BadSecureChannelClosed,
"Channel is {0}.",
State);
}
OwnerManager.RecordChannelActiveChanged(this, 1);
TransitionTo(ChannelState.Ready, error: null, attempt: 0);
Expand Down Expand Up @@ -233,6 +246,9 @@ internal void ReattachParticipant(
?? throw new InvalidOperationException("Participant factory returned null.");
int refCount = 0;
int participantCount = 0;
ChannelState currentState;
ServiceResult? currentError;
int currentAttempt;
bool attached = false;
lock (m_lock)
{
Expand Down Expand Up @@ -260,11 +276,22 @@ internal void ReattachParticipant(
lease.SwapEntry(this);
refCount = m_refcount;
participantCount = m_leases.Count(l => l.IsActive);
currentState = m_state;
currentError = m_lastError;
currentAttempt = m_lastReconnectAttempt;
}

if (attached)
{
OwnerManager.OnEntryParticipantAttached(this, participant.Id, refCount, participantCount);
if (currentState != ChannelState.Disconnected)
{
lease.RaiseStateChanged(new ChannelStateChange(
ChannelState.Disconnected,
currentState,
currentError,
currentAttempt));
}
}
}

Expand Down Expand Up @@ -895,34 +922,56 @@ private async Task EnsureTransportConnectedAsync(CancellationToken ct)
clientCert, clientChain, ct).ConfigureAwait(false);

ITransportChannel? old;
bool entryClosed;
lock (m_lock)
{
old = m_underlying;
m_underlying = fresh;
m_clientCertificateVersion = certVersion;
}
if (old != null)
{
try
{
await old.CloseAsync(default).ConfigureAwait(false);
}
catch
{
// best-effort
}
try
entryClosed = m_state is ChannelState.Closed or ChannelState.Faulted;
if (entryClosed)
{
OwnerManager.CloseChannel(old);
old = null;
}
catch
else
{
// best-effort
old = m_underlying;
m_underlying = fresh;
m_clientCertificateVersion = certVersion;
}
}
if (entryClosed)
{
await CloseTransportBestEffortAsync(fresh).ConfigureAwait(false);
throw ServiceResultException.Create(
StatusCodes.BadSecureChannelClosed,
"Channel is {0}.",
State);
}
if (old != null)
{
await CloseTransportBestEffortAsync(old).ConfigureAwait(false);
OwnerManager.OnEntryClosed(this, ChannelCloseReason.Faulted);
}
}

private async ValueTask CloseTransportBestEffortAsync(ITransportChannel channel)
{
try
{
await channel.CloseAsync(default).ConfigureAwait(false);
}
catch (Exception ex)
{
OwnerManager.Logger?.ChannelEntryLog0(ex);
}
try
{
OwnerManager.CloseChannel(channel);
}
catch (Exception ex)
{
OwnerManager.Logger?.ChannelEntryLog1(ex);
}
}

#if !NET8_0_OR_GREATER
private sealed class DelayState
{
Expand Down
2 changes: 2 additions & 0 deletions src/Opc.Ua.PubSub/Application/PubSubApplication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -652,6 +652,8 @@ public async ValueTask StartAsync(CancellationToken cancellationToken = default)
catch (Exception ex)
{
m_logger.FailedToEnableConnection(ex, connection.Name);
await StopAsync(CancellationToken.None).ConfigureAwait(false);
throw;
}
}
// Start the metadata publisher AFTER the
Expand Down
140 changes: 138 additions & 2 deletions src/Opc.Ua.Types/BuiltIn/ExtensionObject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Contracts;
using System.IO;
using System.Runtime.InteropServices;
using System.Text.Json;
using System.Text.Json.Serialization;
using Opc.Ua.Types;

Expand All @@ -54,7 +57,6 @@ namespace Opc.Ua
/// how it is encoded.
/// </para>
/// </remarks>
// [Union]
public readonly struct ExtensionObject :
IFormattable,
INullable,
Expand Down Expand Up @@ -312,6 +314,136 @@ private static bool TypeIdMatches(ExpandedNodeId typeId, IEncodeable encodeable)
typeId == encodeable.XmlEncodingId;
}

private bool TryDecodeValue(
IServiceMessageContext messageContext,
[NotNullWhen(true)] out IEncodeable? encodeable)
{
try
{
encodeable = m_body switch
{
ByteString binary when !binary.IsNull =>
DecodeBinary(binary, messageContext),
XmlElement xml =>
DecodeXml(xml, messageContext),
string json =>
DecodeJson(json, messageContext),
_ => default
};
return encodeable != null &&
IsMatchingStructureIdentifier(
TypeId,
encodeable,
messageContext.NamespaceUris);
}
catch (Exception ex) when (
ex is ServiceResultException or
FormatException or
InvalidOperationException or
EndOfStreamException or
JsonException)
{
encodeable = default;
return false;
}
}

private IEncodeable? DecodeBinary(ByteString binary, IServiceMessageContext messageContext)
{
if (!messageContext.Factory.TryGetEncodeableType(TypeId, out IEncodeableType? activator))
{
return default;
}

IEncodeable encodeable = activator.CreateInstance();
ReadOnlyMemory<byte> memory = binary;
using BinaryDecoder decoder = MemoryMarshal.TryGetArray(memory, out ArraySegment<byte> segment)
? new BinaryDecoder(segment, messageContext)
: new BinaryDecoder(memory.ToArray(), messageContext);
encodeable.Decode(decoder);
return encodeable;
}
Comment thread
marcschier marked this conversation as resolved.

private IEncodeable? DecodeXml(XmlElement xml, IServiceMessageContext messageContext)
{
if (!messageContext.Factory.TryGetEncodeableType(TypeId, out _))
{
return default;
}

System.Xml.XmlElement? xmlElement = xml.AsXmlElement();
if (xmlElement == null)
{
return default;
}

using var decoder = new XmlDecoder(xmlElement, messageContext);
decoder.PushNamespace(xmlElement.NamespaceURI);
try
{
return decoder.ReadEncodeable<IEncodeable>(
xmlElement.LocalName,
TypeId);
}
finally
{
decoder.PopNamespace();
}
}

private IEncodeable? DecodeJson(string json, IServiceMessageContext messageContext)
{
if (!messageContext.Factory.TryGetEncodeableType(TypeId, out _))
{
return default;
}

using var decoder = new JsonDecoder(
"{\"" + JsonProperties.UaBody + "\":" + json + "}",
messageContext);
return decoder.ReadEncodeable<IEncodeable>(JsonProperties.UaBody, TypeId);
}

private static bool IsMatchingStructureIdentifier(
ExpandedNodeId actual,
IEncodeable expected,
NamespaceTable namespaceUris)
{
return AreEquivalentStructureIdentifiers(
actual,
expected.TypeId,
namespaceUris) ||
AreEquivalentStructureIdentifiers(
actual,
expected.BinaryEncodingId,
namespaceUris) ||
AreEquivalentStructureIdentifiers(
actual,
expected.XmlEncodingId,
namespaceUris);
}

private static bool AreEquivalentStructureIdentifiers(
ExpandedNodeId first,
ExpandedNodeId second,
NamespaceTable namespaceUris)
{
if (first.IsNull || second.IsNull)
{
return false;
}
if (first == second)
{
return true;
}

var firstLocal = ExpandedNodeId.ToNodeId(first, namespaceUris);
var secondLocal = ExpandedNodeId.ToNodeId(second, namespaceUris);
return !firstLocal.IsNull &&
!secondLocal.IsNull &&
firstLocal == secondLocal;
}

/// <inheritdoc/>
public static bool operator ==(ExtensionObject left, ExtensionObject right)
{
Expand Down Expand Up @@ -403,7 +535,11 @@ public bool TryGetValue(
return false;
}

// TODO: Decode if possible
if (TryDecodeValue(messageContext, out encodeable))
{
return true;
}

encodeable = default;
return false;
}
Expand Down
10 changes: 10 additions & 0 deletions src/Opc.Ua.Types/BuiltIn/TypeInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1530,6 +1530,16 @@ public NodeId GetDataTypeId(Variant value, NamespaceTable namespaceUris, ITypeTa
return ExpandedNodeId.ToNodeId(encodeable!.TypeId, namespaceUris);
}

// Opaque ExtensionObjects may carry the DataTypeId itself; FindDataTypeId only resolves
// encoding ids, so accept a known type before falling back to encoding lookup.
var extensionTypeId = ExpandedNodeId.ToNodeId(
Comment thread
marcschier marked this conversation as resolved.
extension.TypeId,
namespaceUris);
if (typeTree.IsKnown(extensionTypeId))
{
return extensionTypeId;
}

return typeTree.FindDataTypeId(extension.TypeId);
}

Expand Down
Loading
Loading