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,118 @@
|
||||
using System.Runtime.Versioning;
|
||||
using System.ServiceModel;
|
||||
using System.ServiceModel.Channels;
|
||||
using AVEVA.Historian.Client.Models;
|
||||
using AVEVA.Historian.Client.Wcf.Contracts;
|
||||
|
||||
namespace AVEVA.Historian.Client.Wcf;
|
||||
|
||||
[SupportedOSPlatform("windows")]
|
||||
internal static class HistorianWcfStatusClient
|
||||
{
|
||||
public static Task<string?> GetSystemParameterAsync(
|
||||
HistorianClientOptions options,
|
||||
string parameterName,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(parameterName);
|
||||
return Task.Run(() => GetSystemParameter(options, parameterName), cancellationToken);
|
||||
}
|
||||
|
||||
public static Task<HistorianConnectionStatus> GetConnectionStatusAsync(
|
||||
HistorianClientOptions options,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.Run(() => SynthesizeConnectionStatus(options), cancellationToken);
|
||||
}
|
||||
|
||||
public static Task<HistorianStoreForwardStatus> GetStoreForwardStatusAsync(
|
||||
HistorianClientOptions options,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.Run(() => SynthesizeStoreForwardStatus(options), cancellationToken);
|
||||
}
|
||||
|
||||
private static string? GetSystemParameter(HistorianClientOptions options, string parameterName)
|
||||
{
|
||||
Guid contextKey = Guid.NewGuid();
|
||||
var (histBinding, histEndpoint, _, _) = HistorianWcfBindingFactory.CreateBindingPair(options);
|
||||
EndpointAddress statusEndpoint = HistorianWcfBindingFactory.CreatePipeEndpointAddress(options.Host, HistorianWcfServiceNames.Status);
|
||||
|
||||
string? value = null;
|
||||
HistorianWcfAuthChainHelper.OpenAuthenticatedConnection(
|
||||
options, histBinding, histEndpoint, contextKey, CancellationToken.None,
|
||||
additionalSetup: (_, context) => value = QuerySystemParameter(histBinding, statusEndpoint, context.ClientHandle, parameterName));
|
||||
return value;
|
||||
}
|
||||
|
||||
private static string? QuerySystemParameter(Binding statusBinding, EndpointAddress statusEndpoint, uint clientHandle, string parameterName)
|
||||
{
|
||||
ChannelFactory<IStatusServiceContract2> factory = new(statusBinding, statusEndpoint);
|
||||
IStatusServiceContract2 channel = factory.CreateChannel();
|
||||
ICommunicationObject co = (ICommunicationObject)channel;
|
||||
try
|
||||
{
|
||||
bool ok = channel.GetSystemParameter(clientHandle, parameterName, out string parameterValue, out _, out _);
|
||||
return ok ? parameterValue : null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
try { if (co.State == CommunicationState.Faulted) co.Abort(); else co.Close(); } catch { try { co.Abort(); } catch { } }
|
||||
try { if (factory.State == CommunicationState.Faulted) factory.Abort(); else factory.Close(); } catch { try { factory.Abort(); } catch { } }
|
||||
}
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// AVEVA's native <c>HistorianAccess.GetConnectionStatus</c> reads local C++
|
||||
/// <c>HistorianClient</c> state (no WCF op exists for it). We synthesize an equivalent
|
||||
/// by attempting an authenticated session open: a successful auth+open implies
|
||||
/// <c>ConnectedToServer = true</c>. Store-forward and partner-connection state are not
|
||||
/// observable from a single client probe and remain false.
|
||||
/// </remarks>
|
||||
private static HistorianConnectionStatus SynthesizeConnectionStatus(HistorianClientOptions options)
|
||||
{
|
||||
bool connected;
|
||||
string? error = null;
|
||||
try
|
||||
{
|
||||
Guid contextKey = Guid.NewGuid();
|
||||
var (histBinding, histEndpoint, _, _) = HistorianWcfBindingFactory.CreateBindingPair(options);
|
||||
HistorianWcfAuthChainHelper.OpenAuthenticatedConnection(
|
||||
options, histBinding, histEndpoint, contextKey, CancellationToken.None);
|
||||
connected = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
connected = false;
|
||||
error = $"{ex.GetType().Name}: {ex.Message}";
|
||||
}
|
||||
|
||||
return new HistorianConnectionStatus(
|
||||
ServerName: options.Host,
|
||||
Pending: false,
|
||||
ErrorOccurred: !connected,
|
||||
Error: error,
|
||||
ConnectedToServer: connected,
|
||||
ConnectedToServerStorage: connected,
|
||||
ConnectedToStoreForward: false,
|
||||
ConnectionKind: HistorianConnectionKind.Process);
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// Native <c>HistorianAccess.GetStoreForwardStatus</c> is also client-side state.
|
||||
/// Without a local store-forward sidecar to probe, we report defaults: not pending,
|
||||
/// no error, no data stored, not actively storing. Connection kind is Process by
|
||||
/// convention (event-only sessions are uncommon for this status helper).
|
||||
/// </remarks>
|
||||
private static HistorianStoreForwardStatus SynthesizeStoreForwardStatus(HistorianClientOptions options)
|
||||
{
|
||||
return new HistorianStoreForwardStatus(
|
||||
ServerName: options.Host,
|
||||
Pending: false,
|
||||
ErrorOccurred: false,
|
||||
Error: null,
|
||||
DataStored: false,
|
||||
Storing: false,
|
||||
ConnectionKind: HistorianConnectionKind.Process);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user