using System; using System.IO; using System.Reflection; using Serilog; using ZB.MOM.WW.LmxOpcUa.Host.Configuration; namespace ZB.MOM.WW.LmxOpcUa.Host.Historian { /// /// Result of the most recent historian plugin load attempt. /// public enum HistorianPluginStatus { /// Historian.Enabled is false; TryLoad was not called. Disabled, /// Plugin DLL was not present in the Historian/ subfolder. NotFound, /// Plugin file exists but could not be loaded or instantiated. LoadFailed, /// Plugin loaded and an IHistorianDataSource was constructed. Loaded } /// /// Structured outcome of a or /// call, used by the status dashboard. /// 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; } } /// /// 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. /// public static class HistorianPluginLoader { private const string PluginSubfolder = "Historian"; private const string PluginAssemblyName = "ZB.MOM.WW.LmxOpcUa.Historian.Aveva"; private const string PluginEntryType = "ZB.MOM.WW.LmxOpcUa.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; /// /// Gets the outcome of the most recent load attempt (or /// if the loader has never been invoked). The dashboard reads this to distinguish "disabled", /// "plugin missing", and "plugin crashed". /// public static HistorianPluginOutcome LastOutcome { get; private set; } = new HistorianPluginOutcome(HistorianPluginStatus.Disabled, string.Empty, null); /// /// Records that the historian plugin is disabled by configuration. Called by /// OpcUaService when Historian.Enabled=false so the status dashboard can /// report the exact reason history is unavailable. /// public static void MarkDisabled() { LastOutcome = new HistorianPluginOutcome(HistorianPluginStatus.Disabled, string.Empty, null); } /// /// Attempts to load the historian plugin and construct an . /// Returns null on any failure so the server can continue with history unsupported. The /// specific reason is published on . /// 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; } } } }