diff --git a/tools/AVEVA.Historian.Grpc2023CaptureHarness/Program.cs b/tools/AVEVA.Historian.Grpc2023CaptureHarness/Program.cs
index e64de42..92c1b02 100644
--- a/tools/AVEVA.Historian.Grpc2023CaptureHarness/Program.cs
+++ b/tools/AVEVA.Historian.Grpc2023CaptureHarness/Program.cs
@@ -60,12 +60,126 @@ namespace AVEVA.Historian.Grpc2023CaptureHarness
{
case "load-check":
return LoadCheck(managedDll, probeDirs);
+ case "connect":
+ return Connect(managedDll, args);
default:
- Console.Error.WriteLine($"Unknown scenario '{scenario}'. Supported: load-check.");
+ Console.Error.WriteLine($"Unknown scenario '{scenario}'. Supported: load-check, connect.");
return 1;
}
}
+ ///
+ /// 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]
+ ///
+ 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= }. 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}");
@@ -133,6 +247,70 @@ namespace AVEVA.Historian.Grpc2023CaptureHarness
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}"); 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 = ""; }
+ Console.WriteLine($"{indent}{prop.Name} = {val}");
+ }
+ }
+
+ private static string DescribeError(object? error)
+ {
+ if (error == null) return "";
+ 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 ? "" : sb.ToString().Trim();
+ }
+
private static string FindRepoRoot()
{
string dir = AppDomain.CurrentDomain.BaseDirectory;