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();
}
}