b80ac07942
Document the WCF-vs-gRPC surface and fix a stale claim. - README: add a "Transport matrix (WCF vs gRPC)" section with a per-operation table. Mark the config ops the gRPC server exposes-but-untooled with a distinct legend state (recovered RPC + bytes buffer named) vs genuinely unavailable, so "not tooled" is not conflated with "not possible". - README: document the gRPC live-test env vars (HISTORIAN_GRPC_HOST/_PORT/_TLS/ _DNSID/_TIMEOUT/_WRITE_SANDBOX_TAG) and refresh the Status section (test count + the live-verified gRPC surface). - The "gRPC requires VerifyServerInterfaceVersion=false against a v12 server" note was obsolete: the gate already accepts History 11 AND 12 (AcceptedVersions), and the live gRPC suite runs with the default verification on. Corrected in the README, CLAUDE.md, and the HistorianServerVersionGate docstring. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC
117 lines
5.6 KiB
C#
117 lines
5.6 KiB
C#
namespace AVEVA.Historian.Client;
|
|
|
|
/// <summary>
|
|
/// Identifies a versioned native Historian service interface whose reported interface
|
|
/// version is validated at connect time by <see cref="HistorianServerVersionGate"/>.
|
|
/// </summary>
|
|
internal enum HistorianServiceInterface
|
|
{
|
|
History,
|
|
Retrieval,
|
|
Status,
|
|
Transaction
|
|
}
|
|
|
|
/// <summary>
|
|
/// Fail-closed check (roadmap item R0.6) that a Historian server reports the native
|
|
/// interface version this SDK's byte serializers were built against.
|
|
///
|
|
/// The opaque native buffers carried inside the WCF/MDAS message body — and, on 2023 R2,
|
|
/// inside the gRPC <c>bytes</c> fields — are framed per native interface version. Parsing
|
|
/// them against an unexpected version risks silent misinterpretation, so per the
|
|
/// "version-pin, fail closed" principle this throws <see cref="ProtocolEvidenceMissingException"/>
|
|
/// rather than best-effort parsing.
|
|
///
|
|
/// Supported versions are evidence-based, discovered from a live AVEVA Historian 2020
|
|
/// server (product 20.0.000) via the reverse-engineering <c>wcf-probe</c> command:
|
|
/// <list type="bullet">
|
|
/// <item>History (<c>Hist</c>) interface version = 11</item>
|
|
/// <item>Retrieval (<c>Retr</c>) interface version = 4</item>
|
|
/// <item>Transaction (<c>Trx</c>) interface version = 2</item>
|
|
/// </list>
|
|
/// The Status (<c>Stat</c>) service's <c>GetInterfaceVersion</c> returns 0 (not a real
|
|
/// version), so the Status interface is validated for reachability only, never value.
|
|
///
|
|
/// A 2023 R2 gRPC server reports History interface version 12 even though it carries the
|
|
/// same proven 2020 native buffers. That value is captured and accepted (see
|
|
/// <see cref="AcceptedVersions"/>), so a v12 server passes with the default
|
|
/// <see cref="HistorianClientOptions.VerifyServerInterfaceVersion"/>=<see langword="true"/>;
|
|
/// the opt-out is only a safety valve for some future, not-yet-captured interface integer.
|
|
/// </summary>
|
|
internal static class HistorianServerVersionGate
|
|
{
|
|
public const uint HistoryInterfaceVersion = 11;
|
|
public const uint RetrievalInterfaceVersion = 4;
|
|
public const uint TransactionInterfaceVersion = 2;
|
|
|
|
/// <summary>
|
|
/// The 2023 R2 gRPC HistoryService reports interface version 12. It is buffer-compatible with
|
|
/// the 2020 version 11 — the OpenConnection3 v6 / token / DataQueryRequest / row buffers are
|
|
/// byte-identical — confirmed by a live end-to-end gRPC read against a real 2023 R2 server
|
|
/// (2026-06-21). So both 11 and 12 are accepted for History. (Retrieval reported 4, matching
|
|
/// the 2020 value, so it needs no widening.)
|
|
/// </summary>
|
|
public const uint HistoryInterfaceVersionGrpc2023R2 = 12;
|
|
|
|
/// <summary>
|
|
/// True when the service interface reports a meaningful version that should be matched.
|
|
/// Status is reachability-only (its <c>GetInterfaceVersion</c> returns 0).
|
|
/// </summary>
|
|
public static bool IsValueGated(HistorianServiceInterface service) => service switch
|
|
{
|
|
HistorianServiceInterface.History => true,
|
|
HistorianServiceInterface.Retrieval => true,
|
|
HistorianServiceInterface.Transaction => true,
|
|
HistorianServiceInterface.Status => false,
|
|
_ => false
|
|
};
|
|
|
|
/// <summary>The canonical interface version this SDK's serializers target for a value-gated service.</summary>
|
|
public static uint ExpectedVersion(HistorianServiceInterface service) => service switch
|
|
{
|
|
HistorianServiceInterface.History => HistoryInterfaceVersion,
|
|
HistorianServiceInterface.Retrieval => RetrievalInterfaceVersion,
|
|
HistorianServiceInterface.Transaction => TransactionInterfaceVersion,
|
|
_ => throw new ArgumentOutOfRangeException(nameof(service), service, "Service interface is not value-gated.")
|
|
};
|
|
|
|
/// <summary>
|
|
/// All interface versions accepted for a value-gated service. Usually a single value, but
|
|
/// History accepts both the 2020 value (11) and the buffer-compatible 2023 R2 gRPC value (12).
|
|
/// </summary>
|
|
public static uint[] AcceptedVersions(HistorianServiceInterface service) => service switch
|
|
{
|
|
HistorianServiceInterface.History => [HistoryInterfaceVersion, HistoryInterfaceVersionGrpc2023R2],
|
|
HistorianServiceInterface.Retrieval => [RetrievalInterfaceVersion],
|
|
HistorianServiceInterface.Transaction => [TransactionInterfaceVersion],
|
|
_ => throw new ArgumentOutOfRangeException(nameof(service), service, "Service interface is not value-gated.")
|
|
};
|
|
|
|
/// <summary>
|
|
/// Throws <see cref="ProtocolEvidenceMissingException"/> when version verification is enabled
|
|
/// and the server's reported interface version differs from the version this SDK targets.
|
|
/// No-op when <see cref="HistorianClientOptions.VerifyServerInterfaceVersion"/> is
|
|
/// <see langword="false"/>, when the service is not value-gated (Status), or on a match.
|
|
/// </summary>
|
|
public static void Validate(HistorianServiceInterface service, uint reportedVersion, HistorianClientOptions options)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(options);
|
|
|
|
if (!options.VerifyServerInterfaceVersion || !IsValueGated(service))
|
|
{
|
|
return;
|
|
}
|
|
|
|
uint[] accepted = AcceptedVersions(service);
|
|
if (Array.IndexOf(accepted, reportedVersion) >= 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
string acceptedList = string.Join(", ", accepted);
|
|
throw new ProtocolEvidenceMissingException(
|
|
$"{service} interface version {reportedVersion} (this SDK's serializers target version {acceptedList}); " +
|
|
$"set {nameof(HistorianClientOptions)}.{nameof(HistorianClientOptions.VerifyServerInterfaceVersion)}=false to bypass at your own risk");
|
|
}
|
|
}
|