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>
This commit is contained in:
dohertj2
2026-05-04 06:31:48 -04:00
commit c95824a65d
230 changed files with 38666 additions and 0 deletions
@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net481</TargetFramework>
<LangVersion>latest</LangVersion>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<Reference Include="System.ServiceModel" />
<Reference Include="System.Web.Extensions" />
</ItemGroup>
<ItemGroup>
<None Include="App.config" />
</ItemGroup>
</Project>
@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.diagnostics>
<sources>
<source name="System.ServiceModel" switchValue="Verbose">
<listeners>
<add name="xml" />
</listeners>
</source>
<source name="System.ServiceModel.MessageLogging" switchValue="Verbose">
<listeners>
<add name="xml" />
</listeners>
</source>
</sources>
<sharedListeners>
<add name="xml"
type="System.Diagnostics.XmlWriterTraceListener"
initializeData="docs\reverse-engineering\native-wcf-message-log.svclog" />
</sharedListeners>
<trace autoflush="true" />
</system.diagnostics>
<system.serviceModel>
<diagnostics>
<messageLogging
logEntireMessage="true"
logMalformedMessages="true"
logMessagesAtServiceLevel="true"
logMessagesAtTransportLevel="true"
maxMessagesToLog="3000"
maxSizeOfMessageToLog="67108864" />
</diagnostics>
</system.serviceModel>
</configuration>
@@ -0,0 +1,968 @@
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; }
}
}
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net481</TargetFramework>
<LangVersion>latest</LangVersion>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<Reference Include="System.ServiceModel" />
</ItemGroup>
</Project>
@@ -0,0 +1,880 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Net.Security;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.Text;
using System.Xml;
internal static class Program
{
private const string Namespace = "aa";
private const string HistoryService = "Hist";
private static int Main(string[] args)
{
string targetName = GetArg(args, "--target") ?? @"NT SERVICE\aahClientAccessPoint";
string endpoint = GetArg(args, "--endpoint") ?? "net.pipe://localhost/Hist";
string retrievalEndpoint = GetArg(args, "--retr-endpoint") ?? "net.pipe://localhost/Retr";
string? open2ReplayPath = GetArg(args, "--open2-replay");
string? dataQueryReplayPath = GetArg(args, "--data-query-replay");
int maxBufferSize = int.TryParse(GetArg(args, "--max-buffer-size"), out int parsedMaxBufferSize)
? parsedMaxBufferSize
: 66303;
try
{
IHistoryServiceContract2 channel = CreatePipeChannel(endpoint, maxBufferSize);
uint getVersionReturn = channel.GetInterfaceVersion(out uint interfaceVersion);
using SspiClient sspi = new("Negotiate", targetName);
byte[] incoming = Array.Empty<byte>();
List<string> roundJson = new();
bool? finalServerSuccess = null;
string? finalStatus = null;
int? finalServerOutputLength = null;
NativeError? finalNativeError = null;
Guid contextKey = Guid.NewGuid();
string handle = contextKey.ToString("D").ToUpperInvariant();
for (int round = 0; round < 8; round++)
{
SspiStepResult clientStep = sspi.Next(incoming);
ApplyNativeNtlmNegotiateVersionFlag(clientStep.Token);
byte[] wrapped = WrapValidateClientCredentialToken(round == 0, clientStep.Token);
bool serverSuccess = channel.ValidateClientCredential(handle, wrapped, out byte[] serverOutput, out byte[] errorBuffer);
serverOutput ??= Array.Empty<byte>();
errorBuffer ??= Array.Empty<byte>();
NativeError? nativeError = TryReadNativeError(errorBuffer);
bool serverContinue = serverOutput.Length > 0 && serverOutput[0] != 0;
byte[] serverToken = serverContinue && serverOutput.Length > 1
? serverOutput.Skip(1).ToArray()
: Array.Empty<byte>();
roundJson.Add("{"
+ JsonProp("Round", round) + ","
+ JsonProp("ClientStatus", clientStep.Status) + ","
+ JsonProp("OutgoingLength", clientStep.Token.Length) + ","
+ JsonProp("OutgoingSha256", HashBytesOrNull(clientStep.Token)) + ","
+ JsonProp("OutgoingPrefixHex", ToPrefixHex(clientStep.Token, 32)) + ","
+ JsonProp("WrappedOutgoingLength", wrapped.Length) + ","
+ JsonProp("WrappedOutgoingSha256", HashBytesOrNull(wrapped)) + ","
+ JsonProp("WrappedOutgoingPrefixHex", ToPrefixHex(wrapped, 32)) + ","
+ JsonProp("ServerSuccess", serverSuccess) + ","
+ JsonProp("ServerOutputLength", serverOutput.Length) + ","
+ JsonProp("ServerOutputSha256", HashBytesOrNull(serverOutput)) + ","
+ JsonProp("ServerOutputPrefixHex", ToPrefixHex(serverOutput, 32)) + ","
+ JsonProp("ServerContinue", serverContinue) + ","
+ JsonProp("ServerTokenLength", serverToken.Length) + ","
+ JsonProp("ErrorLength", errorBuffer.Length) + ","
+ "\"NativeError\":" + FormatNativeError(nativeError)
+ "}");
finalServerSuccess = serverSuccess;
finalStatus = clientStep.Status;
finalServerOutputLength = serverOutput.Length;
finalNativeError = nativeError;
if (!serverSuccess || clientStep.Done || !serverContinue)
{
break;
}
incoming = serverToken;
}
string? chainJson = null;
if (finalServerSuccess == true && finalNativeError is null && open2ReplayPath != null)
{
chainJson = RunOpen2AndQueryChain(channel, retrievalEndpoint, contextKey, open2ReplayPath, dataQueryReplayPath, maxBufferSize);
}
Console.WriteLine("{"
+ JsonProp("Runtime", ".NET Framework") + ","
+ JsonProp("Endpoint", endpoint) + ","
+ JsonProp("Operation", "ValCl") + ","
+ JsonProp("Transport", "NamedPipeNone") + ","
+ JsonProp("GetVersionReturnCode", getVersionReturn) + ","
+ JsonProp("InterfaceVersion", interfaceVersion) + ","
+ JsonProp("TargetName", targetName) + ","
+ JsonProp("HandleSha256", Sha256Utf8(handle)) + ","
+ JsonProp("HandleLength", handle.Length) + ","
+ JsonProp("FinalStatus", finalStatus) + ","
+ JsonProp("FinalServerSuccess", finalServerSuccess) + ","
+ JsonProp("FinalServerOutputLength", finalServerOutputLength) + ","
+ "\"FinalNativeError\":" + FormatNativeError(finalNativeError) + ","
+ "\"Rounds\":[" + string.Join(",", roundJson) + "]"
+ (chainJson is null ? string.Empty : "," + chainJson)
+ "}");
return 0;
}
catch (Exception ex)
{
Console.Error.WriteLine(Environment.GetEnvironmentVariable("AVEVA_HISTORIAN_RE_STACK") == "1" ? ex.ToString() : ex.Message);
return 1;
}
}
private static IHistoryServiceContract2 CreatePipeChannel(string endpoint, int maxBufferSize)
{
ChannelFactory<IHistoryServiceContract2> factory = new(BuildMdasPipeBinding(maxBufferSize), new EndpointAddress(endpoint));
return factory.CreateChannel();
}
private static IRetrievalServiceContract2 CreateRetrievalPipeChannel(string endpoint, int maxBufferSize)
{
ChannelFactory<IRetrievalServiceContract2> factory = new(BuildMdasPipeBinding(maxBufferSize), new EndpointAddress(endpoint));
return factory.CreateChannel();
}
private static CustomBinding BuildMdasPipeBinding(int maxBufferSize)
{
NetNamedPipeBinding nativeShape = new()
{
MaxBufferSize = maxBufferSize,
MaxReceivedMessageSize = maxBufferSize
};
nativeShape.Security.Mode = NetNamedPipeSecurityMode.None;
nativeShape.ReaderQuotas.MaxArrayLength = maxBufferSize;
BindingElementCollection elements = nativeShape.CreateBindingElements();
for (int i = 0; i < elements.Count; i++)
{
if (elements[i] is MessageEncodingBindingElement encoding)
{
elements[i] = new MdasMessageEncodingBindingElement(encoding);
break;
}
}
return new CustomBinding(elements)
{
OpenTimeout = TimeSpan.FromSeconds(10),
CloseTimeout = TimeSpan.FromSeconds(10),
SendTimeout = TimeSpan.FromSeconds(10),
ReceiveTimeout = TimeSpan.FromSeconds(10)
};
}
private static string RunOpen2AndQueryChain(
IHistoryServiceContract2 historyChannel,
string retrievalEndpoint,
Guid contextKey,
string open2ReplayPath,
string? dataQueryReplayPath,
int maxBufferSize)
{
StringBuilder json = new();
json.Append("\"Chain\":{");
byte[] open2RequestRaw = File.ReadAllBytes(open2ReplayPath);
if (open2RequestRaw.Length < 17 || open2RequestRaw[0] != 6)
{
json.Append(JsonProp("Open2ReplaySource", open2ReplayPath));
json.Append("," + JsonProp("Open2ReplayLength", open2RequestRaw.Length));
json.Append("," + JsonProp("Open2Skipped", "replay must be a v6 OpenConnection3 buffer of at least 17 bytes"));
json.Append("}");
return json.ToString();
}
byte[] open2Request = (byte[])open2RequestRaw.Clone();
byte[] keyBytes = contextKey.ToByteArray();
Buffer.BlockCopy(keyBytes, 0, open2Request, 1, 16);
json.Append(JsonProp("Open2RequestLength", open2Request.Length));
json.Append("," + JsonProp("Open2RequestOriginalSha256", Sha256(open2RequestRaw)));
json.Append("," + JsonProp("Open2RequestSplicedSha256", Sha256(open2Request)));
byte[] open2In = open2Request;
bool open2Success;
byte[] open2Out;
byte[] open2Err;
try
{
open2Success = historyChannel.OpenConnection2(ref open2In, out open2Out, out open2Err);
}
catch (Exception ex)
{
json.Append("," + JsonProp("Open2Exception", ex.GetType().Name + ": " + ex.Message));
json.Append("}");
return json.ToString();
}
open2Out ??= Array.Empty<byte>();
open2Err ??= Array.Empty<byte>();
json.Append("," + JsonProp("Open2Success", open2Success));
json.Append("," + JsonProp("Open2ResponseLength", open2Out.Length));
json.Append("," + JsonProp("Open2ResponseSha256", HashBytesOrNull(open2Out)));
json.Append("," + JsonProp("Open2ResponsePrefixHex", ToPrefixHex(open2Out, 8)));
json.Append("," + JsonProp("Open2ErrorLength", open2Err.Length));
json.Append("," + "\"Open2NativeError\":" + FormatNativeError(TryReadNativeError(open2Err)));
uint clientHandle = 0;
byte responseVersion = 0;
if (open2Success && open2Out.Length >= 5)
{
responseVersion = open2Out[0];
clientHandle = ReadUInt32LittleEndian(open2Out, 1);
json.Append("," + JsonProp("Open2ResponseVersion", responseVersion));
json.Append("," + JsonProp("Open2ClientHandlePresent", true));
}
if (!open2Success || clientHandle == 0)
{
json.Append("}");
return json.ToString();
}
IRetrievalServiceContract2 retrChannel = CreateRetrievalPipeChannel(retrievalEndpoint, maxBufferSize);
try
{
uint retrGetVersionReturn = retrChannel.GetInterfaceVersion(out uint retrInterfaceVersion);
json.Append("," + JsonProp("RetrGetVersionReturnCode", retrGetVersionReturn));
json.Append("," + JsonProp("RetrInterfaceVersion", retrInterfaceVersion));
uint isOriginalReturn = retrChannel.IsOriginalAllowed(clientHandle, out bool isAllowed);
json.Append("," + JsonProp("IsOriginalAllowedReturnCode", isOriginalReturn));
json.Append("," + JsonProp("IsOriginalAllowedIsAllowed", isAllowed));
if (dataQueryReplayPath is null)
{
json.Append("," + JsonProp("StartQuery2Skipped", "no --data-query-replay path"));
json.Append("}");
return json.ToString();
}
byte[] dataQueryRequest = File.ReadAllBytes(dataQueryReplayPath);
json.Append("," + JsonProp("StartQuery2RequestLength", dataQueryRequest.Length));
json.Append("," + JsonProp("StartQuery2RequestSha256", Sha256(dataQueryRequest)));
uint queryHandle = 0;
bool startQuerySuccess;
uint responseSize;
byte[] responseBuffer;
uint errorSize;
byte[] errorBuffer;
try
{
startQuerySuccess = retrChannel.StartQuery2(
clientHandle,
queryRequestType: 1,
requestSize: (uint)dataQueryRequest.Length,
requestBuffer: dataQueryRequest,
out responseSize,
out responseBuffer,
ref queryHandle,
out errorSize,
out errorBuffer);
}
catch (Exception ex)
{
json.Append("," + JsonProp("StartQuery2Exception", ex.GetType().Name + ": " + ex.Message));
json.Append("}");
return json.ToString();
}
responseBuffer ??= Array.Empty<byte>();
errorBuffer ??= Array.Empty<byte>();
json.Append("," + JsonProp("StartQuery2Success", startQuerySuccess));
json.Append("," + JsonProp("StartQuery2ResponseSize", (int)responseSize));
json.Append("," + JsonProp("StartQuery2ResponseSha256", HashBytesOrNull(responseBuffer)));
json.Append("," + JsonProp("StartQuery2ResponsePrefixHex", ToPrefixHex(responseBuffer, 8)));
json.Append("," + JsonProp("StartQuery2ErrorSize", (int)errorSize));
json.Append("," + JsonProp("StartQuery2QueryHandlePresent", queryHandle != 0));
json.Append("," + "\"StartQuery2NativeError\":" + FormatNativeError(TryReadNativeError(errorBuffer)));
}
finally
{
try { ((ICommunicationObject)retrChannel).Close(); }
catch { try { ((ICommunicationObject)retrChannel).Abort(); } catch { } }
}
json.Append("}");
return json.ToString();
}
private static byte[] WrapValidateClientCredentialToken(bool isFirstRound, byte[] token)
{
byte[] buffer = new byte[checked(1 + sizeof(uint) + token.Length)];
buffer[0] = isFirstRound ? (byte)1 : (byte)0;
WriteUInt32LittleEndian(buffer, 1, checked((uint)token.Length));
Buffer.BlockCopy(token, 0, buffer, 1 + sizeof(uint), token.Length);
return buffer;
}
private static void ApplyNativeNtlmNegotiateVersionFlag(byte[] token)
{
byte[] ntlmSignature = Encoding.ASCII.GetBytes("NTLMSSP\0");
if (token.Length >= 16 && token.Take(ntlmSignature.Length).SequenceEqual(ntlmSignature)
&& ReadUInt32LittleEndian(token, 8) == 1)
{
uint flags = ReadUInt32LittleEndian(token, 12);
flags |= 0x0010_0000;
WriteUInt32LittleEndian(token, 12, flags);
}
}
private static NativeError? TryReadNativeError(byte[] bytes)
{
if (bytes.Length < 5)
{
return null;
}
uint type = bytes[0];
uint code = ReadUInt32LittleEndian(bytes, 1);
return new NativeError(type, code, code == 1 ? "Failure" : null);
}
private static uint ReadUInt32LittleEndian(byte[] bytes, int offset)
{
return (uint)(bytes[offset]
| bytes[offset + 1] << 8
| bytes[offset + 2] << 16
| bytes[offset + 3] << 24);
}
private static void WriteUInt32LittleEndian(byte[] bytes, int offset, uint value)
{
bytes[offset] = (byte)value;
bytes[offset + 1] = (byte)(value >> 8);
bytes[offset + 2] = (byte)(value >> 16);
bytes[offset + 3] = (byte)(value >> 24);
}
private static string? GetArg(string[] args, string name)
{
for (int i = 0; i < args.Length - 1; i++)
{
if (string.Equals(args[i], name, StringComparison.OrdinalIgnoreCase))
{
return args[i + 1];
}
}
return null;
}
private static string JsonProp(string name, string? value)
{
return "\"" + JsonEscape(name) + "\":" + (value is null ? "null" : "\"" + JsonEscape(value) + "\"");
}
private static string JsonProp(string name, bool value) => "\"" + JsonEscape(name) + "\":" + (value ? "true" : "false");
private static string JsonProp(string name, bool? value) => "\"" + JsonEscape(name) + "\":" + (value.HasValue ? value.Value ? "true" : "false" : "null");
private static string JsonProp(string name, int value) => "\"" + JsonEscape(name) + "\":" + value;
private static string JsonProp(string name, int? value) => "\"" + JsonEscape(name) + "\":" + (value.HasValue ? value.Value.ToString() : "null");
private static string JsonProp(string name, uint value) => "\"" + JsonEscape(name) + "\":" + value;
private static string FormatNativeError(NativeError? error)
{
return error is null
? "null"
: "{" + JsonProp("Type", error.Value.Type) + "," + JsonProp("Code", error.Value.Code) + "," + JsonProp("Name", error.Value.Name) + "}";
}
private static string? HashBytesOrNull(byte[] bytes)
{
return bytes.Length == 0 ? null : Sha256(bytes);
}
private static string Sha256Utf8(string value) => Sha256(Encoding.UTF8.GetBytes(value));
private static string Sha256(byte[] bytes)
{
using SHA256 sha256 = SHA256.Create();
return BitConverter.ToString(sha256.ComputeHash(bytes)).Replace("-", string.Empty).ToLowerInvariant();
}
private static string ToPrefixHex(byte[] bytes, int maxBytes)
{
int count = Math.Min(bytes.Length, maxBytes);
StringBuilder builder = new(count * 2);
for (int i = 0; i < count; i++)
{
builder.Append(bytes[i].ToString("x2"));
}
return builder.ToString();
}
private static string JsonEscape(string value)
{
return value
.Replace("\\", "\\\\")
.Replace("\"", "\\\"")
.Replace("\r", "\\r")
.Replace("\n", "\\n");
}
private readonly struct NativeError
{
public NativeError(uint type, uint code, string? name)
{
Type = type;
Code = code;
Name = name;
}
public uint Type { get; }
public uint Code { get; }
public string? Name { get; }
}
private readonly struct SspiStepResult
{
public SspiStepResult(byte[] token, string status, bool done)
{
Token = token;
Status = status;
Done = done;
}
public byte[] Token { get; }
public string Status { get; }
public bool Done { get; }
}
private sealed class SspiClient : IDisposable
{
private const int SECPKG_CRED_OUTBOUND = 2;
private const int SECBUFFER_TOKEN = 2;
private const int ISC_REQ_REPLAY_DETECT = 0x4;
private const int ISC_REQ_SEQUENCE_DETECT = 0x8;
private const int ISC_REQ_CONFIDENTIALITY = 0x10;
private const int ISC_REQ_CONNECTION = 0x800;
private const int ISC_REQ_IDENTIFY = 0x20000;
private const int ISC_REQ_ALLOCATE_MEMORY = 0x100;
private const int SEC_E_OK = 0;
private const int SEC_I_CONTINUE_NEEDED = 0x00090312;
private readonly string targetName;
private SecHandle credential;
private SecHandle context;
private bool haveContext;
private int roundIndex;
public SspiClient(string package, string targetName)
{
this.targetName = targetName;
credential = default;
long expiry;
int status = AcquireCredentialsHandle(null, package, SECPKG_CRED_OUTBOUND, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, ref credential, out expiry);
ThrowIfFailed(status, "AcquireCredentialsHandle");
}
public SspiStepResult Next(byte[] incoming)
{
SecBufferDesc outBufferDesc = CreateOutputBufferDesc();
SecBufferDesc? inBufferDesc = incoming.Length == 0 ? null : CreateInputBufferDesc(incoming);
try
{
uint contextAttributes;
long expiry;
SecHandle newContext = default;
int status;
int nativeBase = ISC_REQ_REPLAY_DETECT | ISC_REQ_SEQUENCE_DETECT | ISC_REQ_CONFIDENTIALITY | ISC_REQ_CONNECTION;
int contextRequirements = ISC_REQ_ALLOCATE_MEMORY | nativeBase | (roundIndex == 0 ? ISC_REQ_IDENTIFY : 0);
if (inBufferDesc.HasValue)
{
SecBufferDesc input = inBufferDesc.Value;
status = InitializeSecurityContext(
ref credential,
ref context,
targetName,
contextRequirements,
0,
0,
ref input,
0,
ref newContext,
ref outBufferDesc,
out contextAttributes,
out expiry);
}
else
{
status = InitializeSecurityContext(
ref credential,
IntPtr.Zero,
targetName,
contextRequirements,
0,
0,
IntPtr.Zero,
0,
ref newContext,
ref outBufferDesc,
out contextAttributes,
out expiry);
}
if (!haveContext)
{
context = newContext;
haveContext = true;
}
ThrowIfFailed(status, "InitializeSecurityContext", allowContinue: true);
byte[] token = ReadTokenAndFree(outBufferDesc);
roundIndex++;
return new SspiStepResult(token, status == SEC_E_OK ? "Completed" : "ContinueNeeded", status == SEC_E_OK);
}
finally
{
if (inBufferDesc.HasValue)
{
FreeBufferDesc(inBufferDesc.Value, freeToken: true);
}
}
}
public void Dispose()
{
if (haveContext)
{
DeleteSecurityContext(ref context);
}
FreeCredentialsHandle(ref credential);
}
private static byte[] ReadTokenAndFree(SecBufferDesc desc)
{
try
{
SecBuffer buffer = Marshal.PtrToStructure<SecBuffer>(desc.pBuffers);
if (buffer.cbBuffer == 0 || buffer.pvBuffer == IntPtr.Zero)
{
return Array.Empty<byte>();
}
byte[] bytes = new byte[buffer.cbBuffer];
Marshal.Copy(buffer.pvBuffer, bytes, 0, bytes.Length);
FreeContextBuffer(buffer.pvBuffer);
return bytes;
}
finally
{
FreeBufferDesc(desc, freeToken: false);
}
}
private static SecBufferDesc CreateOutputBufferDesc()
{
SecBuffer buffer = new() { BufferType = SECBUFFER_TOKEN, cbBuffer = 0, pvBuffer = IntPtr.Zero };
IntPtr bufferPtr = Marshal.AllocHGlobal(Marshal.SizeOf<SecBuffer>());
Marshal.StructureToPtr(buffer, bufferPtr, false);
return new SecBufferDesc { ulVersion = 0, cBuffers = 1, pBuffers = bufferPtr };
}
private static SecBufferDesc CreateInputBufferDesc(byte[] token)
{
IntPtr tokenPtr = Marshal.AllocHGlobal(token.Length);
Marshal.Copy(token, 0, tokenPtr, token.Length);
SecBuffer buffer = new() { BufferType = SECBUFFER_TOKEN, cbBuffer = token.Length, pvBuffer = tokenPtr };
IntPtr bufferPtr = Marshal.AllocHGlobal(Marshal.SizeOf<SecBuffer>());
Marshal.StructureToPtr(buffer, bufferPtr, false);
return new SecBufferDesc { ulVersion = 0, cBuffers = 1, pBuffers = bufferPtr };
}
private static void FreeBufferDesc(SecBufferDesc desc, bool freeToken)
{
if (desc.pBuffers == IntPtr.Zero)
{
return;
}
if (freeToken)
{
SecBuffer buffer = Marshal.PtrToStructure<SecBuffer>(desc.pBuffers);
if (buffer.pvBuffer != IntPtr.Zero)
{
Marshal.FreeHGlobal(buffer.pvBuffer);
}
}
Marshal.FreeHGlobal(desc.pBuffers);
}
private static void ThrowIfFailed(int status, string operation, bool allowContinue = false)
{
if (status == SEC_E_OK || allowContinue && status == SEC_I_CONTINUE_NEEDED)
{
return;
}
throw new Win32Exception(status, operation + " failed with 0x" + status.ToString("X8"));
}
[DllImport("secur32.dll", CharSet = CharSet.Unicode, SetLastError = false)]
private static extern int AcquireCredentialsHandle(
string? pszPrincipal,
string pszPackage,
int fCredentialUse,
IntPtr pvLogonId,
IntPtr pAuthData,
IntPtr pGetKeyFn,
IntPtr pvGetKeyArgument,
ref SecHandle phCredential,
out long ptsExpiry);
[DllImport("secur32.dll", CharSet = CharSet.Unicode, SetLastError = false)]
private static extern int InitializeSecurityContext(
ref SecHandle phCredential,
IntPtr phContext,
string pszTargetName,
int fContextReq,
int Reserved1,
int TargetDataRep,
IntPtr pInput,
int Reserved2,
ref SecHandle phNewContext,
ref SecBufferDesc pOutput,
out uint pfContextAttr,
out long ptsExpiry);
[DllImport("secur32.dll", CharSet = CharSet.Unicode, SetLastError = false)]
private static extern int InitializeSecurityContext(
ref SecHandle phCredential,
ref SecHandle phContext,
string pszTargetName,
int fContextReq,
int Reserved1,
int TargetDataRep,
ref SecBufferDesc pInput,
int Reserved2,
ref SecHandle phNewContext,
ref SecBufferDesc pOutput,
out uint pfContextAttr,
out long ptsExpiry);
[DllImport("secur32.dll", CharSet = CharSet.Unicode, SetLastError = false)]
private static extern int InitializeSecurityContext(
ref SecHandle phCredential,
ref SecHandle phContext,
string pszTargetName,
int fContextReq,
int Reserved1,
int TargetDataRep,
IntPtr pInput,
int Reserved2,
ref SecHandle phNewContext,
ref SecBufferDesc pOutput,
out uint pfContextAttr,
out long ptsExpiry);
[DllImport("secur32.dll", SetLastError = false)]
private static extern int DeleteSecurityContext(ref SecHandle phContext);
[DllImport("secur32.dll", SetLastError = false)]
private static extern int FreeCredentialsHandle(ref SecHandle phCredential);
[DllImport("secur32.dll", SetLastError = false)]
private static extern int FreeContextBuffer(IntPtr pvContextBuffer);
[StructLayout(LayoutKind.Sequential)]
private struct SecHandle
{
public IntPtr dwLower;
public IntPtr dwUpper;
}
[StructLayout(LayoutKind.Sequential)]
private struct SecBuffer
{
public int cbBuffer;
public int BufferType;
public IntPtr pvBuffer;
}
[StructLayout(LayoutKind.Sequential)]
private struct SecBufferDesc
{
public int ulVersion;
public int cBuffers;
public IntPtr pBuffers;
}
}
}
[ServiceContract(Name = "Hist", Namespace = "aa")]
internal interface IHistoryServiceContract
{
[OperationContract(Name = "GetV")]
uint GetInterfaceVersion(out uint version);
}
[ServiceContract(Name = "Hist", Namespace = "aa")]
internal interface IHistoryServiceContract2 : IHistoryServiceContract
{
[OperationContract(Name = "ValCl")]
[return: MarshalAs(UnmanagedType.U1)]
bool ValidateClientCredential(
string handle,
[MessageParameter(Name = "inBuff")] byte[] inputBuffer,
[MessageParameter(Name = "outBuff")] out byte[] outputBuffer,
out byte[] errorBuffer);
[OperationContract(Name = "Open2")]
[return: MarshalAs(UnmanagedType.U1)]
bool OpenConnection2(
[MessageParameter(Name = "inParameters")] ref byte[] inParameters,
[MessageParameter(Name = "outParameters")] out byte[] outParameters,
[MessageParameter(Name = "err")] out byte[] err);
}
[ServiceContract(Name = "Retr", Namespace = "aa")]
internal interface IRetrievalServiceContract
{
[OperationContract(Name = "GetV")]
uint GetInterfaceVersion(out uint version);
[OperationContract]
uint IsOriginalAllowed(uint clientHandle, out bool isAllowed);
}
[ServiceContract(Name = "Retr", Namespace = "aa")]
internal interface IRetrievalServiceContract2 : IRetrievalServiceContract
{
[OperationContract]
[return: MarshalAs(UnmanagedType.U1)]
bool StartQuery2(
uint clientHandle,
ushort queryRequestType,
uint requestSize,
[MessageParameter(Name = "pRequestBuff")] byte[] requestBuffer,
out uint responseSize,
[MessageParameter(Name = "pResponseBuff")] out byte[] responseBuffer,
ref uint queryHandle,
[MessageParameter(Name = "errSize")] out uint errorSize,
[MessageParameter(Name = "err")] out byte[] errorBuffer);
[OperationContract]
[return: MarshalAs(UnmanagedType.U1)]
bool GetNextQueryResultBuffer2(
uint clientHandle,
uint queryHandle,
out uint resultSize,
[MessageParameter(Name = "pResultBuff")] out byte[] resultBuffer,
[MessageParameter(Name = "errSize")] out uint errorSize,
[MessageParameter(Name = "err")] out byte[] errorBuffer);
}
internal sealed class MdasMessageEncodingBindingElement : MessageEncodingBindingElement
{
private readonly MessageEncodingBindingElement inner;
public MdasMessageEncodingBindingElement(MessageEncodingBindingElement inner)
{
this.inner = inner;
}
private MdasMessageEncodingBindingElement(MdasMessageEncodingBindingElement source)
{
inner = (MessageEncodingBindingElement)source.inner.Clone();
}
public override MessageVersion MessageVersion
{
get => inner.MessageVersion;
set => inner.MessageVersion = value;
}
public override MessageEncoderFactory CreateMessageEncoderFactory()
{
return new MdasMessageEncoderFactory(inner.CreateMessageEncoderFactory());
}
public override BindingElement Clone()
{
return new MdasMessageEncodingBindingElement(this);
}
public override IChannelFactory<TChannel> BuildChannelFactory<TChannel>(BindingContext context)
{
context.BindingParameters.Add(this);
return context.BuildInnerChannelFactory<TChannel>();
}
public override bool CanBuildChannelFactory<TChannel>(BindingContext context)
{
context.BindingParameters.Add(this);
return context.CanBuildInnerChannelFactory<TChannel>();
}
public override T GetProperty<T>(BindingContext context)
{
return inner.GetProperty<T>(context) ?? context.GetInnerProperty<T>();
}
}
internal sealed class MdasMessageEncoderFactory : MessageEncoderFactory
{
private readonly MessageEncoderFactory inner;
private readonly MessageEncoder encoder;
public MdasMessageEncoderFactory(MessageEncoderFactory inner)
{
this.inner = inner;
encoder = new MdasMessageEncoder(inner.Encoder);
}
public override MessageEncoder Encoder => encoder;
public override MessageVersion MessageVersion => inner.MessageVersion;
}
internal sealed class MdasMessageEncoder : MessageEncoder
{
private const string MdasContentType = "application/x-mdas";
private readonly MessageEncoder inner;
public MdasMessageEncoder(MessageEncoder inner)
{
this.inner = inner;
}
public override string ContentType => MdasContentType;
public override string MediaType => MdasContentType;
public override MessageVersion MessageVersion => inner.MessageVersion;
public override bool IsContentTypeSupported(string contentType)
{
return contentType.StartsWith(MdasContentType, StringComparison.OrdinalIgnoreCase)
|| inner.IsContentTypeSupported(contentType);
}
public override Message ReadMessage(ArraySegment<byte> buffer, BufferManager bufferManager, string contentType)
{
return inner.ReadMessage(buffer, bufferManager, inner.ContentType);
}
public override Message ReadMessage(Stream stream, int maxSizeOfHeaders, string contentType)
{
return inner.ReadMessage(stream, maxSizeOfHeaders, inner.ContentType);
}
public override void WriteMessage(Message message, Stream stream)
{
inner.WriteMessage(message, stream);
}
public override ArraySegment<byte> WriteMessage(Message message, int maxMessageSize, BufferManager bufferManager, int messageOffset)
{
return inner.WriteMessage(message, maxMessageSize, bufferManager, messageOffset);
}
}
@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\AVEVA.Historian.Client\AVEVA.Historian.Client.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="dnlib" Version="4.5.0" />
<PackageReference Include="System.ServiceModel.NetNamedPipe" Version="10.0.652802" />
</ItemGroup>
</Project>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net481</TargetFramework>
<LangVersion>latest</LangVersion>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
<SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>ReverseInstrumentation.snk</AssemblyOriginatorKeyFile>
</PropertyGroup>
</Project>
@@ -0,0 +1,501 @@
using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
namespace AVEVA.Historian.ReverseInstrumentation
{
public static class CaptureLogger
{
private static readonly object Gate = new object();
public static void LogBuffer(string phase, IntPtr data, ulong length)
{
try
{
int byteCount = checked((int)Math.Min(length, 1024UL * 1024UL));
byte[] bytes = new byte[byteCount];
if (data != IntPtr.Zero && byteCount > 0 && IsReadableMemoryRange(data, byteCount))
{
Marshal.Copy(data, bytes, 0, byteCount);
}
string path = Environment.GetEnvironmentVariable("AVEVA_HISTORIAN_RE_CAPTURE");
if (string.IsNullOrWhiteSpace(path))
{
path = Path.Combine(Path.GetTempPath(), "aveva-historian-re-capture.ndjson");
}
string directory = Path.GetDirectoryName(Path.GetFullPath(path));
if (!string.IsNullOrWhiteSpace(directory))
{
Directory.CreateDirectory(directory);
}
string json = "{"
+ "\"TimestampUtc\":\"" + JsonEscape(DateTimeOffset.UtcNow.ToString("O")) + "\","
+ "\"Phase\":\"" + JsonEscape(phase) + "\","
+ "\"Length\":" + length + ","
+ "\"CapturedLength\":" + byteCount + ","
+ "\"Sha256\":\"" + ComputeSha256(bytes) + "\","
+ "\"Base64\":\"" + Convert.ToBase64String(bytes) + "\""
+ "}";
lock (Gate)
{
File.AppendAllText(path, json + Environment.NewLine, Encoding.UTF8);
}
}
catch
{
// Reverse-engineering instrumentation must not perturb the native query path.
}
}
private static bool IsReadableMemoryRange(IntPtr address, int byteCount)
{
if (address == IntPtr.Zero || byteCount <= 0)
{
return false;
}
long current = address.ToInt64();
long end = checked(current + byteCount);
while (current < end)
{
if (VirtualQuery(new IntPtr(current), out MEMORY_BASIC_INFORMATION info, (UIntPtr)Marshal.SizeOf(typeof(MEMORY_BASIC_INFORMATION))) == UIntPtr.Zero)
{
return false;
}
if (info.State != MEM_COMMIT || (info.Protect & PAGE_GUARD) != 0 || (info.Protect & PAGE_NOACCESS) != 0)
{
return false;
}
long regionEnd = checked(info.BaseAddress.ToInt64() + (long)info.RegionSize.ToUInt64());
if (regionEnd <= current)
{
return false;
}
current = regionEnd;
}
return true;
}
private const uint MEM_COMMIT = 0x1000;
private const uint PAGE_NOACCESS = 0x01;
private const uint PAGE_GUARD = 0x100;
[DllImport("kernel32.dll")]
private static extern UIntPtr VirtualQuery(IntPtr lpAddress, out MEMORY_BASIC_INFORMATION lpBuffer, UIntPtr dwLength);
[StructLayout(LayoutKind.Sequential)]
private struct MEMORY_BASIC_INFORMATION
{
public IntPtr BaseAddress;
public IntPtr AllocationBase;
public uint AllocationProtect;
public UIntPtr RegionSize;
public uint State;
public uint Protect;
public uint Type;
}
public static void LogByteArraySegment(string phase, byte[] bytes, int offset, int count)
{
try
{
if (bytes == null || count <= 0 || offset < 0 || offset > bytes.Length)
{
WriteRecord(phase, 0UL, new byte[0]);
return;
}
int captureCount = Math.Min(count, bytes.Length - offset);
if (captureCount > 1024 * 1024)
{
captureCount = 1024 * 1024;
}
byte[] captured = new byte[captureCount];
Buffer.BlockCopy(bytes, offset, captured, 0, captureCount);
WriteRecord(phase, (ulong)count, captured);
}
catch
{
// Reverse-engineering instrumentation must not perturb the native query path.
}
}
public static void LogByteArray(string phase, byte[] bytes)
{
try
{
byte[] captured = bytes ?? new byte[0];
if (captured.Length > 1024 * 1024)
{
byte[] truncated = new byte[1024 * 1024];
Buffer.BlockCopy(captured, 0, truncated, 0, truncated.Length);
captured = truncated;
}
WriteRecord(phase, bytes == null ? 0UL : (ulong)bytes.Length, captured);
}
catch
{
// Reverse-engineering instrumentation must not perturb the native query path.
}
}
public static void LogByteArraySummary(string phase, byte[] bytes)
{
try
{
byte[] captured = bytes ?? new byte[0];
if (captured.Length > 1024 * 1024)
{
byte[] truncated = new byte[1024 * 1024];
Buffer.BlockCopy(captured, 0, truncated, 0, truncated.Length);
captured = truncated;
}
WriteSummaryRecord(phase, bytes == null ? 0UL : (ulong)bytes.Length, captured);
}
catch
{
// Reverse-engineering instrumentation must not perturb the native query path.
}
}
public static void LogString(string phase, string value)
{
try
{
string captured = value ?? string.Empty;
byte[] bytes = Encoding.UTF8.GetBytes(captured);
string path = Environment.GetEnvironmentVariable("AVEVA_HISTORIAN_RE_CAPTURE");
if (string.IsNullOrWhiteSpace(path))
{
path = Path.Combine(Path.GetTempPath(), "aveva-historian-re-capture.ndjson");
}
string directory = Path.GetDirectoryName(Path.GetFullPath(path));
if (!string.IsNullOrWhiteSpace(directory))
{
Directory.CreateDirectory(directory);
}
string json = "{"
+ "\"TimestampUtc\":\"" + JsonEscape(DateTimeOffset.UtcNow.ToString("O")) + "\","
+ "\"Phase\":\"" + JsonEscape(phase) + "\","
+ "\"Length\":" + captured.Length + ","
+ "\"Sha256\":\"" + ComputeSha256(bytes) + "\","
+ "\"Value\":\"" + JsonEscape(captured) + "\""
+ "}";
lock (Gate)
{
File.AppendAllText(path, json + Environment.NewLine, Encoding.UTF8);
}
}
catch
{
// Reverse-engineering instrumentation must not perturb the native query path.
}
}
public static void LogStringSummary(string phase, string value)
{
try
{
string captured = value ?? string.Empty;
byte[] bytes = Encoding.UTF8.GetBytes(captured);
int hyphenCount = 0;
int uppercaseCount = 0;
int lowercaseCount = 0;
int digitCount = 0;
for (int index = 0; index < captured.Length; index++)
{
char current = captured[index];
if (current == '-')
{
hyphenCount++;
}
else if (current >= 'A' && current <= 'Z')
{
uppercaseCount++;
}
else if (current >= 'a' && current <= 'z')
{
lowercaseCount++;
}
else if (current >= '0' && current <= '9')
{
digitCount++;
}
}
string path = Environment.GetEnvironmentVariable("AVEVA_HISTORIAN_RE_CAPTURE");
if (string.IsNullOrWhiteSpace(path))
{
path = Path.Combine(Path.GetTempPath(), "aveva-historian-re-capture.ndjson");
}
string directory = Path.GetDirectoryName(Path.GetFullPath(path));
if (!string.IsNullOrWhiteSpace(directory))
{
Directory.CreateDirectory(directory);
}
string json = "{"
+ "\"TimestampUtc\":\"" + JsonEscape(DateTimeOffset.UtcNow.ToString("O")) + "\","
+ "\"Phase\":\"" + JsonEscape(phase) + "\","
+ "\"Length\":" + captured.Length + ","
+ "\"Sha256\":\"" + ComputeSha256(bytes) + "\","
+ "\"HyphenCount\":" + hyphenCount + ","
+ "\"UppercaseCount\":" + uppercaseCount + ","
+ "\"LowercaseCount\":" + lowercaseCount + ","
+ "\"DigitCount\":" + digitCount + ","
+ "\"StartsWithBrace\":" + (captured.StartsWith("{", StringComparison.Ordinal) ? "true" : "false") + ","
+ "\"EndsWithBrace\":" + (captured.EndsWith("}", StringComparison.Ordinal) ? "true" : "false") + ","
+ "\"ContainsBackslash\":" + (captured.IndexOf('\\') >= 0 ? "true" : "false")
+ "}";
lock (Gate)
{
File.AppendAllText(path, json + Environment.NewLine, Encoding.UTF8);
}
}
catch
{
// Reverse-engineering instrumentation must not perturb the native query path.
}
}
public static void LogUInt32(string phase, uint value)
{
try
{
string path = Environment.GetEnvironmentVariable("AVEVA_HISTORIAN_RE_CAPTURE");
if (string.IsNullOrWhiteSpace(path))
{
path = Path.Combine(Path.GetTempPath(), "aveva-historian-re-capture.ndjson");
}
string directory = Path.GetDirectoryName(Path.GetFullPath(path));
if (!string.IsNullOrWhiteSpace(directory))
{
Directory.CreateDirectory(directory);
}
string json = "{"
+ "\"TimestampUtc\":\"" + JsonEscape(DateTimeOffset.UtcNow.ToString("O")) + "\","
+ "\"Phase\":\"" + JsonEscape(phase) + "\","
+ "\"Value\":" + value
+ "}";
lock (Gate)
{
File.AppendAllText(path, json + Environment.NewLine, Encoding.UTF8);
}
}
catch
{
// Reverse-engineering instrumentation must not perturb the native query path.
}
}
public static void LogStdVector(string phase, IntPtr vector, ulong elementSize, ulong maxElements)
{
try
{
if (vector == IntPtr.Zero || elementSize == 0 || maxElements == 0)
{
WriteVectorRecord(phase, elementSize, 0, 0, 0, new byte[0]);
return;
}
IntPtr begin = Marshal.ReadIntPtr(vector, 0);
IntPtr end = Marshal.ReadIntPtr(vector, IntPtr.Size);
if (begin == IntPtr.Zero || end == IntPtr.Zero)
{
WriteVectorRecord(phase, elementSize, 0, 0, 0, new byte[0]);
return;
}
long byteLength = end.ToInt64() - begin.ToInt64();
if (byteLength <= 0 || byteLength % checked((long)elementSize) != 0)
{
WriteVectorRecord(phase, elementSize, 0, 0, 0, new byte[0]);
return;
}
ulong count = checked((ulong)byteLength / elementSize);
ulong capturedCount = Math.Min(count, maxElements);
ulong capturedLength = Math.Min(checked(capturedCount * elementSize), 1024UL * 1024UL);
byte[] captured = new byte[checked((int)capturedLength)];
if (captured.Length > 0)
{
Marshal.Copy(begin, captured, 0, captured.Length);
}
WriteVectorRecord(phase, elementSize, count, capturedCount, (ulong)byteLength, captured);
}
catch
{
// Reverse-engineering instrumentation must not perturb the native query path.
}
}
private static void WriteSummaryRecord(string phase, ulong length, byte[] bytes)
{
string path = Environment.GetEnvironmentVariable("AVEVA_HISTORIAN_RE_CAPTURE");
if (string.IsNullOrWhiteSpace(path))
{
path = Path.Combine(Path.GetTempPath(), "aveva-historian-re-capture.ndjson");
}
string directory = Path.GetDirectoryName(Path.GetFullPath(path));
if (!string.IsNullOrWhiteSpace(directory))
{
Directory.CreateDirectory(directory);
}
string json = "{"
+ "\"TimestampUtc\":\"" + JsonEscape(DateTimeOffset.UtcNow.ToString("O")) + "\","
+ "\"Phase\":\"" + JsonEscape(phase) + "\","
+ "\"Length\":" + length + ","
+ "\"CapturedLength\":" + bytes.Length + ","
+ "\"Sha256\":\"" + ComputeSha256(bytes) + "\","
+ "\"PrefixHex\":\"" + ToPrefixHex(bytes, 32) + "\","
+ "\"PrefixAscii\":\"" + JsonEscape(ToSafeAscii(bytes, 32)) + "\""
+ "}";
lock (Gate)
{
File.AppendAllText(path, json + Environment.NewLine, Encoding.UTF8);
}
}
private static void WriteRecord(string phase, ulong length, byte[] bytes)
{
string path = Environment.GetEnvironmentVariable("AVEVA_HISTORIAN_RE_CAPTURE");
if (string.IsNullOrWhiteSpace(path))
{
path = Path.Combine(Path.GetTempPath(), "aveva-historian-re-capture.ndjson");
}
string directory = Path.GetDirectoryName(Path.GetFullPath(path));
if (!string.IsNullOrWhiteSpace(directory))
{
Directory.CreateDirectory(directory);
}
string json = "{"
+ "\"TimestampUtc\":\"" + JsonEscape(DateTimeOffset.UtcNow.ToString("O")) + "\","
+ "\"Phase\":\"" + JsonEscape(phase) + "\","
+ "\"Length\":" + length + ","
+ "\"CapturedLength\":" + bytes.Length + ","
+ "\"Sha256\":\"" + ComputeSha256(bytes) + "\","
+ "\"Base64\":\"" + Convert.ToBase64String(bytes) + "\""
+ "}";
lock (Gate)
{
File.AppendAllText(path, json + Environment.NewLine, Encoding.UTF8);
}
}
private static void WriteVectorRecord(
string phase,
ulong elementSize,
ulong count,
ulong capturedCount,
ulong length,
byte[] bytes)
{
string path = Environment.GetEnvironmentVariable("AVEVA_HISTORIAN_RE_CAPTURE");
if (string.IsNullOrWhiteSpace(path))
{
path = Path.Combine(Path.GetTempPath(), "aveva-historian-re-capture.ndjson");
}
string directory = Path.GetDirectoryName(Path.GetFullPath(path));
if (!string.IsNullOrWhiteSpace(directory))
{
Directory.CreateDirectory(directory);
}
string json = "{"
+ "\"TimestampUtc\":\"" + JsonEscape(DateTimeOffset.UtcNow.ToString("O")) + "\","
+ "\"Phase\":\"" + JsonEscape(phase) + "\","
+ "\"ElementSize\":" + elementSize + ","
+ "\"Count\":" + count + ","
+ "\"CapturedCount\":" + capturedCount + ","
+ "\"Length\":" + length + ","
+ "\"CapturedLength\":" + bytes.Length + ","
+ "\"Sha256\":\"" + ComputeSha256(bytes) + "\","
+ "\"Base64\":\"" + Convert.ToBase64String(bytes) + "\""
+ "}";
lock (Gate)
{
File.AppendAllText(path, json + Environment.NewLine, Encoding.UTF8);
}
}
private static string ComputeSha256(byte[] bytes)
{
using (SHA256 sha256 = SHA256.Create())
{
byte[] hash = sha256.ComputeHash(bytes);
StringBuilder builder = new StringBuilder(hash.Length * 2);
foreach (byte value in hash)
{
builder.Append(value.ToString("x2"));
}
return builder.ToString();
}
}
private static string ToPrefixHex(byte[] bytes, int maxBytes)
{
int count = Math.Min(bytes.Length, maxBytes);
StringBuilder builder = new StringBuilder(count * 2);
for (int index = 0; index < count; index++)
{
builder.Append(bytes[index].ToString("x2"));
}
return builder.ToString();
}
private static string ToSafeAscii(byte[] bytes, int maxBytes)
{
int count = Math.Min(bytes.Length, maxBytes);
StringBuilder builder = new StringBuilder(count);
for (int index = 0; index < count; index++)
{
byte value = bytes[index];
builder.Append(value >= 32 && value <= 126 ? (char)value : '.');
}
return builder.ToString();
}
private static string JsonEscape(string value)
{
return value
.Replace("\\", "\\\\")
.Replace("\"", "\\\"")
.Replace("\r", "\\r")
.Replace("\n", "\\n");
}
}
}
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net481</TargetFramework>
<LangVersion>latest</LangVersion>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<Reference Include="System.ServiceModel" />
</ItemGroup>
</Project>
@@ -0,0 +1,674 @@
using System;
using System.IO;
using System.IO.Compression;
using System.Net.Security;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.Text;
using System.Threading;
using System.Xml;
namespace AVEVA.Historian.WcfCaptureServer;
internal static class Program
{
private static int Main(string[] args)
{
int port = args.Length > 0 && int.TryParse(args[0], out int parsedPort) ? parsedPort : 33268;
string hostName = args.Length > 1 && !string.IsNullOrWhiteSpace(args[1]) ? args[1] : "localhost";
Uri baseAddress = new($"net.tcp://{hostName}:{port}/");
using ServiceHost host = new(typeof(HistoryCaptureService), baseAddress);
host.AddServiceEndpoint(typeof(IHistoryServiceContract2), MdasBinding.Create(TimeSpan.FromSeconds(30)), "Hist");
host.AddServiceEndpoint(typeof(IHistoryServiceContract2), MdasBinding.CreateWindows(TimeSpan.FromSeconds(30)), "Hist-Integrated");
host.AddServiceEndpoint(typeof(IRetrievalServiceContract4), MdasBinding.Create(TimeSpan.FromSeconds(30)), "Retr");
host.Open();
Console.WriteLine($"READY net.tcp://{hostName}:{port}/Hist");
Console.WriteLine($"READY net.tcp://{hostName}:{port}/Hist-Integrated");
Console.WriteLine($"READY net.tcp://{hostName}:{port}/Retr");
Console.Out.Flush();
Thread.Sleep(Timeout.Infinite);
return 0;
}
}
[ServiceContract(Name = "Hist", Namespace = "aa")]
public interface IHistoryServiceContract
{
[OperationContract(Name = "GetV")]
uint GetInterfaceVersion(out uint version);
[OperationContract(Name = "Open")]
uint OpenConnection(
string HostName,
string ProcessName,
uint ProcessId,
string UserName,
byte[] Password,
ushort pwdLength,
byte clientType,
ushort clientVersion,
uint ConnectionMode,
uint ConnectionTimeout,
ref string StorageSessionId,
out uint Handle,
out long ConnectTime,
out uint ServerStatus);
[OperationContract(Name = "Close")]
uint CloseConnection(uint handle);
[OperationContract(Name = "AddT")]
uint AddTags(uint handle, uint elementCount, uint inByteCount, byte[] inputBuffer, out uint outByteCount, out byte[] outputBuffer);
[OperationContract(Name = "RTag")]
uint RegisterTags(uint handle, uint elementCount, uint inByteCount, byte[] inputBuffer, out uint outByteCount, out byte[] outputBuffer);
}
[ServiceContract(Name = "Hist", Namespace = "aa")]
public interface IHistoryServiceContract2 : IHistoryServiceContract
{
[OperationContract(Name = "Open2")]
bool OpenConnection2(ref byte[] inParameters, out byte[] outParameters, out byte[] err);
[OperationContract(Name = "RTag2")]
bool RegisterTags2(string handle, uint elementCount, byte[] inputBuffer, out byte[] outputBuffer, out byte[] errorBuffer);
}
[ServiceContract(Name = "Retr", Namespace = "aa")]
public interface IRetrievalServiceContract
{
[OperationContract(Name = "GetV")]
uint GetInterfaceVersion(out uint version);
[OperationContract]
uint IsOriginalAllowed(uint clientHandle, out bool isAllowed);
}
[ServiceContract(Name = "Retr", Namespace = "aa")]
public interface IRetrievalServiceContract2 : IRetrievalServiceContract
{
[OperationContract]
bool StartQuery2(
uint clientHandle,
ushort queryRequestType,
uint requestSize,
byte[] requestBuffer,
out uint responseSize,
out byte[] responseBuffer,
ref uint queryHandle,
out uint errorSize,
out byte[] errorBuffer);
[OperationContract]
bool GetNextQueryResultBuffer2(
uint clientHandle,
uint queryHandle,
out uint resultSize,
out byte[] resultBuffer,
out uint errorSize,
out byte[] errorBuffer);
[OperationContract]
bool EndQuery2(uint clientHandle, uint queryHandle, out uint errorSize, out byte[] errorBuffer);
}
[ServiceContract(Name = "Retr", Namespace = "aa")]
public interface IRetrievalServiceContract3 : IRetrievalServiceContract2
{
[OperationContract]
uint StartQuery(
uint clientHandle,
ushort queryRequestType,
uint requestSize,
byte[] requestBuffer,
out uint responseSize,
out byte[] responseBuffer,
ref uint queryHandle);
[OperationContract]
uint GetNextQueryResultBuffer(
uint clientHandle,
uint queryHandle,
out uint resultSize,
out byte[] resultBuffer);
[OperationContract]
uint EndQuery(uint clientHandle, uint queryHandle);
}
[ServiceContract(Name = "Retr", Namespace = "aa")]
public interface IRetrievalServiceContract4 : IRetrievalServiceContract3
{
[OperationContract]
bool StartEventQuery(
uint clientHandle,
ushort queryRequestType,
uint requestSize,
byte[] requestBuffer,
out uint responseSize,
out byte[] responseBuffer,
ref uint queryHandle,
out uint errorSize,
out byte[] errorBuffer);
[OperationContract]
bool GetNextEventQueryResultBuffer(
uint clientHandle,
uint queryHandle,
out uint resultSize,
out byte[] resultBuffer,
out uint errorSize,
out byte[] errorBuffer);
[OperationContract]
bool EndEventQuery(uint clientHandle, uint queryHandle, out uint errorSize, out byte[] errorBuffer);
}
public sealed class HistoryCaptureService : IHistoryServiceContract2, IRetrievalServiceContract4
{
private const uint Handle = 1234;
public uint GetInterfaceVersion(out uint version)
{
version = 11;
Console.WriteLine("{\"Operation\":\"GetV\",\"Version\":11}");
return 0;
}
public uint OpenConnection(
string HostName,
string ProcessName,
uint ProcessId,
string UserName,
byte[] Password,
ushort pwdLength,
byte clientType,
ushort clientVersion,
uint ConnectionMode,
uint ConnectionTimeout,
ref string StorageSessionId,
out uint Handle,
out long ConnectTime,
out uint ServerStatus)
{
string storageSessionIdIn = StorageSessionId ?? string.Empty;
StorageSessionId = Guid.Empty.ToString("D");
Handle = HistoryCaptureService.Handle;
ConnectTime = DateTime.UtcNow.ToFileTimeUtc();
ServerStatus = 0;
Console.WriteLine("{" +
"\"Operation\":\"Open\"," +
$"\"HostName\":\"{Escape(HostName)}\"," +
$"\"ProcessName\":\"{Escape(ProcessName)}\"," +
$"\"ProcessId\":{ProcessId}," +
$"\"UserName\":\"{Escape(UserName)}\"," +
$"\"PasswordLength\":{(Password?.Length ?? 0)}," +
$"\"pwdLength\":{pwdLength}," +
$"\"clientType\":{clientType}," +
$"\"clientVersion\":{clientVersion}," +
$"\"ConnectionMode\":{ConnectionMode}," +
$"\"ConnectionTimeout\":{ConnectionTimeout}," +
$"\"StorageSessionIdIn\":\"{Escape(storageSessionIdIn)}\"," +
$"\"StorageSessionIdOut\":\"{Escape(StorageSessionId)}\"" +
"}");
Console.Out.Flush();
return 0;
}
public uint CloseConnection(uint handle)
{
Console.WriteLine($"{{\"Operation\":\"Close\",\"Handle\":{handle}}}");
return 0;
}
public uint AddTags(uint handle, uint elementCount, uint inByteCount, byte[] inputBuffer, out uint outByteCount, out byte[] outputBuffer)
{
byte[] request = inputBuffer ?? Array.Empty<byte>();
outByteCount = 0;
outputBuffer = Array.Empty<byte>();
bool includeBuffers = string.Equals(Environment.GetEnvironmentVariable("WCF_CAPTURE_INCLUDE_BUFFERS"), "1", StringComparison.Ordinal);
Console.WriteLine("{" +
"\"Operation\":\"AddT\"," +
$"\"Handle\":{handle}," +
$"\"ElementCount\":{elementCount}," +
$"\"InputByteCount\":{inByteCount}," +
$"\"InputByteArrayLength\":{request.Length}," +
$"\"InputSha256\":\"{Hash(request)}\"" +
(includeBuffers ? $",\"InputBase64\":\"{Convert.ToBase64String(request)}\"" : string.Empty) +
"}");
Console.Out.Flush();
return 0;
}
public uint RegisterTags(uint handle, uint elementCount, uint inByteCount, byte[] inputBuffer, out uint outByteCount, out byte[] outputBuffer)
{
byte[] request = inputBuffer ?? Array.Empty<byte>();
outByteCount = 0;
outputBuffer = Array.Empty<byte>();
bool includeBuffers = string.Equals(Environment.GetEnvironmentVariable("WCF_CAPTURE_INCLUDE_BUFFERS"), "1", StringComparison.Ordinal);
Console.WriteLine("{" +
"\"Operation\":\"RTag\"," +
$"\"Handle\":{handle}," +
$"\"ElementCount\":{elementCount}," +
$"\"InputByteCount\":{inByteCount}," +
$"\"InputByteArrayLength\":{request.Length}," +
$"\"InputSha256\":\"{Hash(request)}\"" +
(includeBuffers ? $",\"InputBase64\":\"{Convert.ToBase64String(request)}\"" : string.Empty) +
"}");
Console.Out.Flush();
return 0;
}
public bool OpenConnection2(ref byte[] inParameters, out byte[] outParameters, out byte[] err)
{
byte[] input = inParameters ?? Array.Empty<byte>();
Console.WriteLine("{" +
"\"Operation\":\"Open2\"," +
$"\"InputByteCount\":{input.Length}," +
$"\"InputSha256\":\"{Hash(input)}\"" +
"}");
Console.Out.Flush();
outParameters = CreateOpen2Output();
err = Array.Empty<byte>();
return true;
}
public bool RegisterTags2(string handle, uint elementCount, byte[] inputBuffer, out byte[] outputBuffer, out byte[] errorBuffer)
{
byte[] request = inputBuffer ?? Array.Empty<byte>();
outputBuffer = Array.Empty<byte>();
errorBuffer = Array.Empty<byte>();
bool includeBuffers = string.Equals(Environment.GetEnvironmentVariable("WCF_CAPTURE_INCLUDE_BUFFERS"), "1", StringComparison.Ordinal);
Console.WriteLine("{" +
"\"Operation\":\"RTag2\"," +
$"\"Handle\":\"{Escape(handle)}\"," +
$"\"ElementCount\":{elementCount}," +
$"\"InputByteArrayLength\":{request.Length}," +
$"\"InputSha256\":\"{Hash(request)}\"" +
(includeBuffers ? $",\"InputBase64\":\"{Convert.ToBase64String(request)}\"" : string.Empty) +
"}");
Console.Out.Flush();
return true;
}
public uint IsOriginalAllowed(uint clientHandle, out bool isAllowed)
{
isAllowed = true;
Console.WriteLine($"{{\"Operation\":\"IsOriginalAllowed\",\"ClientHandle\":{clientHandle}}}");
Console.Out.Flush();
return clientHandle == Handle ? 0u : 4u;
}
public bool StartQuery2(
uint clientHandle,
ushort queryRequestType,
uint requestSize,
byte[] requestBuffer,
out uint responseSize,
out byte[] responseBuffer,
ref uint queryHandle,
out uint errorSize,
out byte[] errorBuffer)
{
byte[] request = requestBuffer ?? Array.Empty<byte>();
queryHandle = 1;
responseSize = 0;
responseBuffer = Array.Empty<byte>();
errorSize = 5;
errorBuffer = new byte[] { 4, 1, 0, 0, 0 };
bool includeBuffers = string.Equals(Environment.GetEnvironmentVariable("WCF_CAPTURE_INCLUDE_BUFFERS"), "1", StringComparison.Ordinal);
Console.WriteLine("{" +
"\"Operation\":\"StartQuery2\"," +
$"\"ClientHandle\":{clientHandle}," +
$"\"QueryRequestType\":{queryRequestType}," +
$"\"RequestSize\":{requestSize}," +
$"\"RequestByteCount\":{request.Length}," +
$"\"RequestSha256\":\"{Hash(request)}\"" +
(includeBuffers ? $",\"RequestBase64\":\"{Convert.ToBase64String(request)}\"" : string.Empty) +
"}");
Console.Out.Flush();
return false;
}
public uint StartQuery(
uint clientHandle,
ushort queryRequestType,
uint requestSize,
byte[] requestBuffer,
out uint responseSize,
out byte[] responseBuffer,
ref uint queryHandle)
{
byte[] request = requestBuffer ?? Array.Empty<byte>();
queryHandle = 1;
responseSize = 0;
responseBuffer = Array.Empty<byte>();
bool includeBuffers = string.Equals(Environment.GetEnvironmentVariable("WCF_CAPTURE_INCLUDE_BUFFERS"), "1", StringComparison.Ordinal);
Console.WriteLine("{" +
"\"Operation\":\"StartQuery\"," +
$"\"ClientHandle\":{clientHandle}," +
$"\"QueryRequestType\":{queryRequestType}," +
$"\"RequestSize\":{requestSize}," +
$"\"RequestByteCount\":{request.Length}," +
$"\"RequestSha256\":\"{Hash(request)}\"" +
(includeBuffers ? $",\"RequestBase64\":\"{Convert.ToBase64String(request)}\"" : string.Empty) +
"}");
Console.Out.Flush();
return 4;
}
public bool GetNextQueryResultBuffer2(
uint clientHandle,
uint queryHandle,
out uint resultSize,
out byte[] resultBuffer,
out uint errorSize,
out byte[] errorBuffer)
{
resultSize = 0;
resultBuffer = Array.Empty<byte>();
errorSize = 5;
errorBuffer = new byte[] { 4, 1, 0, 0, 0 };
Console.WriteLine($"{{\"Operation\":\"GetNextQueryResultBuffer2\",\"ClientHandle\":{clientHandle},\"QueryHandle\":{queryHandle}}}");
Console.Out.Flush();
return false;
}
public uint GetNextQueryResultBuffer(
uint clientHandle,
uint queryHandle,
out uint resultSize,
out byte[] resultBuffer)
{
resultSize = 0;
resultBuffer = Array.Empty<byte>();
Console.WriteLine($"{{\"Operation\":\"GetNextQueryResultBuffer\",\"ClientHandle\":{clientHandle},\"QueryHandle\":{queryHandle}}}");
Console.Out.Flush();
return 4;
}
public bool EndQuery2(uint clientHandle, uint queryHandle, out uint errorSize, out byte[] errorBuffer)
{
errorSize = 0;
errorBuffer = Array.Empty<byte>();
Console.WriteLine($"{{\"Operation\":\"EndQuery2\",\"ClientHandle\":{clientHandle},\"QueryHandle\":{queryHandle}}}");
Console.Out.Flush();
return true;
}
public uint EndQuery(uint clientHandle, uint queryHandle)
{
Console.WriteLine($"{{\"Operation\":\"EndQuery\",\"ClientHandle\":{clientHandle},\"QueryHandle\":{queryHandle}}}");
Console.Out.Flush();
return 0;
}
public bool StartEventQuery(
uint clientHandle,
ushort queryRequestType,
uint requestSize,
byte[] requestBuffer,
out uint responseSize,
out byte[] responseBuffer,
ref uint queryHandle,
out uint errorSize,
out byte[] errorBuffer)
{
byte[] request = requestBuffer ?? Array.Empty<byte>();
queryHandle = 1;
responseSize = 0;
responseBuffer = Array.Empty<byte>();
errorSize = 5;
errorBuffer = new byte[] { 4, 1, 0, 0, 0 };
bool includeBuffers = string.Equals(Environment.GetEnvironmentVariable("WCF_CAPTURE_INCLUDE_BUFFERS"), "1", StringComparison.Ordinal);
Console.WriteLine("{" +
"\"Operation\":\"StartEventQuery\"," +
$"\"ClientHandle\":{clientHandle}," +
$"\"QueryRequestType\":{queryRequestType}," +
$"\"RequestSize\":{requestSize}," +
$"\"RequestByteCount\":{request.Length}," +
$"\"RequestSha256\":\"{Hash(request)}\"" +
(includeBuffers ? $",\"RequestBase64\":\"{Convert.ToBase64String(request)}\"" : string.Empty) +
"}");
Console.Out.Flush();
return false;
}
public bool GetNextEventQueryResultBuffer(
uint clientHandle,
uint queryHandle,
out uint resultSize,
out byte[] resultBuffer,
out uint errorSize,
out byte[] errorBuffer)
{
resultSize = 0;
resultBuffer = Array.Empty<byte>();
errorSize = 5;
errorBuffer = new byte[] { 4, 1, 0, 0, 0 };
Console.WriteLine($"{{\"Operation\":\"GetNextEventQueryResultBuffer\",\"ClientHandle\":{clientHandle},\"QueryHandle\":{queryHandle}}}");
Console.Out.Flush();
return false;
}
public bool EndEventQuery(uint clientHandle, uint queryHandle, out uint errorSize, out byte[] errorBuffer)
{
errorSize = 0;
errorBuffer = Array.Empty<byte>();
Console.WriteLine($"{{\"Operation\":\"EndEventQuery\",\"ClientHandle\":{clientHandle},\"QueryHandle\":{queryHandle}}}");
Console.Out.Flush();
return true;
}
private static byte[] CreateOpen2Output()
{
using MemoryStream stream = new MemoryStream();
using BinaryWriter writer = new BinaryWriter(stream);
writer.Write(Handle);
writer.Write(Guid.Empty.ToByteArray());
writer.Write(DateTime.UtcNow.ToFileTimeUtc());
writer.Write(0u);
return stream.ToArray();
}
private static string Escape(string? value)
{
return (value ?? string.Empty).Replace("\\", "\\\\").Replace("\"", "\\\"");
}
private static string Hash(byte[] value)
{
using System.Security.Cryptography.SHA256 sha256 = System.Security.Cryptography.SHA256.Create();
byte[] hash = sha256.ComputeHash(value);
StringBuilder builder = new(hash.Length * 2);
foreach (byte b in hash)
{
builder.Append(b.ToString("x2"));
}
return builder.ToString();
}
}
internal static class MdasBinding
{
public static Binding Create(TimeSpan timeout)
{
CustomBinding binding = new(
new MdasMessageEncodingBindingElement(new BinaryMessageEncodingBindingElement
{
MessageVersion = MessageVersion.Soap12WSAddressing10
}),
new TcpTransportBindingElement
{
MaxReceivedMessageSize = 64 * 1024 * 1024,
TransferMode = TransferMode.Buffered
})
{
OpenTimeout = timeout,
CloseTimeout = timeout,
SendTimeout = timeout,
ReceiveTimeout = timeout
};
return binding;
}
public static Binding CreateWindows(TimeSpan timeout)
{
NetTcpBinding nativeShape = new NetTcpBinding(SecurityMode.Transport)
{
MaxReceivedMessageSize = 64 * 1024 * 1024,
MaxBufferSize = 64 * 1024 * 1024
};
nativeShape.ReaderQuotas.MaxArrayLength = nativeShape.MaxBufferSize;
nativeShape.Security.Transport.ClientCredentialType = TcpClientCredentialType.Windows;
nativeShape.Security.Transport.ProtectionLevel = ProtectionLevel.None;
BindingElementCollection elements = nativeShape.CreateBindingElements();
for (int i = 0; i < elements.Count; i++)
{
if (elements[i] is MessageEncodingBindingElement encoding)
{
elements[i] = new MdasMessageEncodingBindingElement(encoding);
break;
}
}
return new CustomBinding(elements)
{
OpenTimeout = timeout,
CloseTimeout = timeout,
SendTimeout = timeout,
ReceiveTimeout = timeout
};
}
}
internal sealed class MdasMessageEncodingBindingElement : MessageEncodingBindingElement
{
private readonly MessageEncodingBindingElement inner;
public MdasMessageEncodingBindingElement(MessageEncodingBindingElement inner)
{
this.inner = inner ?? throw new ArgumentNullException(nameof(inner));
}
private MdasMessageEncodingBindingElement(MdasMessageEncodingBindingElement source)
{
inner = (MessageEncodingBindingElement)source.inner.Clone();
}
public override MessageVersion MessageVersion
{
get => inner.MessageVersion;
set => inner.MessageVersion = value;
}
public override MessageEncoderFactory CreateMessageEncoderFactory()
{
return new MdasMessageEncoderFactory(inner.CreateMessageEncoderFactory());
}
public override BindingElement Clone()
{
return new MdasMessageEncodingBindingElement(this);
}
public override IChannelFactory<TChannel> BuildChannelFactory<TChannel>(BindingContext context)
{
context.BindingParameters.Add(this);
return context.BuildInnerChannelFactory<TChannel>();
}
public override IChannelListener<TChannel> BuildChannelListener<TChannel>(BindingContext context)
{
context.BindingParameters.Add(this);
return context.BuildInnerChannelListener<TChannel>();
}
public override bool CanBuildChannelFactory<TChannel>(BindingContext context)
{
context.BindingParameters.Add(this);
return context.CanBuildInnerChannelFactory<TChannel>();
}
public override bool CanBuildChannelListener<TChannel>(BindingContext context)
{
context.BindingParameters.Add(this);
return context.CanBuildInnerChannelListener<TChannel>();
}
public override T GetProperty<T>(BindingContext context)
{
return inner.GetProperty<T>(context) ?? context.GetInnerProperty<T>();
}
}
internal sealed class MdasMessageEncoderFactory : MessageEncoderFactory
{
private readonly MessageEncoderFactory inner;
private readonly MessageEncoder encoder;
public MdasMessageEncoderFactory(MessageEncoderFactory inner)
{
this.inner = inner ?? throw new ArgumentNullException(nameof(inner));
encoder = new MdasMessageEncoder(inner.Encoder);
}
public override MessageEncoder Encoder => encoder;
public override MessageVersion MessageVersion => inner.MessageVersion;
}
internal sealed class MdasMessageEncoder : MessageEncoder
{
private const string MdasContentType = "application/x-mdas";
private readonly MessageEncoder inner;
public MdasMessageEncoder(MessageEncoder inner)
{
this.inner = inner ?? throw new ArgumentNullException(nameof(inner));
}
public override string ContentType => MdasContentType;
public override string MediaType => MdasContentType;
public override MessageVersion MessageVersion => inner.MessageVersion;
public override bool IsContentTypeSupported(string contentType)
{
return contentType.StartsWith(MdasContentType, StringComparison.OrdinalIgnoreCase)
|| inner.IsContentTypeSupported(contentType);
}
public override Message ReadMessage(ArraySegment<byte> buffer, BufferManager bufferManager, string contentType)
{
Message message = inner.ReadMessage(buffer, bufferManager);
message.Properties.Encoder = this;
return message;
}
public override Message ReadMessage(Stream stream, int maxSizeOfHeaders, string contentType)
{
return inner.ReadMessage(stream, maxSizeOfHeaders);
}
public override void WriteMessage(Message message, Stream stream)
{
inner.WriteMessage(message, stream);
}
public override ArraySegment<byte> WriteMessage(Message message, int maxMessageSize, BufferManager bufferManager, int messageOffset)
{
return inner.WriteMessage(message, maxMessageSize, bufferManager, messageOffset);
}
}