4da5287d01
Execute HCAL roadmap R1.2 (GetRuntimeParameterAsync) end-to-end, and in doing so
discover that the "string-handle wall" blocking R1.1/R1.4/R1.5/R1.6 was a handle
FORMAT bug, not a missing native session/filter registration.
R1.2 (shipped, live-verified):
- Captured native GetRuntimeParameter -> WCF op aa/Stat/GETRP (string-handle op,
GETHI's shape), via scripts/Capture-RuntimeParam.ps1 + instrument-wcf-{write,read}message.
- HistorianRuntimeParameterProtocol serializes pRequestBuff (54 67 01 00 + uint
nameCount + per-name uint charCount + UTF-16) and parses pResponseBuff (version +
uint resultCount + CRetVariant 0x43 VT_BSTR + uint16 len + uint16 charCount + UTF-16).
- IStatusServiceContract2.GetRuntimeParameter (GETRP) op; HistorianWcfStatusClient
passes the Open2 storage-session GUID as the string handle, UPPERCASE.
- Public HistorianClient.GetRuntimeParameterAsync(name) via the dialect.
- Golden WcfRuntimeParameterProtocolTests + gated live test; returns HistorianVersion.
String-handle wall RESOLVED (proven, public APIs deferred):
- The Open2 storage GUID works as the string handle when sent UPPERCASE
(ToString("D").ToUpperInvariant()); earlier "blocked" probes used lowercase.
- Live-probed GETHI (R1.4) -> returns data; ExeC (R1.1) -> Retr.GetV prime -> ExeC ->
GetR returns a BinaryFormatter-serialized .NET DataTable. Gated
StringHandleProbeDiagnosticTests + scripts/Capture-ExecSql.ps1 + exec-sql harness scenario.
- Docs flipped: wcf-string-handle-wall.md RESOLVED banner; roadmap R1.1/R1.4 reachable,
R1.5/R1.6 likely; wcf-status-localhost.md GETRP section.
- R1.1/R1.4 public APIs NOT shipped: ExeC needs a GetR paging loop + a BinaryFormatter-
stream parser (BinaryFormatter is removed from .NET 10); GETHI full-info struct needs
its own capture.
223 unit tests pass; gated live tests green against the local 2020 Historian.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC
175 lines
8.0 KiB
C#
175 lines
8.0 KiB
C#
using System.Runtime.Versioning;
|
|
using System.ServiceModel;
|
|
using System.ServiceModel.Channels;
|
|
using AVEVA.Historian.Client.Models;
|
|
using AVEVA.Historian.Client.Wcf.Contracts;
|
|
|
|
namespace AVEVA.Historian.Client.Wcf;
|
|
|
|
internal static class HistorianWcfStatusClient
|
|
{
|
|
public static Task<string?> GetSystemParameterAsync(
|
|
HistorianClientOptions options,
|
|
string parameterName,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(parameterName);
|
|
return Task.Run(() => GetSystemParameter(options, parameterName), cancellationToken);
|
|
}
|
|
|
|
/// <summary>Diagnostic: the GETRP return code / error description from the last
|
|
/// <see cref="GetRuntimeParameterAsync"/> call (set only when the server rejects it).</summary>
|
|
public static string? LastRuntimeParameterError { get; private set; }
|
|
|
|
public static Task<string?> GetRuntimeParameterAsync(
|
|
HistorianClientOptions options,
|
|
string parameterName,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(parameterName);
|
|
return Task.Run(() => GetRuntimeParameter(options, parameterName), cancellationToken);
|
|
}
|
|
|
|
public static Task<HistorianConnectionStatus> GetConnectionStatusAsync(
|
|
HistorianClientOptions options,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
return Task.Run(() => SynthesizeConnectionStatus(options), cancellationToken);
|
|
}
|
|
|
|
public static Task<HistorianStoreForwardStatus> GetStoreForwardStatusAsync(
|
|
HistorianClientOptions options,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
return Task.Run(() => SynthesizeStoreForwardStatus(options), cancellationToken);
|
|
}
|
|
|
|
private static string? GetSystemParameter(HistorianClientOptions options, string parameterName)
|
|
{
|
|
Guid contextKey = Guid.NewGuid();
|
|
var (histBinding, histEndpoint, _, _) = HistorianWcfBindingFactory.CreateBindingPair(options);
|
|
Binding statusBinding = HistorianWcfBindingFactory.CreateAuxiliaryBinding(options);
|
|
EndpointAddress statusEndpoint = HistorianWcfBindingFactory.CreateAuxiliaryEndpointAddress(options, HistorianWcfServiceNames.Status);
|
|
|
|
string? value = null;
|
|
HistorianWcfAuthChainHelper.OpenAuthenticatedConnection(
|
|
options, histBinding, histEndpoint, contextKey, CancellationToken.None,
|
|
additionalSetup: (_, context) => value = QuerySystemParameter(statusBinding, statusEndpoint, context.ClientHandle, parameterName));
|
|
return value;
|
|
}
|
|
|
|
private static string? GetRuntimeParameter(HistorianClientOptions options, string parameterName)
|
|
{
|
|
Guid contextKey = Guid.NewGuid();
|
|
var (histBinding, histEndpoint, _, _) = HistorianWcfBindingFactory.CreateBindingPair(options);
|
|
Binding statusBinding = HistorianWcfBindingFactory.CreateAuxiliaryBinding(options);
|
|
EndpointAddress statusEndpoint = HistorianWcfBindingFactory.CreateAuxiliaryEndpointAddress(options, HistorianWcfServiceNames.Status);
|
|
|
|
string? value = null;
|
|
LastRuntimeParameterError = null;
|
|
HistorianWcfAuthChainHelper.OpenAuthenticatedConnection(
|
|
options, histBinding, histEndpoint, contextKey, CancellationToken.None,
|
|
additionalSetup: (_, context) => value = QueryRuntimeParameter(statusBinding, statusEndpoint, context.StorageSessionId, parameterName));
|
|
return value;
|
|
}
|
|
|
|
private static string? QueryRuntimeParameter(Binding statusBinding, EndpointAddress statusEndpoint, Guid storageSessionId, string parameterName)
|
|
{
|
|
// GETRP takes the storage-session GUID as a string handle, formatted exactly as the
|
|
// native client sends it: uppercase, dash-separated, no braces.
|
|
string handle = storageSessionId.ToString("D").ToUpperInvariant();
|
|
byte[] requestBuffer = HistorianRuntimeParameterProtocol.SerializeRequest(parameterName);
|
|
|
|
ChannelFactory<IStatusServiceContract2> factory = new(statusBinding, statusEndpoint);
|
|
IStatusServiceContract2 channel = factory.CreateChannel();
|
|
ICommunicationObject co = (ICommunicationObject)channel;
|
|
try
|
|
{
|
|
bool ok = channel.GetRuntimeParameter(handle, requestBuffer, out byte[] responseBuffer, out byte[] errorBuffer);
|
|
if (!ok)
|
|
{
|
|
LastRuntimeParameterError = $"GETRP returned false (responseLen={responseBuffer?.Length ?? 0}, errorLen={errorBuffer?.Length ?? 0}).";
|
|
return null;
|
|
}
|
|
|
|
return HistorianRuntimeParameterProtocol.ParseSingleStringResult(responseBuffer ?? []);
|
|
}
|
|
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 string? QuerySystemParameter(Binding statusBinding, EndpointAddress statusEndpoint, uint clientHandle, string parameterName)
|
|
{
|
|
ChannelFactory<IStatusServiceContract2> factory = new(statusBinding, statusEndpoint);
|
|
IStatusServiceContract2 channel = factory.CreateChannel();
|
|
ICommunicationObject co = (ICommunicationObject)channel;
|
|
try
|
|
{
|
|
bool ok = channel.GetSystemParameter(clientHandle, parameterName, out string parameterValue, out _, out _);
|
|
return ok ? parameterValue : null;
|
|
}
|
|
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 { } }
|
|
}
|
|
}
|
|
|
|
/// <remarks>
|
|
/// AVEVA's native <c>HistorianAccess.GetConnectionStatus</c> reads local C++
|
|
/// <c>HistorianClient</c> state (no WCF op exists for it). We synthesize an equivalent
|
|
/// by attempting an authenticated session open: a successful auth+open implies
|
|
/// <c>ConnectedToServer = true</c>. Store-forward and partner-connection state are not
|
|
/// observable from a single client probe and remain false.
|
|
/// </remarks>
|
|
private static HistorianConnectionStatus SynthesizeConnectionStatus(HistorianClientOptions options)
|
|
{
|
|
bool connected;
|
|
string? error = null;
|
|
try
|
|
{
|
|
Guid contextKey = Guid.NewGuid();
|
|
var (histBinding, histEndpoint, _, _) = HistorianWcfBindingFactory.CreateBindingPair(options);
|
|
HistorianWcfAuthChainHelper.OpenAuthenticatedConnection(
|
|
options, histBinding, histEndpoint, contextKey, CancellationToken.None);
|
|
connected = true;
|
|
}
|
|
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);
|
|
}
|
|
|
|
/// <remarks>
|
|
/// Native <c>HistorianAccess.GetStoreForwardStatus</c> is also client-side state.
|
|
/// Without a local store-forward sidecar to probe, we report defaults: not pending,
|
|
/// no error, no data stored, not actively storing. Connection kind is Process by
|
|
/// convention (event-only sessions are uncommon for this status helper).
|
|
/// </remarks>
|
|
private static HistorianStoreForwardStatus SynthesizeStoreForwardStatus(HistorianClientOptions options)
|
|
{
|
|
return new HistorianStoreForwardStatus(
|
|
ServerName: options.Host,
|
|
Pending: false,
|
|
ErrorOccurred: false,
|
|
Error: null,
|
|
DataStored: false,
|
|
Storing: false,
|
|
ConnectionKind: HistorianConnectionKind.Process);
|
|
}
|
|
}
|