using Google.Protobuf;
using Grpc.Core;
using AVEVA.Historian.Client.Models;
using AVEVA.Historian.Client.Wcf;
using GrpcStatus = ArchestrA.Grpc.Contract.Status;
namespace AVEVA.Historian.Client.Grpc;
///
/// 2023 R2 gRPC status client (roadmap item R0.3). Mirrors
/// over the gRPC transport: it opens an authenticated
/// History session via and queries the StatusService for the
/// resulting client handle. GetSystemParameter carries the parameter name as a protobuf
/// string and returns the value string directly — there is no opaque native buffer to decode.
///
internal static class HistorianGrpcStatusClient
{
public static Task GetSystemParameterAsync(
HistorianClientOptions options,
string parameterName,
CancellationToken cancellationToken)
{
ArgumentException.ThrowIfNullOrWhiteSpace(parameterName);
return Task.Run(() => GetSystemParameter(options, parameterName, cancellationToken), cancellationToken);
}
private static string? GetSystemParameter(HistorianClientOptions options, string parameterName, CancellationToken cancellationToken)
{
using HistorianGrpcConnection connection = HistorianGrpcChannelFactory.Create(options);
uint clientHandle = HistorianGrpcHandshake.OpenAuthenticatedConnection(connection, options, cancellationToken);
var statusClient = new GrpcStatus.StatusService.StatusServiceClient(connection.Channel);
GrpcStatus.GetSystemParameterResponse response = statusClient.GetSystemParameter(
new GrpcStatus.GetSystemParameterRequest { UiHandle = clientHandle, StrParameterName = parameterName },
connection.Metadata,
DateTime.UtcNow.Add(options.RequestTimeout),
cancellationToken);
return (response.Status?.BSuccess ?? false) ? response.StrParameterValue : null;
}
///
/// Returns a measured store-forward status over the 2023 R2 gRPC transport (R4.3).
///
/// Unlike the non-gRPC path — which synthesizes an
/// all-false result without contacting the server — this opens an authenticated session
/// and calls StatusService.GetHistorianConsoleStatus, the only SF-adjacent signal reachable
/// from a pure managed client. (The direct StorageService.GetSFParameter /
/// GetRemainingSnapshotsSize RPCs that carry the SF buffer magnitude require the
/// OpenStorageConnection storage-engine console handle, which is gated behind the D2
/// storage-engine-pipe wall and is unobtainable here — see
/// docs/plans/store-forward-cache-reverse-engineering.md §9.7.)
///
///
/// Semantics: a successful console-status read means the server is reachable and its storage
/// console is reporting normally ⇒ the not-storing baseline (all flags false), but now
/// measured rather than blindly assumed. If the server cannot be reached/authenticated,
/// or the console-status call itself fails,
/// is set with the underlying error. The active-SF state (
/// / /
/// magnitude) is NOT observable from this signal and remains false; populating it requires the
/// D2-gated storage-console path.
///
///
public static Task GetStoreForwardStatusAsync(
HistorianClientOptions options,
CancellationToken cancellationToken)
=> Task.Run(() => GetStoreForwardStatus(options, cancellationToken), cancellationToken);
private static HistorianStoreForwardStatus GetStoreForwardStatus(HistorianClientOptions options, CancellationToken cancellationToken)
{
HistorianStoreForwardStatus NotStoring(bool errorOccurred, string? error) => new(
ServerName: options.Host,
Pending: false,
ErrorOccurred: errorOccurred,
Error: error,
DataStored: false,
Storing: false,
ConnectionKind: HistorianConnectionKind.Process);
try
{
using HistorianGrpcConnection connection = HistorianGrpcChannelFactory.Create(options);
HistorianGrpcHandshake.Session session = HistorianGrpcHandshake.OpenSession(connection, options, cancellationToken);
var statusClient = new GrpcStatus.StatusService.StatusServiceClient(connection.Channel);
GrpcStatus.GetHistorianConsoleStatusResponse response = statusClient.GetHistorianConsoleStatus(
new GrpcStatus.GetHistorianConsoleStatusRequest { StrHandle = session.StringHandle },
connection.Metadata,
DateTime.UtcNow.Add(options.RequestTimeout),
cancellationToken);
if (response.Status?.BSuccess ?? false)
{
// Measured: server reachable, storage console reporting normally → not-storing baseline.
return NotStoring(errorOccurred: false, error: null);
}
byte[] err = response.Status?.BtError?.ToByteArray() ?? [];
string detail = err.Length == 0 ? "GetHistorianConsoleStatus returned failure." : Convert.ToHexString(err);
return NotStoring(errorOccurred: true, error: $"GetHistorianConsoleStatus failed: {detail}");
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception ex)
{
// Server unreachable / auth failed — genuinely measured: report it instead of a silent all-false.
return NotStoring(errorOccurred: true, error: $"{ex.GetType().Name}: {ex.Message}");
}
}
///
/// Returns a measured connection status over the 2023 R2 gRPC transport (plan #5). Mirrors
/// 's synthesize-from-handshake approach: it opens an
/// authenticated session and reports /
/// from whether the handshake
/// (GetInterfaceVersion → ValidateClientCredential token loop → OpenConnection, which yields the
/// storage-session GUID) succeeds. There is no dedicated connection-status RPC on either transport.
/// Store-forward connectivity is not observable here (D2-gated) and stays false.
///
public static Task GetConnectionStatusAsync(
HistorianClientOptions options,
CancellationToken cancellationToken)
=> Task.Run(() => GetConnectionStatus(options, cancellationToken), cancellationToken);
private static HistorianConnectionStatus GetConnectionStatus(HistorianClientOptions options, CancellationToken cancellationToken)
{
bool connected;
string? error = null;
try
{
using HistorianGrpcConnection connection = HistorianGrpcChannelFactory.Create(options);
// A successful OpenConnection yields a non-empty storage-session GUID — proof the server and
// its storage session are reachable, the gRPC analog of the WCF handshake probe.
HistorianGrpcHandshake.Session session = HistorianGrpcHandshake.OpenSession(connection, options, cancellationToken);
connected = session.StorageSessionId != Guid.Empty;
if (!connected)
{
error = "OpenConnection returned an empty storage-session handle.";
}
}
catch (OperationCanceledException)
{
throw;
}
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);
}
///
/// Reads the Historian server's system time-zone name (roadmap item R1.3,
/// StatusService.GetSystemTimeZoneName). Unlike the 2020 WCF surface — where the native
/// GetSystemTimeZoneName is a client-side stub that returns an empty string — the 2023 R2
/// gRPC front door returns the real Windows time-zone display name (live-verified:
/// "Eastern Daylight Time"). Takes the transient uint client handle; the response carries
/// the value as a protobuf string with no opaque buffer to decode.
///
public static Task GetSystemTimeZoneNameAsync(
HistorianClientOptions options,
CancellationToken cancellationToken)
=> Task.Run(() => GetSystemTimeZoneName(options, cancellationToken), cancellationToken);
private static string? GetSystemTimeZoneName(HistorianClientOptions options, CancellationToken cancellationToken)
{
using HistorianGrpcConnection connection = HistorianGrpcChannelFactory.Create(options);
uint clientHandle = HistorianGrpcHandshake.OpenAuthenticatedConnection(connection, options, cancellationToken);
var statusClient = new GrpcStatus.StatusService.StatusServiceClient(connection.Channel);
GrpcStatus.GetSystemTimeZoneNameResponse response = statusClient.GetSystemTimeZoneName(
new GrpcStatus.GetSystemTimeZoneNameRequest { UiHandle = clientHandle },
connection.Metadata,
DateTime.UtcNow.Add(options.RequestTimeout),
cancellationToken);
if (!(response.Status?.BSuccess ?? false))
{
return null;
}
string? value = response.StrSystemTimeZoneName;
return string.IsNullOrEmpty(value) ? null : value;
}
///
/// Reads a Historian runtime parameter over gRPC (StatusService.GetRuntimeParameter).
/// The request/response byte buffers are the proven 2020 GETRP wire format
/// () carried unchanged inside the protobuf
/// btRequest/btResponse fields; the op keys on the uppercase string session handle.
///
public static Task GetRuntimeParameterAsync(
HistorianClientOptions options,
string parameterName,
CancellationToken cancellationToken)
{
ArgumentException.ThrowIfNullOrWhiteSpace(parameterName);
return Task.Run(() => GetRuntimeParameter(options, parameterName, cancellationToken), cancellationToken);
}
private static string? GetRuntimeParameter(HistorianClientOptions options, string parameterName, CancellationToken cancellationToken)
{
using HistorianGrpcConnection connection = HistorianGrpcChannelFactory.Create(options);
HistorianGrpcHandshake.Session session = HistorianGrpcHandshake.OpenSession(connection, options, cancellationToken);
byte[] request = HistorianRuntimeParameterProtocol.SerializeRequest(parameterName);
var statusClient = new GrpcStatus.StatusService.StatusServiceClient(connection.Channel);
GrpcStatus.GetRuntimeParameterResponse response = statusClient.GetRuntimeParameter(
new GrpcStatus.GetRuntimeParameterRequest
{
StrHandle = session.StringHandle,
BtRequest = ByteString.CopyFrom(request)
},
connection.Metadata,
DateTime.UtcNow.Add(options.RequestTimeout),
cancellationToken);
if (!(response.Status?.BSuccess ?? false))
{
return null;
}
byte[] responseBuffer = response.BtResponse?.ToByteArray() ?? [];
return HistorianRuntimeParameterProtocol.ParseSingleStringResult(responseBuffer);
}
}