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: (optional) IL-rewrite dir FIRST (so the instrumented // Archestra.Historian.GrpcClient.dll + ReverseInstrumentation.dll win), then the core // bin dir, then the gRPC-runtime msi-extract dir. string? rewriteDir = GetOption(args, "--grpc-rewrite"); var probeList = new System.Collections.Generic.List(); if (!string.IsNullOrEmpty(rewriteDir) && Directory.Exists(rewriteDir)) probeList.Add(rewriteDir!); probeList.Add(binDir); if (Directory.Exists(msiX64)) probeList.Add(msiX64); string[] probeDirs = probeList.ToArray(); 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; } } /// /// 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}"); 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}"); 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; 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; } } }