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
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <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}");
|
||||
@@ -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}<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;
|
||||
|
||||
Reference in New Issue
Block a user