During a reconnect to a session the method CreateOrModifySubscription in Subscription.cs is called. In line 2022 the m_transferId and hence the TransferId is overwritten even if previously the TransferId was set.
Expected Behavior
The TransferId does not change so the new subscription can be associated with the previous subscription.
Steps To Reproduce
Operating system: Microsoft Windows 10
Tested with commit 0b23e5f3 on branch release/1.5.374
Create a new C# console project with the name OpcUaExample and .NET 8
Add the project Opc.Ua.Client.csproj from UA-.NETStandard/Libraries/Opc.Ua.Client_ to the solution
Add a project reference to Opc.Ua.Client to OpcUaExample
Replace the contents of Program.cs with the following:
using Opc.Ua;
using Opc.Ua.Client;
using Opc.Ua.Configuration;
using ISession = Opc.Ua.Client.ISession;
namespace OpcUaExample;
///
/// Class containing the entry point of the program.
///
public class Program
{
/// <summary>
/// Entry point of the program.
/// </summary>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static async Task Main()
{
using OpcUaSessionKeeper opcUaSessionKeeper = new("opc.tcp://localhost:62541/Quickstarts/ReferenceServer");
await opcUaSessionKeeper.ConnectOpcUaSession();
Console.WriteLine("Executing {0}", nameof(opcUaSessionKeeper.MonitorNodeValue));
opcUaSessionKeeper.MonitorNodeValue();
opcUaSessionKeeper.WriteNodeValue(100);
await Task.Delay(TimeSpan.FromSeconds(2));
Console.WriteLine();
Console.WriteLine("Please restart the OPC UA server to see the reconnection handler in action. Press any key to continue after the restart.");
Console.WriteLine();
Console.ReadKey(intercept: true);
opcUaSessionKeeper.WriteNodeValue(101);
await Task.Delay(TimeSpan.FromSeconds(2));
opcUaSessionKeeper.RemoveSubscriptions();
Console.WriteLine();
await opcUaSessionKeeper.DisconnectOpcUaSession();
Console.WriteLine("Press any key to exit");
Console.ReadKey(intercept: true);
}
}
///
/// Class to manage a OPC UA session.
///
public class OpcUaSessionKeeper : IDisposable
{
private readonly string _opcUaUri;
private ISession? _opcUaSession;
private SessionReconnectHandler? _sessionReconnectHandler;
private readonly ushort _opcUaCertificateLifetimeInMonths = 180;
private readonly List _subscriptions = [];
/// <summary>
/// Constructor to instantiate an <see cref="OpcUaSessionKeeper"/>.
/// </summary>
public OpcUaSessionKeeper(string uri)
{
_opcUaUri = uri;
}
/// <inheritdoc/>
public void Dispose()
{
DisconnectOpcUaSession().Wait();
}
/// <summary>
/// Creates a new OPC UA <see cref="ISession"/> and connects to it.
/// </summary>
public async Task ConnectOpcUaSession()
{
// define the OPC UA client application
ApplicationInstance application = new()
{
ApplicationType = ApplicationType.Client,
};
// load the application configuration
string applicationConfigurationFilePath = Path.Combine(AppContext.BaseDirectory, "OpcUaExample.Config.xml");
ApplicationConfiguration config = await application.LoadApplicationConfiguration(applicationConfigurationFilePath, silent: false);
try
{
// check the application certificate.
await application.CheckApplicationInstanceCertificate(silent: false, minimumKeySize: 0, _opcUaCertificateLifetimeInMonths);
}
catch (Exception ex)
{
string baseLoggingMessage = "Exception occured during check of certificate.";
Console.WriteLine("{0} Cause: {1}: {2}.", baseLoggingMessage, ex.GetType(), ex.Message);
// try deleting the old certificate and creating a new one
await application.DeleteApplicationInstanceCertificate();
application.ApplicationConfiguration.SecurityConfiguration.ApplicationCertificate.Certificate = null;
// create a new application certificate
await application.CheckApplicationInstanceCertificate(silent: false, minimumKeySize: 0, _opcUaCertificateLifetimeInMonths);
Console.WriteLine("Deleted the old application certificate and created a new one successfully.");
}
// SessionTimeOut >= KeepAliveTimeout
uint sessionTimeOutMs = (uint)TimeSpan.FromSeconds(60).TotalMilliseconds;
int keepAliveIntervalMs = (int)TimeSpan.FromSeconds(30).TotalMilliseconds;
string serverUri = _opcUaUri;
UserIdentity? userIdentity = null;
Console.WriteLine("Connecting to OPC UA server {0}.", serverUri);
// configure endpoint for OPC UA
EndpointDescription endpointDescription = CoreClientUtils.SelectEndpoint(config, serverUri, useSecurity: true);
EndpointConfiguration endpointConfiguration = EndpointConfiguration.Create(config);
ConfiguredEndpoint endpoint = new(null, endpointDescription, endpointConfiguration);
Session session = await Session.Create(
config,
endpoint,
updateBeforeConnect: false,
sessionName: config.ApplicationName,
sessionTimeOutMs,
userIdentity,
preferredLocales: null
);
session.KeepAliveInterval = keepAliveIntervalMs;
session.DeleteSubscriptionsOnClose = false;
session.TransferSubscriptionsOnReconnect = true;
Console.WriteLine("New session for OPC UA created with session name {0} for server {1}.", session.SessionName, serverUri);
_sessionReconnectHandler = new SessionReconnectHandler(reconnectAbort: true, maxReconnectPeriod: (int)TimeSpan.FromMinutes(5).TotalMilliseconds);
session.KeepAlive += RecoverSessionOnError;
_opcUaSession = session;
}
private void RecoverSessionOnError(ISession session, KeepAliveEventArgs e)
{
if (e.Status?.StatusCode.Code != StatusCodes.BadSecureChannelClosed)
{
return;
}
Console.WriteLine("Received bad status {0} for session with name {1} for server {2}. Recovering session with id {3}.",
e.Status, session.SessionName, string.Join(", ", session.ServerUris.ToArray()), session.SessionId);
_sessionReconnectHandler!.BeginReconnect(session, (int)TimeSpan.FromSeconds(1).TotalMilliseconds, ReconnectCompleted);
// Cancel sending a new keep alive request because reconnect is triggered.
e.CancelKeepAlive = true;
}
private void ReconnectCompleted(object? sender, EventArgs e)
{
SessionReconnectHandler sessionReconnectHandler = (SessionReconnectHandler)sender!;
if (sessionReconnectHandler.Session == null)
{
Console.WriteLine("Session which was tried to recover recovered itself.");
return;
}
ISession originalSession = _opcUaSession!;
// ensure only a new instance is disposed
// after reactivate, the same session instance may be returned
if (ReferenceEquals(originalSession, sessionReconnectHandler.Session))
{
Console.WriteLine("Session with name {0} for server {1} was reactivated.",
sessionReconnectHandler.Session.SessionName, string.Join(", ", sessionReconnectHandler.Session.ServerUris.ToArray()));
return;
}
_opcUaSession = sessionReconnectHandler.Session;
originalSession.Dispose();
Console.WriteLine("Reconnected to a new session with name {0} for server {1}.",
sessionReconnectHandler.Session.SessionName, string.Join(", ", sessionReconnectHandler.Session.ServerUris.ToArray()));
}
/// <summary>
/// Closes the OPC UA <see cref="ISession"/> if a connection exists.
/// </summary>
public async Task DisconnectOpcUaSession()
{
if (_opcUaSession == null)
{
return;
}
_opcUaSession.KeepAlive -= RecoverSessionOnError;
await _opcUaSession.CloseAsync();
_opcUaSession.Dispose();
}
/// <summary>
/// Updates the value of a node.
/// </summary>
/// <param name="value">The new value for a node.</param>
/// <exception cref="InvalidOperationException">
/// <see cref="InvalidOperationException"/> is thrown when this
/// function is called when the client is not connected.
/// </exception>
public void WriteNodeValue(int value)
{
if (_opcUaSession == null)
{
throw new InvalidOperationException("Session is null");
}
NodeId nodeToWrite = new(value: 2044, namespaceIndex: 3);
WriteValueCollection writeValueCollection = [];
WriteValue writeValue = new()
{
NodeId = nodeToWrite,
AttributeId = Attributes.Value,
Value = new DataValue(value),
};
writeValueCollection.Add(writeValue);
ResponseHeader responseHeader = _opcUaSession.Write(null, writeValueCollection,
out StatusCodeCollection statusCodeCollection, out DiagnosticInfoCollection diagnosticInfoCollection);
}
/// <summary>
/// Creates a subscription to monitor value changes of a node.
/// </summary>
/// <exception cref="InvalidOperationException">
/// <see cref="InvalidOperationException"/> is thrown when this
/// function is called when the client is not connected.
/// </exception>
public void MonitorNodeValue()
{
if (_opcUaSession == null)
{
throw new InvalidOperationException("Session is null");
}
NodeId nodeToMonitor = new(value: 2044, namespaceIndex: 3);
int publishingIntervalMs = (int)TimeSpan.FromSeconds(1).TotalMilliseconds;
int samplingIntervalMs = (int)TimeSpan.FromSeconds(0.5).TotalMilliseconds;
uint queueSize = (uint)Math.Ceiling((double)publishingIntervalMs / samplingIntervalMs);
Subscription subscription = new(_opcUaSession.DefaultSubscription)
{
DisplayName = $"Subscription of OpcUaExample",
PublishingEnabled = true,
PublishingInterval = publishingIntervalMs,
MinLifetimeInterval = (uint)TimeSpan.FromMinutes(2).TotalMilliseconds,
};
_opcUaSession.AddSubscription(subscription);
// Create the subscription on server side
subscription.Create();
// Create MonitoredItems for data changes
MonitoredItem monitoredItem = new(subscription.DefaultItem)
{
StartNodeId = nodeToMonitor,
AttributeId = Attributes.Value,
QueueSize = queueSize,
SamplingInterval = samplingIntervalMs,
DiscardOldest = true,
};
monitoredItem.Notification += NotificationEventHandler;
subscription.AddItem(monitoredItem);
// Create the monitored items on server side
subscription.ApplyChanges();
Console.WriteLine("MonitoredItems created for SubscriptionId = {0}.", subscription.Id);
_subscriptions.Add(subscription);
}
/// <summary>
/// A function to print details of updates of a monitored node.
/// </summary>
/// <param name="opcUaMonitoredItem">The monitored node which was updated.</param>
/// <param name="e">Additional information for the monitored node.</param>
public void NotificationEventHandler(MonitoredItem opcUaMonitoredItem, MonitoredItemNotificationEventArgs e)
{
MonitoredItemNotification notification = (MonitoredItemNotification)e.NotificationValue;
Console.WriteLine("Subscription id: {0}, sequence number: {1}, node id: {2}, sampling time: {3}, value: {4}",
opcUaMonitoredItem.Subscription.Id,
opcUaMonitoredItem.Subscription.SequenceNumber,
opcUaMonitoredItem.StartNodeId,
notification.Value.SourceTimestamp,
notification.Value.Value);
}
/// <summary>
/// Removes all subscriptions.
/// </summary>
/// <exception cref="InvalidOperationException">
/// <see cref="InvalidOperationException"/> is thrown when this
/// function is called when the client is not connected.
/// </exception>
public void RemoveSubscriptions()
{
if (_opcUaSession == null)
{
return;
}
Console.WriteLine("Removing subscriptions with ids [{0}].", string.Join(", ", _subscriptions.Select(subscription => subscription.Id)));
_opcUaSession.RemoveSubscriptions(_subscriptions);
_subscriptions.Clear();
}
}
8. Create a file _OpcUaExample.Config.xml_ and paste the following:
```xml
<?xml version="1.0" encoding="utf-8"?>
<ApplicationConfiguration
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:ua="http://opcfoundation.org/UA/2008/02/Types.xsd"
xmlns="http://opcfoundation.org/UA/SDK/Configuration.xsd"
>
<ApplicationName>OpcUaExample</ApplicationName>
<ApplicationUri>urn:localhost:OpcUaExample</ApplicationUri>
<ApplicationType>Client_1</ApplicationType>
<SecurityConfiguration>
<!-- Where the application instance certificate is stored (MachineDefault) -->
<ApplicationCertificate>
<StoreType>Directory</StoreType>
<StorePath>%LocalFolder%/Certificates/own</StorePath>
<SubjectName>CN=OpcUaExample,DC=localhost</SubjectName>
</ApplicationCertificate>
<!-- Where the issuer certificate are stored (certificate authorities) -->
<TrustedIssuerCertificates>
<StoreType>Directory</StoreType>
<StorePath>%LocalFolder%/Certificates/issuer</StorePath>
</TrustedIssuerCertificates>
<!-- Where the trust list is stored -->
<TrustedPeerCertificates>
<StoreType>Directory</StoreType>
<StorePath>%LocalFolder%/Certificates/trusted</StorePath>
</TrustedPeerCertificates>
<!-- The directory used to store invalid certficates for later review by the administrator. -->
<RejectedCertificateStore>
<StoreType>Directory</StoreType>
<StorePath>%LocalFolder%/Certificates/rejected</StorePath>
</RejectedCertificateStore>
<!-- WARNING: The following setting (to automatically accept untrusted certificates) should be used
for easy debugging purposes ONLY and turned off for production deployments! -->
<AutoAcceptUntrustedCertificates>true</AutoAcceptUntrustedCertificates>
<!-- WARNING: SHA1 signed certficates are by default rejected and should be phased out.
only nano and embedded profiles are allowed to use sha1 signed certificates. -->
<RejectSHA1SignedCertificates>true</RejectSHA1SignedCertificates>
<RejectUnknownRevocationStatus>true</RejectUnknownRevocationStatus>
<MinimumCertificateKeySize>2048</MinimumCertificateKeySize>
<AddAppCertToTrustedStore>false</AddAppCertToTrustedStore>
<SendCertificateChain>true</SendCertificateChain>
<!-- Where the User trust list is stored-->
<TrustedUserCertificates>
<StoreType>Directory</StoreType>
<StorePath>%LocalFolder%/Certificates/trustedUser</StorePath>
</TrustedUserCertificates>
</SecurityConfiguration>
<TransportConfigurations></TransportConfigurations>
<TransportQuotas>
<OperationTimeout>120000</OperationTimeout>
<MaxStringLength>4194304</MaxStringLength>
<MaxByteStringLength>4194304</MaxByteStringLength>
<MaxArrayLength>65535</MaxArrayLength>
<MaxMessageSize>4194304</MaxMessageSize>
<MaxBufferSize>65535</MaxBufferSize>
<ChannelLifetime>300000</ChannelLifetime>
<SecurityTokenLifetime>3600000</SecurityTokenLifetime>
</TransportQuotas>
<ClientConfiguration>
<DefaultSessionTimeout>60000</DefaultSessionTimeout>
<WellKnownDiscoveryUrls>
<ua:String>opc.tcp://{0}:4840</ua:String>
<ua:String>http://{0}:52601/UADiscovery</ua:String>
<ua:String>http://{0}/UADiscovery/Default.svc</ua:String>
</WellKnownDiscoveryUrls>
<DiscoveryServers></DiscoveryServers>
<MinSubscriptionLifetime>10000</MinSubscriptionLifetime>
<OperationLimits>
<MaxNodesPerRead>2500</MaxNodesPerRead>
<MaxNodesPerHistoryReadData>1000</MaxNodesPerHistoryReadData>
<MaxNodesPerHistoryReadEvents>1000</MaxNodesPerHistoryReadEvents>
<MaxNodesPerWrite>2500</MaxNodesPerWrite>
<MaxNodesPerHistoryUpdateData>1000</MaxNodesPerHistoryUpdateData>
<MaxNodesPerHistoryUpdateEvents>1000</MaxNodesPerHistoryUpdateEvents>
<MaxNodesPerMethodCall>2500</MaxNodesPerMethodCall>
<MaxNodesPerBrowse>2500</MaxNodesPerBrowse>
<MaxNodesPerRegisterNodes>2500</MaxNodesPerRegisterNodes>
<MaxNodesPerTranslateBrowsePathsToNodeIds>2500</MaxNodesPerTranslateBrowsePathsToNodeIds>
<MaxNodesPerNodeManagement>2500</MaxNodesPerNodeManagement>
<MaxMonitoredItemsPerCall>2500</MaxMonitoredItemsPerCall>
</OperationLimits>
</ClientConfiguration>
</ApplicationConfiguration>
Make sure that the configuration file is copied to output directory, e.g. with adding the following to OpcUaExample.csproj:
Open the solution UA-.NETStandard/UA Reference.sln
Start the project ConsoleReferenceServer
Start the project OpcUaExample
OpcUaExample fails with ServiceResultException due to an untrusted certificate
Move the rejected certificate from ~\AppData\Local\OPC Foundation\pki\rejected\certs to ~\AppData\Local\OPC Foundation\pki\trusted\certs
Start OpcUaExample again
Wait until "Please restart the OPC UA server to see the reconnection handler in action. Press any key to continue after the restart." is printed to the console
Set a breakpoint in Opc.Ua.Client/Subscription/Subscription.cs in line 2022 (in solution with OpcUaExample)
Restart the ConsoleReferenceServer
When the breakpoint is hit check the value of `mtransferId which should have the value of the previous subscription id (compare with value printed in console)
Step over the line and check the value of m_transferId which is now overwritten
Environment
- OS: Microsoft Windows 10
- Environment: Visual Studio 2022 17.11.5
- Runtime: .NET 8.0
- Component: Opc.Ua.Client
- Server: Reference Server
- Client: self-made
Type of issue
Current Behavior
During a reconnect to a session the method
CreateOrModifySubscription
in Subscription.cs is called. In line 2022 the m_transferId and hence the TransferId is overwritten even if previously the TransferId was set.Expected Behavior
The TransferId does not change so the new subscription can be associated with the previous subscription.
Steps To Reproduce
namespace OpcUaExample;
///
/// Class containing the entry point of the program.
///
public class Program
{
}
///
/// Class to manage a OPC UA session.
///
public class OpcUaSessionKeeper : IDisposable
{
private readonly string _opcUaUri;
private ISession? _opcUaSession;
private SessionReconnectHandler? _sessionReconnectHandler;
private readonly ushort _opcUaCertificateLifetimeInMonths = 180;
private readonly List _subscriptions = [];
}
m_transferId
which is now overwrittenEnvironment
Anything else?
No response