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!; }; // Pre-load the instrumented GrpcClient (+ logger) from the rewrite dir BEFORE anything // touches aahClientManaged. The CLR reuses an already-loaded assembly (matched by // identity) before probing the filesystem, so this wins over aahClientManaged's // LoadFrom-context sibling probing (which would otherwise pick the original next to it). if (!string.IsNullOrEmpty(rewriteDir) && Directory.Exists(rewriteDir)) { foreach (string dll in new[] { "AVEVA.Historian.ReverseInstrumentation.dll", "Archestra.Historian.GrpcClient.dll" }) { string path = Path.Combine(rewriteDir!, dll); if (File.Exists(path)) { Assembly pre = Assembly.LoadFrom(path); Console.WriteLine($"Pre-loaded {pre.GetName().Name} from {pre.Location}"); } } } switch (scenario) { case "load-check": return LoadCheck(managedDll, probeDirs); case "connect": return Connect(managedDll, args); case "capture-write": return CaptureWrite(managedDll, args); case "delete-tag": return DeleteTag(managedDll, args); case "capture-event": return CaptureEvent(managedDll, args); case "capture-send-event": return CaptureSendEvent(managedDll, args); default: Console.Error.WriteLine($"Unknown scenario '{scenario}'. Supported: load-check, connect, capture-write, delete-tag, capture-event, capture-send-event."); return 1; } } /// /// Deletes a tag via the native client's DeleteTags (the path that removes the tag /// cleanly, unlike the SDK's WCF DelT). Used to clean up the capture sandbox tag. /// Usage: delete-tag --tag SdkM3CaptureSandbox [--server …] [--port …] [--cert …] /// private static int DeleteTag(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 certInfoType = Req(asm, "ArchestrA.CertificateInfo"); Type secModeType = Req(asm, "ArchestrA.HistorianSecurityMode"); Type tagStatusType = Req(asm, "ArchestrA.HistorianTagStatus"); Type tagStatusListType = Req(asm, "ArchestrA.HistorianTagStatusList"); string server = GetOption(args, "--server") ?? Environment.GetEnvironmentVariable("HISTORIAN_GRPC_HOST") ?? "localhost"; int port = int.TryParse(GetOption(args, "--port"), out int p) ? p : 32565; string certName = GetOption(args, "--cert") ?? server; string tagName = GetOption(args, "--tag") ?? "SdkM3CaptureSandbox"; string? user = Environment.GetEnvironmentVariable("HISTORIAN_USER"); string? password = Environment.GetEnvironmentVariable("HISTORIAN_PASSWORD"); if (string.IsNullOrEmpty(user)) { Console.Error.WriteLine("Set HISTORIAN_USER/HISTORIAN_PASSWORD."); return 1; } object connArgs = Activator.CreateInstance(connArgsType)!; SetProp(connArgs, "ServerName", server); SetProp(connArgs, "TcpPort", checked((ushort)port)); SetProp(connArgs, "ConnectionMode", Enum.Parse(connModeType, "Historian")); SetProp(connArgs, "ConnectionType", Enum.Parse(connTypeType, "Process")); SetProp(connArgs, "ReadOnly", false); SetProp(connArgs, "IntegratedSecurity", false); SetProp(connArgs, "AllowUnTrustedConnection", true); SetProp(connArgs, "UserName", user); SetProp(connArgs, "Password", password ?? string.Empty); 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 openErr = Activator.CreateInstance(errorType)!; object?[] openArgs = { connArgs, openErr }; bool opened = (bool)accessType.GetMethod("OpenConnection", new[] { connArgsType, errorType.MakeByRefType() })! .Invoke(access, openArgs)!; Console.WriteLine($"OpenConnection: {opened} err={DescribeError(openArgs[1])}"); if (!opened) { return 2; } try { // Prime the write session exactly as the capture flow does — DeleteTags on a fresh // connection returns UnknownClient(51) until AddTag has registered the client // (UpdateClientStatus). AddTag on the existing tag is idempotent here. Type tagType = Req(asm, "ArchestrA.HistorianTag"); Type tagDataTypeEnum = Req(asm, "ArchestrA.HistorianDataType"); Type tagStorageEnum = Req(asm, "ArchestrA.HistorianStorageType"); object primeTag = Activator.CreateInstance(tagType)!; SetProp(primeTag, "TagName", tagName); TrySetProp(primeTag, "TagDataType", Enum.Parse(tagDataTypeEnum, "Float", true)); TrySetProp(primeTag, "TagStorageType", Enum.Parse(tagStorageEnum, "Cyclic", true)); object primeErr = Activator.CreateInstance(errorType)!; object?[] primeArgs = { primeTag, 0u, primeErr }; bool primed = (bool)accessType.GetMethod("AddTag", new[] { tagType, typeof(uint).MakeByRefType(), errorType.MakeByRefType() })! .Invoke(access, primeArgs)!; Console.WriteLine($"Prime AddTag({tagName}): {primed} err={DescribeError(primeArgs[2])}"); System.Threading.Thread.Sleep(2000); object list = Activator.CreateInstance(tagStatusListType)!; object status = Activator.CreateInstance(tagStatusType)!; SetProp(status, "TagName", tagName); tagStatusListType.GetMethods().First(m => m.Name == "Add" && m.GetParameters().Length == 1).Invoke(list, new[] { status }); MethodInfo delete = accessType.GetMethods().First(m => m.Name == "DeleteTags" && m.GetParameters().Length == 2); object delErr = Activator.CreateInstance(errorType)!; object?[] delArgs = { list, delErr }; bool ok = (bool)delete.Invoke(access, delArgs)!; Console.WriteLine($"DeleteTags({tagName}): {ok} err={DescribeError(delArgs[1])}"); return ok ? 0 : 3; } finally { try { MethodInfo? close = accessType.GetMethod("CloseConnection", new[] { errorType.MakeByRefType() }); if (close != null) close.Invoke(access, new object?[] { Activator.CreateInstance(errorType) }); } catch { /* best-effort */ } } } /// /// Drives the native 2023 R2 client through a non-streamed (historical backfill) write so the /// IL-rewritten GrpcHistoryClient dumps the two buffers (RegisterTags.tagInfos + /// AddNonStreamValues.inBuff) to the capture NDJSON. Sequence: open write-enabled gRPC -> /// (optional) AddTag sandbox -> GetTagInfoByName (real TagKey + primes the per-connection /// cache, the gate mitigation) -> CreateHistorianDataValueList(NonStreamedOriginal) -> /// NonStreamedValuesBegin -> AddNonStreamedValue -> AddNonStreamedValuesEnd -> SendValues. /// SendValues (the actual wire push) only runs with --commit. Run with --grpc-rewrite pointing /// at the instrumented copy and AVEVA_HISTORIAN_RE_CAPTURE set to the output file. /// Usage: capture-write --tag SdkM3CaptureSandbox [--create] [--commit] /// [--server ] [--port 32565] [--cert ] [--value 123.0] /// private static int CaptureWrite(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 certInfoType = Req(asm, "ArchestrA.CertificateInfo"); Type secModeType = Req(asm, "ArchestrA.HistorianSecurityMode"); Type tagType = Req(asm, "ArchestrA.HistorianTag"); Type tagDataTypeEnum = Req(asm, "ArchestrA.HistorianDataType"); Type tagStorageEnum = Req(asm, "ArchestrA.HistorianStorageType"); Type valueType = Req(asm, "ArchestrA.HistorianDataValue"); Type valueDataTypeEnum = Req(asm, "ArchestrA.HistorianDataType"); Type listType = Req(asm, "ArchestrA.HistorianDataValueList"); Type categoryEnum = Req(asm, "ArchestrA.HistorianDataCategory"); string server = GetOption(args, "--server") ?? Environment.GetEnvironmentVariable("HISTORIAN_GRPC_HOST") ?? "localhost"; int port = int.TryParse(GetOption(args, "--port"), out int p) ? p : 32565; string certName = GetOption(args, "--cert") ?? server; string tagName = GetOption(args, "--tag") ?? "SdkM3CaptureSandbox"; bool create = args.Contains("--create"); bool commit = args.Contains("--commit"); string dataType = GetOption(args, "--data-type") ?? "Float"; // Float|Double|Int2|Int4|UInt4 string rawValue = GetOption(args, "--value") ?? "123"; // Box the value as the CLR type the HistorianDataValue expects for this tag type. object sampleValue = dataType switch { "Double" => (object)double.Parse(rawValue), "Int2" => (object)short.Parse(rawValue), "Int4" => (object)int.Parse(rawValue), "UInt4" => (object)uint.Parse(rawValue), _ => (object)float.Parse(rawValue), }; string? user = Environment.GetEnvironmentVariable("HISTORIAN_USER"); string? password = Environment.GetEnvironmentVariable("HISTORIAN_PASSWORD"); if (string.IsNullOrEmpty(user)) { Console.Error.WriteLine("Set HISTORIAN_USER/HISTORIAN_PASSWORD."); return 1; } // Default capture sink if the caller didn't set one. if (string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("AVEVA_HISTORIAN_RE_CAPTURE"))) { string defaultCap = Path.GetFullPath(Path.Combine( "artifacts", "reverse-engineering", "grpc-nonstream-capture", "capture.ndjson")); Environment.SetEnvironmentVariable("AVEVA_HISTORIAN_RE_CAPTURE", defaultCap); } string capPath = Environment.GetEnvironmentVariable("AVEVA_HISTORIAN_RE_CAPTURE")!; Console.WriteLine($"Capture sink: {capPath}"); // --- open write-enabled gRPC connection --- object connArgs = Activator.CreateInstance(connArgsType)!; SetProp(connArgs, "ServerName", server); SetProp(connArgs, "TcpPort", checked((ushort)port)); SetProp(connArgs, "ConnectionMode", Enum.Parse(connModeType, "Historian")); SetProp(connArgs, "ConnectionType", Enum.Parse(connTypeType, "Process")); SetProp(connArgs, "ReadOnly", false); // write-enabled SetProp(connArgs, "IntegratedSecurity", false); SetProp(connArgs, "AllowUnTrustedConnection", true); SetProp(connArgs, "UserName", user); SetProp(connArgs, "Password", password ?? string.Empty); 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 openErr = Activator.CreateInstance(errorType)!; object?[] openArgs = { connArgs, openErr }; bool opened = (bool)accessType.GetMethod("OpenConnection", new[] { connArgsType, errorType.MakeByRefType() })! .Invoke(access, openArgs)!; Console.WriteLine($"OpenConnection(write-enabled): {opened} err={DescribeError(openArgs[1])}"); if (!opened) { return 2; } try { // --- (optional) create the sandbox tag --- if (create) { object tag = Activator.CreateInstance(tagType)!; SetProp(tag, "TagName", tagName); TrySetProp(tag, "TagDescription", "histsdk M3 non-streamed-write capture sandbox"); TrySetProp(tag, "TagDataType", Enum.Parse(tagDataTypeEnum, dataType, true)); TrySetProp(tag, "TagStorageType", Enum.Parse(tagStorageEnum, "Cyclic", true)); object addErr = Activator.CreateInstance(errorType)!; object?[] addArgs = { tag, 0u, addErr }; bool addOk = (bool)accessType.GetMethod("AddTag", new[] { tagType, typeof(uint).MakeByRefType(), errorType.MakeByRefType() })! .Invoke(access, addArgs)!; Console.WriteLine($"AddTag({tagName}): {addOk} synthKey={addArgs[1]} err={DescribeError(addArgs[2])}"); System.Threading.Thread.Sleep(3000); // let the server pick up the new tag } // --- resolve real TagKey + prime the per-connection cache (gate mitigation) --- // AddTag returns a synthetic key (10000000); the real wwTagKey is assigned server-side, // so force a server fetch (cache:false) after a resync wait. GetTagInfoByName also // primes the per-connection tag cache that AddNonStreamedValue's gate checks. uint tagKey = 0; if (uint.TryParse(GetOption(args, "--tag-key"), out uint overrideKey) && overrideKey != 0) { tagKey = overrideKey; Console.WriteLine($"TagKey override: {tagKey}"); } MethodInfo? getInfo = accessType.GetMethods().FirstOrDefault(m => m.Name == "GetTagInfoByName" && m.GetParameters().Length == 4); int resyncWait = int.TryParse(GetOption(args, "--resync-wait"), out int rw2) ? rw2 : 10; if (getInfo != null) { if (create && resyncWait > 0) { Console.WriteLine($"Waiting {resyncWait}s for server tag-key assignment..."); System.Threading.Thread.Sleep(resyncWait * 1000); } // Try server-fetch (cache:false) first for the real key, then cache:true as a prime. foreach (bool useCache in new[] { false, true }) { object infoErr = Activator.CreateInstance(errorType)!; object?[] infoArgs = { tagName, useCache, null, infoErr }; bool infoOk = (bool)getInfo.Invoke(access, infoArgs)!; object? tagInfo = infoArgs[2]; uint k = 0; if (tagInfo != null && tagInfo.GetType().GetProperty("TagKey")?.GetValue(tagInfo) is { } kv) k = Convert.ToUInt32(kv); Console.WriteLine($"GetTagInfoByName({tagName}, cache={useCache}): {infoOk} TagKey={k} err={DescribeError(infoArgs[3])}"); // Prefer a real (non-synthetic) server key. if (k != 0 && (tagKey == 0 || (k != 10000000 && tagKey == 10000000))) tagKey = k; } } if (tagKey == 0) { Console.Error.WriteLine("Could not resolve a TagKey — aborting before the write. Pass --tag-key ."); return 3; } Console.WriteLine($"Using TagKey={tagKey}"); // --- build the historical (backfill) value --- object value = Activator.CreateInstance(valueType)!; SetProp(value, "TagKey", tagKey); TrySetProp(value, "DataValueType", Enum.Parse(valueDataTypeEnum, dataType, true)); TrySetProp(value, "OpcQuality", (ushort)192); TrySetProp(value, "Value", sampleValue); DateTime ts = DateTime.UtcNow.AddHours(-2); // backfill = past timestamp TrySetProp(value, "StartDateTime", ts); TrySetProp(value, "EndDateTime", ts); TrySetProp(value, "ApplyScaling", false); const BindingFlags allInstance = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; // --- non-streamed write sequence --- MethodInfo createList = accessType.GetMethods().Where(m => m.Name == "CreateHistorianDataValueList").OrderBy(m => m.GetParameters().Length).First(); object?[] clArgs = createList.GetParameters().Select(pi => { if (pi.ParameterType == categoryEnum) return Enum.Parse(categoryEnum, "NonStreamedOriginal"); if (pi.ParameterType.Name == "ConnectionIndex") return Enum.ToObject(pi.ParameterType, 0); return pi.ParameterType.IsValueType ? Activator.CreateInstance(pi.ParameterType) : null; }).ToArray(); object list = createList.Invoke(access, clArgs)!; Invoke0(list, listType.GetMethods(allInstance).First(m => m.Name == "NonStreamedValuesBegin"), out object? beginRes); Console.WriteLine($"NonStreamedValuesBegin: {beginRes}"); MethodInfo addVal = listType.GetMethods(allInstance).Where(m => m.Name == "AddNonStreamedValue").OrderBy(m => m.GetParameters().Length).First(); object addValErr = Activator.CreateInstance(errorType)!; object?[] addValArgs = new object?[addVal.GetParameters().Length]; addValArgs[0] = value; for (int i = 1; i < addValArgs.Length - 1; i++) { Type pt = addVal.GetParameters()[i].ParameterType; addValArgs[i] = pt == typeof(bool) ? true : pt.IsValueType ? Activator.CreateInstance(pt) : null; } addValArgs[addValArgs.Length - 1] = addValErr; object addValRes = addVal.Invoke(list, addValArgs)!; Console.WriteLine($"AddNonStreamedValue: {addValRes} err={DescribeError(addValArgs[addValArgs.Length - 1])}"); // SendValues reads GetBatchID(); call it BEFORE AddNonStreamedValuesEnd, which // resets the batch (End-before-Send yielded error 160 InvalidBatchId). if (commit) { MethodInfo send = accessType.GetMethods(allInstance).Where(m => m.Name == "SendValues").OrderBy(m => m.GetParameters().Length).First(); object sendErr = Activator.CreateInstance(errorType)!; object?[] sendArgs = new object?[send.GetParameters().Length]; sendArgs[0] = list; for (int i = 1; i < sendArgs.Length - 1; i++) { Type pt = send.GetParameters()[i].ParameterType; sendArgs[i] = pt.IsValueType ? Activator.CreateInstance(pt) : null; } sendArgs[sendArgs.Length - 1] = sendErr; bool sendOk = (bool)send.Invoke(access, sendArgs)!; Console.WriteLine($"SendValues: {sendOk} err={DescribeError(sendArgs[sendArgs.Length - 1])}"); } else { Console.WriteLine("SendValues SKIPPED (pass --commit to push + capture btInput)."); } Invoke0(list, listType.GetMethods(allInstance).First(m => m.Name == "AddNonStreamedValuesEnd"), out object? endRes); Console.WriteLine($"AddNonStreamedValuesEnd: {endRes}"); } finally { try { MethodInfo? close = accessType.GetMethod("CloseConnection", new[] { errorType.MakeByRefType() }); if (close != null) close.Invoke(access, new object?[] { Activator.CreateInstance(errorType) }); } catch { /* best-effort */ } } foreach (Assembly a in AppDomain.CurrentDomain.GetAssemblies().Where(a => a.GetName().Name == "Archestra.Historian.GrpcClient")) { Console.WriteLine($"Loaded GrpcClient from: {a.Location}"); } int capLines = File.Exists(capPath) ? File.ReadAllLines(capPath).Length : 0; Console.WriteLine($"Capture file lines: {capLines} ({capPath})"); return 0; } private static void Invoke0(object target, MethodInfo m, out object? result) { object?[] a = m.GetParameters().Select(pi => pi.ParameterType.IsValueType ? Activator.CreateInstance(pi.ParameterType) : null).ToArray(); result = m.Invoke(target, a); } /// /// Drives the native 2023 R2 client through a read-only gRPC EVENT query so the IL-rewritten /// GrpcRetrievalClient dumps the uncaptured event buffers: StartEventQuery.requestBuffer (the /// empty-filter request shape our SDK's CreateNativeEmptyFilterAttempt is being compared against) /// and GetNextEventQueryResultBuffer.result (the row buffer — proves rows flow when driven right). /// /// CRITICAL: the connection is opened with ConnectionType=Event (NOT Process). CreateEventQuery() /// returns null unless IsEventConnectionRequested() — the native event read runs on ConnectionIndex 1, /// a separate connection from the process/data path. This is the prime suspect for why the SDK's /// gRPC empty-filter query returns zero rows despite the server holding events. /// /// Sequence: OpenConnection(Event, read-only, gRPC) -> CreateEventQuery() -> /// EventQueryArgs{StartDateTime,EndDateTime,EventCount} -> EventQuery.StartQuery(args) -> /// loop EventQuery.MoveNext()/QueryResult -> EventQuery.EndQuery() -> CloseConnection. /// Run with --grpc-rewrite pointing at the instrumented Archestra.Historian.GrpcClient.dll and /// AVEVA_HISTORIAN_RE_CAPTURE set to the output NDJSON. Read-only — non-destructive. /// Usage: capture-event [--server ] [--port 32565] [--cert ] /// [--lookback-hours 720] [--max-events 50] [--integrated] /// private static int CaptureEvent(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"); Type eventQueryType = Req(asm, "ArchestrA.EventQuery"); Type eventArgsType = Req(asm, "ArchestrA.EventQueryArgs"); string server = GetOption(args, "--server") ?? Environment.GetEnvironmentVariable("HISTORIAN_GRPC_HOST") ?? "localhost"; int port = int.TryParse(GetOption(args, "--port"), out int p) ? p : 32565; string certName = GetOption(args, "--cert") ?? server; int lookbackHours = int.TryParse(GetOption(args, "--lookback-hours"), out int lh) ? lh : 720; int maxEvents = int.TryParse(GetOption(args, "--max-events"), out int me) ? me : 50; 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, "Event")); // EVENT connection SetProp(connArgs, "ReadOnly", true); SetProp(connArgs, "IntegratedSecurity", integrated); SetProp(connArgs, "AllowUnTrustedConnection", true); if (!integrated) { SetProp(connArgs, "UserName", user!); SetProp(connArgs, "Password", password ?? string.Empty); } 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)!; object?[] openArgs = { connArgs, error }; Console.WriteLine($"OpenConnection: server={server} port={port} mode=Historian type=Event cert={certName} integrated={integrated} readonly=true"); bool opened; try { opened = (bool)accessType.GetMethod("OpenConnection", new[] { connArgsType, errorType.MakeByRefType() })! .Invoke(access, openArgs)!; } catch (TargetInvocationException tie) { Console.Error.WriteLine($"OpenConnection threw: {tie.InnerException?.GetType().Name}: {tie.InnerException?.Message}"); return 2; } Console.WriteLine($"OpenConnection returned: {opened} err={DescribeError(openArgs[1])}"); if (!opened) { return 2; } try { // Let the event connection (ConnectionIndex 1) come up. MethodInfo getStatus = accessType.GetMethod("GetConnectionStatus", new[] { statusType.MakeByRefType() }) ?? accessType.GetMethods().First(m => m.Name == "GetConnectionStatus" && m.GetParameters().Length == 1); for (int i = 0; i < 10; i++) { object?[] sArgs = { null }; getStatus.Invoke(access, sArgs); if (ReadBoolProp(sArgs[0], "ConnectedToServer") || !ReadBoolProp(sArgs[0], "Pending")) break; System.Threading.Thread.Sleep(500); } // CreateEventQuery() is non-null only when the connection is an event connection. MethodInfo createEventQuery = accessType.GetMethod("CreateEventQuery", Type.EmptyTypes) ?? accessType.GetMethods().First(m => m.Name == "CreateEventQuery" && m.GetParameters().Length == 0); object? eventQuery = createEventQuery.Invoke(access, null); Console.WriteLine($"CreateEventQuery: {(eventQuery == null ? "NULL (event connection not established!)" : "ok")}"); if (eventQuery == null) { return 3; } // Build EventQueryArgs over the populated window. Times in UTC. object eventArgs = Activator.CreateInstance(eventArgsType)!; DateTime endUtc = DateTime.UtcNow; DateTime startUtc = endUtc.AddHours(-lookbackHours); TrySetProp(eventArgs, "StartDateTime", DateTime.SpecifyKind(startUtc, DateTimeKind.Utc)); TrySetProp(eventArgs, "EndDateTime", DateTime.SpecifyKind(endUtc, DateTimeKind.Utc)); TrySetProp(eventArgs, "EventCount", checked((uint)maxEvents)); Console.WriteLine($"EventQueryArgs: start={startUtc:o} end={endUtc:o} eventCount={maxEvents}"); // StartQuery -> triggers GrpcRetrievalClient.StartEventQuery (requestBuffer CAPTURED). MethodInfo startQuery = eventQueryType.GetMethods() .First(m => m.Name == "StartQuery" && m.GetParameters().Length == 2); object?[] startArgs = { eventArgs, Activator.CreateInstance(errorType) }; bool started = (bool)startQuery.Invoke(eventQuery, startArgs)!; Console.WriteLine($"StartQuery: {started} err={DescribeError(startArgs[1])}"); // Poll rows -> triggers GetNextEventQueryResultBuffer (result buffer CAPTURED). MethodInfo moveNext = eventQueryType.GetMethods() .First(m => m.Name == "MoveNext" && m.GetParameters().Length == 1); PropertyInfo? queryResult = eventQueryType.GetProperty("QueryResult"); int rows = 0; while (rows < maxEvents) { object?[] mnArgs = { Activator.CreateInstance(errorType) }; bool more; try { more = (bool)moveNext.Invoke(eventQuery, mnArgs)!; } catch (TargetInvocationException tie) { Console.WriteLine($"MoveNext threw: {tie.InnerException?.GetType().Name}: {tie.InnerException?.Message}"); break; } if (!more) { Console.WriteLine($"MoveNext: end after {rows} row(s) err={DescribeError(mnArgs[0])}"); break; } rows++; if (rows <= 3 && queryResult != null) { // Print only the event TYPE + time (non-identity) to confirm rows flow. object? res = queryResult.GetValue(eventQuery); string typ = res?.GetType().GetProperty("Type")?.GetValue(res)?.ToString() ?? "?"; object? t = res?.GetType().GetProperty("EventTime")?.GetValue(res); Console.WriteLine($" row {rows}: Type={typ} EventTime={t}"); } } Console.WriteLine($"Rows iterated: {rows}"); MethodInfo? endQuery = eventQueryType.GetMethods() .FirstOrDefault(m => m.Name == "EndQuery" && m.GetParameters().Length == 1); if (endQuery != null) { object?[] eqArgs = { Activator.CreateInstance(errorType) }; endQuery.Invoke(eventQuery, eqArgs); } Console.WriteLine(rows > 0 ? "CAPTURE-EVENT: PASS (rows flowed)" : "CAPTURE-EVENT: request captured (zero rows)"); return 0; } finally { try { MethodInfo? close = accessType.GetMethod("CloseConnection", new[] { errorType.MakeByRefType() }); if (close != null) close.Invoke(access, new object?[] { Activator.CreateInstance(errorType) }); } catch { /* best-effort */ } } } /// /// Drives the native 2023 R2 client through an event SEND so the IL-rewritten GrpcClient dumps /// the AddStreamValues.btValues (the event VTQ storage-sample buffer — resolves whether a gRPC /// event send uses the "OS" or "ON" outer signature) AND the Event-connection EnsureTags.btTagInfos /// (the 83-vs-86-byte CM_EVENT registration byte-diff). Opens a WRITE-ENABLED Event connection, /// builds a clearly-marked test HistorianEvent, calls AddStreamedValue, then CloseConnection to /// flush the queued event onto the wire. WRITES a real test event into the server's event history. /// Run with --grpc-rewrite pointing at the instrumented copy and AVEVA_HISTORIAN_RE_CAPTURE set. /// Usage: capture-send-event [--server ] [--port 32565] [--cert ] /// [--event-type SdkCaptureProbe] [--flush-seconds 6] /// private static int CaptureSendEvent(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 eventType = Req(asm, "ArchestrA.HistorianEvent"); Type propTypeEnum = Req(asm, "ArchestrA.HistorianEventPropertyType"); Type certInfoType = Req(asm, "ArchestrA.CertificateInfo"); Type secModeType = Req(asm, "ArchestrA.HistorianSecurityMode"); string server = GetOption(args, "--server") ?? Environment.GetEnvironmentVariable("HISTORIAN_GRPC_HOST") ?? "localhost"; int port = int.TryParse(GetOption(args, "--port"), out int p) ? p : 32565; string certName = GetOption(args, "--cert") ?? server; string evtTypeName = GetOption(args, "--event-type") ?? "SdkCaptureProbe"; int flushSeconds = int.TryParse(GetOption(args, "--flush-seconds"), out int fs) ? fs : 6; string? user = Environment.GetEnvironmentVariable("HISTORIAN_USER"); string? password = Environment.GetEnvironmentVariable("HISTORIAN_PASSWORD"); if (string.IsNullOrEmpty(user)) { Console.Error.WriteLine("Set HISTORIAN_USER/HISTORIAN_PASSWORD."); return 1; } if (string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("AVEVA_HISTORIAN_RE_CAPTURE"))) { string defaultCap = Path.GetFullPath(Path.Combine( "artifacts", "reverse-engineering", "grpc-event-capture", "send-event-capture.ndjson")); Environment.SetEnvironmentVariable("AVEVA_HISTORIAN_RE_CAPTURE", defaultCap); } Console.WriteLine($"Capture sink: {Environment.GetEnvironmentVariable("AVEVA_HISTORIAN_RE_CAPTURE")}"); object connArgs = Activator.CreateInstance(connArgsType)!; SetProp(connArgs, "ServerName", server); SetProp(connArgs, "TcpPort", checked((ushort)port)); SetProp(connArgs, "ConnectionMode", Enum.Parse(connModeType, "Historian")); SetProp(connArgs, "ConnectionType", Enum.Parse(connTypeType, "Event")); // EVENT connection SetProp(connArgs, "ReadOnly", false); // WRITE-enabled SetProp(connArgs, "IntegratedSecurity", false); SetProp(connArgs, "AllowUnTrustedConnection", true); SetProp(connArgs, "UserName", user); SetProp(connArgs, "Password", password ?? string.Empty); 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?[] openArgs = { connArgs, Activator.CreateInstance(errorType) }; Console.WriteLine($"OpenConnection: server={server} port={port} type=Event readonly=false"); bool opened; try { opened = (bool)accessType.GetMethod("OpenConnection", new[] { connArgsType, errorType.MakeByRefType() })! .Invoke(access, openArgs)!; } catch (TargetInvocationException tie) { Console.Error.WriteLine($"OpenConnection threw: {tie.InnerException?.GetType().Name}: {tie.InnerException?.Message}"); return 2; } Console.WriteLine($"OpenConnection returned: {opened} err={DescribeError(openArgs[1])}"); if (!opened) { return 2; } try { // Build a clearly-marked test event. Required: Type (≤32 chars), Id, EventTime. object evt = Activator.CreateInstance(eventType)!; SetProp(evt, "Type", evtTypeName); TrySetProp(evt, "Id", Guid.NewGuid()); TrySetProp(evt, "EventTime", DateTime.SpecifyKind(DateTime.UtcNow, DateTimeKind.Utc)); TrySetProp(evt, "Namespace", "SdkCapture"); TrySetProp(evt, "Source", "SdkCaptureProbe"); // One string property to exercise the property-bag framing. MethodInfo? addProp = eventType.GetMethods().FirstOrDefault(m => m.Name == "AddProperty" && m.GetParameters().Length == 4); if (addProp != null) { try { object strEnum = Enum.Parse(propTypeEnum, "String", true); object?[] apArgs = { "SdkProbeProp", "SdkProbeValue", strEnum, Activator.CreateInstance(errorType) }; addProp.Invoke(evt, apArgs); Console.WriteLine($"AddProperty: err={DescribeError(apArgs[3])}"); } catch (Exception ex) { Console.WriteLine($"AddProperty skipped: {ex.GetType().Name}"); } } MethodInfo addStreamed = accessType.GetMethods().First(m => m.Name == "AddStreamedValue" && m.GetParameters().Length == 2 && m.GetParameters()[0].ParameterType == eventType); object?[] asArgs = { evt, Activator.CreateInstance(errorType) }; bool sent = (bool)addStreamed.Invoke(access, asArgs)!; Console.WriteLine($"AddStreamedValue({evtTypeName}): {sent} err={DescribeError(asArgs[1])}"); // Let the native delivery queue flush the event onto the wire (AddStreamValues). System.Threading.Thread.Sleep(flushSeconds * 1000); Console.WriteLine(sent ? "CAPTURE-SEND-EVENT: AddStreamedValue accepted (buffer captured on flush)" : "CAPTURE-SEND-EVENT: AddStreamedValue rejected"); return sent ? 0 : 3; } finally { try { // CloseConnection flushes any remaining queued values before teardown. MethodInfo? close = accessType.GetMethod("CloseConnection", new[] { errorType.MakeByRefType() }); if (close != null) close.Invoke(access, new object?[] { Activator.CreateInstance(errorType) }); } catch { /* best-effort */ } } } /// /// 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 [--port 32565] [--cert ] [--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") ?? Environment.GetEnvironmentVariable("HISTORIAN_GRPC_HOST") ?? "localhost"; 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; } } }