Phase 0 — mechanical rename ZB.MOM.WW.LmxOpcUa.* → ZB.MOM.WW.OtOpcUa.*
Renames all 11 projects (5 src + 6 tests), the .slnx solution file, all source-file namespaces, all axaml namespace references, and all v1 documentation references in CLAUDE.md and docs/*.md (excluding docs/v2/ which is already in OtOpcUa form). Also updates the TopShelf service registration name from "LmxOpcUa" to "OtOpcUa" per Phase 0 Task 0.6.
Preserves runtime identifiers per Phase 0 Out-of-Scope rules to avoid breaking v1/v2 client trust during coexistence: OPC UA `ApplicationUri` defaults (`urn:{GalaxyName}:LmxOpcUa`), server `EndpointPath` (`/LmxOpcUa`), `ServerName` default (feeds cert subject CN), `MxAccessConfiguration.ClientName` default (defensive — stays "LmxOpcUa" for MxAccess audit-trail consistency), client OPC UA identifiers (`ApplicationName = "LmxOpcUaClient"`, `ApplicationUri = "urn:localhost:LmxOpcUaClient"`, cert directory `%LocalAppData%\LmxOpcUaClient\pki\`), and the `LmxOpcUaServer` class name (class rename out of Phase 0 scope per Task 0.5 sed pattern; happens in Phase 1 alongside `LmxNodeManager → GenericDriverNodeManager` Core extraction). 23 LmxOpcUa references retained, all enumerated and justified in `docs/v2/implementation/exit-gate-phase-0.md`.
Build clean: 0 errors, 30 warnings (lower than baseline 167). Tests at strict improvement over baseline: 821 passing / 1 failing vs baseline 820 / 2 (one flaky pre-existing failure passed this run; the other still fails — both pre-existing and unrelated to the rename). `Client.UI.Tests`, `Historian.Aveva.Tests`, `Client.Shared.Tests`, `IntegrationTests` all match baseline exactly. Exit gate compliance results recorded in `docs/v2/implementation/exit-gate-phase-0.md` with all 7 checks PASS or DEFERRED-to-PR-review (#7 service install verification needs Windows service permissions on the reviewer's box).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
180
src/ZB.MOM.WW.OtOpcUa.Host/Historian/HistorianPluginLoader.cs
Normal file
180
src/ZB.MOM.WW.OtOpcUa.Host/Historian/HistorianPluginLoader.cs
Normal file
@@ -0,0 +1,180 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using Serilog;
|
||||
using ZB.MOM.WW.OtOpcUa.Host.Configuration;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Host.Historian
|
||||
{
|
||||
/// <summary>
|
||||
/// Result of the most recent historian plugin load attempt.
|
||||
/// </summary>
|
||||
public enum HistorianPluginStatus
|
||||
{
|
||||
/// <summary>Historian.Enabled is false; TryLoad was not called.</summary>
|
||||
Disabled,
|
||||
/// <summary>Plugin DLL was not present in the Historian/ subfolder.</summary>
|
||||
NotFound,
|
||||
/// <summary>Plugin file exists but could not be loaded or instantiated.</summary>
|
||||
LoadFailed,
|
||||
/// <summary>Plugin loaded and an IHistorianDataSource was constructed.</summary>
|
||||
Loaded
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Structured outcome of a <see cref="HistorianPluginLoader.TryLoad"/> or
|
||||
/// <see cref="HistorianPluginLoader.MarkDisabled"/> call, used by the status dashboard.
|
||||
/// </summary>
|
||||
public sealed class HistorianPluginOutcome
|
||||
{
|
||||
public HistorianPluginOutcome(HistorianPluginStatus status, string pluginPath, string? error)
|
||||
{
|
||||
Status = status;
|
||||
PluginPath = pluginPath;
|
||||
Error = error;
|
||||
}
|
||||
|
||||
public HistorianPluginStatus Status { get; }
|
||||
public string PluginPath { get; }
|
||||
public string? Error { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads the Wonderware historian plugin assembly from the Historian/ subfolder next to
|
||||
/// the host executable. Used so the aahClientManaged SDK is not needed on hosts that run
|
||||
/// with Historian.Enabled=false.
|
||||
/// </summary>
|
||||
public static class HistorianPluginLoader
|
||||
{
|
||||
private const string PluginSubfolder = "Historian";
|
||||
private const string PluginAssemblyName = "ZB.MOM.WW.OtOpcUa.Historian.Aveva";
|
||||
private const string PluginEntryType = "ZB.MOM.WW.OtOpcUa.Historian.Aveva.AvevaHistorianPluginEntry";
|
||||
private const string PluginEntryMethod = "Create";
|
||||
|
||||
private static readonly ILogger Log = Serilog.Log.ForContext(typeof(HistorianPluginLoader));
|
||||
private static readonly object ResolverGate = new object();
|
||||
private static bool _resolverInstalled;
|
||||
private static string? _resolvedProbeDirectory;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the outcome of the most recent load attempt (or <see cref="HistorianPluginStatus.Disabled"/>
|
||||
/// if the loader has never been invoked). The dashboard reads this to distinguish "disabled",
|
||||
/// "plugin missing", and "plugin crashed".
|
||||
/// </summary>
|
||||
public static HistorianPluginOutcome LastOutcome { get; private set; }
|
||||
= new HistorianPluginOutcome(HistorianPluginStatus.Disabled, string.Empty, null);
|
||||
|
||||
/// <summary>
|
||||
/// Records that the historian plugin is disabled by configuration. Called by
|
||||
/// <c>OpcUaService</c> when <c>Historian.Enabled=false</c> so the status dashboard can
|
||||
/// report the exact reason history is unavailable.
|
||||
/// </summary>
|
||||
public static void MarkDisabled()
|
||||
{
|
||||
LastOutcome = new HistorianPluginOutcome(HistorianPluginStatus.Disabled, string.Empty, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to load the historian plugin and construct an <see cref="IHistorianDataSource"/>.
|
||||
/// Returns null on any failure so the server can continue with history unsupported. The
|
||||
/// specific reason is published on <see cref="LastOutcome"/>.
|
||||
/// </summary>
|
||||
public static IHistorianDataSource? TryLoad(HistorianConfiguration config)
|
||||
{
|
||||
var pluginDirectory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, PluginSubfolder);
|
||||
var pluginPath = Path.Combine(pluginDirectory, PluginAssemblyName + ".dll");
|
||||
|
||||
if (!File.Exists(pluginPath))
|
||||
{
|
||||
Log.Warning(
|
||||
"Historian plugin not found at {PluginPath} — history read operations will return BadHistoryOperationUnsupported",
|
||||
pluginPath);
|
||||
LastOutcome = new HistorianPluginOutcome(HistorianPluginStatus.NotFound, pluginPath, null);
|
||||
return null;
|
||||
}
|
||||
|
||||
EnsureAssemblyResolverInstalled(pluginDirectory);
|
||||
|
||||
try
|
||||
{
|
||||
var assembly = Assembly.LoadFrom(pluginPath);
|
||||
var entryType = assembly.GetType(PluginEntryType, throwOnError: false);
|
||||
if (entryType == null)
|
||||
{
|
||||
Log.Warning("Historian plugin {PluginPath} does not expose {EntryType}", pluginPath, PluginEntryType);
|
||||
LastOutcome = new HistorianPluginOutcome(
|
||||
HistorianPluginStatus.LoadFailed, pluginPath,
|
||||
$"Plugin assembly does not expose entry type {PluginEntryType}");
|
||||
return null;
|
||||
}
|
||||
|
||||
var create = entryType.GetMethod(PluginEntryMethod, BindingFlags.Public | BindingFlags.Static);
|
||||
if (create == null)
|
||||
{
|
||||
Log.Warning("Historian plugin entry type {EntryType} missing static {Method}", PluginEntryType, PluginEntryMethod);
|
||||
LastOutcome = new HistorianPluginOutcome(
|
||||
HistorianPluginStatus.LoadFailed, pluginPath,
|
||||
$"Plugin entry type {PluginEntryType} is missing a public static {PluginEntryMethod} method");
|
||||
return null;
|
||||
}
|
||||
|
||||
var result = create.Invoke(null, new object[] { config });
|
||||
if (result is IHistorianDataSource dataSource)
|
||||
{
|
||||
Log.Information("Historian plugin loaded from {PluginPath}", pluginPath);
|
||||
LastOutcome = new HistorianPluginOutcome(HistorianPluginStatus.Loaded, pluginPath, null);
|
||||
return dataSource;
|
||||
}
|
||||
|
||||
Log.Warning("Historian plugin {PluginPath} returned an object that does not implement IHistorianDataSource", pluginPath);
|
||||
LastOutcome = new HistorianPluginOutcome(
|
||||
HistorianPluginStatus.LoadFailed, pluginPath,
|
||||
"Plugin entry method returned an object that does not implement IHistorianDataSource");
|
||||
return null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "Failed to load historian plugin from {PluginPath} — history disabled", pluginPath);
|
||||
LastOutcome = new HistorianPluginOutcome(
|
||||
HistorianPluginStatus.LoadFailed, pluginPath,
|
||||
ex.GetBaseException().Message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static void EnsureAssemblyResolverInstalled(string pluginDirectory)
|
||||
{
|
||||
lock (ResolverGate)
|
||||
{
|
||||
_resolvedProbeDirectory = pluginDirectory;
|
||||
if (_resolverInstalled)
|
||||
return;
|
||||
|
||||
AppDomain.CurrentDomain.AssemblyResolve += ResolveFromPluginDirectory;
|
||||
_resolverInstalled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private static Assembly? ResolveFromPluginDirectory(object? sender, ResolveEventArgs args)
|
||||
{
|
||||
var probeDirectory = _resolvedProbeDirectory;
|
||||
if (string.IsNullOrEmpty(probeDirectory))
|
||||
return null;
|
||||
|
||||
var requested = new AssemblyName(args.Name);
|
||||
var candidate = Path.Combine(probeDirectory!, requested.Name + ".dll");
|
||||
if (!File.Exists(candidate))
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
return Assembly.LoadFrom(candidate);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Debug(ex, "Historian plugin resolver failed to load {Candidate}", candidate);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user