Files
lmxopcua/src/Server/ZB.MOM.WW.OtOpcUa.Host/Drivers/DriverFactoryBootstrap.cs
T
Joseph Doherty 61193629b6
v2-ci / build (push) Failing after 36s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped
fix(adminui): wire Test Connect probes + live panels on admin-only nodes
Both bugs surfaced only on split-role deployments (the MAIN cluster's
admin-only nodes), where the AdminUI runs without the driver role.

- Test Connect returned "No probe registered" for every driver: the
  IDriverProbe set was registered only under the driver role, but the
  admin-operations singleton that consumes it is pinned to admin. Extract
  AddOtOpcUaDriverProbes() (idempotent via TryAddEnumerable) and call it
  in the hasAdmin path too.

- Live driver-status/alerts/script-log panels showed "SignalR error:
  Connection refused": these Blazor Server components opened a HubConnection
  to their own hub via the browser's public URL, which server-side code
  can't reach behind Traefik (host :9200 -> container :9000). Read the
  in-process source directly instead -- DriverStatus via
  IDriverStatusSnapshotStore.SnapshotChanged, Alerts/ScriptLog via a new
  IInProcessBroadcaster<T>. Fleet status was unaffected (reads DB/ActorSystem).

Adds unit tests for probe registration, the snapshot-store event, and the
broadcaster.
2026-05-29 16:38:32 -04:00

109 lines
6.1 KiB
C#

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Core.Hosting;
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 HistorianProbe = Driver.Historian.Wonderware.Client.WonderwareHistorianDriverProbe;
/// <summary>
/// Wires every cross-platform driver assembly's <c>Register(registry, loggerFactory)</c>
/// extension into a single <see cref="DriverFactoryRegistry"/> singleton and binds the
/// v2 <see cref="IDriverFactory"/> abstraction to a <see cref="DriverFactoryRegistryAdapter"/>
/// over it. Replaces the F7 seam's <c>NullDriverFactory</c> default so deploys actually
/// materialise real <see cref="IDriver"/> 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 <c>hasDriver</c> flag). The driver <em>probe</em>
/// set is the exception: it backs the AdminUI Test Connect button and so must also be wired on
/// admin nodes — see <see cref="AddOtOpcUaDriverProbes"/>.
/// </summary>
public static class DriverFactoryBootstrap
{
/// <summary>
/// Register the cross-platform driver factories + bind <see cref="IDriverFactory"/>.
/// Must be called BEFORE <c>services.AddAkka</c> so the runtime extension can resolve
/// <see cref="IDriverFactory"/> from DI when spawning <c>DriverHostActor</c>.
/// </summary>
/// <param name="services">The service collection to register driver factories with.</param>
public static IServiceCollection AddOtOpcUaDriverFactories(this IServiceCollection services)
{
services.AddSingleton<DriverFactoryRegistry>(sp =>
{
var registry = new DriverFactoryRegistry();
var loggerFactory = sp.GetService<ILoggerFactory>();
Register(registry, loggerFactory);
return registry;
});
services.AddSingleton<IDriverFactory>(sp =>
new DriverFactoryRegistryAdapter(sp.GetRequiredService<DriverFactoryRegistry>()));
// 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;
}
/// <summary>
/// Register one <see cref="IDriverProbe"/> per driver type. These back the AdminUI's
/// "Test Connect" button: the <c>admin-operations</c> cluster singleton resolves
/// <see cref="IEnumerable{T}"/> of <see cref="IDriverProbe"/> and dispatches by DriverType.
/// <para>
/// That singleton is role-pinned to <c>admin</c>, so this MUST be wired on admin nodes —
/// including admin-only nodes that lack the <c>driver</c> 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
/// <see cref="AddOtOpcUaDriverFactories"/>.
/// </para>
/// <para>
/// Uses <c>TryAddEnumerable</c> 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 <c>ToDictionary(p =&gt; p.DriverType)</c> throw.
/// </para>
/// </summary>
/// <param name="services">The service collection to register driver probes with.</param>
public static IServiceCollection AddOtOpcUaDriverProbes(this IServiceCollection services)
{
services.TryAddEnumerable(ServiceDescriptor.Singleton<IDriverProbe, ModbusProbe>());
services.TryAddEnumerable(ServiceDescriptor.Singleton<IDriverProbe, AbCipProbe>());
services.TryAddEnumerable(ServiceDescriptor.Singleton<IDriverProbe, AbLegacyProbe>());
services.TryAddEnumerable(ServiceDescriptor.Singleton<IDriverProbe, S7Probe>());
services.TryAddEnumerable(ServiceDescriptor.Singleton<IDriverProbe, TwinCATProbe>());
services.TryAddEnumerable(ServiceDescriptor.Singleton<IDriverProbe, FocasProbe>());
services.TryAddEnumerable(ServiceDescriptor.Singleton<IDriverProbe, OpcUaProbe>());
services.TryAddEnumerable(ServiceDescriptor.Singleton<IDriverProbe, GalaxyProbe>());
services.TryAddEnumerable(ServiceDescriptor.Singleton<IDriverProbe, HistorianProbe>());
return services;
}
/// <summary>
/// Invoke every cross-platform driver's <c>Register</c> extension. New driver assemblies
/// get added here — one line per type. ShouldStub() in <c>DriverInstanceActor</c> still
/// handles platform/role-dependent stubbing (e.g. Galaxy on macOS), so registering a
/// factory here doesn't mean it always runs in production.
/// </summary>
private static void Register(DriverFactoryRegistry registry, ILoggerFactory? loggerFactory)
{
Driver.AbCip.AbCipDriverFactoryExtensions.Register(registry);
Driver.AbLegacy.AbLegacyDriverFactoryExtensions.Register(registry, loggerFactory);
Driver.FOCAS.FocasDriverFactoryExtensions.Register(registry);
Driver.Galaxy.GalaxyDriverFactoryExtensions.Register(registry, loggerFactory);
Driver.Modbus.ModbusDriverFactoryExtensions.Register(registry, loggerFactory);
Driver.S7.S7DriverFactoryExtensions.Register(registry);
Driver.TwinCAT.TwinCATDriverFactoryExtensions.Register(registry);
}
}