using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using ZB.MOM.WW.OtOpcUa.Commons.OpcUa; using ZB.MOM.WW.OtOpcUa.OpcUaServer; using ZB.MOM.WW.OtOpcUa.OpcUaServer.Security; namespace ZB.MOM.WW.OtOpcUa.Host.OpcUa; /// /// Owns the OPC UA SDK lifecycle on driver-role hosts. Reads /// from the OpcUa config section, boots /// an through , then /// swaps a real into the /// singleton so OpcUaPublishActor's writes /// start landing in the real address space. /// /// Tests boot the OPC UA server directly via ; this /// hosted service is the production wiring. /// public sealed class OtOpcUaServerHostedService : IHostedService, IAsyncDisposable { private readonly OpcUaApplicationHostOptions _options; private readonly DeferredAddressSpaceSink _deferredSink; private readonly DeferredServiceLevelPublisher _deferredServiceLevel; private readonly IOpcUaUserAuthenticator _userAuthenticator; private readonly ILoggerFactory _loggerFactory; private readonly ILogger _logger; private OpcUaApplicationHost? _appHost; private OtOpcUaSdkServer? _server; /// /// Initializes a new instance of the OtOpcUaServerHostedService class. /// /// The validated OPC UA host options (bound from the OpcUa section and validated at startup via ValidateOnStart). /// The deferred address space sink that receives the real sink once the server is ready. /// The deferred service level publisher that receives the real publisher once the server is ready. /// The OPC UA user authenticator. /// The logger factory for creating loggers. public OtOpcUaServerHostedService( IOptions options, DeferredAddressSpaceSink deferredSink, DeferredServiceLevelPublisher deferredServiceLevel, IOpcUaUserAuthenticator userAuthenticator, ILoggerFactory loggerFactory) { _options = options.Value; _deferredSink = deferredSink; _deferredServiceLevel = deferredServiceLevel; _userAuthenticator = userAuthenticator; _loggerFactory = loggerFactory; _logger = loggerFactory.CreateLogger(); } /// /// Starts the OPC UA server asynchronously. /// /// Cancellation token. public async Task StartAsync(CancellationToken cancellationToken) { _server = new OtOpcUaSdkServer(); _appHost = new OpcUaApplicationHost( _options, _loggerFactory.CreateLogger(), _userAuthenticator); try { await _appHost.StartAsync(_server, cancellationToken).ConfigureAwait(false); } catch (Exception ex) { _logger.LogError(ex, "OtOpcUaServerHostedService: SDK start failed; OpcUaPublishActor writes will continue to no-op"); // Don't rethrow — the rest of the host (admin UI, driver actors, etc.) can still boot. // Operators see the failure via the logs + can correct config without a process bounce // of the whole binary. return; } if (_server.NodeManager is null) { _logger.LogWarning( "OtOpcUaServerHostedService: SDK reported started but NodeManager is null; sink stays Null"); return; } _deferredSink.SetSink(new SdkAddressSpaceSink(_server.NodeManager)); // ServiceLevel publisher needs IServerInternal — only available after Start. if (_server.CurrentInstance is { } serverInternal) { _deferredServiceLevel.SetInner(new SdkServiceLevelPublisher( serverInternal, _loggerFactory.CreateLogger())); } _logger.LogInformation("OtOpcUaServerHostedService: SDK started, address-space + ServiceLevel sinks bound"); } /// /// Stops the OPC UA server asynchronously. /// /// Cancellation token. public Task StopAsync(CancellationToken cancellationToken) { // Revert to Null adapters so any in-flight writes from a poison-pilled actor don't hit a // half-disposed NodeManager. _deferredSink.SetSink(null); _deferredServiceLevel.SetInner(null); return Task.CompletedTask; } /// /// Disposes the hosted service and its resources asynchronously. /// public async ValueTask DisposeAsync() { if (_appHost is not null) await _appHost.DisposeAsync(); } }