using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; using ZB.MOM.WW.OtOpcUa.Core.Hosting; using ZB.MOM.WW.OtOpcUa.Core.Resilience; using ZB.MOM.WW.Secrets.Abstractions; using ZB.MOM.WW.OtOpcUa.Host.Configuration; using ZB.MOM.WW.OtOpcUa.Runtime.Redundancy; namespace ZB.MOM.WW.OtOpcUa.Host.Drivers; // Probe type aliases — keep the using list concise. using ModbusProbe = Driver.Modbus.ModbusDriverProbe; using AbCipProbe = Driver.AbCip.AbCipDriverProbe; using AbLegacyProbe = Driver.AbLegacy.AbLegacyDriverProbe; using S7Probe = Driver.S7.S7DriverProbe; using TwinCATProbe = Driver.TwinCAT.TwinCATDriverProbe; using FocasProbe = Driver.FOCAS.FocasDriverProbe; using OpcUaProbe = Driver.OpcUaClient.OpcUaClientDriverProbe; using GalaxyProbe = Driver.Galaxy.GalaxyDriverProbe; using CalculationProbe = Driver.Calculation.CalculationDriverProbe; using SqlProbe = Driver.Sql.SqlDriverProbe; using MqttProbe = Driver.Mqtt.MqttDriverProbe; using MTConnectProbe = Driver.MTConnect.MTConnectDriverProbe; /// /// Wires every cross-platform driver assembly's Register(registry, loggerFactory) /// extension into a single singleton and binds the /// v2 abstraction to a /// over it. Replaces the F7 seam's NullDriverFactory default so deploys actually /// materialise real instances on driver-role nodes. /// /// The factory registry is skipped on admin-only nodes — they never run drivers, so it doesn't /// need to exist (Program.cs guards via the hasDriver flag). The driver probe /// set is the exception: it backs the AdminUI Test Connect button and so must also be wired on /// admin nodes — see . /// public static class DriverFactoryBootstrap { /// /// Register the cross-platform driver factories + bind . /// Must be called BEFORE services.AddAkka so the runtime extension can resolve /// from DI when spawning DriverHostActor. /// /// The service collection to register driver factories with. /// The same service collection, for chaining. public static IServiceCollection AddOtOpcUaDriverFactories(this IServiceCollection services) { // Gitea #511: the Primary data-plane verdict the MQTT driver's Sparkplug host claim is gated on. // // Registered HERE as well as in AddOtOpcUaRuntime, and both sides TryAdd the same concrete // RedundancyRoleView, so whichever extension runs first creates it and the other resolves the // SAME instance. That is deliberately not a defensive duplicate: the resolve below is // GetRequiredService (a null would silently fall the driver back to "claim unconditionally", // which on a redundant pair is the dual-claim this mechanism exists to prevent), so without // this line the registration becomes order-dependent on two extensions a caller is free to // invoke in either order — and any graph that wires driver factories without the full runtime // fails at resolution. Two Host integration suites do exactly that, and caught it. services.TryAddSingleton(); services.TryAddSingleton(sp => sp.GetRequiredService()); services.AddSingleton(sp => { var registry = new DriverFactoryRegistry(); var loggerFactory = sp.GetService(); // The calc driver needs the root script logger so a script failure fans out onto the // script-logs topic; resolve it here (null on nodes without the script pipeline wired). var scriptRoot = sp.GetService(); // The Galaxy driver resolves its Gateway.ApiKeySecretRef secret: arm through the // shared ISecretResolver (registered unconditionally by AddZbSecrets on every node). var secretResolver = sp.GetRequiredService(); // Gitea #511: the MQTT driver's Sparkplug primary-host claim is Primary-gated, and unlike // every other gated operation it is a STANDING assertion on the driver's own connection — // there is no inbound message for DriverHostActor to intercept — so the verdict has to // reach the driver itself. Registered unconditionally on driver-role nodes by // AddOtOpcUaRuntime. GetRequiredService, not GetService: a null here silently falls the // driver back to "claim unconditionally", which on a redundant pair is the dual-claim this // whole mechanism exists to prevent, and it would fail as a plant outage rather than as a // startup error. var primaryRoleSource = sp.GetRequiredService(); // Final review I2: the native AB transport's process-wide pool logs options conflicts and close // failures; without this it writes to NullLogger in production. ConfigureAbTransportLogging(sp); Register(registry, loggerFactory, secretResolver, primaryRoleSource, ResolveAbCipDefaultTransport(sp), scriptRoot); return registry; }); services.AddSingleton(sp => new DriverFactoryRegistryAdapter(sp.GetRequiredService())); // Phase 6.1 resilience infrastructure — the process-singleton tracker + Polly pipeline builder, // and the per-driver-instance invoker factory the DriverHostActor uses to wrap every capability // call. Registered here (driver nodes) because the factory needs DriverFactoryRegistry.GetTier; // admin-only nodes skip this and the runtime falls back to the pass-through factory. services.AddSingleton(); services.AddSingleton(sp => new DriverResiliencePipelineBuilder( timeProvider: null, // TimeProvider.System statusTracker: sp.GetRequiredService(), logger: sp.GetService()?.CreateLogger("ZB.MOM.WW.OtOpcUa.Core.Resilience.DriverResiliencePipelineBuilder"))); services.AddSingleton(sp => { var registry = sp.GetRequiredService(); return new DriverCapabilityInvokerFactory( sp.GetRequiredService(), sp.GetRequiredService(), registry.GetTier, logger: sp.GetService()?.CreateLogger("ZB.MOM.WW.OtOpcUa.Core.Resilience.DriverCapabilityInvokerFactory")); }); // The tracker's operator-facing reader: a periodic DPS publisher that mirrors each snapshot to the // AdminUI resilience panel (R2-10 — closes the "tracker fed by everything, read by nothing" gap). // Lives on driver nodes exactly where the tracker exists. The lazy Func is idempotent // with the one Program.cs registers for the alarm-command router. services.TryAddSingleton>(sp => () => sp.GetRequiredService()); services.AddHostedService(); // Driver nodes also carry the probe set so a fused admin,driver node has it; the admin-only // case is covered by Program.cs calling AddOtOpcUaDriverProbes() in the hasAdmin block. services.AddOtOpcUaDriverProbes(); return services; } /// /// Register one per driver type. These back the AdminUI's /// "Test Connect" button: the admin-operations cluster singleton resolves /// of and dispatches by DriverType. /// /// That singleton is role-pinned to admin, so this MUST be wired on admin nodes — /// including admin-only nodes that lack the driver role (e.g. the MAIN cluster's /// admin-a/admin-b). Probes are lightweight (cheap connect, no persistent state) and don't /// need the driver-factory registry, so they register independently of /// . /// /// /// Uses TryAddEnumerable so a fused admin,driver node — which reaches this from both /// the driver-factory path and the admin path — registers each probe exactly once. A /// duplicate would make the singleton's ToDictionary(p => p.DriverType) throw. /// /// /// The service collection to register driver probes with. /// The same service collection, for chaining. public static IServiceCollection AddOtOpcUaDriverProbes(this IServiceCollection services) { services.TryAddEnumerable(ServiceDescriptor.Singleton()); // Factory-built so the probe resolves an absent "transport" key to the SAME node default the driver // factory uses (Drivers:AbCip:DefaultTransport, final review I1) — Test Connect probes the stack the // driver would run. The generic factory overload keeps AbCipProbe as the implementation type, which // TryAddEnumerable de-dups on. services.TryAddEnumerable(ServiceDescriptor.Singleton( sp => { ConfigureAbTransportLogging(sp); // admin-only nodes reach the pool through the probes alone return new AbCipProbe(tagFactory: null, defaultTransport: ResolveAbCipDefaultTransport(sp)); })); services.TryAddEnumerable(ServiceDescriptor.Singleton(sp => { ConfigureAbTransportLogging(sp); return new AbLegacyProbe(); })); services.TryAddEnumerable(ServiceDescriptor.Singleton()); services.TryAddEnumerable(ServiceDescriptor.Singleton()); services.TryAddEnumerable(ServiceDescriptor.Singleton()); services.TryAddEnumerable(ServiceDescriptor.Singleton()); services.TryAddEnumerable(ServiceDescriptor.Singleton()); services.TryAddEnumerable(ServiceDescriptor.Singleton()); services.TryAddEnumerable(ServiceDescriptor.Singleton()); services.TryAddEnumerable(ServiceDescriptor.Singleton()); services.TryAddEnumerable(ServiceDescriptor.Singleton()); return services; } /// /// Gives the native AB transport's process-wide DeviceSessionPool.Shared the host logger factory /// (final review I2). Called from every DI factory that can put the pool to use on this node (the driver /// registry, and the AbCip/AbLegacy probes for admin-only nodes); idempotent. A container without an /// leaves the pool on its null logger. /// /// The service provider. internal static void ConfigureAbTransportLogging(IServiceProvider sp) { if (sp.GetService() is { } loggerFactory) Driver.Ab.Transport.DeviceSessionPool.ConfigureSharedLogger(loggerFactory); } /// /// The node-level AbCip default transport (Drivers:AbCip:DefaultTransport, bound + validated in /// Program.cs). A container without the options bound (test graphs) gets the compiled default. /// /// The service provider. /// The transport an AbCip driver with no transport key runs on. internal static Driver.AbCip.AbCipTransport ResolveAbCipDefaultTransport(IServiceProvider sp) => sp.GetService>()?.Value.ResolveDefaultTransport() ?? Driver.AbCip.AbCipTransportResolver.Default; /// /// Invoke every cross-platform driver's Register extension. New driver assemblies /// get added here — one line per type. ShouldStub() in DriverInstanceActor still /// handles platform/role-dependent stubbing (e.g. Galaxy on macOS), so registering a /// factory here doesn't mean it always runs in production. /// private static void Register( DriverFactoryRegistry registry, ILoggerFactory? loggerFactory, ISecretResolver secretResolver, IDriverPrimaryRoleSource primaryRoleSource, Driver.AbCip.AbCipTransport abCipDefaultTransport, ZB.MOM.WW.OtOpcUa.Core.Scripting.ScriptRootLogger? scriptRoot = null) { Driver.AbCip.AbCipDriverFactoryExtensions.Register(registry, loggerFactory, abCipDefaultTransport); Driver.AbLegacy.AbLegacyDriverFactoryExtensions.Register(registry, loggerFactory); Driver.Calculation.CalculationDriverFactoryExtensions.Register(registry, loggerFactory, scriptRoot); Driver.FOCAS.FocasDriverFactoryExtensions.Register(registry, loggerFactory); Driver.Galaxy.GalaxyDriverFactoryExtensions.Register(registry, secretResolver, loggerFactory); Driver.Modbus.ModbusDriverFactoryExtensions.Register(registry, loggerFactory); Driver.Mqtt.MqttDriverFactoryExtensions.Register(registry, loggerFactory, primaryRoleSource); // Tier A (the Register default): fully managed — HttpClient + System.Xml.Linq, no native SDK, // no COM. Tier C is the only tier that arms process-level recycle (MemoryRecycle hard-breach / // ScheduledRecycleScheduler), which would be wrong here: the driver is in-process and killing // it kills every OPC UA session. The long-lived /sample stream does not argue for a slower // tier either — SubscribeAsync returns synchronously after starting the pump on a background // task, so the Tier A 5s Subscribe budget never covers the stream's lifetime. Driver.MTConnect.MTConnectDriverFactoryExtensions.Register(registry, loggerFactory); Driver.OpcUaClient.OpcUaClientDriverFactoryExtensions.Register(registry, loggerFactory, secretResolver); Driver.S7.S7DriverFactoryExtensions.Register(registry, loggerFactory); Driver.Sql.SqlDriverFactoryExtensions.Register(registry, loggerFactory); Driver.TwinCAT.TwinCATDriverFactoryExtensions.Register(registry, loggerFactory); } }