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;
+ }
+ }
+}