using System.ServiceModel; using System.ServiceModel.Channels; using System.Text; using System.Runtime.Versioning; using AVEVA.Historian.Client.Wcf; using AVEVA.Historian.Client.Wcf.Contracts; using Xunit.Abstractions; namespace AVEVA.Historian.Client.Tests; /// /// Diagnostic: retest the "string-handle wall" ops (GETHI / ExeC) using the Open2 /// storage-session GUID formatted UPPERCASE (the format the native client sends, and the /// one that made GETRP punch through). Not an assertion test — it prints the server's /// return code / buffer lengths so we can judge whether the wall is a handle-format issue. /// [SupportedOSPlatform("windows")] public sealed class StringHandleProbeDiagnosticTests { private readonly ITestOutputHelper _output; public StringHandleProbeDiagnosticTests(ITestOutputHelper output) { _output = output; } private static bool ShouldRun(out string host) { host = Environment.GetEnvironmentVariable("HISTORIAN_HOST") ?? string.Empty; return !string.IsNullOrWhiteSpace(host) && string.Equals(host, "localhost", StringComparison.OrdinalIgnoreCase) && OperatingSystem.IsWindows(); } [Fact] public void GETHI_WithUppercaseStorageGuid_AgainstLocalHistorian() { if (!ShouldRun(out string host)) return; HistorianClientOptions options = new() { Host = host, IntegratedSecurity = true, Transport = HistorianTransport.LocalPipe }; // Native GETHI pRequestBuff: 53 67 02 00 (sig 0x6753 + version 2) + uint charCount + UTF-16 name. const string name = "HistorianVersion"; using MemoryStream ms = new(); using (BinaryWriter w = new(ms, Encoding.Unicode, leaveOpen: true)) { w.Write(new byte[] { 0x53, 0x67, 0x02, 0x00 }); w.Write((uint)name.Length); w.Write(Encoding.Unicode.GetBytes(name)); } byte[] requestBuffer = ms.ToArray(); ProbeOnStatusChannel(options, (channel, handle) => { bool ok = channel.GetHistorianInfo(handle, requestBuffer, out byte[] resp, out byte[] err); _output.WriteLine($"GETHI returned={ok} respLen={resp?.Length ?? 0} errLen={err?.Length ?? 0}"); if (resp is { Length: > 0 }) { _output.WriteLine(" resp[0..64]=" + Convert.ToHexString(resp.AsSpan(0, Math.Min(64, resp.Length)))); } }); } [Fact] public void ExeC_WithUppercaseStorageGuid_AgainstLocalHistorian() { if (!ShouldRun(out string host)) return; HistorianClientOptions options = new() { Host = host, IntegratedSecurity = true, Transport = HistorianTransport.LocalPipe }; Guid contextKey = Guid.NewGuid(); var (histBinding, histEndpoint, retrBinding, retrEndpoint) = HistorianWcfBindingFactory.CreateBindingPair(options); HistorianWcfAuthChainHelper.OpenAuthenticatedConnection( options, histBinding, histEndpoint, contextKey, CancellationToken.None, additionalSetup: (_, context) => { string handle = context.StorageSessionId.ToString("D").ToUpperInvariant(); ChannelFactory factory = new(retrBinding, retrEndpoint); HistorianWcfClientCredentialsHelper.Configure(factory, options); IRetrievalServiceContract3 channel = factory.CreateChannel(); ICommunicationObject co = (ICommunicationObject)channel; try { // Prime the Retr service version handshake (Retr.GetV), as the native client does. channel.GetInterfaceVersion(out uint retrVersion); _output.WriteLine($"Retr.GetV version={retrVersion}"); uint queryHandle = 0; bool execOk = channel.ExecuteSqlCommand( handle, "SELECT 1 AS ProbeValue", 0u, ref queryHandle, out int retValue, out uint errSize, out byte[] errBuf); _output.WriteLine($"ExeC returned={execOk} retValue={retValue} queryHandle={queryHandle} errSize={errSize} errLen={errBuf?.Length ?? 0}"); if (execOk) { uint sequence = 0; bool getrOk = channel.GetRecordSetByteStream( handle, queryHandle, ref sequence, out uint resultSize, out byte[] resultBuf, out uint gErrSize, out byte[] gErrBuf); _output.WriteLine($"GetR returned={getrOk} resultSize={resultSize} resultLen={resultBuf?.Length ?? 0} sequence={sequence}"); if (resultBuf is { Length: > 0 }) { _output.WriteLine(" result[0..96]=" + Convert.ToHexString(resultBuf.AsSpan(0, Math.Min(96, resultBuf.Length)))); } } } finally { try { if (co.State == CommunicationState.Faulted) co.Abort(); else co.Close(); } catch { try { co.Abort(); } catch { } } try { if (factory.State == CommunicationState.Faulted) factory.Abort(); else factory.Close(); } catch { try { factory.Abort(); } catch { } } } }); } private static void ProbeOnStatusChannel(HistorianClientOptions options, Action probe) { Guid contextKey = Guid.NewGuid(); var (histBinding, histEndpoint, _, _) = HistorianWcfBindingFactory.CreateBindingPair(options); Binding statusBinding = HistorianWcfBindingFactory.CreateAuxiliaryBinding(options); EndpointAddress statusEndpoint = HistorianWcfBindingFactory.CreateAuxiliaryEndpointAddress(options, HistorianWcfServiceNames.Status); HistorianWcfAuthChainHelper.OpenAuthenticatedConnection( options, histBinding, histEndpoint, contextKey, CancellationToken.None, additionalSetup: (_, context) => { string handle = context.StorageSessionId.ToString("D").ToUpperInvariant(); ChannelFactory factory = new(statusBinding, statusEndpoint); IStatusServiceContract2 channel = factory.CreateChannel(); ICommunicationObject co = (ICommunicationObject)channel; try { probe(channel, handle); } catch (Exception ex) { throw new InvalidOperationException($"probe raised: {ex.GetType().Name}: {ex.Message}", ex); } finally { try { if (co.State == CommunicationState.Faulted) co.Abort(); else co.Close(); } catch { try { co.Abort(); } catch { } } try { if (factory.State == CommunicationState.Faulted) factory.Abort(); else factory.Close(); } catch { try { factory.Abort(); } catch { } } } }); } /// /// R1.3 / R1.4 reachability probe: ask GetSystemParameter (config) and GetRuntimeParameter (GETRP, /// live runtime state) for timezone + event-storage-mode candidate keys. If the server timezone or /// EventStorageMode surfaces through either named-parameter op, R1.3/R1.4 are deliverable on 2020; /// if every candidate returns null, they are confirmed 2023R2/gRPC-only from the parameter angle. /// Prints results — not an assertion test. /// [Fact] public async Task TimezoneAndStorageMode_ParameterProbe_AgainstLocalHistorian() { if (!ShouldRun(out string host)) return; HistorianClient client = new(new HistorianClientOptions { Host = host, IntegratedSecurity = true, Transport = HistorianTransport.LocalPipe }); // Timezone (R1.3) candidates + the one existing time-ish SystemParameter (TimeStampRule); // EventStorageMode (R1.4) candidates + the existing EventStorage* params as controls. string[] candidates = [ "TimeZone", "ServerTimeZone", "SystemTimeZone", "TimeZoneName", "SystemTimeZoneName", "TimeStampRule", "ServerTime", "EventStorageMode", "StorageMode", "EventStorage", "EventStorageDuration", "HistorianVersion", // known-good control ]; foreach (string key in candidates) { string? sys = null, run = null; string sysErr = "", runErr = ""; try { sys = await client.GetSystemParameterAsync(key); } catch (Exception ex) { sysErr = ex.GetType().Name; } try { run = await client.GetRuntimeParameterAsync(key); } catch (Exception ex) { runErr = ex.GetType().Name; } _output.WriteLine($"{key,-22} SystemParameter={(sys is null ? "" : $"\"{sys}\"")}{(sysErr.Length > 0 ? $" [{sysErr}]" : "")} RuntimeParameter={(run is null ? "" : $"\"{run}\"")}{(runErr.Length > 0 ? $" [{runErr}]" : "")}"); } } }