diff --git a/docs/plans/revision-write-path.md b/docs/plans/revision-write-path.md index 85de6e6..e7b1ac2 100644 --- a/docs/plans/revision-write-path.md +++ b/docs/plans/revision-write-path.md @@ -169,15 +169,41 @@ finding what populates Trx's session table — likely: show up in the IL we've inspected (e.g., the `aahClientCommon.CClientCommon` calls during InitializeProxy) -A future session that wants to push further should: -1. Add `RTag2` for the sandbox tag and retry Begin2 — quick experiment -2. If that fails, try sending the IStorageServiceContract.AddT or - similar to "introduce" the tag to Trx -3. If that fails, do an IL walk of `aahClientCommon.CClientCommon` - methods called between Open2 and AddNonStreamValuesBegin in a - working native scenario (using a system tag the wrapper would - accept — or capturing actual on-wire bytes via the IL-rewrite - instrumentation if possible) +A future session that wants to push further should try (in order): + +1. ✅ **DONE 2026-05-05.** Add `RTag2(CM_EVENT tag id)` to the priming + chain — confirmed `RTag2` itself succeeds (returns 25-byte response), + but `AddNonStreamValuesBegin2` still fails with `UnknownClient`. + So RTag2 doesn't cascade client identity to Trx. +2. Try `IStorageServiceContract` ops (`AddT`, `AddTP`) on `/Storage` + — that endpoint isn't currently bound by our SDK but the contract + is declared in `Wcf/Contracts/IStorageServiceContract.cs`. Maybe + one of its ops registers the client with Trx as a side effect. +3. Decompile / IL-walk `aahClientCommon.CClientCommon` methods that + the native code calls between Open2 and AddNonStreamValuesBegin + to find any "client-with-Trx" registration we're missing. +4. As a last resort, decompile `aahClientAccessPoint.exe` (the server + binary) to find what populates Trx's session table — the answer + is in there, just not in the client surface. + +### Current state of the SDK-direct probe + +`HistorianWcfRevisionOrchestrator.ProbeBeginAsync` does: + +``` +Open2 (write-enabled, 0x401) + → priming (Stat.GetV ×2, Stat.GETHI ×2, UpdC3, 6× GetSystemParameter, + AllowRenameTags, Trx.GetV, Stat.GetV, Retr.GetV) + → RTag2(CM_EVENT tag id) // succeeds + → Trx.GetInterfaceVersion // succeeds, returns version 2 + → Trx.AddNonStreamValuesBegin2 ×4 // all four handle formats fail with + // 04 33 00 00 00 (UnknownClient 51) +``` + +The probe is committed as a gated test +(`HistorianWcfRevisionProbeTests.AddNonStreamValuesBegin_ProbeReturnsServerResult`) +that can be re-run any time to verify the gate is still where we think +it is, or to test future priming additions. ## Decision diff --git a/src/AVEVA.Historian.Client/Wcf/HistorianWcfRevisionOrchestrator.cs b/src/AVEVA.Historian.Client/Wcf/HistorianWcfRevisionOrchestrator.cs index 48a7c40..ae895dc 100644 --- a/src/AVEVA.Historian.Client/Wcf/HistorianWcfRevisionOrchestrator.cs +++ b/src/AVEVA.Historian.Client/Wcf/HistorianWcfRevisionOrchestrator.cs @@ -55,6 +55,29 @@ internal sealed class HistorianWcfRevisionOrchestrator EndpointAddress retrievalEndpoint = HistorianWcfBindingFactory.CreateAuxiliaryEndpointAddress(_options, HistorianWcfServiceNames.Retrieval); RunPrimingChain(historyChannel, context, auxBinding, statusEndpoint, transactionEndpoint, retrievalEndpoint); + // Hypothesis: calling RTag2 (RegisterTags2) cascades client identity into + // the Trx service's session table. The event flow uses RTag2 with the + // CM_EVENT tag id and subsequent ops succeed. Try RTag2 with that same + // tag id here as a registration probe. + try + { + string handle = context.StorageSessionId.ToString("D").ToUpperInvariant(); + byte[] rtag2Buffer = BuildRTag2CmEventInputBuffer(); + bool rtag2Ok = historyChannel.RegisterTags2( + handle: handle, + elementCount: 1, + inputBuffer: rtag2Buffer, + outputBuffer: out byte[] rtag2Out, + errorBuffer: out byte[] rtag2Err); + result.RTag2Succeeded = rtag2Ok; + result.RTag2OutHex = rtag2Out is null || rtag2Out.Length == 0 ? null : Convert.ToHexString(rtag2Out); + result.RTag2ErrorHex = rtag2Err is null || rtag2Err.Length == 0 ? null : Convert.ToHexString(rtag2Err); + } + catch (Exception ex) + { + result.RTag2Exception = $"{ex.GetType().Name}: {ex.Message}"; + } + ChannelFactory trxFactory = new(auxBinding, transactionEndpoint); HistorianWcfClientCredentialsHelper.Configure(trxFactory, _options); ITransactionServiceContract2 trxChannel = trxFactory.CreateChannel(); @@ -176,6 +199,22 @@ internal sealed class HistorianWcfRevisionOrchestrator } } + /// Same 24-byte RTag2 buffer the event flow uses (CM_EVENT tag id). + private static byte[] BuildRTag2CmEventInputBuffer() + { + byte[] buffer = new byte[24]; + buffer[0] = 0x50; + buffer[1] = 0x67; + buffer[2] = 0x02; + buffer[3] = 0x00; + BinaryPrimitives.WriteUInt32LittleEndian(buffer.AsSpan(4, 4), 1u); + // CM_EVENT tag id — duplicated here to avoid a cross-class dependency on the + // event orchestrator. Verify against HistorianWcfEventOrchestrator.CmEventTagId + // if the value ever needs updating. + new Guid("353b8145-5df0-4d46-a253-871aef49b321").ToByteArray().CopyTo(buffer.AsSpan(8, 16)); + return buffer; + } + private static byte[] BuildUpdC3ClientStatusBlob() { byte[] blob = new byte[81]; @@ -219,6 +258,10 @@ internal sealed class HistorianRevisionProbeResult public string? BeginErrorHex { get; set; } public string? BeginException { get; set; } public List BeginAttempts { get; } = new(); + public bool RTag2Succeeded { get; set; } + public string? RTag2OutHex { get; set; } + public string? RTag2ErrorHex { get; set; } + public string? RTag2Exception { get; set; } } internal sealed class HistorianRevisionBeginAttempt diff --git a/tests/AVEVA.Historian.Client.Tests/HistorianWcfRevisionProbeTests.cs b/tests/AVEVA.Historian.Client.Tests/HistorianWcfRevisionProbeTests.cs index a10ac71..cd7647e 100644 --- a/tests/AVEVA.Historian.Client.Tests/HistorianWcfRevisionProbeTests.cs +++ b/tests/AVEVA.Historian.Client.Tests/HistorianWcfRevisionProbeTests.cs @@ -47,6 +47,7 @@ public sealed class HistorianWcfRevisionProbeTests _output.WriteLine($"ClientHandle: {result.ClientHandle}"); _output.WriteLine($"StorageSessionId: {result.StorageSessionId}"); _output.WriteLine($"TrxInterfaceVersion: {result.TrxInterfaceVersion} (rc={result.TrxInterfaceVersionReturnCode}) ex={result.TrxInterfaceVersionException}"); + _output.WriteLine($"RTag2Succeeded: {result.RTag2Succeeded} OutHex={result.RTag2OutHex} ErrHex={result.RTag2ErrorHex} Ex={result.RTag2Exception}"); _output.WriteLine($"BeginSucceeded: {result.BeginSucceeded}"); _output.WriteLine($"BeginTransactionId: {result.BeginTransactionId}"); foreach (HistorianRevisionBeginAttempt attempt in result.BeginAttempts)