diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OpcUaApplicationHost.cs b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OpcUaApplicationHost.cs new file mode 100644 index 0000000..1dbaa27 --- /dev/null +++ b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OpcUaApplicationHost.cs @@ -0,0 +1,117 @@ +using Microsoft.Extensions.Logging; +using Opc.Ua; +using Opc.Ua.Configuration; +using Opc.Ua.Server; + +namespace ZB.MOM.WW.OtOpcUa.OpcUaServer; + +public sealed class OpcUaApplicationHostOptions +{ + public string ApplicationName { get; set; } = "OtOpcUa"; + public string ApplicationUri { get; set; } = "urn:OtOpcUa"; + public string ProductUri { get; set; } = "https://zb.com/otopcua"; + + /// Listening port for the binary endpoint (default 4840). + public int OpcUaPort { get; set; } = 4840; + + /// Hostname or IP advertised in endpoint descriptions. + public string PublicHostname { get; set; } = "0.0.0.0"; + + /// Application config XML path; when set, loaded instead of building from defaults. + public string? ApplicationConfigPath { get; set; } +} + +/// +/// Thin facade over the OPC Foundation .NET Standard SDK's application bootstrap. +/// Owns the + lifetime +/// and starts a with the supplied node-manager factory. +/// +/// Full extraction from legacy OtOpcUa.Server (security wiring, ScriptedAlarmDescriptor +/// pipeline, ResilienceController, history backend, observability hooks) is tracked as +/// follow-up F13. This facade compiles + boots the SDK so Task 53 can wire the fused Host's +/// driver-role startup against it. +/// +public sealed class OpcUaApplicationHost : IAsyncDisposable +{ + private readonly OpcUaApplicationHostOptions _options; + private readonly ILogger _logger; + private ApplicationInstance? _application; + private StandardServer? _server; + + public OpcUaApplicationHost( + OpcUaApplicationHostOptions options, + ILogger logger) + { + _options = options; + _logger = logger; + } + + public ApplicationInstance? ApplicationInstance => _application; + public StandardServer? Server => _server; + + public async Task StartAsync(StandardServer server, CancellationToken cancellationToken) + { + _server = server; + _application = new ApplicationInstance + { + ApplicationName = _options.ApplicationName, + ApplicationType = ApplicationType.Server, + ConfigSectionName = "OtOpcUa", + }; + + _ = await BuildConfigurationAsync(cancellationToken); + // Certificate validation + auto-creation is part of the full extraction (F13). + // For the facade we trust that the configured cert store already exists. + await _application.Start(server).ConfigureAwait(false); + + _logger.LogInformation("OPC UA server started on opc.tcp://{Host}:{Port}", + _options.PublicHostname, _options.OpcUaPort); + } + + private async Task BuildConfigurationAsync(CancellationToken ct) + { + if (!string.IsNullOrWhiteSpace(_options.ApplicationConfigPath)) + { + return await _application!.LoadApplicationConfiguration(_options.ApplicationConfigPath, silent: true); + } + + // Minimal defaults — security and certificate stores hardcoded to local files in + // the app's working directory. Full security wiring stays in legacy Server until F13. + var config = new ApplicationConfiguration + { + ApplicationName = _options.ApplicationName, + ApplicationUri = _options.ApplicationUri, + ProductUri = _options.ProductUri, + ApplicationType = ApplicationType.Server, + ServerConfiguration = new ServerConfiguration + { + BaseAddresses = { $"opc.tcp://{_options.PublicHostname}:{_options.OpcUaPort}/OtOpcUa" }, + MinRequestThreadCount = 5, + MaxRequestThreadCount = 100, + MaxQueuedRequestCount = 200, + }, + SecurityConfiguration = new SecurityConfiguration + { + ApplicationCertificate = new CertificateIdentifier { StoreType = "Directory", StorePath = "pki/own", SubjectName = $"CN={_options.ApplicationName}" }, + TrustedIssuerCertificates = new CertificateTrustList { StoreType = "Directory", StorePath = "pki/issuer" }, + TrustedPeerCertificates = new CertificateTrustList { StoreType = "Directory", StorePath = "pki/trusted" }, + RejectedCertificateStore = new CertificateTrustList { StoreType = "Directory", StorePath = "pki/rejected" }, + AutoAcceptUntrustedCertificates = false, + }, + TransportQuotas = new TransportQuotas(), + ClientConfiguration = new ClientConfiguration(), + TraceConfiguration = new TraceConfiguration(), + }; + + await config.Validate(ApplicationType.Server).ConfigureAwait(false); + _application!.ApplicationConfiguration = config; + return config; + } + + public ValueTask DisposeAsync() + { + try { _application?.Stop(); } + catch (Exception ex) { _logger.LogWarning(ex, "OpcUaApplicationHost: Stop threw on dispose"); } + return ValueTask.CompletedTask; + } +}