using System.Buffers.Binary; using System.ServiceModel; using System.ServiceModel.Channels; using AVEVA.Historian.Client.Wcf.Contracts; namespace AVEVA.Historian.Client.Wcf; /// /// Drives the AddNonStreamValuesBegin / AddNonStreamValues / AddNonStreamValuesEnd /// WCF op group on the /Trx service end-to-end. The native AVEVA wrapper's /// equivalent surface (HistorianAccess.AddRevisionValues*) is gated by the /// C++ HistorianClient's per-connection cache and rejects all writes from a /// managed client with err 129 TagNotFoundInCache. This SDK orchestrator /// bypasses the wrapper entirely — talks WCF directly — to test whether the SERVER /// gates on the same condition. /// /// Live behavior is unverified. The first iteration is probe-only: open the auth /// chain, drive the standard write priming, call AddNonStreamValuesBegin and /// surface whatever the server returns. /// internal sealed class HistorianWcfRevisionOrchestrator { private readonly HistorianClientOptions _options; public HistorianWcfRevisionOrchestrator(HistorianClientOptions options) { _options = options ?? throw new ArgumentNullException(nameof(options)); } public Task ProbeBeginAsync(CancellationToken cancellationToken) => Task.Run(() => ProbeBegin(cancellationToken), cancellationToken); private HistorianRevisionProbeResult ProbeBegin(CancellationToken cancellationToken) { Guid contextKey = Guid.NewGuid(); var (histBinding, histEndpoint, _, _) = HistorianWcfBindingFactory.CreateBindingPair(_options); Binding auxBinding = HistorianWcfBindingFactory.CreateAuxiliaryBinding(_options); EndpointAddress transactionEndpoint = HistorianWcfBindingFactory.CreateAuxiliaryEndpointAddress(_options, HistorianWcfServiceNames.Transaction); HistorianRevisionProbeResult result = new(); HistorianWcfAuthChainHelper.OpenAuthenticatedConnection( _options, histBinding, histEndpoint, contextKey, cancellationToken, connectionMode: HistorianWcfAuthChainHelper.NativeIntegratedWriteEnabledConnectionMode, additionalSetup: (historyChannel, context) => { result.OpenSucceeded = true; result.ClientHandle = context.ClientHandle; result.StorageSessionId = context.StorageSessionId; // Run the same priming chain that EnsT2/DelT use — without it, the Trx // service rejects calls with err 51 UnknownClient because the client // hasn't registered itself across the auxiliary services. EndpointAddress statusEndpoint = HistorianWcfBindingFactory.CreateAuxiliaryEndpointAddress(_options, HistorianWcfServiceNames.Status); 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(); ICommunicationObject trxCo = (ICommunicationObject)trxChannel; try { // Get interface version first to register the client in the Trx service's // session table (matches the cross-service GetV priming pattern used by // RunWritePriming for EnsT2/DelT). try { uint trxRc = trxChannel.GetInterfaceVersion(out uint trxVersion); result.TrxInterfaceVersionReturnCode = trxRc; result.TrxInterfaceVersion = trxVersion; } catch (Exception ex) { result.TrxInterfaceVersionException = $"{ex.GetType().Name}: {ex.Message}"; } // Probe V2 AddNonStreamValuesBegin2. Try BOTH possible handle formats — // the server returns 0433000000 (UnknownClient = 51) when the wrong one // is sent. Capture which one (if any) is recognized. foreach ((string label, string handle) in new[] { ("contextKey", contextKey.ToString("D").ToUpperInvariant()), ("storageSessionId", context.StorageSessionId.ToString("D").ToUpperInvariant()), ("contextKey-lower", contextKey.ToString("D")), ("clientHandle-as-string", context.ClientHandle.ToString()), }) { try { string? transactionId = null; byte[]? errorBuffer = null; bool ok = trxChannel.AddNonStreamValuesBegin2(handle, out transactionId, out errorBuffer); result.BeginAttempts.Add(new HistorianRevisionBeginAttempt { HandleLabel = label, HandleSent = handle, Succeeded = ok, TransactionId = transactionId, ErrorHex = errorBuffer is null || errorBuffer.Length == 0 ? null : Convert.ToHexString(errorBuffer), }); if (ok && !string.IsNullOrEmpty(transactionId)) { result.BeginSucceeded = true; result.BeginTransactionId = transactionId; break; } } catch (Exception ex) { result.BeginAttempts.Add(new HistorianRevisionBeginAttempt { HandleLabel = label, HandleSent = handle, Exception = $"{ex.GetType().Name}: {ex.Message}", }); } } } finally { try { if (trxCo.State == CommunicationState.Faulted) trxCo.Abort(); else trxCo.Close(); } catch { try { trxCo.Abort(); } catch { } } try { if (trxFactory.State == CommunicationState.Faulted) trxFactory.Abort(); else trxFactory.Close(); } catch { try { trxFactory.Abort(); } catch { } } } }); return result; } /// /// Mirrors HistorianWcfTagWriteOrchestrator.RunWritePriming. The cross-service GetV /// calls + UpdC3 register the client in each aux service's session table so that /// subsequent ops (like AddNonStreamValuesBegin2 on /Trx) recognize the handle. /// private static void RunPrimingChain( IHistoryServiceContract2 historyChannel, HistorianWcfAuthChainHelper.OpenConnectionContext context, Binding auxBinding, EndpointAddress statusEndpoint, EndpointAddress transactionEndpoint, EndpointAddress retrievalEndpoint) { string handle = context.StorageSessionId.ToString("D").ToUpperInvariant(); ChannelFactory statusFactory = new(auxBinding, statusEndpoint); IStatusServiceContract2 statusChannel = statusFactory.CreateChannel(); ChannelFactory transactionFactory = new(auxBinding, transactionEndpoint); ITransactionServiceContract transactionChannel = transactionFactory.CreateChannel(); ChannelFactory retrievalFactory = new(auxBinding, retrievalEndpoint); IRetrievalServiceContract4 retrievalChannel = retrievalFactory.CreateChannel(); try { TryRun(() => statusChannel.GetInterfaceVersion(out _)); TryRun(() => statusChannel.GetInterfaceVersion(out _)); byte[] historianVersionRequest = BuildGetHistorianInfoRequest("HistorianVersion"); TryRun(() => statusChannel.GetHistorianInfo(handle, historianVersionRequest, out _, out _)); TryRun(() => statusChannel.GetHistorianInfo(handle, historianVersionRequest, out _, out _)); byte[] clientStatus = BuildUpdC3ClientStatusBlob(); TryRun(() => historyChannel.UpdateClientStatus3(handle, (uint)clientStatus.Length, ref clientStatus, out _, out _, out _, out _)); foreach (string parameterName in new[] { "AllowOriginals", "HistorianPartner", "HistorianVersion", "MaxCyclicStorageTimeout", "RealTimeWindow", "FutureTimeThreshold", "AllowRenameTags" }) { TryRun(() => statusChannel.GetSystemParameter(context.ClientHandle, parameterName, out _, out _, out _)); } TryRun(() => transactionChannel.GetInterfaceVersion(out _)); TryRun(() => statusChannel.GetInterfaceVersion(out _)); TryRun(() => retrievalChannel.GetInterfaceVersion(out _)); } finally { CloseSafely(retrievalChannel, retrievalFactory); CloseSafely(transactionChannel, transactionFactory); CloseSafely(statusChannel, statusFactory); } } /// 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]; blob[0] = 0x02; blob[1] = 0x01; blob[77] = 0x1E; return blob; } private static byte[] BuildGetHistorianInfoRequest(string parameterName) { byte[] nameBytes = System.Text.Encoding.Unicode.GetBytes(parameterName); int payloadLength = nameBytes.Length > 0 ? nameBytes.Length - 1 : 0; byte[] buffer = new byte[8 + payloadLength]; BinaryPrimitives.WriteUInt16LittleEndian(buffer.AsSpan(0, 2), 0x6753); BinaryPrimitives.WriteUInt16LittleEndian(buffer.AsSpan(2, 2), 0x0002); BinaryPrimitives.WriteUInt32LittleEndian(buffer.AsSpan(4, 4), (uint)parameterName.Length); Buffer.BlockCopy(nameBytes, 0, buffer, 8, payloadLength); return buffer; } private static void TryRun(Action a) { try { a(); } catch { } } private static void CloseSafely(object channel, ICommunicationObject factory) { try { if (channel is ICommunicationObject co) { if (co.State == CommunicationState.Faulted) co.Abort(); else co.Close(); } } catch { } try { if (factory.State == CommunicationState.Faulted) factory.Abort(); else factory.Close(); } catch { } } } internal sealed class HistorianRevisionProbeResult { public bool OpenSucceeded { get; set; } public uint ClientHandle { get; set; } public Guid StorageSessionId { get; set; } public uint? TrxInterfaceVersionReturnCode { get; set; } public uint? TrxInterfaceVersion { get; set; } public string? TrxInterfaceVersionException { get; set; } public string? BeginTransactionId { get; set; } public bool BeginSucceeded { get; set; } 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 { public string HandleLabel { get; set; } = ""; public string HandleSent { get; set; } = ""; public bool Succeeded { get; set; } public string? TransactionId { get; set; } public string? ErrorHex { get; set; } public string? Exception { get; set; } }