1e9a87fce9
Stands up HistorianTransport.RemoteGrpc end-to-end for the read path, built on the recovered 2023 R2 gRPC contract (gRPC-Web/HTTP-1.1, port 32565, gzip). The opaque protobuf `bytes` fields carry the SAME native binary payloads as the 2020 WCF/MDAS path, so the proven serializers and parsers are reused unchanged. - Grpc/Protos/*.proto: 6 protoc-validated contracts recovered from embedded FileDescriptors (authoritative, not guessed). - Grpc/HistorianGrpcChannelFactory: GrpcWebHandler/HTTP-1.1 channel, ResolvePort/ResolveAddress, optional TLS + gzip. - Grpc/HistorianGrpcReadOrchestrator: mirrors the WCF read chain over gRPC; auth uses HistoryService.ExchangeKey (the gRPC ValCl op). - Wcf/HistorianNativeHandshake: transport-agnostic Open2 request builder + SSPI/Negotiate token loop + response decode, shared by WCF and gRPC. - Op map (2020 -> gRPC): ValCl->ExchangeKey, Open2->OpenConnection, StartQuery2->StartQuery, GetNextQueryResultBuffer2->GetNextQueryResultBuffer. - HistorianClientOptions: DefaultGrpcPort=32565, GrpcUseTls. - csproj: Google.Protobuf, Grpc.Net.Client(.Web), Grpc.Tools codegen. Not yet live-verified against a 2023 R2 server: ExchangeKey is the first thing to revisit if a live server rejects the handshake; the inner byte payloads are the proven 2020 protocol. Gated live test via HISTORIAN_GRPC_HOST. 188 unit tests green; build clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
93 lines
3.4 KiB
C#
93 lines
3.4 KiB
C#
using System.Net.Security;
|
|
using System.Security.Cryptography.X509Certificates;
|
|
using Grpc.Core;
|
|
using Grpc.Net.Client;
|
|
using Grpc.Net.Client.Web;
|
|
|
|
namespace AVEVA.Historian.Client.Grpc;
|
|
|
|
/// <summary>
|
|
/// Builds a <see cref="GrpcChannel"/> for the 2023 R2 Historian Client Access Point,
|
|
/// replicating the stock <c>Archestra.Historian.GrpcClient.GrpcClientBase.InitializeBase</c>
|
|
/// transport shape: gRPC-Web (binary) over HTTP/1.1, optional TLS with an
|
|
/// untrusted-certificate bypass, and gzip request encoding.
|
|
/// </summary>
|
|
internal static class HistorianGrpcChannelFactory
|
|
{
|
|
/// <summary>
|
|
/// Resolves the effective gRPC port: when the caller left <see cref="HistorianClientOptions.Port"/>
|
|
/// at the WCF default (32568), the 2023 R2 gRPC default (32565) is substituted; otherwise the
|
|
/// explicit value is honoured.
|
|
/// </summary>
|
|
internal static int ResolvePort(HistorianClientOptions options) =>
|
|
options.Port == HistorianClientOptions.DefaultPort ? HistorianClientOptions.DefaultGrpcPort : options.Port;
|
|
|
|
/// <summary>
|
|
/// Builds the channel address. TLS uses <c>https://{ServerDnsIdentity|Host}:{port}</c> (the
|
|
/// DNS-identity override lets the URL match the server certificate name when connecting by IP);
|
|
/// plaintext uses <c>http://{Host}:{port}</c>.
|
|
/// </summary>
|
|
internal static string ResolveAddress(HistorianClientOptions options)
|
|
{
|
|
int port = ResolvePort(options);
|
|
if (options.GrpcUseTls)
|
|
{
|
|
string tlsHost = !string.IsNullOrEmpty(options.ServerDnsIdentity) ? options.ServerDnsIdentity! : options.Host;
|
|
return $"https://{tlsHost}:{port}";
|
|
}
|
|
|
|
return $"http://{options.Host}:{port}";
|
|
}
|
|
|
|
public static HistorianGrpcConnection Create(HistorianClientOptions options)
|
|
{
|
|
string address = ResolveAddress(options);
|
|
|
|
var httpHandler = new HttpClientHandler();
|
|
if (options.AllowUntrustedServerCertificate)
|
|
{
|
|
httpHandler.ServerCertificateCustomValidationCallback = AcceptAnyCertificate;
|
|
}
|
|
|
|
// gRPC-Web binary mode over HTTP/1.1 — matches the stock client (GrpcWebMode.GrpcWeb,
|
|
// HttpVersion 1.1). The 2023 R2 HCAP endpoint speaks gRPC-Web, not bare HTTP/2 gRPC.
|
|
var webHandler = new GrpcWebHandler(GrpcWebMode.GrpcWeb, httpHandler)
|
|
{
|
|
HttpVersion = new Version(1, 1)
|
|
};
|
|
|
|
var channelOptions = new GrpcChannelOptions
|
|
{
|
|
HttpHandler = webHandler
|
|
};
|
|
|
|
GrpcChannel channel = GrpcChannel.ForAddress(address, channelOptions);
|
|
|
|
// The stock client always advertises gzip request encoding; honour the option so
|
|
// bandwidth-limited links can disable it.
|
|
var metadata = new Metadata();
|
|
if (options.Compression)
|
|
{
|
|
metadata.Add("grpc-internal-encoding-request", "gzip");
|
|
}
|
|
|
|
return new HistorianGrpcConnection(channel, metadata);
|
|
}
|
|
|
|
private static bool AcceptAnyCertificate(
|
|
HttpRequestMessage request,
|
|
X509Certificate2? certificate,
|
|
X509Chain? chain,
|
|
SslPolicyErrors errors) => true;
|
|
}
|
|
|
|
/// <summary>A live gRPC channel plus the per-call metadata header set.</summary>
|
|
internal sealed class HistorianGrpcConnection(GrpcChannel channel, Metadata metadata) : IDisposable
|
|
{
|
|
public GrpcChannel Channel { get; } = channel;
|
|
|
|
public Metadata Metadata { get; } = metadata;
|
|
|
|
public void Dispose() => Channel.Dispose();
|
|
}
|