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); } }