diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OpcUaApplicationHost.cs b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OpcUaApplicationHost.cs index 6a806fab..73272688 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OpcUaApplicationHost.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OpcUaApplicationHost.cs @@ -173,6 +173,9 @@ public sealed class OpcUaApplicationHost : IAsyncDisposable private ApplicationInstance? _application; private StandardServer? _server; private ImpersonateEventHandler? _impersonateHandler; + private readonly object _lifetimeLock = new(); + private readonly TaskCompletionSource _disposeCompletion = new(TaskCreationOptions.RunContinuationsAsynchronously); + private int _disposeStarted; /// Initializes a new instance of the OPC UA application host. /// The host configuration options. @@ -200,16 +203,18 @@ public sealed class OpcUaApplicationHost : IAsyncDisposable /// A task that represents the asynchronous operation. public async Task StartAsync(StandardServer server, CancellationToken cancellationToken) { - _server = server; - // 1.5.378 requires an ITelemetryContext on the ApplicationInstance ctor (the parameterless ctor - // is obsolete). DefaultTelemetry.Create wires the SDK's internal logging; an empty builder keeps - // the SDK's trace off our ILogger (the host keeps its own _logger) — sufficient for the bootstrap. - _application = new ApplicationInstance(DefaultTelemetry.Create(_ => { })) + lock (_lifetimeLock) { - ApplicationName = _options.ApplicationName, - ApplicationType = ApplicationType.Server, - ConfigSectionName = "OtOpcUa", - }; + ObjectDisposedException.ThrowIf(Volatile.Read(ref _disposeStarted) != 0, this); + _server = server; + // 1.5.378 requires telemetry on ApplicationInstance; the empty builder keeps SDK tracing separate. + _application = new ApplicationInstance(DefaultTelemetry.Create(_ => { })) + { + ApplicationName = _options.ApplicationName, + ApplicationType = ApplicationType.Server, + ConfigSectionName = "OtOpcUa", + }; + } _ = await BuildConfigurationAsync(cancellationToken); await EnsureApplicationCertificateAsync(cancellationToken).ConfigureAwait(false); @@ -657,25 +662,60 @@ public sealed class OpcUaApplicationHost : IAsyncDisposable /// Disposes the application host and cleans up resources. /// A task that represents the asynchronous operation. - public async ValueTask DisposeAsync() + public ValueTask DisposeAsync() { + if (Interlocked.CompareExchange(ref _disposeStarted, 1, 0) == 0) + _ = CompleteDisposalAsync(); + return new ValueTask(_disposeCompletion.Task); + } + + private async Task CompleteDisposalAsync() + { + Exception? failure = null; try { - // An explicit SDK stop may already have halted CurrentInstance; still continue disposal. - if (_impersonateHandler is not null && _server?.CurrentInstance?.SessionManager is { } sessionManager) - sessionManager.ImpersonateUser -= _impersonateHandler; - } - catch (Exception ex) - { - _logger.LogWarning(ex, "OpcUaApplicationHost: detaching ImpersonateUser threw"); - } - _impersonateHandler = null; + ApplicationInstance? application; + StandardServer? server; + ImpersonateEventHandler? handler; + lock (_lifetimeLock) + { + application = _application; + server = _server; + handler = _impersonateHandler; + _application = null; + _server = null; + _impersonateHandler = null; + } - if (_application is not null) + try + { + try + { + // An explicit SDK stop may already have halted CurrentInstance; terminal disposal still follows. + if (handler is not null && server?.CurrentInstance?.SessionManager is { } sessionManager) + sessionManager.ImpersonateUser -= handler; + } + catch (Exception ex) { _logger.LogWarning(ex, "OpcUaApplicationHost: detaching ImpersonateUser threw"); } + + if (application is not null) + { + try { await application.StopAsync().ConfigureAwait(false); } + catch (Exception ex) { _logger.LogWarning(ex, "OpcUaApplicationHost: Stop threw on dispose"); } + } + } + finally + { + // SDK1 Stop does not release the server's request queue or terminal event registrations (#807). + try { server?.Dispose(); } + catch (Exception ex) { _logger.LogWarning(ex, "OpcUaApplicationHost: server disposal threw"); } + } + } + catch (Exception error) { failure = error; } + finally { - // 1.5.378: ApplicationInstance.Stop() → StopAsync(). - try { await _application.StopAsync().ConfigureAwait(false); } - catch (Exception ex) { _logger.LogWarning(ex, "OpcUaApplicationHost: Stop threw on dispose"); } + // All callers observe completion, including when a logger or cleanup callback throws. + if (failure is null) _disposeCompletion.TrySetResult(); + else _disposeCompletion.TrySetException(failure); } } }