diff --git a/src/AVEVA.Historian.Client/Grpc/HistorianGrpcEventOrchestrator.cs b/src/AVEVA.Historian.Client/Grpc/HistorianGrpcEventOrchestrator.cs index a61e88c..756685b 100644 --- a/src/AVEVA.Historian.Client/Grpc/HistorianGrpcEventOrchestrator.cs +++ b/src/AVEVA.Historian.Client/Grpc/HistorianGrpcEventOrchestrator.cs @@ -58,6 +58,12 @@ internal sealed class HistorianGrpcEventOrchestrator /// Diagnostic: type+code description of the most recent error/terminal buffer. public string LastErrorBufferDescription { get; private set; } = string.Empty; + /// Diagnostic: hex of the most recent result buffer (first 48 bytes). + public string LastResultBufferHex { get; private set; } = string.Empty; + + /// Diagnostic: hex of the most recent GetNext error buffer. + public string LastErrorBufferHex { get; private set; } = string.Empty; + public async IAsyncEnumerable ReadEventsAsync( DateTime startUtc, DateTime endUtc, @@ -206,59 +212,55 @@ internal sealed class HistorianGrpcEventOrchestrator { var historyClient = new GrpcHistory.HistoryService.HistoryServiceClient(connection.Channel); var statusClient = new GrpcStatus.StatusService.StatusServiceClient(connection.Channel); - var retrievalClient = new GrpcRetrieval.RetrievalService.RetrievalServiceClient(connection.Channel); - var transactionClient = new GrpcTransaction.TransactionService.TransactionServiceClient(connection.Channel); - - // Discovery dance the native event flow runs between Open2 and EnsT2. All bounded by the - // short RegistrationDeadline (several stall server-side on the remote box). - TryRun(() => statusClient.GetStatusInterfaceVersion(new GrpcStatus.GetStatusInterfaceVersionRequest(), connection.Metadata, RegistrationDeadline(), cancellationToken)); - TryRun(() => statusClient.GetStatusInterfaceVersion(new GrpcStatus.GetStatusInterfaceVersionRequest(), connection.Metadata, RegistrationDeadline(), cancellationToken)); - - byte[] historianVersionRequest = HistorianEventRegistrationProtocol.BuildGetHistorianInfoRequest("HistorianVersion"); - TryRun(() => statusClient.GetHistorianInfo( - new GrpcStatus.GetHistorianInfoRequest { StrHandle = session.StringHandle, BtRequest = ByteString.CopyFrom(historianVersionRequest) }, - connection.Metadata, RegistrationDeadline(), cancellationToken)); - TryRun(() => statusClient.GetHistorianInfo( - new GrpcStatus.GetHistorianInfoRequest { StrHandle = session.StringHandle, BtRequest = ByteString.CopyFrom(historianVersionRequest) }, - connection.Metadata, RegistrationDeadline(), cancellationToken)); + // Native 2023 R2 gRPC event-connection registration sequence (captured order): + // UpdateClientStatus -> RegisterTags(CM_EVENT) -> EnsureTags(CM_EVENT) -> GetHistorianInfo + // -> GetSystemParameter x7. (StartEventQuery follows in RunEventQuery.) The 2020-WCF-era extra + // probes (cross-service GetV, params-before-register) are NOT in the gRPC event flow. byte[] clientStatus = HistorianEventRegistrationProtocol.BuildUpdateClientStatusBlob(); TryRun(() => historyClient.UpdateClientStatus( new GrpcHistory.UpdateClientStatusRequest { StrHandle = session.StringHandle, BtClientStatus = ByteString.CopyFrom(clientStatus) }, connection.Metadata, RegistrationDeadline(), cancellationToken)); - // Records 11-16: 6 system-parameter queries before RTag2. - foreach (string parameterName in HistorianEventRegistrationProtocol.StatusParametersBeforeRegister) + byte[] registerBuffer = HistorianEventRegistrationProtocol.BuildRegisterCmEventInputBuffer(); + try + { + GrpcHistory.RegisterTagsResponse rt = historyClient.RegisterTags( + new GrpcHistory.RegisterTagsRequest { StrHandle = session.StringHandle, BtTagInfos = ByteString.CopyFrom(registerBuffer) }, + connection.Metadata, RegistrationDeadline(), cancellationToken); + RegistrationDiag += $"RTag={rt.Status?.BSuccess} e={Convert.ToHexString(rt.Status?.BtError?.ToByteArray() ?? [])}; "; + } + catch (Exception ex) { RegistrationDiag += $"RTag=EX:{ex.GetType().Name}; "; } + + // gRPC CM_EVENT EnsureTags uses the 86-byte native format (8-byte header + the …2f27 event-type + // GUID), NOT the 2020 WCF CTagMetadata. + byte[] payload = HistorianAddTagsProtocol.SerializeCmEventEnsureTagsGrpc(DateTime.UtcNow); + try + { + GrpcHistory.EnsureTagsResponse et = historyClient.EnsureTags( + new GrpcHistory.EnsureTagsRequest { StrHandle = session.StringHandle, BtTagInfos = ByteString.CopyFrom(payload), ElementCount = 1 }, + connection.Metadata, RegistrationDeadline(), cancellationToken); + RegistrationDiag += $"EnsT={et.Status?.BSuccess} e={Convert.ToHexString(et.Status?.BtError?.ToByteArray() ?? [])} out={Convert.ToHexString(et.BtTagStatus?.ToByteArray() ?? [])}; "; + } + catch (Exception ex) { RegistrationDiag += $"EnsT=EX:{ex.GetType().Name}; "; } + + byte[] historianVersionRequest = HistorianEventRegistrationProtocol.BuildGetHistorianInfoRequest("HistorianVersion"); + TryRun(() => statusClient.GetHistorianInfo( + new GrpcStatus.GetHistorianInfoRequest { StrHandle = session.StringHandle, BtRequest = ByteString.CopyFrom(historianVersionRequest) }, + connection.Metadata, RegistrationDeadline(), cancellationToken)); + + string[] eventParams = ["AllowOriginals", "HistorianPartner", "HistorianVersion", "MaxCyclicStorageTimeout", "RealTimeWindow", "FutureTimeThreshold", "AllowRenameTags"]; + foreach (string parameterName in eventParams) { TryRun(() => statusClient.GetSystemParameter( new GrpcStatus.GetSystemParameterRequest { UiHandle = session.ClientHandle, StrParameterName = parameterName }, connection.Metadata, RegistrationDeadline(), cancellationToken)); } - - byte[] registerBuffer = HistorianEventRegistrationProtocol.BuildRegisterCmEventInputBuffer(); - TryRun(() => historyClient.RegisterTags( - new GrpcHistory.RegisterTagsRequest { StrHandle = session.StringHandle, BtTagInfos = ByteString.CopyFrom(registerBuffer) }, - connection.Metadata, RegistrationDeadline(), cancellationToken)); - - // Record 18: one more system-parameter query after RTag2 before EnsT2. - TryRun(() => statusClient.GetSystemParameter( - new GrpcStatus.GetSystemParameterRequest { UiHandle = session.ClientHandle, StrParameterName = "AllowRenameTags" }, - connection.Metadata, RegistrationDeadline(), cancellationToken)); - - // Records 19-21: cross-service version probes between RTag2 and EnsT2 (session-table registration). - TryRun(() => transactionClient.GetTransactionInterfaceVersion(new GrpcTransaction.GetTransactionInterfaceVersionRequest(), connection.Metadata, RegistrationDeadline(), cancellationToken)); - TryRun(() => statusClient.GetStatusInterfaceVersion(new GrpcStatus.GetStatusInterfaceVersionRequest(), connection.Metadata, RegistrationDeadline(), cancellationToken)); - TryRun(() => retrievalClient.GetRetrievalInterfaceVersion(new GrpcRetrieval.GetRetrievalInterfaceVersionRequest(), connection.Metadata, RegistrationDeadline(), cancellationToken)); - - // gRPC CM_EVENT EnsureTags uses the 86-byte native format (8-byte header + the …2f27 event-type - // GUID), NOT the 2020 WCF CTagMetadata — required for the server to establish CM_EVENT so the - // event query returns rows. - byte[] payload = HistorianAddTagsProtocol.SerializeCmEventEnsureTagsGrpc(DateTime.UtcNow); - TryRun(() => historyClient.EnsureTags( - new GrpcHistory.EnsureTagsRequest { StrHandle = session.StringHandle, BtTagInfos = ByteString.CopyFrom(payload), ElementCount = 1 }, - connection.Metadata, RegistrationDeadline(), cancellationToken)); } + /// Diagnostic: outcomes of the key CM_EVENT registration RPCs. + public string RegistrationDiag { get; private set; } = string.Empty; + private List RunEventQuery( HistorianGrpcConnection connection, HistorianGrpcHandshake.Session session, @@ -280,7 +282,7 @@ internal sealed class HistorianGrpcEventOrchestrator IReadOnlyList attempts = HistorianEventQueryProtocol.CreateStartEventQueryAttempts( startUtc.ToUniversalTime(), endUtc.ToUniversalTime(), - eventCount: 5, + eventCount: 100, filter, version: 6); byte[] requestBuffer = attempts[0].RequestBuffer; @@ -304,6 +306,7 @@ internal sealed class HistorianGrpcEventOrchestrator } uint queryHandle = startResponse.UiQueryHandle; + RegistrationDiag += $"QH={queryHandle} clientH={session.ClientHandle} SEQresp={Convert.ToHexString(startResponse.BtResonse?.ToByteArray() ?? [])}; "; try { List events = []; @@ -339,6 +342,8 @@ internal sealed class HistorianGrpcEventOrchestrator LastResultBufferLength = resultBuffer.Length; LastErrorBufferDescription = HistorianEventRegistrationProtocol.DescribeNativeError(errorBuffer); + LastResultBufferHex = Convert.ToHexString(resultBuffer.Length <= 48 ? resultBuffer : resultBuffer[..48]); + LastErrorBufferHex = Convert.ToHexString(errorBuffer); // Any 5-byte type=4 error is a soft terminal (code 30 NoMoreData is canonical; code // 85 / 0x55 is the missing-registration signal seen on early runs). Mirror the WCF diff --git a/src/AVEVA.Historian.Client/Grpc/HistorianGrpcHandshake.cs b/src/AVEVA.Historian.Client/Grpc/HistorianGrpcHandshake.cs index 0cbe42c..5307ab7 100644 --- a/src/AVEVA.Historian.Client/Grpc/HistorianGrpcHandshake.cs +++ b/src/AVEVA.Historian.Client/Grpc/HistorianGrpcHandshake.cs @@ -63,23 +63,30 @@ internal static class HistorianGrpcHandshake new GrpcHistory.GetInterfaceVersionRequest(), connection.Metadata, Deadline(), cancellationToken); HistorianServerVersionGate.Validate(HistorianServiceInterface.History, historyVersion.UiVersion, options); - var storageClient = new GrpcStorage.StorageService.StorageServiceClient(connection.Channel); - HistorianNativeHandshake.RunTokenRounds( - (handle, wrapped, _) => - { - GrpcStorage.ValidateClientCredentialResponse response = storageClient.ValidateClientCredential( - new GrpcStorage.ValidateClientCredentialRequest { Handle = handle, InBuff = ByteString.CopyFrom(wrapped) }, - connection.Metadata, - Deadline(), - cancellationToken); - byte[] serverOutput = response.OutBuff?.ToByteArray() ?? []; - byte[] error = response.Status?.BtError?.ToByteArray() ?? []; - bool success = response.Status?.BSuccess ?? false; - return new HistorianNativeHandshake.TokenExchangeResult(success, serverOutput, error); - }, - contextKey, - options, - cancellationToken); + // The v6 (read/write) path authenticates via StorageService.ValidateClientCredential (Negotiate). + // The v8 EVENT path authenticates entirely via ExchangeKey (ECDH) + the RC4 credential token — + // the native client does NOT run ValidateClientCredential for an event connection, and doing so + // establishes a different session scope under which the event query returns zero rows. So skip it. + if (!eventConnection) + { + var storageClient = new GrpcStorage.StorageService.StorageServiceClient(connection.Channel); + HistorianNativeHandshake.RunTokenRounds( + (handle, wrapped, _) => + { + GrpcStorage.ValidateClientCredentialResponse response = storageClient.ValidateClientCredential( + new GrpcStorage.ValidateClientCredentialRequest { Handle = handle, InBuff = ByteString.CopyFrom(wrapped) }, + connection.Metadata, + Deadline(), + cancellationToken); + byte[] serverOutput = response.OutBuff?.ToByteArray() ?? []; + byte[] error = response.Status?.BtError?.ToByteArray() ?? []; + bool success = response.Status?.BSuccess ?? false; + return new HistorianNativeHandshake.TokenExchangeResult(success, serverOutput, error); + }, + contextKey, + options, + cancellationToken); + } // Event reads require an Event-type connection (ConnectionType=Event), which only the native // v8 OpenConnection format carries — the v6 buffer has no such field. The v8 path authenticates diff --git a/tests/AVEVA.Historian.Client.Tests/HistorianGrpcIntegrationTests.cs b/tests/AVEVA.Historian.Client.Tests/HistorianGrpcIntegrationTests.cs index 63e0517..549cea1 100644 --- a/tests/AVEVA.Historian.Client.Tests/HistorianGrpcIntegrationTests.cs +++ b/tests/AVEVA.Historian.Client.Tests/HistorianGrpcIntegrationTests.cs @@ -533,7 +533,7 @@ public sealed class HistorianGrpcIntegrationTests string outcome; try { - await foreach (HistorianEvent evt in orch.ReadEventsAsync(DateTime.UtcNow.AddDays(-30), DateTime.UtcNow, null, CancellationToken.None)) + await foreach (HistorianEvent evt in orch.ReadEventsAsync(DateTime.UtcNow.AddDays(-90), DateTime.UtcNow, null, CancellationToken.None)) { events.Add(evt); if (events.Count >= 3) { break; } @@ -546,7 +546,8 @@ public sealed class HistorianGrpcIntegrationTests } throw new Xunit.Sdk.XunitException( - $"[DIAG] outcome={outcome} | events={events.Count} | LastResultLen={orch.LastResultBufferLength} | LastErr='{orch.LastErrorBufferDescription}'"); + $"[DIAG] outcome={outcome} | events={events.Count} | LastResultLen={orch.LastResultBufferLength} " + + $"| ResultHex={orch.LastResultBufferHex} | ErrHex={orch.LastErrorBufferHex} | Reg=[{orch.RegistrationDiag}]"); } [Fact]