diff --git a/tools/AVEVA.Historian.Grpc2023CaptureHarness/AVEVA.Historian.Grpc2023CaptureHarness.csproj b/tools/AVEVA.Historian.Grpc2023CaptureHarness/AVEVA.Historian.Grpc2023CaptureHarness.csproj new file mode 100644 index 0000000..1cc351a --- /dev/null +++ b/tools/AVEVA.Historian.Grpc2023CaptureHarness/AVEVA.Historian.Grpc2023CaptureHarness.csproj @@ -0,0 +1,22 @@ + + + + + Exe + net481 + latest + disable + enable + x64 + Grpc2023CaptureHarness + + + diff --git a/tools/AVEVA.Historian.Grpc2023CaptureHarness/Program.cs b/tools/AVEVA.Historian.Grpc2023CaptureHarness/Program.cs new file mode 100644 index 0000000..e64de42 --- /dev/null +++ b/tools/AVEVA.Historian.Grpc2023CaptureHarness/Program.cs @@ -0,0 +1,150 @@ +using System; +using System.IO; +using System.Linq; +using System.Reflection; + +namespace AVEVA.Historian.Grpc2023CaptureHarness +{ + /// + /// Capture harness for the M3 R3.1 follow-up. Loads the 2023 R2 mixed-mode + /// aahClientManaged.dll by path and drives it over gRPC to emit the two uncaptured + /// non-streamed-write buffers (regular-tag RegisterTags btTagInfos + + /// AddNonStreamValues btInput) — see docs/plans/revision-write-path.md + /// §"R3.1 capture plan". The byte[] payloads are captured by IL-rewriting + /// Archestra.Historian.GrpcClient.dll's GrpcHistoryClient.RegisterTags / + /// AddNonStreamValues (separate dnlib step). + /// + /// This file currently implements only the load-check scenario: a local, no-network + /// feasibility probe that confirms the mixed-mode assembly loads in this net481 x64 process and + /// that the connection API is reflectable (notably the HistorianConnectionMode enum, whose + /// gRPC value the live-connect step will need). Live scenarios (open/read/write) are added once + /// load-check passes. + /// + internal static class Program + { + private static int Main(string[] args) + { + string scenario = args.FirstOrDefault(a => !a.StartsWith("--", StringComparison.Ordinal)) ?? "load-check"; + + // Default to the sibling analysis tree; overridable with --bin . + string repoRoot = FindRepoRoot(); + string defaultBin = Path.GetFullPath(Path.Combine(repoRoot, "..", "histsdk-2023r2-analysis", "bin")); + string binDir = GetOption(args, "--bin") ?? defaultBin; + string msiX64 = Path.GetFullPath(Path.Combine(binDir, "..", "msi-extract", "ArchestrA", "Toolkits", "Bin", "x64")); + + string managedDll = Path.Combine(binDir, "aahClientManaged.dll"); + if (!File.Exists(managedDll)) + { + Console.Error.WriteLine($"aahClientManaged.dll not found at: {managedDll}"); + Console.Error.WriteLine("Pass --bin pointing at histsdk-2023r2-analysis/bin."); + return 1; + } + + // Resolve siblings from both the core bin dir and the gRPC-runtime msi-extract dir. + string[] probeDirs = Directory.Exists(msiX64) ? new[] { binDir, msiX64 } : new[] { binDir }; + AppDomain.CurrentDomain.AssemblyResolve += (_, e) => + { + string simpleName = new AssemblyName(e.Name).Name + ".dll"; + foreach (string dir in probeDirs) + { + string candidate = Path.Combine(dir, simpleName); + if (File.Exists(candidate)) + { + return Assembly.LoadFrom(candidate); + } + } + return null!; + }; + + switch (scenario) + { + case "load-check": + return LoadCheck(managedDll, probeDirs); + default: + Console.Error.WriteLine($"Unknown scenario '{scenario}'. Supported: load-check."); + return 1; + } + } + + private static int LoadCheck(string managedDll, string[] probeDirs) + { + Console.WriteLine($"Process: {(Environment.Is64BitProcess ? "x64" : "x86")}, CLR {Environment.Version}"); + Console.WriteLine($"Probe dirs:"); + foreach (string d in probeDirs) + { + Console.WriteLine($" {d} (exists={Directory.Exists(d)})"); + } + + Assembly asm; + try + { + asm = Assembly.LoadFrom(managedDll); + } + catch (Exception ex) + { + Console.Error.WriteLine($"LoadFrom FAILED: {ex.GetType().Name}: {ex.Message}"); + if (ex is BadImageFormatException) + { + Console.Error.WriteLine(" -> likely an x86/x64 mismatch or missing VC++ runtime (MSVCP140/VCRUNTIME140_1)."); + } + return 2; + } + + Console.WriteLine($"Loaded: {asm.FullName}"); + + Type? access = asm.GetType("ArchestrA.HistorianAccess"); + Type? connArgs = asm.GetType("ArchestrA.HistorianConnectionArgs"); + Type? connMode = asm.GetType("ArchestrA.HistorianConnectionMode"); + Console.WriteLine($"HistorianAccess resolved: {access != null}"); + Console.WriteLine($"HistorianConnectionArgs resolved:{connArgs != null}"); + Console.WriteLine($"HistorianConnectionMode resolved:{connMode != null}"); + + if (connMode != null && connMode.IsEnum) + { + Console.WriteLine("HistorianConnectionMode values (the gRPC vs legacy selector):"); + foreach (object v in Enum.GetValues(connMode)) + { + Console.WriteLine($" {v} = {Convert.ToInt64(v)}"); + } + } + + // Confirm the managed gRPC client (IL-rewrite capture target) is reachable too. + try + { + Assembly grpc = Assembly.Load("Archestra.Historian.GrpcClient"); + Type? historyClient = grpc.GetType("Archestra.Historian.GrpcClient.GrpcHistoryClient"); + bool hasRegister = historyClient?.GetMethod("RegisterTags") != null; + bool hasAddNonStream = historyClient?.GetMethod("AddNonStreamValues") != null; + Console.WriteLine($"GrpcHistoryClient resolved: {historyClient != null} (RegisterTags={hasRegister}, AddNonStreamValues={hasAddNonStream})"); + } + catch (Exception ex) + { + Console.WriteLine($"GrpcHistoryClient load note: {ex.GetType().Name}: {ex.Message}"); + } + + bool ok = access != null && connArgs != null && connMode != null; + Console.WriteLine(ok ? "LOAD-CHECK: PASS" : "LOAD-CHECK: PARTIAL (some types unresolved)"); + return ok ? 0 : 3; + } + + private static string? GetOption(string[] args, string name) + { + int i = Array.IndexOf(args, name); + return i >= 0 && i + 1 < args.Length ? args[i + 1] : null; + } + + private static string FindRepoRoot() + { + string dir = AppDomain.CurrentDomain.BaseDirectory; + for (int i = 0; i < 8 && dir != null; i++) + { + if (File.Exists(Path.Combine(dir, "Histsdk.slnx"))) + { + return dir; + } + dir = Path.GetDirectoryName(dir.TrimEnd(Path.DirectorySeparatorChar))!; + } + return AppDomain.CurrentDomain.BaseDirectory; + } + } +}