fix(grpc): extended-property read parser + GetConnectionStatus over gRPC
- HistorianTagExtendedPropertyProtocol.ParseResponse: fix the multi-property/ multi-group response shape captured live from the 2023 R2 server. The server returns one group per property (the tag name repeats), each propertyCount=1, and a uint16 searchability-flags trailer per property (0x0003 built-in, 0x0001 user- added) — NOT the single-byte group trailer the old model assumed, which drifted one byte per group and threw "expected 0x09 found 0x01" on any buffer with more than one property. Now reads the per-property uint16 trailer (tolerates a legacy 1-byte tail). Fixes read-back on both WCF and gRPC. Adds GetTagExtendedPropertiesRaw for future captures. - HistorianGrpcStatusClient.GetConnectionStatusAsync (plan #5): synthesize connection status from a measured gRPC handshake (OpenConnection yielding a storage-session GUID => connected), mirroring the WCF synthesize-from-probe approach. Routed in Historian2020ProtocolDialect on UseGrpc (the WCF path used the MDAS binding, which can't reach the gRPC port). - HistorianGrpcSqlClient: record the negative plan-#4 result — a HistoryService. RegisterTags prime does NOT clear the server-side CSrvDbConnection fault (tried live on both 0x402/0x401); the op stays bounded behind ProtocolEvidenceMissingException. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC
This commit is contained in:
@@ -25,9 +25,17 @@ namespace AVEVA.Historian.Client.Wcf;
|
||||
/// 1-byte group marker (observed <c>0x01</c>) + compact-ASCII tag name (<c>0x09</c> + uint16 byte
|
||||
/// length + ASCII), <c>uint32 propertyCount</c>, then per property a 1-byte marker (observed
|
||||
/// <c>0x02</c>) + compact-ASCII property name + a CRetVariant value (<c>0x43</c> VT_BSTR + uint16
|
||||
/// payload length + uint16 charCount + UTF-16LE), and a 1-byte trailing marker (observed
|
||||
/// <c>0x01</c>). Only the string value variant (<c>0x43</c>) is evidence-backed; other variant
|
||||
/// types throw <see cref="ProtocolEvidenceMissingException"/>.</para>
|
||||
/// payload length + uint16 charCount + UTF-16LE) + a <b>uint16 searchability-flags</b> trailer
|
||||
/// (Facetable|Searchable|SubstringSearchable; e.g. <c>0x0003</c> for a built-in property,
|
||||
/// <c>0x0001</c> for a user-added one — captured live 2026-06-22). Only the string value variant
|
||||
/// (<c>0x43</c>) is evidence-backed; other variant types throw
|
||||
/// <see cref="ProtocolEvidenceMissingException"/>.</para>
|
||||
///
|
||||
/// <para><b>Per-property, not per-group:</b> the server returns one group per property (the tag name
|
||||
/// repeats), each with <c>propertyCount = 1</c>, and the uint16 flags trailer belongs to the
|
||||
/// property — there is no separate group trailer. An earlier revision modelled the trailer as a
|
||||
/// single byte after the property block; that happened to parse a single-property buffer (the second
|
||||
/// flags byte was the buffer's end) but drifted one byte per group on multi-property responses.</para>
|
||||
///
|
||||
/// <para>The op is sequence-paged: call with <c>sequence = 0</c>, parse the buffer, then re-call
|
||||
/// with the returned sequence until the response carries no rows. Only string-valued properties
|
||||
@@ -219,13 +227,11 @@ internal static class HistorianTagExtendedPropertyProtocol
|
||||
string propertyName = ReadCompactAsciiString(buffer, ref cursor);
|
||||
string value = ReadVariantStringValue(buffer, ref cursor);
|
||||
rows.Add(new HistorianTagExtendedPropertyRow(tagName, propertyName, value));
|
||||
}
|
||||
|
||||
// 1-byte trailing marker (observed 0x01) after the property block. Read it only if a
|
||||
// byte remains so a tightly-packed terminal buffer doesn't over-read.
|
||||
if (cursor < buffer.Length)
|
||||
{
|
||||
SkipByte(buffer, ref cursor);
|
||||
// uint16 searchability-flags trailer for this property (captured live). Tolerate a
|
||||
// legacy single-byte tail or a tightly-packed terminal buffer so older single-property
|
||||
// captures still parse.
|
||||
SkipPropertyTrailer(buffer, ref cursor);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -275,6 +281,22 @@ internal static class HistorianTagExtendedPropertyProtocol
|
||||
cursor++;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Consumes the per-property uint16 searchability-flags trailer. Consumes 2 bytes when available;
|
||||
/// tolerates a legacy single-byte tail (consumes 1) so older single-property captures still parse.
|
||||
/// </summary>
|
||||
private static void SkipPropertyTrailer(ReadOnlySpan<byte> buffer, ref int cursor)
|
||||
{
|
||||
if (cursor <= buffer.Length - 2)
|
||||
{
|
||||
cursor += 2;
|
||||
}
|
||||
else if (cursor < buffer.Length)
|
||||
{
|
||||
cursor += 1;
|
||||
}
|
||||
}
|
||||
|
||||
private static ushort ReadUInt16(ReadOnlySpan<byte> buffer, ref int cursor)
|
||||
{
|
||||
EnsureAvailable(buffer, cursor, 2);
|
||||
|
||||
Reference in New Issue
Block a user