Files
histsdk/tools/AVEVA.Historian.Grpc2023CaptureHarness/Program.cs
T
Joseph Doherty ce8576bd6e M3 R3.1 capture: read-only gRPC connect scenario — LIVE-VERIFIED
Added the `connect` scenario to the 2023 R2 capture harness and ran it read-only against the
live server. The native mixed-mode client connects end-to-end over gRPC from this box:

  OpenConnection -> True (ErrorCode=Success)
  ConnectedToServer        = True
  ConnectedToServerStorage = True   <-- native client HAS the storage-engine session
  ConnectedToStoreForward  = False

Connection args that work (HistorianConnectionArgs): ServerName, TcpPort=32565,
ConnectionMode=Historian (gRPC), ConnectionType=Process, ReadOnly=true, IntegratedSecurity=false,
UserName/Password (explicit), AllowUnTrustedConnection=true, SecurityInfo=CertificateInfo{
SecurityMode=TransportCertificate, CertificateName=WONDER-SQL-VD03 } (the https:// host over the
loopback tunnel). Creds from HISTORIAN_USER/HISTORIAN_PASSWORD.

Significance: ConnectedToServerStorage=True means the native client establishes the storage
session the pure-managed SDK couldn't — so a write driven through it should route
AddNonStreamValues with a live storage session, and the cache-gate mitigation (read-first)
is promising. Next: IL-rewrite Archestra.Historian.GrpcClient.dll + a write-enabled run to
capture the RegisterTags btTagInfos + AddNonStreamValues btInput (prod write; per-action auth).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC
2026-06-21 19:12:42 -04:00

329 lines
15 KiB
C#

