docs(xmldoc): fill missing XML docs + strip tracking-ID comments across src
v2-ci / build (push) Failing after 41s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped
v2-ci / build (push) Failing after 41s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped
Adds <summary>/<param>/<returns>/<inheritdoc> where missing and removes project bookkeeping IDs (task/tracking refs) from shipped code comments, so the docs read cleanly and CommentChecker is quiet except for known false positives (PLC/protocol terms, event/IEqualityComparer inheritdoc). Doc/comment-only; no logic changed; solution builds clean.
This commit is contained in:
@@ -32,6 +32,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
{
|
||||
private readonly ILogger<OpcUaClientDriver> _logger;
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="OpcUaClientDriver"/> class.</summary>
|
||||
/// <param name="options">Driver configuration.</param>
|
||||
/// <param name="driverInstanceId">Stable logical ID from the config DB.</param>
|
||||
/// <param name="logger">Optional logger; defaults to NullLogger when not supplied.</param>
|
||||
@@ -89,6 +90,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
/// can be exercised deterministically without standing up a live reconnect handler.
|
||||
/// Production code never calls this — the SDK reconnect handler owns the real swap.
|
||||
/// </summary>
|
||||
/// <param name="session">The session instance to install, or <c>null</c> to clear it.</param>
|
||||
internal void SetSessionForTest(ISession? session) => Session = session;
|
||||
|
||||
private DriverHealth _health = new(DriverState.Unknown, null, null);
|
||||
@@ -99,7 +101,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
/// Cert-validation delegate wired when <see cref="OpcUaClientDriverOptions.AutoAcceptCertificates"/>
|
||||
/// is <c>true</c>. Stored so <see cref="Dispose"/> / <see cref="DisposeAsync"/> can
|
||||
/// detach it from the (potentially process-shared) <see cref="CertificateValidator"/>
|
||||
/// and avoid leaking the closure (Driver.OpcUaClient-012).
|
||||
/// and avoid leaking the closure.
|
||||
/// </summary>
|
||||
private CertificateValidationEventHandler? _certValidationHandler;
|
||||
/// <summary>The <see cref="CertificateValidator"/> that owns <see cref="_certValidationHandler"/>.</summary>
|
||||
@@ -107,8 +109,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
/// <summary>
|
||||
/// Approximate count of discovered nodes (folders + variables). Updated by
|
||||
/// <see cref="DiscoverAsync"/> and used to report a non-zero
|
||||
/// <see cref="GetMemoryFootprint"/> to the Core allocation-slope detector
|
||||
/// (Driver.OpcUaClient-013).
|
||||
/// <see cref="GetMemoryFootprint"/> to the Core allocation-slope detector.
|
||||
/// </summary>
|
||||
private volatile int _discoveredNodeCount;
|
||||
/// <summary>
|
||||
@@ -116,7 +117,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
/// when the session's keep-alive channel reports a bad status. Null outside the
|
||||
/// reconnecting window; constructed lazily inside the keep-alive handler. Guarded by
|
||||
/// <see cref="_probeLock"/> — keep-alive callbacks fire from the SDK timer thread and
|
||||
/// can race a check-then-set if left unsynchronized (Driver.OpcUaClient-005).
|
||||
/// can race a check-then-set if left unsynchronized.
|
||||
/// </summary>
|
||||
private SessionReconnectHandler? _reconnectHandler;
|
||||
|
||||
@@ -125,19 +126,17 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
/// Stored NodeIds embed the server-stable namespace <b>URI</b> rather than the
|
||||
/// session-relative <c>ns=N</c> index, so a remote-server namespace-table reorder
|
||||
/// across a restart does not silently re-point stored references at the wrong
|
||||
/// namespace (driver-specs.md §8 "Namespace Remapping", finding Driver.OpcUaClient-004).
|
||||
/// namespace (driver-specs.md §8 "Namespace Remapping").
|
||||
/// Null until <see cref="InitializeAsync"/> returns cleanly.
|
||||
/// </summary>
|
||||
private NamespaceMap? _namespaceMap;
|
||||
|
||||
/// <summary>Gets the stable logical identifier for this driver instance from the config database.</summary>
|
||||
/// <inheritdoc />
|
||||
public string DriverInstanceId => _driverInstanceId;
|
||||
/// <summary>Gets the driver type identifier.</summary>
|
||||
/// <inheritdoc />
|
||||
public string DriverType => "OpcUaClient";
|
||||
|
||||
/// <summary>Initializes the OPC UA client driver with the given configuration.</summary>
|
||||
/// <param name="driverConfigJson">JSON-serialized driver configuration.</param>
|
||||
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
||||
/// <inheritdoc />
|
||||
public async Task InitializeAsync(string driverConfigJson, CancellationToken cancellationToken)
|
||||
{
|
||||
_health = new DriverHealth(DriverState.Initializing, null, null);
|
||||
@@ -276,7 +275,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
await config.ValidateAsync(ApplicationType.Client, ct).ConfigureAwait(false);
|
||||
|
||||
// AutoAccept=true is a dev-only escape hatch. Emit a prominent warning so a
|
||||
// production misconfiguration is immediately visible in logs (Driver.OpcUaClient-012).
|
||||
// production misconfiguration is immediately visible in logs.
|
||||
if (_options.AutoAcceptCertificates)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
@@ -289,7 +288,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
// Accept the full set of certificate-validation error codes: a real dev cert can
|
||||
// fail with BadCertificateChainIncomplete, BadCertificateTimeInvalid, or
|
||||
// BadCertificateHostNameInvalid, not only BadCertificateUntrusted. Only accepting
|
||||
// the latter would silently fail for those certs (Driver.OpcUaClient-012).
|
||||
// the latter would silently fail for those certs.
|
||||
CertificateValidationEventHandler handler = (_, e) => e.Accept = true;
|
||||
config.CertificateValidator.CertificateValidation += handler;
|
||||
// Store refs so ShutdownAsync + Dispose can detach the delegate and avoid
|
||||
@@ -312,6 +311,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
/// existing single-endpoint configs keep working without migration.
|
||||
/// </summary>
|
||||
/// <param name="opts">Driver options containing endpoint configuration.</param>
|
||||
/// <returns>The ordered list of endpoint URLs to try.</returns>
|
||||
internal static IReadOnlyList<string> ResolveEndpointCandidates(OpcUaClientDriverOptions opts)
|
||||
{
|
||||
if (opts.EndpointUrls is { Count: > 0 }) return opts.EndpointUrls;
|
||||
@@ -361,6 +361,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
/// private key N times, wasteful + keeps the password in memory longer.
|
||||
/// </summary>
|
||||
/// <param name="options">Driver options containing authentication configuration.</param>
|
||||
/// <returns>The constructed <see cref="UserIdentity"/> for session activation.</returns>
|
||||
internal static UserIdentity BuildUserIdentity(OpcUaClientDriverOptions options) =>
|
||||
options.AuthType switch
|
||||
{
|
||||
@@ -464,6 +465,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
/// challenge during session activation).
|
||||
/// </summary>
|
||||
/// <param name="options">Driver options containing certificate configuration.</param>
|
||||
/// <returns>The certificate-backed <see cref="UserIdentity"/>.</returns>
|
||||
internal static UserIdentity BuildCertificateIdentity(OpcUaClientDriverOptions options)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(options.UserCertificatePath))
|
||||
@@ -491,6 +493,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
|
||||
/// <summary>Convert a driver <see cref="OpcUaSecurityPolicy"/> to the OPC UA policy URI.</summary>
|
||||
/// <param name="policy">The driver security policy to map.</param>
|
||||
/// <returns>The corresponding OPC UA security policy URI.</returns>
|
||||
internal static string MapSecurityPolicy(OpcUaSecurityPolicy policy) => policy switch
|
||||
{
|
||||
OpcUaSecurityPolicy.None => SecurityPolicies.None,
|
||||
@@ -505,17 +508,14 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
private static string ShortPolicyName(string policyUri) =>
|
||||
policyUri?.Substring(policyUri.LastIndexOf('#') + 1) ?? "(null)";
|
||||
|
||||
/// <summary>Reinitializes the driver with new configuration, shutting down and restarting the session.</summary>
|
||||
/// <param name="driverConfigJson">JSON-serialized driver configuration.</param>
|
||||
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
||||
/// <inheritdoc />
|
||||
public async Task ReinitializeAsync(string driverConfigJson, CancellationToken cancellationToken)
|
||||
{
|
||||
await ShutdownAsync(cancellationToken).ConfigureAwait(false);
|
||||
await InitializeAsync(driverConfigJson, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>Gracefully shuts down the OPC UA session, unsubscribing all active monitoring items and closing the connection.</summary>
|
||||
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
||||
/// <inheritdoc />
|
||||
public async Task ShutdownAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
// Tear down remote subscriptions first — otherwise Session.Close will try and may fail
|
||||
@@ -523,7 +523,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
// whether or not the wire-side delete succeeds since the local handles are useless
|
||||
// after close anyway. Before deleting each subscription we detach the Notification
|
||||
// handlers we attached at subscribe time so the SDK's invocation list no longer
|
||||
// holds the driver instance through the closure (Driver.OpcUaClient-014).
|
||||
// holds the driver instance through the closure.
|
||||
foreach (var rs in _subscriptions.Values)
|
||||
{
|
||||
DetachNotificationHandlers(rs.ItemHandlers);
|
||||
@@ -544,8 +544,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
// Abort any in-flight reconnect attempts before touching the session — BeginReconnect's
|
||||
// retry loop holds a reference to the current session and would fight Session.CloseAsync
|
||||
// if left spinning. Take the handler under _probeLock so a keep-alive callback racing
|
||||
// through OnKeepAlive can't arm a fresh handler after we've torn this one down
|
||||
// (Driver.OpcUaClient-005).
|
||||
// through OnKeepAlive can't arm a fresh handler after we've torn this one down.
|
||||
SessionReconnectHandler? handlerToCancel;
|
||||
lock (_probeLock)
|
||||
{
|
||||
@@ -556,9 +555,9 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
handlerToCancel?.Dispose();
|
||||
|
||||
// Take the session reference under _probeLock before touching it, so we can't race
|
||||
// an OnReconnectComplete that is simultaneously swapping to a new session
|
||||
// (Driver.OpcUaClient-006). We clear Session to null here so any concurrent caller
|
||||
// that checks inside _gate sees null immediately after shutdown begins.
|
||||
// an OnReconnectComplete that is simultaneously swapping to a new session. We clear
|
||||
// Session to null here so any concurrent caller that checks inside _gate sees null
|
||||
// immediately after shutdown begins.
|
||||
ISession? sessionToClose;
|
||||
lock (_probeLock)
|
||||
{
|
||||
@@ -578,8 +577,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
_connectedEndpointUrl = null;
|
||||
|
||||
// Detach the cert-validation handler so the (potentially process-shared)
|
||||
// CertificateValidator doesn't hold a delegate to a shutting-down driver
|
||||
// (Driver.OpcUaClient-012).
|
||||
// CertificateValidator doesn't hold a delegate to a shutting-down driver.
|
||||
if (_certValidationHandler is not null && _certValidatorRef is not null)
|
||||
{
|
||||
try { _certValidatorRef.CertificateValidation -= _certValidationHandler; } catch { }
|
||||
@@ -591,28 +589,21 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
_health = new DriverHealth(DriverState.Unknown, _health.LastSuccessfulRead, null);
|
||||
}
|
||||
|
||||
/// <summary>Gets the current health status of the OPC UA client driver.</summary>
|
||||
/// <inheritdoc />
|
||||
public DriverHealth GetHealth() => _health;
|
||||
|
||||
/// <summary>
|
||||
/// Returns an approximate in-driver memory footprint for the Core allocation-slope
|
||||
/// detector. Each discovered node (folder or variable) contributes ~512 bytes to cover
|
||||
/// the <see cref="DriverAttributeInfo"/> record, the browse-name string, and the stable
|
||||
/// <c>nsu=</c> reference string stored in the address-space builder. The real number
|
||||
/// depends on string length + box sizes; the constant is conservative enough that a
|
||||
/// 10k-node remote server reports ~5 MB — well within the budget and detectable by the
|
||||
/// Core slope alarm (Driver.OpcUaClient-013).
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
// Each discovered node (folder or variable) contributes ~512 bytes to cover the
|
||||
// DriverAttributeInfo record, the browse-name string, and the stable nsu= reference string
|
||||
// stored in the address-space builder. The real number depends on string length + box sizes;
|
||||
// the constant is conservative enough that a 10k-node remote server reports ~5 MB — well
|
||||
// within the budget and detectable by the Core slope alarm.
|
||||
public long GetMemoryFootprint() => _discoveredNodeCount * 512L;
|
||||
|
||||
/// <summary>
|
||||
/// Drops the discovered-node count so the Core's cache-budget enforcement can request
|
||||
/// a flush when footprint budget is breached. The OPC UA Client driver holds no
|
||||
/// independently-flushable cache beyond what the address-space builder retains — a
|
||||
/// flush here resets the footprint counter and signals the Core that re-discovery
|
||||
/// will rebuild it cleanly from the remote server.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
||||
/// <inheritdoc />
|
||||
// The OPC UA Client driver holds no independently-flushable cache beyond what the
|
||||
// address-space builder retains — a flush here resets the discovered-node footprint
|
||||
// counter and signals Core that re-discovery will rebuild it cleanly from the remote server.
|
||||
public Task FlushOptionalCachesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_discoveredNodeCount = 0;
|
||||
@@ -621,15 +612,13 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
|
||||
// ---- IReadable ----
|
||||
|
||||
/// <summary>Reads the current values of the specified nodes from the remote OPC UA server.</summary>
|
||||
/// <param name="fullReferences">Fully-qualified node identifiers to read.</param>
|
||||
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<DataValueSnapshot>> ReadAsync(
|
||||
IReadOnlyList<string> fullReferences, CancellationToken cancellationToken)
|
||||
{
|
||||
// Make sure a session exists before queuing on the gate, but do NOT bind the wire
|
||||
// call to this reference — a reconnect can swap Session while we wait on _gate. The
|
||||
// session actually used is re-read inside the gate (Driver.OpcUaClient-001/-006).
|
||||
// session actually used is re-read inside the gate.
|
||||
_ = RequireSession();
|
||||
var results = new DataValueSnapshot[fullReferences.Count];
|
||||
var now = DateTime.UtcNow;
|
||||
@@ -712,14 +701,12 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
|
||||
// ---- IWritable ----
|
||||
|
||||
/// <summary>Writes values to the specified nodes on the remote OPC UA server.</summary>
|
||||
/// <param name="writes">Write requests specifying nodes and values to write.</param>
|
||||
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<WriteResult>> WriteAsync(
|
||||
IReadOnlyList<Core.Abstractions.WriteRequest> writes, CancellationToken cancellationToken)
|
||||
{
|
||||
// See ReadAsync — the wire call must use the session current inside the gate, not a
|
||||
// reference captured before WaitAsync (Driver.OpcUaClient-001/-006).
|
||||
// reference captured before WaitAsync.
|
||||
_ = RequireSession();
|
||||
var results = new WriteResult[writes.Count];
|
||||
|
||||
@@ -729,9 +716,8 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
var session = Session;
|
||||
if (session is null)
|
||||
{
|
||||
// Writes are non-idempotent (decision #44/#45) — but here the request never
|
||||
// reached the wire, so BadCommunicationError ("definitely did not happen") is
|
||||
// the honest code.
|
||||
// Writes are non-idempotent — but here the request never reached the wire, so
|
||||
// BadCommunicationError ("definitely did not happen") is the honest code.
|
||||
for (var i = 0; i < writes.Count; i++)
|
||||
results[i] = new WriteResult(StatusBadCommunicationError);
|
||||
return results;
|
||||
@@ -777,10 +763,10 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Timeout / cancellation after the wire request may have been dispatched.
|
||||
// Writes are non-idempotent (decision #44/#45) — BadTimeout ("outcome unknown,
|
||||
// do not blindly retry") is more honest than BadCommunicationError ("definitely
|
||||
// did not happen"). Downstream callers that need retry semantics check for
|
||||
// BadTimeout and can decide whether to re-issue (Driver.OpcUaClient-009).
|
||||
// Writes are non-idempotent — BadTimeout ("outcome unknown, do not blindly
|
||||
// retry") is more honest than BadCommunicationError ("definitely did not
|
||||
// happen"). Downstream callers that need retry semantics check for BadTimeout
|
||||
// and can decide whether to re-issue.
|
||||
const uint StatusBadTimeout = 0x800A0000u;
|
||||
for (var w = 0; w < indexMap.Count; w++)
|
||||
results[indexMap[w]] = new WriteResult(StatusBadTimeout);
|
||||
@@ -810,6 +796,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
/// <param name="session">The OPC UA session to resolve the node ID against.</param>
|
||||
/// <param name="fullReference">The full reference string to parse.</param>
|
||||
/// <param name="nodeId">The parsed node ID when successful.</param>
|
||||
/// <returns><c>true</c> if the reference was parsed successfully; otherwise <c>false</c>.</returns>
|
||||
internal static bool TryParseNodeId(ISession session, string fullReference, out NodeId nodeId) =>
|
||||
NamespaceMap.TryResolve(session, fullReference, out nodeId);
|
||||
|
||||
@@ -826,23 +813,19 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
|
||||
// ---- ITagDiscovery ----
|
||||
|
||||
/// <summary>
|
||||
/// Run-once: <see cref="DiscoverAsync"/> recursively browses the remote server's address
|
||||
/// space and registers every variable within the single call (browse + enrich passes are
|
||||
/// fully awaited) — nothing fills in asynchronously after connect, so a single discovery
|
||||
/// pass is sufficient.
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
// Run-once: DiscoverAsync recursively browses the remote server's address space and
|
||||
// registers every variable within the single call (browse + enrich passes are fully
|
||||
// awaited) — nothing fills in asynchronously after connect, so a single discovery pass
|
||||
// is sufficient.
|
||||
public DiscoveryRediscoverPolicy RediscoverPolicy => DiscoveryRediscoverPolicy.Once;
|
||||
|
||||
/// <summary>Discovers the remote OPC UA server's address space and materializes it through the supplied builder.</summary>
|
||||
/// <param name="builder">Address space builder for materializing discovered nodes.</param>
|
||||
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
||||
/// <inheritdoc />
|
||||
public async Task DiscoverAsync(IAddressSpaceBuilder builder, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(builder);
|
||||
// Confirm a session exists before queuing; the session actually browsed is re-read
|
||||
// inside the gate so a reconnect mid-wait can't leave us browsing a closed session
|
||||
// (Driver.OpcUaClient-001/-006).
|
||||
// inside the gate so a reconnect mid-wait can't leave us browsing a closed session.
|
||||
_ = RequireSession();
|
||||
|
||||
var rootFolder = builder.Folder("Remote", "Remote");
|
||||
@@ -880,7 +863,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
.ConfigureAwait(false);
|
||||
|
||||
// Update the footprint counter so GetMemoryFootprint() returns a real estimate
|
||||
// after each discovery pass (Driver.OpcUaClient-013).
|
||||
// after each discovery pass.
|
||||
_discoveredNodeCount = discovered;
|
||||
}
|
||||
finally { _gate.Release(); }
|
||||
@@ -937,8 +920,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
// per node in a single response; when a folder has more children than the cap,
|
||||
// BrowseResult.ContinuationPoint is non-empty and the remainder must be pulled
|
||||
// with BrowseNext. Without this loop a large remote folder is silently truncated
|
||||
// and discovered tags go missing from the local address space
|
||||
// (Driver.OpcUaClient-003).
|
||||
// and discovered tags go missing from the local address space.
|
||||
var continuationPoint = result.ContinuationPoint;
|
||||
while (continuationPoint is { Length: > 0 })
|
||||
{
|
||||
@@ -954,8 +936,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Caller cancelled mid-pagination. Release the server-side cursor so it
|
||||
// doesn't hold resources until session close (Driver.OpcUaClient-016;
|
||||
// same pattern as Driver.OpcUaClient.Browser-002). Use CancellationToken.None
|
||||
// doesn't hold resources until session close. Use CancellationToken.None
|
||||
// for the release — the original token is already cancelled so we must not
|
||||
// gate the release on it. Fire-and-forget: if the server is also unreachable,
|
||||
// the release fails silently and the cursor times out server-side anyway.
|
||||
@@ -1132,13 +1113,14 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
/// StatusCode + timestamps.
|
||||
/// </summary>
|
||||
/// <param name="dataType">The OPC UA data type NodeId to map.</param>
|
||||
/// <returns>The mapped <see cref="DriverDataType"/>.</returns>
|
||||
internal static DriverDataType MapUpstreamDataType(NodeId dataType)
|
||||
{
|
||||
if (dataType == DataTypeIds.Boolean) return DriverDataType.Boolean;
|
||||
// SByte (signed 8-bit) shares Int16 — DriverDataType has no narrower signed type.
|
||||
// Byte (unsigned 8-bit) belongs in the unsigned family → UInt16, not Int16
|
||||
// (Driver.OpcUaClient-010: mapping an unsigned 0-255 type onto Int16 misrepresents
|
||||
// type metadata and confuses range/validation logic keyed off DriverDataType).
|
||||
// (mapping an unsigned 0-255 type onto Int16 misrepresents type metadata and
|
||||
// confuses range/validation logic keyed off DriverDataType).
|
||||
if (dataType == DataTypeIds.SByte || dataType == DataTypeIds.Int16) return DriverDataType.Int16;
|
||||
if (dataType == DataTypeIds.Byte || dataType == DataTypeIds.UInt16) return DriverDataType.UInt16;
|
||||
if (dataType == DataTypeIds.Int32) return DriverDataType.Int32;
|
||||
@@ -1160,6 +1142,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
/// <see cref="SecurityClassification.Operate"/>; read-only as <see cref="SecurityClassification.ViewOnly"/>.
|
||||
/// </summary>
|
||||
/// <param name="accessLevel">The OPC UA access level bitmask.</param>
|
||||
/// <returns>The mapped security classification.</returns>
|
||||
internal static SecurityClassification MapAccessLevelToSecurityClass(byte accessLevel)
|
||||
{
|
||||
const byte CurrentWrite = 2; // AccessLevels.CurrentWrite = 0x02
|
||||
@@ -1170,16 +1153,13 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
|
||||
// ---- ISubscribable ----
|
||||
|
||||
/// <summary>Subscribes to monitored value changes on the specified nodes from the remote OPC UA server.</summary>
|
||||
/// <param name="fullReferences">Fully-qualified node identifiers to monitor.</param>
|
||||
/// <param name="publishingInterval">Desired minimum interval between publish cycles.</param>
|
||||
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
||||
/// <inheritdoc />
|
||||
public async Task<ISubscriptionHandle> SubscribeAsync(
|
||||
IReadOnlyList<string> fullReferences, TimeSpan publishingInterval, CancellationToken cancellationToken)
|
||||
{
|
||||
// Fast-fail before queuing if the driver isn't connected, but do NOT bind the
|
||||
// subscription to this reference — a reconnect can swap Session while we wait on _gate.
|
||||
// The session actually used is re-read inside the gate (Driver.OpcUaClient-001/-006).
|
||||
// The session actually used is re-read inside the gate.
|
||||
_ = RequireSession();
|
||||
var id = Interlocked.Increment(ref _nextSubscriptionId);
|
||||
var handle = new OpcUaSubscriptionHandle(id);
|
||||
@@ -1215,9 +1195,9 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
await subscription.CreateAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Track each (MonitoredItem, handler) pair so UnsubscribeAsync / ShutdownAsync
|
||||
// can detach the Notification delegate before disposing the session
|
||||
// (Driver.OpcUaClient-014). The lambda captures `handle`, so we must hold the
|
||||
// exact delegate instance returned by `+=` to be able to remove it.
|
||||
// can detach the Notification delegate before disposing the session. The lambda
|
||||
// captures `handle`, so we must hold the exact delegate instance returned by
|
||||
// `+=` to be able to remove it.
|
||||
var itemHandlers = new List<MonitoredItemNotificationHandle>();
|
||||
foreach (var fullRef in fullReferences)
|
||||
{
|
||||
@@ -1252,9 +1232,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
return handle;
|
||||
}
|
||||
|
||||
/// <summary>Unsubscribes from monitored value changes for the specified subscription handle.</summary>
|
||||
/// <param name="handle">The subscription handle to unsubscribe.</param>
|
||||
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
||||
/// <inheritdoc />
|
||||
public async Task UnsubscribeAsync(ISubscriptionHandle handle, CancellationToken cancellationToken)
|
||||
{
|
||||
if (handle is not OpcUaSubscriptionHandle h) return;
|
||||
@@ -1265,9 +1243,9 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
{
|
||||
// Detach Notification handlers BEFORE deleting the subscription so the SDK's
|
||||
// MonitoredItem.Notification multicast invocation list no longer holds a
|
||||
// closure that captures the driver instance (Driver.OpcUaClient-014). The
|
||||
// delegate stored on RemoteSubscription is the exact instance that was added,
|
||||
// so `-=` removes it cleanly.
|
||||
// closure that captures the driver instance. The delegate stored on
|
||||
// RemoteSubscription is the exact instance that was added, so `-=` removes it
|
||||
// cleanly.
|
||||
DetachNotificationHandlers(rs.ItemHandlers);
|
||||
try { await rs.Subscription.DeleteAsync(silent: true, cancellationToken).ConfigureAwait(false); }
|
||||
catch { /* best-effort — the subscription may already be gone on reconnect */ }
|
||||
@@ -1305,8 +1283,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
/// Live data-change subscription bookkeeping. Holds the SDK <see cref="Subscription"/>,
|
||||
/// the local handle, and the per-MonitoredItem (item, handler) pairs so
|
||||
/// <see cref="UnsubscribeAsync"/> / <see cref="ShutdownAsync"/> can detach the
|
||||
/// Notification delegates before the SDK disposes the subscription
|
||||
/// (Driver.OpcUaClient-014).
|
||||
/// Notification delegates before the SDK disposes the subscription.
|
||||
/// </summary>
|
||||
private sealed record RemoteSubscription(
|
||||
Subscription Subscription,
|
||||
@@ -1326,7 +1303,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
|
||||
private sealed record OpcUaSubscriptionHandle(long Id) : ISubscriptionHandle
|
||||
{
|
||||
/// <summary>Gets the diagnostic identifier for this subscription.</summary>
|
||||
/// <inheritdoc />
|
||||
public string DiagnosticId => $"opcua-sub-{Id}";
|
||||
}
|
||||
|
||||
@@ -1344,15 +1321,13 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
private const int AlarmFieldTime = 5;
|
||||
private const int AlarmFieldConditionId = 6;
|
||||
|
||||
/// <summary>Subscribes to alarm and event notifications from the remote OPC UA server.</summary>
|
||||
/// <param name="sourceNodeIds">Source node identifiers to subscribe alarms from.</param>
|
||||
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
||||
/// <inheritdoc />
|
||||
public async Task<IAlarmSubscriptionHandle> SubscribeAlarmsAsync(
|
||||
IReadOnlyList<string> sourceNodeIds, CancellationToken cancellationToken)
|
||||
{
|
||||
// Fast-fail before queuing if the driver isn't connected, but do NOT bind the alarm
|
||||
// subscription to this reference — a reconnect can swap Session while we wait on _gate.
|
||||
// The session actually used is re-read inside the gate (Driver.OpcUaClient-001/-006).
|
||||
// The session actually used is re-read inside the gate.
|
||||
_ = RequireSession();
|
||||
var id = Interlocked.Increment(ref _nextAlarmSubscriptionId);
|
||||
var handle = new OpcUaAlarmSubscriptionHandle(id);
|
||||
@@ -1424,9 +1399,9 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
Handle = handle,
|
||||
};
|
||||
// Capture the exact delegate instance so UnsubscribeAlarmsAsync / ShutdownAsync
|
||||
// can `-=` it later (Driver.OpcUaClient-014). The lambda captures `handle` and
|
||||
// `sourceFilter`, so without the explicit detach the SDK's invocation list keeps
|
||||
// the driver instance alive until the session itself is disposed.
|
||||
// can `-=` it later. The lambda captures `handle` and `sourceFilter`, so without
|
||||
// the explicit detach the SDK's invocation list keeps the driver instance alive
|
||||
// until the session itself is disposed.
|
||||
MonitoredItemNotificationEventHandler notifHandler = (mi, args) =>
|
||||
OnEventNotification(handle, sourceFilter, mi, args);
|
||||
eventItem.Notification += notifHandler;
|
||||
@@ -1440,9 +1415,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
return handle;
|
||||
}
|
||||
|
||||
/// <summary>Unsubscribes from alarm and event notifications for the specified alarm subscription handle.</summary>
|
||||
/// <param name="handle">The alarm subscription handle to unsubscribe.</param>
|
||||
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
||||
/// <inheritdoc />
|
||||
public async Task UnsubscribeAlarmsAsync(IAlarmSubscriptionHandle handle, CancellationToken cancellationToken)
|
||||
{
|
||||
if (handle is not OpcUaAlarmSubscriptionHandle h) return;
|
||||
@@ -1453,7 +1426,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
{
|
||||
// Detach the Notification handler before deleting the subscription so the SDK's
|
||||
// multicast invocation list no longer holds the driver instance through the
|
||||
// closure (Driver.OpcUaClient-014).
|
||||
// closure.
|
||||
try { rs.EventItem.Notification -= rs.Handler; }
|
||||
catch { /* best-effort */ }
|
||||
try { await rs.Subscription.DeleteAsync(silent: true, cancellationToken).ConfigureAwait(false); }
|
||||
@@ -1462,9 +1435,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
finally { _gate.Release(); }
|
||||
}
|
||||
|
||||
/// <summary>Acknowledges multiple alarms by calling the remote OPC UA server's Acknowledge method.</summary>
|
||||
/// <param name="acknowledgements">List of alarm acknowledgement requests.</param>
|
||||
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
||||
/// <inheritdoc />
|
||||
public async Task AcknowledgeAsync(
|
||||
IReadOnlyList<AlarmAcknowledgeRequest> acknowledgements, CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -1474,8 +1445,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
if (acknowledgements.Count == 0) return;
|
||||
// Fast-fail before queuing if the driver isn't connected, but do NOT bind the ack calls
|
||||
// (or the namespace-relative ConditionId parse) to this reference — a reconnect can swap
|
||||
// Session while we wait on _gate. The session actually used is re-read inside the gate
|
||||
// (Driver.OpcUaClient-001/-006).
|
||||
// Session while we wait on _gate. The session actually used is re-read inside the gate.
|
||||
_ = RequireSession();
|
||||
|
||||
await _gate.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
@@ -1518,7 +1488,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
// Inspect per-ack results — the upstream server can reject individual acks
|
||||
// (BadConditionAlreadyAcked, BadNodeIdUnknown, BadUserAccessDenied) even when
|
||||
// the batch transport succeeds. Operators acking a critical alarm deserve to
|
||||
// know if the ack didn't take (Driver.OpcUaClient-008).
|
||||
// know if the ack didn't take.
|
||||
if (resp?.Results is not null)
|
||||
{
|
||||
for (var i = 0; i < resp.Results.Count; i++)
|
||||
@@ -1590,6 +1560,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
/// guidance: 1-200 Low, 201-500 Medium, 501-800 High, 801-1000 Critical.
|
||||
/// </summary>
|
||||
/// <param name="opcSeverity">The OPC UA severity value (1-1000).</param>
|
||||
/// <returns>The mapped coarse-grained alarm severity bucket.</returns>
|
||||
internal static AlarmSeverity MapSeverity(ushort opcSeverity) => opcSeverity switch
|
||||
{
|
||||
<= 200 => AlarmSeverity.Low,
|
||||
@@ -1602,7 +1573,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
/// Live alarm-event subscription bookkeeping. Holds the SDK <see cref="Subscription"/>,
|
||||
/// the local handle, the single event-MonitoredItem (`Server/Events`), and the exact
|
||||
/// handler delegate instance so unsubscribe / shutdown can detach the Notification
|
||||
/// event before the SDK disposes the subscription (Driver.OpcUaClient-014).
|
||||
/// event before the SDK disposes the subscription.
|
||||
/// </summary>
|
||||
private sealed record RemoteAlarmSubscription(
|
||||
Subscription Subscription,
|
||||
@@ -1612,18 +1583,13 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
|
||||
private sealed record OpcUaAlarmSubscriptionHandle(long Id) : IAlarmSubscriptionHandle
|
||||
{
|
||||
/// <summary>Gets the diagnostic identifier for this alarm subscription.</summary>
|
||||
/// <inheritdoc />
|
||||
public string DiagnosticId => $"opcua-alarm-sub-{Id}";
|
||||
}
|
||||
|
||||
// ---- IHistoryProvider (passthrough to upstream server) ----
|
||||
|
||||
/// <summary>Reads raw historical data from the remote OPC UA server.</summary>
|
||||
/// <param name="fullReference">Fully-qualified node identifier to read history for.</param>
|
||||
/// <param name="startUtc">Start time in UTC for the history query.</param>
|
||||
/// <param name="endUtc">End time in UTC for the history query.</param>
|
||||
/// <param name="maxValuesPerNode">Maximum number of values to return.</param>
|
||||
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
||||
/// <inheritdoc />
|
||||
public async Task<Core.Abstractions.HistoryReadResult> ReadRawAsync(
|
||||
string fullReference, DateTime startUtc, DateTime endUtc, uint maxValuesPerNode,
|
||||
CancellationToken cancellationToken)
|
||||
@@ -1640,13 +1606,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>Reads processed (aggregated) historical data from the remote OPC UA server.</summary>
|
||||
/// <param name="fullReference">Fully-qualified node identifier to read history for.</param>
|
||||
/// <param name="startUtc">Start time in UTC for the history query.</param>
|
||||
/// <param name="endUtc">End time in UTC for the history query.</param>
|
||||
/// <param name="interval">Time interval for aggregation.</param>
|
||||
/// <param name="aggregate">The aggregation function to apply.</param>
|
||||
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
||||
/// <inheritdoc />
|
||||
public async Task<Core.Abstractions.HistoryReadResult> ReadProcessedAsync(
|
||||
string fullReference, DateTime startUtc, DateTime endUtc, TimeSpan interval,
|
||||
HistoryAggregateType aggregate, CancellationToken cancellationToken)
|
||||
@@ -1663,10 +1623,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>Reads historical data at specific timestamps from the remote OPC UA server.</summary>
|
||||
/// <param name="fullReference">Fully-qualified node identifier to read history for.</param>
|
||||
/// <param name="timestampsUtc">List of specific timestamps to read values at.</param>
|
||||
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
||||
/// <inheritdoc />
|
||||
public async Task<Core.Abstractions.HistoryReadResult> ReadAtTimeAsync(
|
||||
string fullReference, IReadOnlyList<DateTime> timestampsUtc, CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -1690,8 +1647,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
{
|
||||
// Make sure a session exists before queuing on the gate, but do NOT bind the wire call
|
||||
// (or the namespace-relative NodeId parse) to this reference — a reconnect can swap
|
||||
// Session while we wait on _gate. The session actually used is re-read inside the gate
|
||||
// (Driver.OpcUaClient-001/-006).
|
||||
// Session while we wait on _gate. The session actually used is re-read inside the gate.
|
||||
_ = RequireSession();
|
||||
|
||||
await _gate.WaitAsync(ct).ConfigureAwait(false);
|
||||
@@ -1748,6 +1704,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
|
||||
/// <summary>Map <see cref="HistoryAggregateType"/> to the OPC UA Part 13 standard aggregate NodeId.</summary>
|
||||
/// <param name="aggregate">The aggregation function type to map.</param>
|
||||
/// <returns>The standard aggregate function NodeId.</returns>
|
||||
internal static NodeId MapAggregateToNodeId(HistoryAggregateType aggregate) => aggregate switch
|
||||
{
|
||||
HistoryAggregateType.Average => ObjectIds.AggregateFunction_Average,
|
||||
@@ -1847,30 +1804,22 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
catch (Exception ex) when (ex is FormatException or InvalidCastException or OverflowException) { return 0; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Forwards OPC UA HistoryReadEvents to the upstream server. Sends the fixed canonical
|
||||
/// <see cref="BuildBaseEventFilter"/> and maps the result onto <see cref="HistoricalEvent"/>
|
||||
/// (the OtOpcUa server projects only those six BaseEventType fields, so a richer client
|
||||
/// filter would be discarded server-side — the driver supplies the canonical set itself).
|
||||
/// </summary>
|
||||
/// <param name="sourceName">
|
||||
/// Upstream event-notifier NodeId to read from (mirrors how <c>fullReference</c> is the
|
||||
/// upstream NodeId for raw reads). Null/empty → the upstream Server object (<c>i=2253</c>),
|
||||
/// the standard server-wide event notifier. An unparseable id short-circuits to an empty
|
||||
/// result, matching the raw path's malformed-NodeId behavior.
|
||||
/// </param>
|
||||
/// <param name="startUtc">Inclusive lower bound on event time (UTC).</param>
|
||||
/// <param name="endUtc">Exclusive upper bound on event time (UTC).</param>
|
||||
/// <param name="maxEvents">Upper cap; <c><= 0</c> means "no cap" (NumValuesPerNode = 0).</param>
|
||||
/// <param name="cancellationToken">Request cancellation.</param>
|
||||
/// <returns>The historical events plus the upstream continuation point (null when complete).</returns>
|
||||
/// <inheritdoc />
|
||||
// Forwards OPC UA HistoryReadEvents to the upstream server. Sends the fixed canonical
|
||||
// BuildBaseEventFilter and maps the result onto HistoricalEvent (the OtOpcUa server projects
|
||||
// only those six BaseEventType fields, so a richer client filter would be discarded
|
||||
// server-side — the driver supplies the canonical set itself). sourceName is the upstream
|
||||
// event-notifier NodeId to read from (mirrors how fullReference is the upstream NodeId for
|
||||
// raw reads); null/empty resolves to the upstream Server object (i=2253), the standard
|
||||
// server-wide event notifier, and an unparseable id short-circuits to an empty result,
|
||||
// matching the raw path's malformed-NodeId behavior.
|
||||
public async Task<Core.Abstractions.HistoricalEventsResult> ReadEventsAsync(
|
||||
string? sourceName, DateTime startUtc, DateTime endUtc, int maxEvents,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// Confirm a session exists before queuing; the session actually used — and the
|
||||
// namespace-relative source-NodeId parse — are re-read inside the gate so a reconnect
|
||||
// mid-wait can't leave us reading from a closed session (Driver.OpcUaClient-001/-006).
|
||||
// mid-wait can't leave us reading from a closed session.
|
||||
_ = RequireSession();
|
||||
|
||||
await _gate.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
@@ -1943,7 +1892,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
?? ResolveEndpointCandidates(_options).FirstOrDefault()
|
||||
?? _options.EndpointUrl;
|
||||
|
||||
/// <summary>Gets the current connectivity status of the remote OPC UA server host.</summary>
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<HostConnectivityStatus> GetHostStatuses()
|
||||
{
|
||||
lock (_probeLock)
|
||||
@@ -1971,7 +1920,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
// while the channel stays down — the check-then-set must be atomic, otherwise two
|
||||
// callbacks both observe null, both construct a SessionReconnectHandler, and the
|
||||
// second assignment leaks the first (its retry loop keeps running, unreferenced and
|
||||
// never disposed). Guard with _probeLock (Driver.OpcUaClient-005).
|
||||
// never disposed). Guard with _probeLock.
|
||||
SessionReconnectHandler handler;
|
||||
lock (_probeLock)
|
||||
{
|
||||
@@ -2008,7 +1957,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
// OnReconnectComplete, OnKeepAlive, and ShutdownAsync cannot race each other:
|
||||
// a session swap visible to concurrent ReadAsync/WriteAsync/DiscoverAsync callers
|
||||
// (which re-read Session inside _gate) must be atomic w.r.t. disposal and
|
||||
// re-arming (Driver.OpcUaClient-006).
|
||||
// re-arming.
|
||||
ISession? oldSession;
|
||||
lock (_probeLock)
|
||||
{
|
||||
@@ -2051,10 +2000,10 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
// maxReconnectPeriod and invoked the callback with a null Session. Without an
|
||||
// explicit Faulted signal the driver is permanently wedged: no session, no live
|
||||
// keep-alive to re-trigger OnKeepAlive, and the Core never learns it must offer an
|
||||
// operator reinitialize (Driver.OpcUaClient-002). Surface Faulted so the Core fans
|
||||
// out Bad quality and ReinitializeAsync becomes available, and arm a fresh reconnect
|
||||
// attempt against the last-known session for an always-on gateway rather than
|
||||
// abandoning recovery entirely.
|
||||
// operator reinitialize. Surface Faulted so the Core fans out Bad quality and
|
||||
// ReinitializeAsync becomes available, and arm a fresh reconnect attempt against
|
||||
// the last-known session for an always-on gateway rather than abandoning recovery
|
||||
// entirely.
|
||||
TransitionTo(HostState.Faulted);
|
||||
_health = new DriverHealth(
|
||||
DriverState.Faulted, _health.LastSuccessfulRead,
|
||||
@@ -2127,7 +2076,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
/// handler detach. The async session-close is intentionally skipped — it requires a
|
||||
/// live session + network round-trip and is unsafe to block-on from a potentially
|
||||
/// single-threaded context (OPC UA stack thread). The session will be cleaned up by
|
||||
/// the SDK's own finalizer on GC (Driver.OpcUaClient-007: no sync-over-async).
|
||||
/// the SDK's own finalizer on GC (no sync-over-async).
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
@@ -2154,7 +2103,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
|
||||
// Detach the cert-validation handler registered during InitializeAsync so the
|
||||
// CertificateValidator (which may be process-shared) doesn't hold a reference to
|
||||
// a disposed driver (Driver.OpcUaClient-012).
|
||||
// a disposed driver.
|
||||
if (_certValidationHandler is not null && _certValidatorRef is not null)
|
||||
{
|
||||
try { _certValidatorRef.CertificateValidation -= _certValidationHandler; } catch { }
|
||||
@@ -2165,7 +2114,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
// Acquire the gate once so any in-flight gated operation (ReadAsync / WriteAsync /
|
||||
// DiscoverAsync) has definitely released before we dispose the gate. Without this
|
||||
// drain, a background read that calls _gate.Release() after Dispose throws
|
||||
// ObjectDisposedException (Driver.OpcUaClient-007).
|
||||
// ObjectDisposedException.
|
||||
try
|
||||
{
|
||||
if (_gate.Wait(TimeSpan.FromSeconds(2)))
|
||||
@@ -2176,6 +2125,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
}
|
||||
|
||||
/// <summary>Asynchronously disposes the driver and releases all associated resources.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (_disposed) return;
|
||||
@@ -2183,7 +2133,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
try { await ShutdownAsync(CancellationToken.None).ConfigureAwait(false); }
|
||||
catch { /* disposal is best-effort */ }
|
||||
|
||||
// Detach the cert-validation handler (Driver.OpcUaClient-012).
|
||||
// Detach the cert-validation handler.
|
||||
if (_certValidationHandler is not null && _certValidatorRef is not null)
|
||||
{
|
||||
try { _certValidatorRef.CertificateValidation -= _certValidationHandler; } catch { }
|
||||
@@ -2192,7 +2142,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
}
|
||||
|
||||
// Drain the gate before disposal so no in-flight _gate.Release() fires after
|
||||
// Dispose (Driver.OpcUaClient-007).
|
||||
// Dispose.
|
||||
try
|
||||
{
|
||||
await _gate.WaitAsync(TimeSpan.FromSeconds(2)).ConfigureAwait(false);
|
||||
|
||||
Reference in New Issue
Block a user