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:
@@ -0,0 +1,275 @@
|
||||
using System.Buffers.Binary;
|
||||
using System.Text;
|
||||
|
||||
namespace AVEVA.Historian.Client.Wcf;
|
||||
|
||||
internal static class HistorianOpen2Protocol
|
||||
{
|
||||
public static byte[] SerializeLegacyVersion1(HistorianOpen2Request request)
|
||||
{
|
||||
using MemoryStream stream = new();
|
||||
using BinaryWriter writer = new(stream, Encoding.Unicode, leaveOpen: true);
|
||||
|
||||
writer.Write((ushort)1);
|
||||
WriteHistorianString(writer, request.HostName);
|
||||
WriteHistorianString(writer, request.ProcessName);
|
||||
writer.Write(request.ProcessId);
|
||||
WriteHistorianString(writer, request.UserName);
|
||||
writer.Write((uint)request.Password.Length);
|
||||
writer.Write(request.Password);
|
||||
writer.Write(request.ClientType);
|
||||
writer.Write(request.ClientVersion);
|
||||
writer.Write(request.ConnectionMode);
|
||||
WriteMetadataNamespace(writer, request.MetadataNamespace);
|
||||
return stream.ToArray();
|
||||
}
|
||||
|
||||
public static byte[] SerializeNativeVersion3(HistorianOpen2Request request, HistorianClientCommonInfo commonInfo)
|
||||
{
|
||||
using MemoryStream stream = new();
|
||||
using BinaryWriter writer = new(stream, Encoding.Unicode, leaveOpen: true);
|
||||
|
||||
writer.Write((byte)3);
|
||||
WriteNativeOpenConnectionContent(writer, request, commonInfo);
|
||||
return stream.ToArray();
|
||||
}
|
||||
|
||||
public static byte[] SerializeNativeOpenConnection3Version6(
|
||||
HistorianOpen2Request request,
|
||||
HistorianClientCommonInfo commonInfo,
|
||||
Guid clientKey,
|
||||
byte[]? credentialBlock = null)
|
||||
{
|
||||
using MemoryStream stream = new();
|
||||
using BinaryWriter writer = new(stream, Encoding.Unicode, leaveOpen: true);
|
||||
|
||||
writer.Write((byte)6);
|
||||
writer.Write(clientKey.ToByteArray());
|
||||
writer.Write((byte)0);
|
||||
WriteNativeOpenConnectionContent(writer, request, commonInfo, credentialBlock, useCompactMetadataNamespace: true);
|
||||
return stream.ToArray();
|
||||
}
|
||||
|
||||
private static void WriteNativeOpenConnectionContent(
|
||||
BinaryWriter writer,
|
||||
HistorianOpen2Request request,
|
||||
HistorianClientCommonInfo commonInfo,
|
||||
byte[]? credentialBlock = null,
|
||||
bool useCompactMetadataNamespace = false)
|
||||
{
|
||||
byte[] secretBytes = credentialBlock ?? request.Password;
|
||||
WriteHistorianString(writer, request.HostName);
|
||||
checked
|
||||
{
|
||||
writer.Write((ushort)secretBytes.Length);
|
||||
}
|
||||
|
||||
writer.Write(secretBytes);
|
||||
writer.Write(request.ClientType);
|
||||
writer.Write(request.ConnectionMode);
|
||||
if (useCompactMetadataNamespace)
|
||||
{
|
||||
WriteCompactMetadataNamespace(writer, request.MetadataNamespace);
|
||||
}
|
||||
else
|
||||
{
|
||||
WriteMetadataNamespace(writer, request.MetadataNamespace);
|
||||
}
|
||||
|
||||
WriteHistorianString(writer, string.Empty);
|
||||
WriteHistorianString(writer, string.Empty);
|
||||
WriteClientCommonInfo(writer, commonInfo);
|
||||
}
|
||||
|
||||
public static HistorianNativeError? TryReadNativeError(ReadOnlySpan<byte> buffer)
|
||||
{
|
||||
if (buffer.Length < 5)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
byte type = buffer[0];
|
||||
uint code = BinaryPrimitives.ReadUInt32LittleEndian(buffer[1..5]);
|
||||
return new HistorianNativeError(type, code, GetKnownErrorName(code));
|
||||
}
|
||||
|
||||
public static HistorianLegacyOpen2Output? TryReadLegacyOpen2Output(ReadOnlySpan<byte> buffer)
|
||||
{
|
||||
if (buffer.Length != 32)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
uint handle = BinaryPrimitives.ReadUInt32LittleEndian(buffer[..4]);
|
||||
Guid storageSessionId = new(buffer.Slice(4, 16));
|
||||
long connectTime = BinaryPrimitives.ReadInt64LittleEndian(buffer.Slice(20, 8));
|
||||
uint serverStatus = BinaryPrimitives.ReadUInt32LittleEndian(buffer.Slice(28, 4));
|
||||
return new HistorianLegacyOpen2Output(handle, storageSessionId, connectTime, serverStatus);
|
||||
}
|
||||
|
||||
public static HistorianNativeOpen3Output? TryReadNativeOpen3Output(ReadOnlySpan<byte> buffer)
|
||||
{
|
||||
if (buffer.Length < 29)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
byte protocolVersion = buffer[0];
|
||||
if (protocolVersion is not (2 or 3))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
int minimumLength = protocolVersion >= 3 ? 37 : 29;
|
||||
if (buffer.Length < minimumLength)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
uint handle = BinaryPrimitives.ReadUInt32LittleEndian(buffer.Slice(1, 4));
|
||||
Guid storageSessionId = new(buffer.Slice(5, 16));
|
||||
long connectTime = BinaryPrimitives.ReadInt64LittleEndian(buffer.Slice(21, 8));
|
||||
long? serverTime = null;
|
||||
if (protocolVersion >= 3)
|
||||
{
|
||||
serverTime = BinaryPrimitives.ReadInt64LittleEndian(buffer.Slice(29, 8));
|
||||
}
|
||||
|
||||
byte[] trailingBytes = buffer[minimumLength..].ToArray();
|
||||
return new HistorianNativeOpen3Output(
|
||||
protocolVersion,
|
||||
handle,
|
||||
storageSessionId,
|
||||
connectTime,
|
||||
serverTime,
|
||||
trailingBytes);
|
||||
}
|
||||
|
||||
public static byte[] EncodeWidePassword(string password)
|
||||
{
|
||||
return string.IsNullOrEmpty(password) ? [] : Encoding.Unicode.GetBytes(password);
|
||||
}
|
||||
|
||||
private static void WriteMetadataNamespace(BinaryWriter writer, HistorianMetadataNamespace metadataNamespace)
|
||||
{
|
||||
writer.Write(metadataNamespace.HasValue ? (byte)1 : (byte)0);
|
||||
WriteHistorianString(writer, metadataNamespace.Namespace);
|
||||
WriteHistorianString(writer, metadataNamespace.TagPrefix);
|
||||
WriteHistorianString(writer, metadataNamespace.PropertyPrefix);
|
||||
}
|
||||
|
||||
private static void WriteCompactMetadataNamespace(BinaryWriter writer, HistorianMetadataNamespace metadataNamespace)
|
||||
{
|
||||
if (!metadataNamespace.HasValue
|
||||
|| metadataNamespace.Namespace.Length != 0
|
||||
|| metadataNamespace.TagPrefix.Length != 0
|
||||
|| metadataNamespace.PropertyPrefix.Length != 0)
|
||||
{
|
||||
throw new ProtocolEvidenceMissingException("OpenConnection3 non-empty metadata namespace");
|
||||
}
|
||||
|
||||
writer.Write((byte)1);
|
||||
WriteCompactEmptyString(writer);
|
||||
WriteCompactEmptyString(writer);
|
||||
WriteCompactEmptyString(writer);
|
||||
}
|
||||
|
||||
private static void WriteCompactEmptyString(BinaryWriter writer)
|
||||
{
|
||||
writer.Write((ushort)1);
|
||||
writer.Write((byte)0);
|
||||
}
|
||||
|
||||
private static void WriteHistorianString(BinaryWriter writer, string value)
|
||||
{
|
||||
writer.Write((uint)value.Length);
|
||||
if (value.Length > 0)
|
||||
{
|
||||
writer.Write(Encoding.Unicode.GetBytes(value));
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteClientCommonInfo(BinaryWriter writer, HistorianClientCommonInfo commonInfo)
|
||||
{
|
||||
writer.Write(commonInfo.FormatVersion);
|
||||
WriteHistorianString(writer, commonInfo.ServerNodeName);
|
||||
WriteHistorianString(writer, commonInfo.ClientNodeName);
|
||||
writer.Write(commonInfo.ProcessId);
|
||||
writer.Write(commonInfo.HcalVersion);
|
||||
WriteHistorianString(writer, commonInfo.ProcessName);
|
||||
WriteHistorianString(writer, commonInfo.Proxy);
|
||||
WriteHistorianString(writer, commonInfo.DataSourceId);
|
||||
writer.Write(commonInfo.ShardId.ToByteArray());
|
||||
writer.Write(commonInfo.ClientVersion);
|
||||
if (commonInfo.FormatVersion >= 3)
|
||||
{
|
||||
writer.Write(commonInfo.ClientTimestamp);
|
||||
}
|
||||
|
||||
if (commonInfo.FormatVersion >= 4)
|
||||
{
|
||||
WriteHistorianString(writer, commonInfo.ClientDllVersion);
|
||||
}
|
||||
}
|
||||
|
||||
private static string? GetKnownErrorName(uint code)
|
||||
{
|
||||
return code switch
|
||||
{
|
||||
1 => "Failure",
|
||||
73 => "InvalidPacketVersion",
|
||||
171 => "AuthenticationFailed",
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed record HistorianOpen2Request(
|
||||
string HostName,
|
||||
string ProcessName,
|
||||
uint ProcessId,
|
||||
string UserName,
|
||||
byte[] Password,
|
||||
byte ClientType,
|
||||
ushort ClientVersion,
|
||||
uint ConnectionMode,
|
||||
HistorianMetadataNamespace MetadataNamespace);
|
||||
|
||||
internal sealed record HistorianMetadataNamespace(
|
||||
bool HasValue,
|
||||
string Namespace,
|
||||
string TagPrefix,
|
||||
string PropertyPrefix)
|
||||
{
|
||||
public static HistorianMetadataNamespace Empty { get; } = new(true, string.Empty, string.Empty, string.Empty);
|
||||
}
|
||||
|
||||
internal sealed record HistorianNativeError(byte Type, uint Code, string? Name);
|
||||
|
||||
internal sealed record HistorianLegacyOpen2Output(
|
||||
uint Handle,
|
||||
Guid StorageSessionId,
|
||||
long ConnectTimeFileTimeUtc,
|
||||
uint ServerStatus);
|
||||
|
||||
internal sealed record HistorianNativeOpen3Output(
|
||||
byte ProtocolVersion,
|
||||
uint Handle,
|
||||
Guid StorageSessionId,
|
||||
long ConnectTimeFileTimeUtc,
|
||||
long? ServerTimeFileTimeUtc,
|
||||
byte[] TrailingBytes);
|
||||
|
||||
internal sealed record HistorianClientCommonInfo(
|
||||
byte FormatVersion,
|
||||
string ServerNodeName,
|
||||
string ClientNodeName,
|
||||
uint ProcessId,
|
||||
ushort HcalVersion,
|
||||
string ProcessName,
|
||||
string Proxy,
|
||||
string DataSourceId,
|
||||
Guid ShardId,
|
||||
uint ClientVersion,
|
||||
ulong ClientTimestamp,
|
||||
string ClientDllVersion);
|
||||
Reference in New Issue
Block a user