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", true); 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 && 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 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; } } }