using System;
using System.IO;
using System.Linq;
using System.Reflection;
namespace AVEVA.Historian.Grpc2023CaptureHarness
{
/// <summary>
/// Capture harness for the M3 R3.1 follow-up. Loads the 2023 R2 mixed-mode
/// <c>aahClientManaged.dll</c> by path and drives it over gRPC to emit the two uncaptured
/// non-streamed-write buffers (regular-tag <c>RegisterTags</c> <c>btTagInfos</c> +
/// <c>AddNonStreamValues</c> <c>btInput</c>) — see <c>docs/plans/revision-write-path.md</c>
/// §"R3.1 capture plan". The byte[] payloads are captured by IL-rewriting
/// <c>Archestra.Historian.GrpcClient.dll</c>'s <c>GrpcHistoryClient.RegisterTags</c> /
/// <c>AddNonStreamValues</c> (separate dnlib step).
///
/// This file currently implements only the <c>load-check</c> 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 <c>HistorianConnectionMode</c> enum, whose
/// gRPC value the live-connect step will need). Live scenarios (open/read/write) are added once
/// load-check passes.
/// </summary>
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 <dir>.
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 <dir> 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);
case "connect":
return Connect(managedDll, args);
default:
Console.Error.WriteLine($"Unknown scenario '{scenario}'. Supported: load-check, connect.");
return 1;
}
}
/// <summary>
/// Read-only gRPC connect probe: opens a 2023 R2 Historian (mode=Historian) connection via the
/// native client and reports the resulting connection status. Proves the mixed-mode client can
/// reach the live server over gRPC from this box — the foundation for the write-capture step.
/// Reads creds from HISTORIAN_USER / HISTORIAN_PASSWORD (explicit) or uses IntegratedSecurity.
/// Usage: connect --server WONDER-SQL-VD03 [--port 32565] [--cert WONDER-SQL-VD03] [--integrated]
/// </summary>
private static int Connect(string managedDll, string[] args)
{
Assembly asm = Assembly.LoadFrom(managedDll);
Type accessType = Req(asm, "ArchestrA.HistorianAccess");
Type connArgsType = Req(asm, "ArchestrA.HistorianConnectionArgs");
Type connModeType = Req(asm, "ArchestrA.HistorianConnectionMode");
Type connTypeType = Req(asm, "ArchestrA.HistorianConnectionType");
Type errorType = Req(asm, "ArchestrA.HistorianAccessError");
Type statusType = Req(asm, "ArchestrA.HistorianConnectionStatus");
Type certInfoType = Req(asm, "ArchestrA.CertificateInfo");
Type secModeType = Req(asm, "ArchestrA.HistorianSecurityMode");
string server = GetOption(args, "--server") ?? "WONDER-SQL-VD03";
int port = int.TryParse(GetOption(args, "--port"), out int p) ? p : 32565;
string certName = GetOption(args, "--cert") ?? server;
bool integrated = args.Contains("--integrated");
string? user = Environment.GetEnvironmentVariable("HISTORIAN_USER");
string? password = Environment.GetEnvironmentVariable("HISTORIAN_PASSWORD");
if (!integrated && string.IsNullOrEmpty(user))
{
Console.Error.WriteLine("Set HISTORIAN_USER/HISTORIAN_PASSWORD or pass --integrated.");
return 1;
}
object connArgs = Activator.CreateInstance(connArgsType)!;
SetProp(connArgs, "ServerName", server);
SetProp(connArgs, "TcpPort", checked((ushort)port));
SetProp(connArgs, "ConnectionMode", Enum.Parse(connModeType, "Historian")); // 2 = gRPC
SetProp(connArgs, "ConnectionType", Enum.Parse(connTypeType, "Process"));
SetProp(connArgs, "ReadOnly", true);
SetProp(connArgs, "IntegratedSecurity", integrated);
SetProp(connArgs, "AllowUnTrustedConnection", true);
if (!integrated)
{
SetProp(connArgs, "UserName", user!);
SetProp(connArgs, "Password", password ?? string.Empty);
}
// TLS transport: SecurityInfo = CertificateInfo { SecurityMode=TransportCertificate,
// CertificateName=<dns/cert name used as the https:// host> }. AllowUnTrustedConnection
// skips chain validation (the box reaches the server cert CN over the loopback tunnel).
object certInfo = Activator.CreateInstance(certInfoType)!;
TrySetProp(certInfo, "CertificateName", certName);
TrySetProp(certInfo, "SecurityMode", Enum.Parse(secModeType, "TransportCertificate"));
TrySetProp(connArgs, "SecurityInfo", certInfo);
object access = Activator.CreateInstance(accessType)!;
object error = Activator.CreateInstance(errorType)!;
MethodInfo open = accessType.GetMethod("OpenConnection", new[] { connArgsType, errorType.MakeByRefType() })
?? throw new MissingMethodException("OpenConnection");
Console.WriteLine($"OpenConnection: server={server} port={port} mode=Historian cert={certName} integrated={integrated} readonly=true");
object?[] openArgs = { connArgs, error };
bool ok;
try
{
ok = (bool)open.Invoke(access, openArgs)!;
}
catch (TargetInvocationException tie)
{
Console.Error.WriteLine($"OpenConnection threw: {tie.InnerException?.GetType().Name}: {tie.InnerException?.Message}");
return 2;
}
error = openArgs[1]!;
Console.WriteLine($"OpenConnection returned: {ok}");
Console.WriteLine($" error: {DescribeError(error)}");
// Poll connection status for a few seconds.
MethodInfo getStatus = accessType.GetMethod("GetConnectionStatus", new[] { statusType.MakeByRefType() })
?? accessType.GetMethods().First(m => m.Name == "GetConnectionStatus" && m.GetParameters().Length == 1);
object? status = null;
for (int i = 0; i < 10; i++)
{
object?[] sArgs = { null };
getStatus.Invoke(access, sArgs);
status = sArgs[0];
bool connected = ReadBoolProp(status, "ConnectedToServer");
bool pending = ReadBoolProp(status, "Pending");
if (connected || !pending)
{
break;
}
System.Threading.Thread.Sleep(500);
}
Console.WriteLine("ConnectionStatus:");
DumpProps(status, " ");
bool connectedToServer = ReadBoolProp(status, "ConnectedToServer");
// Always close cleanly.
try
{
MethodInfo? close = accessType.GetMethod("CloseConnection", new[] { errorType.MakeByRefType() });
if (close != null)
{
object?[] cArgs = { Activator.CreateInstance(errorType) };
close.Invoke(access, cArgs);
}
}
catch { /* close best-effort */ }
Console.WriteLine(connectedToServer ? "CONNECT: PASS (ConnectedToServer)" : "CONNECT: FAIL (not connected)");
return connectedToServer ? 0 : 3;
}
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 Type Req(Assembly asm, string name) =>
asm.GetType(name) ?? throw new TypeLoadException($"Type not found: {name}");
private static void SetProp(object target, string name, object value)
{
PropertyInfo prop = target.GetType().GetProperty(name)
?? throw new MissingMemberException(target.GetType().FullName, name);
prop.SetValue(target, value);
}
private static void TrySetProp(object target, string name, object value)
{
try
{
PropertyInfo? prop = target.GetType().GetProperty(name);
if (prop != null && prop.CanWrite)
{
prop.SetValue(target, value);
}
}
catch (Exception ex)
{
Console.WriteLine($" (note: could not set {name}: {ex.GetType().Name})");
}
}
private static bool ReadBoolProp(object? target, string name)
{
if (target == null) return false;
PropertyInfo? prop = target.GetType().GetProperty(name);
return prop != null && prop.PropertyType == typeof(bool) && (bool)(prop.GetValue(target) ?? false);
}
private static void DumpProps(object? target, string indent)
{
if (target == null) { Console.WriteLine($"{indent}<null>"); return; }
foreach (PropertyInfo prop in target.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance))
{
if (prop.GetIndexParameters().Length != 0) continue;
object? val;
try { val = prop.GetValue(target); }
catch { val = "<throw>"; }
Console.WriteLine($"{indent}{prop.Name} = {val}");
}
}
private static string DescribeError(object? error)
{
if (error == null) return "<null>";
var sb = new System.Text.StringBuilder();
foreach (PropertyInfo prop in error.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance))
{
if (prop.GetIndexParameters().Length != 0) continue;
object? val;
try { val = prop.GetValue(error); }
catch { continue; }
if (val != null && !string.IsNullOrEmpty(val.ToString()))
{
sb.Append($"{prop.Name}={val} ");
}
}
return sb.Length == 0 ? "<empty>" : sb.ToString().Trim();
}
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;
}
}
}