Files
histsdk/tools/AVEVA.Historian.NativeTraceHarness/Program.cs
T
dohertj2 c95824a65d Initial commit: managed .NET 10 AVEVA Historian SDK + reverse-engineering toolkit
Full read-only SDK (src/AVEVA.Historian.Client) implementing the CLAUDE.md required
surface against AVEVA Historian's binary WCF protocol — no native AVEVA runtime
dependency. All operations live-verified against a local Historian:

- ProbeAsync, ReadRawAsync, ReadAggregateAsync, ReadAtTimeAsync, ReadEventsAsync
- BrowseTagNamesAsync, GetTagMetadataAsync (17 native data-type codes mapped)
- GetConnectionStatusAsync, GetStoreForwardStatusAsync, GetSystemParameterAsync
- 108/108 unit + integration tests pass

Includes the reverse-engineering toolkit (tools/AVEVA.Historian.ReverseEngineering)
used to decode the protocol: WCF probes, IL inspection via dnlib, and IL-rewrite
instrumentation (instrument-wcf-{write,read}message etc.) plus the .NET Framework
trace harness (tools/AVEVA.Historian.NativeTraceHarness) for parity testing.

Sanitized handoff evidence under docs/reverse-engineering/. Native AVEVA binaries
(current/, aveva-install-x64/, aveva-install-x86/) are gitignored — fetch separately
from the AVEVA installer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 06:31:48 -04:00

969 lines
38 KiB
C#

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<string, object?> 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<object> rows = [];
if (openSuccess && status.ConnectedToServer && IsEventScenario(scenario))
{
object query = accessType.GetMethod("CreateEventQuery", Type.EmptyTypes)!.Invoke(access, Array.Empty<object>())!;
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<object>())!;
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<object>())!;
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<object> DumpRuntimeMethodPointers(Assembly assembly, string filter)
{
List<object> 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, "<Module>", 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<object> 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<object> 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<string, object?> SnapshotObject(object target)
{
Dictionary<string, object?> 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<object?> read)
{
try
{
return read();
}
catch (Exception ex)
{
return "<unreadable:" + ex.GetType().Name + ">";
}
}
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<string> result = [];
foreach (string? item in strings)
{
if (item is not null)
{
result.Add(item);
}
}
return result;
}
if (value is Array array)
{
List<object?> 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("<truncated:" + array.Length + ">");
}
return result;
}
return "<" + type.FullName + ">";
}
private static List<object?> SummarizeTagList(object? tagList)
{
List<object?> 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("<truncated:" + length + ">");
}
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; }
}
}