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,167 @@
|
||||
using System.Net.Security;
|
||||
using System.Runtime.Versioning;
|
||||
using System.ServiceModel;
|
||||
using System.ServiceModel.Channels;
|
||||
|
||||
namespace AVEVA.Historian.Client.Wcf;
|
||||
|
||||
internal static class HistorianWcfBindingFactory
|
||||
{
|
||||
public const string Scheme = "net.tcp";
|
||||
public const int DefaultPort = 32568;
|
||||
|
||||
public static Binding CreateMdasNetTcpBinding(TimeSpan timeout, long maxReceivedMessageSize = 64 * 1024 * 1024)
|
||||
{
|
||||
var encoding = new MdasMessageEncodingBindingElement(
|
||||
new BinaryMessageEncodingBindingElement
|
||||
{
|
||||
MessageVersion = MessageVersion.Soap12WSAddressing10
|
||||
});
|
||||
|
||||
var transport = new TcpTransportBindingElement
|
||||
{
|
||||
MaxReceivedMessageSize = maxReceivedMessageSize,
|
||||
TransferMode = TransferMode.Buffered
|
||||
};
|
||||
|
||||
return new CustomBinding(encoding, transport)
|
||||
{
|
||||
CloseTimeout = timeout,
|
||||
OpenTimeout = timeout,
|
||||
ReceiveTimeout = timeout,
|
||||
SendTimeout = timeout
|
||||
};
|
||||
}
|
||||
|
||||
public static Binding CreateMdasNetTcpWindowsBinding(TimeSpan timeout, long maxReceivedMessageSize = 64 * 1024 * 1024)
|
||||
{
|
||||
NetTcpBinding nativeShape = new(SecurityMode.Transport)
|
||||
{
|
||||
MaxReceivedMessageSize = maxReceivedMessageSize,
|
||||
MaxBufferSize = checked((int)Math.Min(maxReceivedMessageSize, int.MaxValue))
|
||||
};
|
||||
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)
|
||||
{
|
||||
CloseTimeout = timeout,
|
||||
OpenTimeout = timeout,
|
||||
ReceiveTimeout = timeout,
|
||||
SendTimeout = timeout
|
||||
};
|
||||
}
|
||||
|
||||
public static Binding CreateMdasNetTcpCertificateBinding(TimeSpan timeout, long maxReceivedMessageSize = 64 * 1024 * 1024)
|
||||
{
|
||||
NetTcpBinding nativeShape = new(SecurityMode.Transport)
|
||||
{
|
||||
MaxReceivedMessageSize = maxReceivedMessageSize,
|
||||
MaxBufferSize = checked((int)Math.Min(maxReceivedMessageSize, int.MaxValue))
|
||||
};
|
||||
nativeShape.ReaderQuotas.MaxArrayLength = nativeShape.MaxBufferSize;
|
||||
nativeShape.Security.Transport.ClientCredentialType = TcpClientCredentialType.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)
|
||||
{
|
||||
CloseTimeout = timeout,
|
||||
OpenTimeout = timeout,
|
||||
ReceiveTimeout = timeout,
|
||||
SendTimeout = timeout
|
||||
};
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("windows")]
|
||||
public static Binding CreateMdasNetNamedPipeBinding(TimeSpan timeout, int maxBufferSize = 64 * 1024 * 1024)
|
||||
{
|
||||
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)
|
||||
{
|
||||
CloseTimeout = timeout,
|
||||
OpenTimeout = timeout,
|
||||
ReceiveTimeout = timeout,
|
||||
SendTimeout = timeout
|
||||
};
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("windows")]
|
||||
public static (Binding HistoryBinding, EndpointAddress HistoryEndpoint, Binding RetrievalBinding, EndpointAddress RetrievalEndpoint) CreateBindingPair(
|
||||
HistorianClientOptions options)
|
||||
{
|
||||
TimeSpan timeout = options.RequestTimeout;
|
||||
|
||||
return options.Transport switch
|
||||
{
|
||||
HistorianTransport.LocalPipe => (
|
||||
CreateMdasNetNamedPipeBinding(timeout),
|
||||
CreatePipeEndpointAddress(options.Host, HistorianWcfServiceNames.History),
|
||||
CreateMdasNetNamedPipeBinding(timeout),
|
||||
CreatePipeEndpointAddress(options.Host, HistorianWcfServiceNames.Retrieval)),
|
||||
HistorianTransport.RemoteTcpIntegrated => (
|
||||
CreateMdasNetTcpWindowsBinding(timeout),
|
||||
CreateEndpointAddress(options.Host, options.Port, HistorianWcfServiceNames.HistoryIntegrated),
|
||||
CreateMdasNetTcpBinding(timeout),
|
||||
CreateEndpointAddress(options.Host, options.Port, HistorianWcfServiceNames.Retrieval)),
|
||||
HistorianTransport.RemoteTcpCertificate => (
|
||||
CreateMdasNetTcpCertificateBinding(timeout),
|
||||
CreateEndpointAddress(options.Host, options.Port, HistorianWcfServiceNames.HistoryCertificate),
|
||||
CreateMdasNetTcpBinding(timeout),
|
||||
CreateEndpointAddress(options.Host, options.Port, HistorianWcfServiceNames.Retrieval)),
|
||||
_ => throw new NotSupportedException($"Transport {options.Transport} is not supported.")
|
||||
};
|
||||
}
|
||||
|
||||
public static EndpointAddress CreateEndpointAddress(string host, int port, string serviceName)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(host);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(serviceName);
|
||||
|
||||
return new EndpointAddress($"{Scheme}://{host}:{port}/{serviceName}");
|
||||
}
|
||||
|
||||
public static EndpointAddress CreatePipeEndpointAddress(string host, string serviceName)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(host);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(serviceName);
|
||||
|
||||
return new EndpointAddress($"net.pipe://{host}/{serviceName}");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user