R0.1 browse over gRPC SHIPPED — QueryTag cracked, M0 gRPC parity complete
Wires HistorianClient.BrowseTagNamesAsync over gRPC (Transport==RemoteGrpc) via
Grpc/HistorianGrpcTagClient.BrowseTagNamesAsync: StartTagQuery(OData) -> paged
QueryTag -> EndTagQuery. Live-verified against a real 2023 R2 server (returns Sys* tags).
QueryTag packet-id recovered WITHOUT native disassembly: a .rdata packet-descriptor
table in aahClientManaged.dll lists {0x6751,1}=StartTagQuery immediately followed by
{0x6752,1}=QueryTag (found via pefile byte-scan of .rdata), confirmed live.
Wire format (live-verified):
- request btRequest = u16 0x6752 + u16 version(1) + u16 queryType(1=names) + u32 startIndex + u32 count
- response btResonse = u32 count + per-name(u32 charCount + UTF-16LE) + trailer (NextIndex/metadata, ignored)
- new HistorianTagQueryProtocol.ParseTagNameQueryPage tolerates the trailer
- GlobToODataFilter translates the SDK glob filter to OData (Pre*->startswith, *suf->endswith,
*mid*->contains, exact->eq); the 2023 R2 metadata-server parses filters as OData.
Replaces the earlier RE probe helpers with the shipped browse path. Adds golden-byte
(BuildQueryTagRequest) + 8 glob-translation unit tests + gated live browse test.
226 unit tests pass; 5/5 live gRPC tests pass (read, probe, system-param, metadata, browse).
Milestone 0 (full gRPC parity) is complete.
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:
@@ -102,6 +102,34 @@ internal static class HistorianTagQueryProtocol
|
||||
return tagNames;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses one page of a gRPC <c>QueryTag</c> tag-name response: <c>uint count + per-name(uint
|
||||
/// charCount + UTF-16LE)</c>, then a trailing region (NextIndex + optional metadata buffer) that
|
||||
/// is intentionally ignored. Unlike <see cref="ParseGetLikeTagNamesResponse"/> this tolerates the
|
||||
/// trailer rather than requiring the buffer to end exactly after the names.
|
||||
/// </summary>
|
||||
public static IReadOnlyList<string> ParseTagNameQueryPage(ReadOnlySpan<byte> response)
|
||||
{
|
||||
if (response.Length < 4)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
int cursor = 0;
|
||||
uint count = ReadUInt32(response, ref cursor);
|
||||
List<string> tagNames = new(checked((int)count));
|
||||
for (uint index = 0; index < count; index++)
|
||||
{
|
||||
uint charLength = ReadUInt32(response, ref cursor);
|
||||
int byteLength = checked((int)charLength * 2);
|
||||
EnsureAvailable(response, cursor, byteLength);
|
||||
tagNames.Add(Encoding.Unicode.GetString(response.Slice(cursor, byteLength)));
|
||||
cursor += byteLength;
|
||||
}
|
||||
|
||||
return tagNames;
|
||||
}
|
||||
|
||||
private static void WriteHistorianString(BinaryWriter writer, string value)
|
||||
{
|
||||
writer.Write((uint)value.Length);
|
||||
|
||||
Reference in New Issue
Block a user