using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Threading; using System.Web.Script.Serialization; namespace AVEVA.Historian.NativeTraceHarness; internal static class Program { private static int Main(string[] args) { string repoRoot = FindRepoRoot(); Directory.SetCurrentDirectory(repoRoot); string tagName = GetArg(args, "--tag") ?? "OtOpcUaParityTest_001.Counter"; string serverName = GetArg(args, "--server-name") ?? "localhost"; int tcpPort = int.TryParse(GetArg(args, "--tcp-port"), out int parsedTcpPort) ? parsedTcpPort : 32568; int lookbackMinutes = int.TryParse(GetArg(args, "--lookback-minutes"), out int parsedLookback) ? parsedLookback : 1440; int maxRows = int.TryParse(GetArg(args, "--max-rows"), out int parsedMaxRows) ? parsedMaxRows : 1; int waitSeconds = int.TryParse(GetArg(args, "--connection-wait-seconds"), out int parsedWait) ? parsedWait : 15; int preLoadSleepSeconds = int.TryParse(GetArg(args, "--pre-load-sleep-seconds"), out int parsedPreLoadSleep) ? parsedPreLoadSleep : 0; int preOpenSleepSeconds = int.TryParse(GetArg(args, "--pre-open-sleep-seconds"), out int parsedPreOpenSleep) ? parsedPreOpenSleep : 0; int preStartSleepSeconds = int.TryParse(GetArg(args, "--pre-start-sleep-seconds"), out int parsedPreStartSleep) ? parsedPreStartSleep : 0; string scenario = GetArg(args, "--scenario") ?? "history"; string retrievalModeName = GetArg(args, "--retrieval-mode") ?? "Full"; bool directConnection = HasFlag(args, "--direct-connection"); bool integratedSecurity = !HasFlag(args, "--no-integrated-security"); string? proxyServer = GetArg(args, "--proxy-server"); string? runtimeMethodPointerOutput = GetArg(args, "--runtime-method-pointer-output"); string runtimeMethodPointerFilters = GetArg(args, "--runtime-method-pointer-filters") ?? "StartDataQuery;StartQuery;GetNextRow;StartEventQuery"; ulong resolutionTicks = ulong.TryParse(GetArg(args, "--resolution-ticks"), out ulong parsedResolutionTicks) ? parsedResolutionTicks : 0; DateTime endUtc = TryParseUtc(GetArg(args, "--end-utc")) ?? DateTime.UtcNow; DateTime startUtc = TryParseUtc(GetArg(args, "--start-utc")) ?? endUtc.AddMinutes(-lookbackMinutes); string current = Path.GetFullPath(GetArg(args, "--current-dir") ?? Path.Combine(repoRoot, "current")); string managedDll = Path.GetFullPath(GetArg(args, "--managed-dll-path") ?? Path.Combine(current, "aahClientManaged.dll")); if (!File.Exists(managedDll)) { throw new FileNotFoundException("Missing aahClientManaged.dll.", managedDll); } if (!Directory.Exists(current)) { throw new DirectoryNotFoundException($"Missing dependency folder: {current}"); } AppDomain.CurrentDomain.AssemblyResolve += (_, eventArgs) => { AssemblyName name = new(eventArgs.Name); string candidate = Path.Combine(current, name.Name + ".dll"); return File.Exists(candidate) ? Assembly.LoadFrom(candidate) : null!; }; Directory.CreateDirectory(Path.Combine(repoRoot, "docs", "reverse-engineering")); TryDelete(Path.Combine(repoRoot, "docs", "reverse-engineering", "native-wcf-message-log.svclog")); TraceSource diagnosticProbe = new("System.ServiceModel"); diagnosticProbe.TraceInformation("NativeTraceHarness diagnostics probe"); diagnosticProbe.Flush(); Directory.SetCurrentDirectory(current); if (preLoadSleepSeconds > 0) { Thread.Sleep(TimeSpan.FromSeconds(preLoadSleepSeconds)); } Assembly assembly = Assembly.LoadFrom(managedDll); string? methodPointerFilter = GetArg(args, "--dump-method-pointers"); if (methodPointerFilter is not null) { Console.WriteLine(Serialize(DumpRuntimeMethodPointers(assembly, methodPointerFilter))); return 0; } Type accessType = GetType(assembly, "ArchestrA.HistorianAccess"); Type connectionArgsType = GetType(assembly, "ArchestrA.HistorianConnectionArgs"); Type connectionStatusType = GetType(assembly, "ArchestrA.HistorianConnectionStatus"); Type connectionType = GetType(assembly, "ArchestrA.HistorianConnectionType"); Type historyQueryArgsType = GetType(assembly, "ArchestrA.HistoryQueryArgs"); Type eventQueryArgsType = GetType(assembly, "ArchestrA.EventQueryArgs"); Type tagQueryArgsType = GetType(assembly, "ArchestrA.TagQueryArgs"); Type eventQueryTypeType = GetType(assembly, "ArchestrA.HistorianEventQueryType"); Type eventOrderType = GetType(assembly, "ArchestrA.HistorianEventOrder"); Type errorType = GetType(assembly, "ArchestrA.HistorianAccessError"); Type retrievalModeType = GetType(assembly, "ArchestrA.HistorianRetrievalMode"); object access = Activator.CreateInstance(accessType)!; object connectionArgs = Activator.CreateInstance(connectionArgsType)!; SetProperty(connectionArgs, "ServerName", serverName); SetProperty(connectionArgs, "TcpPort", checked((ushort)tcpPort)); SetProperty(connectionArgs, "ReadOnly", !IsWriteScenario(scenario)); SetProperty(connectionArgs, "IntegratedSecurity", integratedSecurity); SetProperty(connectionArgs, "ConnectionType", Enum.Parse(connectionType, IsEventScenario(scenario) ? "Event" : "Process")); if (directConnection) { SetProperty(connectionArgs, "DirectConnection", true); SetField(connectionArgs, "directConnection", true); } if (!string.IsNullOrWhiteSpace(proxyServer)) { SetProperty(connectionArgs, "ProxyServer", proxyServer!); } Dictionary snapshots = []; if (preOpenSleepSeconds > 0) { Thread.Sleep(TimeSpan.FromSeconds(preOpenSleepSeconds)); } object openError = Activator.CreateInstance(errorType)!; MethodInfo openMethod = accessType.GetMethod("OpenConnection", new[] { connectionArgsType, errorType.MakeByRefType() }) ?? throw new MissingMethodException("HistorianAccess.OpenConnection"); object?[] openArgs = [connectionArgs, openError]; bool openSuccess = (bool)openMethod.Invoke(access, openArgs)!; openError = openArgs[1]!; snapshots["ConnectionArgs"] = SnapshotObject(connectionArgs); snapshots["AccessAfterOpen"] = SnapshotObject(access); ConnectionStatusSnapshot status = WaitForConnection(access, accessType, connectionStatusType, waitSeconds); bool startSuccess = false; object? startError = null; string? startQueryException = null; string? moveTerminalDescription = null; List rows = []; if (openSuccess && status.ConnectedToServer && IsEventScenario(scenario)) { object query = accessType.GetMethod("CreateEventQuery", Type.EmptyTypes)!.Invoke(access, Array.Empty())!; Type queryType = query.GetType(); snapshots["EventQueryAfterCreate"] = SnapshotObject(query); object queryArgs = Activator.CreateInstance(eventQueryArgsType)!; SetProperty(queryArgs, "StartDateTime", startUtc); SetProperty(queryArgs, "EndDateTime", endUtc); SetProperty(queryArgs, "EventCount", checked((uint)Math.Max(maxRows, 1))); SetProperty(queryArgs, "QueryType", Enum.Parse(eventQueryTypeType, "Events")); SetProperty(queryArgs, "EventOrder", Enum.Parse(eventOrderType, "Ascending")); snapshots["EventQueryArgsBeforeStart"] = SnapshotObject(queryArgs); startError = Activator.CreateInstance(errorType)!; MethodInfo startMethod = queryType.GetMethod("StartQuery", new[] { eventQueryArgsType, errorType.MakeByRefType() }) ?? throw new MissingMethodException("EventQuery.StartQuery"); WriteRuntimeMethodPointerSnapshot(assembly, runtimeMethodPointerOutput, runtimeMethodPointerFilters, repoRoot, scenario, "before-event-start"); if (preStartSleepSeconds > 0) { Thread.Sleep(TimeSpan.FromSeconds(preStartSleepSeconds)); } object?[] startArgs = [queryArgs, startError]; try { startSuccess = (bool)startMethod.Invoke(query, startArgs)!; } catch (TargetInvocationException ex) { startQueryException = FormatException(ex.InnerException ?? ex); } startError = startArgs[1]; snapshots["EventQueryAfterStart"] = SnapshotObject(query); snapshots["EventQueryArgsAfterStart"] = SnapshotObject(queryArgs); if (startSuccess) { MethodInfo moveMethod = queryType.GetMethod("MoveNext", new[] { errorType.MakeByRefType() }) ?? throw new MissingMethodException("EventQuery.MoveNext"); for (int i = 0; i < maxRows; i++) { object moveError = Activator.CreateInstance(errorType)!; object?[] moveArgs = [moveError]; bool hasRow = (bool)moveMethod.Invoke(query, moveArgs)!; moveError = moveArgs[0]!; if (!hasRow) { moveTerminalDescription = GetPropertyText(moveError, "ErrorDescription"); break; } object result = GetPropertyValue(query, "QueryResult")!; snapshots["EventQueryAfterFirstMove"] = SnapshotObject(query); snapshots["EventResultAfterFirstMove"] = SnapshotObject(result); rows.Add(new { EventTime = FormatDateProperty(result, "EventTime"), ReceivedTime = FormatDateProperty(result, "ReceivedTime"), EventType = TryGetPropertyValue(result, "EventType"), Type = TryGetPropertyValue(result, "Type"), DisplayText = TryGetPropertyValue(result, "DisplayText"), Area = TryGetPropertyValue(result, "Area"), Source = TryGetPropertyValue(result, "Source"), System = TryGetPropertyValue(result, "System"), Severity = TryGetPropertyValue(result, "Severity"), Priority = TryGetPropertyValue(result, "Priority"), IsAlarm = TryGetPropertyValue(result, "IsAlarm") }); } } MethodInfo? endMethod = queryType.GetMethod("EndQuery", new[] { errorType.MakeByRefType() }); if (endMethod is not null) { object endError = Activator.CreateInstance(errorType)!; object?[] endArgs = [endError]; _ = endMethod.Invoke(query, endArgs); } if (query is IDisposable disposableQuery) { disposableQuery.Dispose(); } } else if (openSuccess && status.ConnectedToServer && IsWriteScenario(scenario)) { // Per docs/plans/write-commands-reverse-engineering.md safety §1, refuse to run // unless the sandbox tag name is whitelisted. string sandboxTag = GetArg(args, "--write-sandbox-tag") ?? "RetestSdkWriteSandbox"; if (!sandboxTag.StartsWith("RetestSdkWrite", StringComparison.Ordinal)) { throw new InvalidOperationException( "Write scenario refuses to run against tags whose name doesn't start with 'RetestSdkWrite'. Pass --write-sandbox-tag RetestSdkWriteSandbox."); } string writeDataTypeName = GetArg(args, "--write-data-type") ?? "Float"; double writeValue = double.TryParse(GetArg(args, "--write-value"), out double parsedValue) ? parsedValue : 42.5; // --write-skip-add-tag lets the value-only second pass run without re-creating // the sandbox. The connection's tag cache is bound at OpenConnection time, so the // server-cache refresh after a fresh AddTag requires a NEW process / connection. bool skipAddTag = HasFlag(args, "--write-skip-add-tag"); bool skipAddValue = HasFlag(args, "--write-skip-add-value"); // Decoded via dnlib — actual enum field types on HistorianTag: // set_TagDataType stfld ArchestrA.HistorianDataType HistorianTag::dataType // set_TagStorageType stfld ArchestrA.HistorianStorageType HistorianTag::tagStorageType Type tagDefType = GetType(assembly, "ArchestrA.HistorianTag"); Type tagDataTypeEnum = GetType(assembly, "ArchestrA.HistorianDataType"); Type tagStorageTypeEnum = GetType(assembly, "ArchestrA.HistorianStorageType"); Type dataValueType = GetType(assembly, "ArchestrA.HistorianDataValue"); Type dataValueDataTypeEnum = GetType(assembly, "ArchestrA.HistorianDataType"); Type connectionIndexEnum = GetType(assembly, "ArchestrA.ConnectionIndex"); // Build HistorianTag for the sandbox. object tag = Activator.CreateInstance(tagDefType)!; SetProperty(tag, "TagName", sandboxTag); SetProperty(tag, "TagDescription", "SDK write-RE sandbox tag"); SetProperty(tag, "EngineeringUnit", "test"); SetProperty(tag, "TagDataType", Enum.Parse(tagDataTypeEnum, writeDataTypeName, ignoreCase: true)); SetProperty(tag, "TagStorageType", Enum.Parse(tagStorageTypeEnum, "Cyclic", ignoreCase: true)); SetProperty(tag, "MinEU", 0.0); SetProperty(tag, "MaxEU", 100.0); SetProperty(tag, "MinRaw", 0.0); SetProperty(tag, "MaxRaw", 100.0); SetProperty(tag, "StorageRate", 1000u); SetProperty(tag, "ApplyScaling", false); uint tagKey = 0; if (!skipAddTag) { object addError = Activator.CreateInstance(errorType)!; MethodInfo addTagMethod = accessType.GetMethod("AddTag", new[] { tagDefType, typeof(uint).MakeByRefType(), errorType.MakeByRefType() }) ?? throw new MissingMethodException("HistorianAccess.AddTag"); WriteRuntimeMethodPointerSnapshot(assembly, runtimeMethodPointerOutput, runtimeMethodPointerFilters, repoRoot, scenario, "before-add-tag"); object?[] addTagArgs = [tag, tagKey, addError]; bool addTagSuccess = (bool)addTagMethod.Invoke(access, addTagArgs)!; tagKey = (uint)addTagArgs[1]!; addError = addTagArgs[2]!; snapshots["TagAfterAddTag"] = SnapshotObject(tag); snapshots["AddTagError"] = SnapshotObject(addError); rows.Add(new { Kind = "AddTag", Success = addTagSuccess, TagKey = tagKey, ErrorDescription = GetPropertyText(addError, "ErrorDescription"), }); } // ALWAYS look up the real wwTagKey from SQL — AddTag returns a synthetic // placeholder key (~10000000) when the tag is freshly created, but the server // session cache only recognizes the real Runtime.dbo.Tag.wwTagKey value // (small int). Using the synthetic key in AddStreamedValue causes server-side // error 168 "Tag not added to server". using (System.Data.SqlClient.SqlConnection sql = new("Server=.;Database=Runtime;Integrated Security=SSPI;")) { sql.Open(); using System.Data.SqlClient.SqlCommand cmd = sql.CreateCommand(); cmd.CommandText = "SELECT wwTagKey FROM Tag WHERE TagName = @t"; cmd.Parameters.AddWithValue("@t", sandboxTag); object? result = cmd.ExecuteScalar(); if (result is int existingKey) { uint realKey = (uint)existingKey; if (realKey != tagKey) { rows.Add(new { Kind = "TagKeyOverride", Synthetic = tagKey, RealFromSql = realKey }); tagKey = realKey; } } } // Server cache may not pick up new tags immediately. Allow a wait between AddTag // and AddStreamedValue so the server side has time to add the new tag to its // in-memory cache. Configurable via --write-resync-wait-seconds (default 8). int resyncWait = int.TryParse(GetArg(args, "--write-resync-wait-seconds"), out int w) ? w : 8; if (resyncWait > 0) { Thread.Sleep(TimeSpan.FromSeconds(resyncWait)); } // Build HistorianDataValue + push it (drives AddS2 on the wire). if (tagKey != 0 && !skipAddValue) { object value = Activator.CreateInstance(dataValueType)!; SetProperty(value, "TagKey", tagKey); SetProperty(value, "DataValueType", Enum.Parse(dataValueDataTypeEnum, "Float", ignoreCase: true)); SetProperty(value, "OpcQuality", (ushort)192); // Good SetProperty(value, "Value", writeValue); SetProperty(value, "StartDateTime", DateTime.UtcNow); SetProperty(value, "EndDateTime", DateTime.UtcNow); SetProperty(value, "ApplyScaling", false); // The public AddStreamedValue overloads (per dnlib) are: // 0x0600618C — public instance, locals(HistorianDataValue, DateTime) // 0x0600618D — public instance, no locals (simplest dispatcher) // The 0x0600618E impl is private and not reachable by reflection. // Pick the public instance overload whose parameters are // (HistorianDataValue, [bool/DateTime], HistorianAccessError&) — the // 0x0600618D dispatcher matches 3 params. MethodInfo addValueMethod = accessType.GetMethods() .Where(m => m.Name == "AddStreamedValue") .OrderBy(m => m.GetParameters().Length) .First(m => { ParameterInfo[] ps = m.GetParameters(); return ps.Length >= 2 && ps[0].ParameterType == dataValueType && ps[ps.Length - 1].ParameterType.IsByRef && ps[ps.Length - 1].ParameterType.GetElementType() == errorType; }); WriteRuntimeMethodPointerSnapshot(assembly, runtimeMethodPointerOutput, runtimeMethodPointerFilters, repoRoot, scenario, "before-add-value"); object addValueError = Activator.CreateInstance(errorType)!; ParameterInfo[] addValueParams = addValueMethod.GetParameters(); object?[] addValueArgs = new object?[addValueParams.Length]; addValueArgs[0] = value; for (int i = 1; i < addValueParams.Length - 1; i++) { Type pt = addValueParams[i].ParameterType; addValueArgs[i] = pt == typeof(bool) ? false : pt == typeof(DateTime) ? DateTime.UtcNow : pt.IsValueType ? Activator.CreateInstance(pt) : null; } addValueArgs[addValueParams.Length - 1] = addValueError; bool addValueSuccess = (bool)addValueMethod.Invoke(access, addValueArgs)!; addValueError = addValueArgs[addValueArgs.Length - 1]!; snapshots["ValueAfterAddStreamedValue"] = SnapshotObject(value); snapshots["AddValueError"] = SnapshotObject(addValueError); rows.Add(new { Kind = "AddStreamedValue", Success = addValueSuccess, Value = writeValue, ErrorDescription = GetPropertyText(addValueError, "ErrorDescription"), }); // Optionally delete the tag for clean rollback. if (HasFlag(args, "--write-delete-after")) { object deleteError = Activator.CreateInstance(errorType)!; MethodInfo deleteMethod = accessType.GetMethods().FirstOrDefault(m => m.Name == "DeleteTags" && m.GetParameters().Length == 2) ?? throw new MissingMethodException("HistorianAccess.DeleteTags"); StringCollection tagsToDelete = []; tagsToDelete.Add(sandboxTag); object?[] deleteArgs = [tagsToDelete, deleteError]; bool deleteSuccess = (bool)deleteMethod.Invoke(access, deleteArgs)!; deleteError = deleteArgs[1]!; rows.Add(new { Kind = "DeleteTags", Success = deleteSuccess, ErrorDescription = GetPropertyText(deleteError, "ErrorDescription"), }); } } } else if (openSuccess && status.ConnectedToServer && IsTagScenario(scenario)) { object query = accessType.GetMethod("CreateTagQuery", Type.EmptyTypes)!.Invoke(access, Array.Empty())!; Type queryType = query.GetType(); snapshots["TagQueryAfterCreate"] = SnapshotObject(query); object queryArgs = Activator.CreateInstance(tagQueryArgsType)!; SetProperty(queryArgs, "TagFilter", tagName); SetProperty(queryArgs, "CacheTagInfo", true); SetProperty(queryArgs, "RetrieveTagExtendedPropertyInfo", false); snapshots["TagQueryArgsBeforeStart"] = SnapshotObject(queryArgs); startError = Activator.CreateInstance(errorType)!; MethodInfo startMethod = queryType.GetMethods().First(method => method.Name == "StartQuery" && method.GetParameters().Length == 3); WriteRuntimeMethodPointerSnapshot(assembly, runtimeMethodPointerOutput, runtimeMethodPointerFilters, repoRoot, scenario, "before-tag-start"); if (preStartSleepSeconds > 0) { Thread.Sleep(TimeSpan.FromSeconds(preStartSleepSeconds)); } object?[] startArgs = [queryArgs, 0u, startError]; try { startSuccess = (bool)startMethod.Invoke(query, startArgs)!; } catch (TargetInvocationException ex) { startQueryException = FormatException(ex.InnerException ?? ex); } uint tagCount = startArgs[1] is uint count ? count : 0; startError = startArgs[2]; snapshots["TagQueryAfterStart"] = SnapshotObject(query); snapshots["TagQueryArgsAfterStart"] = SnapshotObject(queryArgs); if (startSuccess) { uint requestedRows = checked((uint)Math.Max(maxRows, 1)); MethodInfo? getTagNamesMethod = queryType.GetMethods().FirstOrDefault(method => method.Name == "GetTagNames" && method.GetParameters().Length == 4); if (getTagNamesMethod is not null) { object tagNamesError = Activator.CreateInstance(errorType)!; object?[] tagNameArgs = [0u, requestedRows, null, tagNamesError]; bool namesSuccess = (bool)getTagNamesMethod.Invoke(query, tagNameArgs)!; tagNamesError = tagNameArgs[3]!; rows.Add(new { Kind = "TagNames", Success = namesSuccess, ErrorDescription = GetPropertyText(tagNamesError, "ErrorDescription"), Names = ToSerializableValue(tagNameArgs[2]) }); } MethodInfo? getTagInfoMethod = queryType.GetMethods().FirstOrDefault(method => method.Name == "GetTagInfo" && method.GetParameters().Length == 4); if (getTagInfoMethod is not null) { object tagInfoError = Activator.CreateInstance(errorType)!; object?[] tagInfoArgs = [0u, requestedRows, null, tagInfoError]; bool infoSuccess = (bool)getTagInfoMethod.Invoke(query, tagInfoArgs)!; tagInfoError = tagInfoArgs[3]!; rows.Add(new { Kind = "TagInfo", Success = infoSuccess, ErrorDescription = GetPropertyText(tagInfoError, "ErrorDescription"), Tags = SummarizeTagList(tagInfoArgs[2]) }); } } MethodInfo? endMethod = queryType.GetMethod("EndQuery", new[] { errorType.MakeByRefType() }); if (endMethod is not null) { object endError = Activator.CreateInstance(errorType)!; object?[] endArgs = [endError]; _ = endMethod.Invoke(query, endArgs); } if (query is IDisposable disposableQuery) { disposableQuery.Dispose(); } } else if (openSuccess && status.ConnectedToServer) { object query = accessType.GetMethod("CreateHistoryQuery", Type.EmptyTypes)!.Invoke(access, Array.Empty())!; Type queryType = query.GetType(); snapshots["QueryAfterCreate"] = SnapshotObject(query); object queryArgs = Activator.CreateInstance(historyQueryArgsType)!; StringCollection tags = []; tags.Add(tagName); SetProperty(queryArgs, "TagNames", tags); SetProperty(queryArgs, "StartDateTime", startUtc); SetProperty(queryArgs, "EndDateTime", endUtc); SetProperty(queryArgs, "BatchSize", checked((uint)Math.Max(maxRows, 1))); SetProperty(queryArgs, "RetrievalMode", Enum.Parse(retrievalModeType, retrievalModeName, ignoreCase: true)); if (resolutionTicks > 0) { SetProperty(queryArgs, "Resolution", resolutionTicks); } snapshots["QueryArgsBeforeStart"] = SnapshotObject(queryArgs); startError = Activator.CreateInstance(errorType)!; MethodInfo startMethod = queryType.GetMethod("StartQuery", new[] { historyQueryArgsType, errorType.MakeByRefType() }) ?? throw new MissingMethodException("HistoryQuery.StartQuery"); WriteRuntimeMethodPointerSnapshot(assembly, runtimeMethodPointerOutput, runtimeMethodPointerFilters, repoRoot, scenario, "before-history-start"); if (preStartSleepSeconds > 0) { Thread.Sleep(TimeSpan.FromSeconds(preStartSleepSeconds)); } object?[] startArgs = [queryArgs, startError]; try { startSuccess = (bool)startMethod.Invoke(query, startArgs)!; } catch (TargetInvocationException ex) { startQueryException = FormatException(ex.InnerException ?? ex); } startError = startArgs[1]; snapshots["QueryAfterStart"] = SnapshotObject(query); snapshots["QueryArgsAfterStart"] = SnapshotObject(queryArgs); if (startSuccess) { MethodInfo moveMethod = queryType.GetMethod("MoveNext", new[] { errorType.MakeByRefType() }) ?? throw new MissingMethodException("HistoryQuery.MoveNext"); for (int i = 0; i < maxRows; i++) { object moveError = Activator.CreateInstance(errorType)!; object?[] moveArgs = [moveError]; bool hasRow = (bool)moveMethod.Invoke(query, moveArgs)!; moveError = moveArgs[0]!; if (!hasRow) { moveTerminalDescription = GetPropertyText(moveError, "ErrorDescription"); break; } object result = GetPropertyValue(query, "QueryResult")!; snapshots["QueryAfterFirstMove"] = SnapshotObject(query); snapshots["QueryResultAfterFirstMove"] = SnapshotObject(result); rows.Add(new { StartDateTime = ((DateTime)GetPropertyValue(result, "StartDateTime")!).ToString("O"), EndDateTime = ((DateTime)GetPropertyValue(result, "EndDateTime")!).ToString("O"), Quality = GetPropertyValue(result, "Quality"), OpcQuality = GetPropertyValue(result, "OpcQuality"), QualityDetail = GetPropertyValue(result, "QualityDetail"), Value = GetPropertyValue(result, "Value"), PercentGood = GetPropertyValue(result, "PercentGood") }); } } MethodInfo? endMethod = queryType.GetMethod("EndQuery", new[] { errorType.MakeByRefType() }); if (endMethod is not null) { object endError = Activator.CreateInstance(errorType)!; object?[] endArgs = [endError]; _ = endMethod.Invoke(query, endArgs); } if (query is IDisposable disposableQuery) { disposableQuery.Dispose(); } } if (openSuccess) { object closeError = Activator.CreateInstance(errorType)!; MethodInfo? closeMethod = accessType.GetMethod("CloseConnection", new[] { errorType.MakeByRefType() }); if (closeMethod is not null) { object?[] closeArgs = [closeError]; _ = closeMethod.Invoke(access, closeArgs); } } if (access is IDisposable disposableAccess) { disposableAccess.Dispose(); } Trace.Flush(); string tracePath = Path.Combine(repoRoot, "docs", "reverse-engineering", "native-wcf-message-log.svclog"); var output = new { Operation = "NativeTraceHarness.IntegratedRead", Scenario = scenario, ServerName = serverName, DirectConnection = directConnection, ProxyServer = proxyServer, TagName = tagName, LookbackMinutes = lookbackMinutes, RetrievalMode = retrievalModeName, ResolutionTicks = resolutionTicks, StartUtc = startUtc.ToString("O"), EndUtc = endUtc.ToString("O"), OpenSuccess = openSuccess, OpenErrorType = GetPropertyText(openError, "ErrorType"), OpenErrorCode = GetPropertyText(openError, "ErrorCode"), OpenErrorDescription = GetPropertyText(openError, "ErrorDescription"), status.ConnectedToServer, status.Pending, status.ErrorOccurred, StartQuerySuccess = startSuccess, StartQueryErrorType = GetPropertyText(startError, "ErrorType"), StartQueryErrorCode = GetPropertyText(startError, "ErrorCode"), StartQueryErrorDescription = GetPropertyText(startError, "ErrorDescription"), StartQueryException = startQueryException, MoveTerminalDescription = moveTerminalDescription, RowCount = rows.Count, Rows = rows, Snapshots = snapshots, TracePath = tracePath, TraceExists = File.Exists(tracePath), TraceBytes = File.Exists(tracePath) ? new FileInfo(tracePath).Length : 0 }; Console.WriteLine(Serialize(output)); return openSuccess && startSuccess ? 0 : 1; } private static ConnectionStatusSnapshot WaitForConnection(object access, Type accessType, Type statusType, int waitSeconds) { MethodInfo method = accessType.GetMethod("GetConnectionStatus", new[] { statusType.MakeByRefType() }) ?? throw new MissingMethodException("HistorianAccess.GetConnectionStatus"); DateTime deadline = DateTime.UtcNow.AddSeconds(Math.Max(waitSeconds, 1)); ConnectionStatusSnapshot snapshot; do { object status = Activator.CreateInstance(statusType)!; object?[] args = [status]; _ = method.Invoke(access, args); status = args[0]!; snapshot = new ConnectionStatusSnapshot( (bool)GetPropertyValue(status, "ConnectedToServer")!, (bool)GetPropertyValue(status, "Pending")!, (bool)GetPropertyValue(status, "ErrorOccurred")!); if ((snapshot.ConnectedToServer && !snapshot.Pending) || snapshot.ErrorOccurred || (!snapshot.ConnectedToServer && !snapshot.Pending)) { return snapshot; } Thread.Sleep(250); } while (DateTime.UtcNow < deadline); return snapshot; } private static IReadOnlyList DumpRuntimeMethodPointers(Assembly assembly, string filter) { List results = []; BindingFlags flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance; LoadedModuleInfo? moduleInfo = FindLoadedModule(Path.GetFileName(assembly.Location)); foreach (MethodInfo method in assembly.ManifestModule.GetMethods(flags)) { AddRuntimeMethodPointer(results, "", method, filter, moduleInfo); } foreach (Type type in assembly.GetTypes()) { foreach (MethodInfo method in type.GetMethods(flags)) { AddRuntimeMethodPointer(results, type.FullName ?? type.Name, method, filter, moduleInfo); } } return results; } private static void WriteRuntimeMethodPointerSnapshot( Assembly assembly, string? outputPath, string filtersText, string repoRoot, string scenario, string phase) { if (string.IsNullOrWhiteSpace(outputPath)) { return; } string resolvedPath = Path.IsPathRooted(outputPath!) ? outputPath! : Path.Combine(repoRoot, outputPath!); string? directory = Path.GetDirectoryName(resolvedPath); if (!string.IsNullOrEmpty(directory)) { Directory.CreateDirectory(directory); } List methodPointers = []; foreach (string rawFilter in filtersText.Split(new[] { ';', ',' }, StringSplitOptions.RemoveEmptyEntries)) { string filter = rawFilter.Trim(); if (filter.Length == 0) { continue; } methodPointers.Add(new { Filter = filter, Methods = DumpRuntimeMethodPointers(assembly, filter) }); } var snapshot = new { Operation = "NativeTraceHarness.RuntimeMethodPointerSnapshot", ProcessId = Process.GetCurrentProcess().Id, Scenario = scenario, Phase = phase, TimestampUtc = DateTime.UtcNow.ToString("O"), AssemblyPath = assembly.Location, MethodPointers = methodPointers }; File.WriteAllText(resolvedPath, Serialize(snapshot)); } private static void AddRuntimeMethodPointer(List results, string declaringType, MethodInfo method, string filter, LoadedModuleInfo? moduleInfo) { string fullName = declaringType + "." + method.Name; if (fullName.IndexOf(filter, StringComparison.OrdinalIgnoreCase) < 0) { return; } string? pointer = null; string? prepareError = null; try { RuntimeHelpers.PrepareMethod(method.MethodHandle); long pointerValue = method.MethodHandle.GetFunctionPointer().ToInt64(); pointer = "0x" + pointerValue.ToString("X"); bool pointerInModule = moduleInfo is not null && pointerValue >= moduleInfo.BaseAddress && pointerValue < moduleInfo.EndAddress; long? pointerRva = pointerInModule ? pointerValue - moduleInfo!.BaseAddress : null; results.Add(new { DeclaringType = declaringType, method.Name, MetadataToken = "0x" + method.MetadataToken.ToString("X8"), IsStatic = method.IsStatic, IsPublic = method.IsPublic, ModuleBase = moduleInfo is not null ? "0x" + moduleInfo.BaseAddress.ToString("X") : null, ModuleSize = moduleInfo is not null ? "0x" + moduleInfo.Size.ToString("X") : null, FunctionPointer = pointer, FunctionPointerInModule = pointerInModule, FunctionPointerRva = pointerRva.HasValue ? "0x" + pointerRva.Value.ToString("X") : null, PrepareError = prepareError }); return; } catch (Exception ex) { prepareError = FormatException(ex); } results.Add(new { DeclaringType = declaringType, method.Name, MetadataToken = "0x" + method.MetadataToken.ToString("X8"), IsStatic = method.IsStatic, IsPublic = method.IsPublic, ModuleBase = moduleInfo is not null ? "0x" + moduleInfo.BaseAddress.ToString("X") : null, ModuleSize = moduleInfo is not null ? "0x" + moduleInfo.Size.ToString("X") : null, FunctionPointer = pointer, FunctionPointerInModule = false, FunctionPointerRva = (string?)null, PrepareError = prepareError }); } private static LoadedModuleInfo? FindLoadedModule(string moduleName) { foreach (ProcessModule module in Process.GetCurrentProcess().Modules) { if (string.Equals(module.ModuleName, moduleName, StringComparison.OrdinalIgnoreCase)) { return new LoadedModuleInfo(module.BaseAddress.ToInt64(), module.ModuleMemorySize); } } return null; } private sealed class LoadedModuleInfo { public LoadedModuleInfo(long baseAddress, int size) { BaseAddress = baseAddress; Size = size; } public long BaseAddress { get; } public int Size { get; } public long EndAddress => BaseAddress + Size; } private static string Serialize(object value) { return new JavaScriptSerializer { MaxJsonLength = int.MaxValue }.Serialize(value); } private static string FindRepoRoot() { string? directory = AppContext.BaseDirectory; while (!string.IsNullOrEmpty(directory)) { if (File.Exists(Path.Combine(directory, "Histsdk.slnx"))) { return directory!; } directory = Directory.GetParent(directory)?.FullName; } return Directory.GetCurrentDirectory(); } private static string? GetArg(string[] args, string name) { for (int i = 0; i < args.Length - 1; i++) { if (args[i].Equals(name, StringComparison.OrdinalIgnoreCase)) { return args[i + 1]; } } return null; } private static bool HasFlag(string[] args, string name) { foreach (string arg in args) { if (arg.Equals(name, StringComparison.OrdinalIgnoreCase)) { return true; } } return false; } private static DateTime? TryParseUtc(string? value) { if (string.IsNullOrWhiteSpace(value)) { return null; } if (!DateTime.TryParse( value, null, System.Globalization.DateTimeStyles.AssumeUniversal | System.Globalization.DateTimeStyles.AdjustToUniversal, out DateTime parsed)) { throw new ArgumentException("Invalid UTC timestamp: " + value); } return DateTime.SpecifyKind(parsed, DateTimeKind.Utc); } private static Type GetType(Assembly assembly, string name) { return assembly.GetType(name, throwOnError: true)!; } private static void SetProperty(object target, string name, object value) { PropertyInfo? property = target.GetType().GetProperty(name); if (property is not null && property.CanWrite) { MethodInfo? setter = property.GetSetMethod(nonPublic: true); if (setter is not null) { setter.Invoke(target, [value]); } } } private static void SetField(object target, string name, object value) { FieldInfo? field = target.GetType().GetField(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (field is not null) { field.SetValue(target, value); } } private static object? GetPropertyValue(object target, string name) { PropertyInfo? property = target.GetType().GetProperty(name); return property is null || !property.CanRead ? null : property.GetValue(target); } private static object? TryGetPropertyValue(object target, string name) { return TryRead(() => GetPropertyValue(target, name)); } private static string? FormatDateProperty(object target, string name) { object? value = TryGetPropertyValue(target, name); return value is DateTime dateTime ? dateTime.ToString("O") : value?.ToString(); } private static string? GetPropertyText(object? target, string name) { if (target is null) { return null; } return GetPropertyValue(target, name)?.ToString(); } private static string FormatException(Exception ex) { return ex.GetType().Name + ": " + ex.Message; } private static bool IsEventScenario(string scenario) { return scenario.Equals("event", StringComparison.OrdinalIgnoreCase) || scenario.Equals("events", StringComparison.OrdinalIgnoreCase); } private static bool IsTagScenario(string scenario) { return scenario.Equals("tag", StringComparison.OrdinalIgnoreCase) || scenario.Equals("tags", StringComparison.OrdinalIgnoreCase) || scenario.Equals("tag-query", StringComparison.OrdinalIgnoreCase); } private static bool IsWriteScenario(string scenario) { return scenario.Equals("write", StringComparison.OrdinalIgnoreCase) || scenario.Equals("writes", StringComparison.OrdinalIgnoreCase) || scenario.Equals("tag-write", StringComparison.OrdinalIgnoreCase); } private static Dictionary SnapshotObject(object target) { Dictionary snapshot = new(StringComparer.OrdinalIgnoreCase); BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; foreach (FieldInfo field in target.GetType().GetFields(flags)) { if (ShouldSkipMember(field.Name)) { continue; } snapshot["field:" + field.Name] = TryRead(() => ToSerializableValue(field.GetValue(target))); } foreach (PropertyInfo property in target.GetType().GetProperties(flags)) { if (!property.CanRead || property.GetIndexParameters().Length != 0 || ShouldSkipMember(property.Name)) { continue; } snapshot["property:" + property.Name] = TryRead(() => ToSerializableValue(property.GetValue(target))); } return snapshot; } private static bool ShouldSkipMember(string name) { return name.IndexOf("password", StringComparison.OrdinalIgnoreCase) >= 0 || name.IndexOf("user", StringComparison.OrdinalIgnoreCase) >= 0 || name.IndexOf("security", StringComparison.OrdinalIgnoreCase) >= 0; } private static object? TryRead(Func read) { try { return read(); } catch (Exception ex) { return ""; } } private static object? ToSerializableValue(object? value) { if (value is null) { return null; } Type type = value.GetType(); if (type.IsEnum) { return value.ToString(); } if (value is DateTime dateTime) { return dateTime.ToString("O"); } if (value is string or bool or byte or sbyte or short or ushort or int or uint or long or ulong or float or double or decimal) { return value; } if (value is StringCollection strings) { List result = []; foreach (string? item in strings) { if (item is not null) { result.Add(item); } } return result; } if (value is Array array) { List result = []; int count = Math.Min(array.Length, 8); for (int i = 0; i < count; i++) { result.Add(ToSerializableValue(array.GetValue(i))); } if (array.Length > count) { result.Add(""); } return result; } return "<" + type.FullName + ">"; } private static List SummarizeTagList(object? tagList) { List result = []; if (tagList is null) { return result; } object? lengthValue = TryGetPropertyValue(tagList, "Length") ?? TryGetPropertyValue(tagList, "Count"); int length = Convert.ToInt32(lengthValue); int count = Math.Min(length, 8); MethodInfo? itemMethod = tagList.GetType().GetMethods().FirstOrDefault(method => method.Name == "Item" && method.GetParameters().Length == 1); for (int i = 0; i < count; i++) { object? tag = null; try { tag = itemMethod?.Invoke(tagList, [checked((uint)i)]); } catch { tag = null; } if (tag is null) { try { tag = itemMethod?.Invoke(tagList, [checked((uint)(i + 1))]); } catch { tag = null; } } result.Add(tag is null ? null : new { TagName = TryGetPropertyValue(tag, "TagName"), TagDescription = TryGetPropertyValue(tag, "TagDescription"), EngineeringUnit = TryGetPropertyValue(tag, "EngineeringUnit"), TagKey = TryGetPropertyValue(tag, "TagKey"), TagDataType = TryGetPropertyValue(tag, "TagDataType"), TagStorageType = TryGetPropertyValue(tag, "TagStorageType"), SourceTag = TryGetPropertyValue(tag, "SourceTag"), SourceServer = TryGetPropertyValue(tag, "SourceServer"), IsInActive = TryGetPropertyValue(tag, "IsInActive") }); } if (length > count) { result.Add(""); } return result; } private static void TryDelete(string path) { try { if (File.Exists(path)) { File.Delete(path); } } catch { // Best-effort cleanup; WCF may still hold the previous trace file. } } private sealed class ConnectionStatusSnapshot { public ConnectionStatusSnapshot(bool connectedToServer, bool pending, bool errorOccurred) { ConnectedToServer = connectedToServer; Pending = pending; ErrorOccurred = errorOccurred; } public bool ConnectedToServer { get; } public bool Pending { get; } public bool ErrorOccurred { get; } } }