From ae536bb4b84bb710ff9c8e513ed2d945ffd16925 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Tue, 23 Jun 2026 15:13:11 -0400 Subject: [PATCH] Record 2023 R2 binary-dive verdicts; fix revision-probe scope comment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mined the full decompiled stock 2023 R2 managed client as the oracle for every still-pending gRPC item. Governing fact: ArchestrA.HistorianAccess is a C++/CLI shim into native HistorianClient; the managed Grpc*Client wrappers have zero call sites, so buffer-building/dispatch for the pending items is native (absent from the binaries). Sharpened verdicts into handoff.md: - Items 4/5/6 + OpenStorageConnection: hard-confirmed walled, with real reasons (SQL gated out client-side via IsManagedHistorian; no Revision RPC in the gRPC contract; LoadBlocks response is a native blob behind the storage console handle). - Items 3 (SendEvent) and 7 (DeleteTEP): moved from vague to precise, LOCAL-box capturable targets (PackToVtq btValues / DeleteTagExtendedPropertiesByName BtInput with deleteFromServer=true). Also correct the HistorianGrpcRevisionProbe doc comment: it probes the non-streamed ORIGINAL-insert path (AddNonStreamValues), a distinct capability from revision EDITS (native AddRevisionValues trio, no gRPC RPC) — the prior comment conflated them. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC --- docs/reverse-engineering/handoff.md | 68 +++++++++++++++++++ .../Grpc/HistorianGrpcRevisionProbe.cs | 12 ++++ 2 files changed, 80 insertions(+) diff --git a/docs/reverse-engineering/handoff.md b/docs/reverse-engineering/handoff.md index 5010545..d619bcd 100644 --- a/docs/reverse-engineering/handoff.md +++ b/docs/reverse-engineering/handoff.md @@ -101,6 +101,74 @@ Live-server gRPC probe recipe: set quotes — `reference_wonder_sql_vd03_credentials`) and run the gated `HistorianGrpcIntegrationTests`. +### 2023 R2 stock-client binary dive (2026-06-23) — sharpened verdicts + +Re-read the full decompiled stock 2023 R2 managed client +(`histsdk-2023r2-analysis/decompiled/`: `Archestra.Historian.GrpcClient`, +`ArchestrA.HistorianAccess`, `Archestra.Grpc.Contract`, `HistorianEvent`, +`HistorianAccessUtil`) as the oracle for every still-pending item. **Governing +fact:** `ArchestrA.HistorianAccess.dll` is a C++/CLI mixed assembly — every +data/config/write method is a thin shim into native `.HistorianClient.*`, +and the managed `Grpc*Client` wrappers are instantiated by **nothing** in the +decompiled set (`new Grpc*Client(` → zero call sites). So the buffer-building and +RPC-dispatch sequencing for these items lives in native C++ not present in the +binaries. That confirms the "gated" calls were not from missing managed steps — +with these refinements: + +- **Item 1 (gRPC event rows)** — **confirmed native/server-side.** Stock event + call graph is provably identical to ours (transport, per-service channels, + gzip-only metadata, CM_EVENT registration, v8 ECDH Event-open, `StartEventQuery` + request bytes). `EventQuery.StartQuery`/`MoveNext` dispatch straight into native + `HistorianClient.StartEventQuery`/`GetNextRow`; the query orchestration that + would differ is native and not on the wire. One untested low-effort check + remains: byte-diff a captured **Event-connection** EnsureTags/RegisterTags + against our replay (the 83-vs-86-byte EnsT gap was never actually compared). +- **Item 3 (SendEvent over gRPC)** — **sharpened from "maybe no RPC" to a precise + capture.** RPC **confirmed** = `HistoryService.AddStreamValues` (the "no distinct + RPC" note is TRUE; an event rides the same RPC as a streamed sample, discriminated + inside `btValues`). Public API `HistorianAccess.AddStreamedValue(HistorianEvent)` + → native `AddHistorianValue`; prereqs known (write-enabled Event conn, CM_EVENT + tag handle, quality 192); field set/order recovered from `HistorianEvent.PackToVtq`. + **Only the `btValues` VTQ byte layout is missing** — built by native + `CCommonArchestraEventValue::PackToVtq` and copied out as an opaque `CDataChunk`. + Our read parser already decodes the inverse property-bag format. **Capturable + against the local Historian** (instrument `PackToVtq` output / the `AddStreamValues` + body) → then build `HistorianEventWriteProtocol` and reuse the + `HistorianGrpcHistoricalWriteOrchestrator` plumbing. +- **Item 4 (ExecuteSql over gRPC)** — **confirmed walled + explained.** The stock + client gates SQL **out client-side**: `HistorianAccess.ExecuteSqlCommand` returns + `OperationNotSupported` when `IsManagedHistorian(node)` or `!IsProcessConnectionRequested()` + (decompile ~:6198/:6214) and never sends the RPC. SQL-over-gRPC is unsupported by + design on a managed/gRPC historian; our `ProtocolEvidenceMissingException` is correct. +- **Item 5 (R4.2 revision edits)** — **confirmed HARD.** There is **no Revision RPC + in the gRPC contract** (zero "Revision" message types); the stock client reaches a + revision edit only via the native `HistorianClient.AddRevisionValuesBegin/AddRevisionValue/ + AddRevisionValuesEnd` transaction trio over the storage-engine channel. NOTE: this is + a **distinct capability** from `AddNonStreamValues` (non-streamed original insert) — + `HistorianGrpcRevisionProbe` probes the latter; its doc comment was corrected to say so. +- **Item 6 (ReadBlocks/LoadBlocks)** — `LoadBlocks` request is a trivial + handle+sequence cursor but the `historyBlocks` response is a native blob with no + managed decoder, and it needs the D2-blocked `OpenStorageConnection` console handle. + Walled. +- **Item 7 (DeleteTagExtendedProperties)** — **reframed to a capturable lead.** RPC + + string handle are **correct** in our SDK; ADD and DELETE are structurally identical + and **neither** routes through `StartJob`. The differentiator is the `deleteFromServer` + flag carried inside the native-built `BtInput` plus the native HCAL **cache-sync + background worker** that actually propagates the delete server-side (config writes hit + the in-process cache first, then sync). **Capturable**: capture native + `DeleteTagExtendedPropertiesByName(deleteFromServer=true)`'s `BtInput` to learn whether + one well-formed RPC durably deletes (→ shippable) or whether it genuinely depends on + the cache-sync worker (→ walled). +- **SF/snapshot/shard/ForwardSnapshot ops** — only `Get/SetSFParameter` are managed-built + (typed strings); all others carry opaque native buffers and need the storage console + handle. Walled / tooling-internal. + +**Net:** 3 items hard-confirmed walled with real explanations (4, 5, 6 + OpenStorageConnection), +and **2 moved to a precise, local-box-capturable target**: **SendEvent** (`PackToVtq` output) +and **DeleteTEP** (`BtInput` with `deleteFromServer=true`). Both need native instrumentation of +`aahClientManaged.dll` (Frida / IL-rewrite — repo tooling exists under +`tools/AVEVA.Historian.NativeTraceHarness` + `scripts/frida/`), not a special server. + ## Project Direction The project goal is still a fully managed .NET 10 C# AVEVA Historian client. diff --git a/src/AVEVA.Historian.Client/Grpc/HistorianGrpcRevisionProbe.cs b/src/AVEVA.Historian.Client/Grpc/HistorianGrpcRevisionProbe.cs index e82e287..8269607 100644 --- a/src/AVEVA.Historian.Client/Grpc/HistorianGrpcRevisionProbe.cs +++ b/src/AVEVA.Historian.Client/Grpc/HistorianGrpcRevisionProbe.cs @@ -20,6 +20,18 @@ namespace AVEVA.Historian.Client.Grpc; /// write-enabled session and calls AddNonStreamValuesBegin, surfacing whatever the server /// returns. It writes NO data — if Begin succeeds it immediately calls AddNonStreamValuesEnd /// with bCommit=false to discard the transaction. +/// +/// Scope note (corrected 2026-06-23 after a 2023 R2 binary re-read). Despite the type +/// name, this probes the non-streamed ORIGINAL / backfill insert capability +/// (AddNonStreamValues), which is a distinct capability from a revision EDIT +/// (overwriting an existing historized value with a new revision). The stock high-level client +/// reaches a revision edit via a separate native transaction trio +/// HistorianClient.AddRevisionValuesBegin/AddRevisionValue/AddRevisionValuesEnd +/// (ArchestrA.HistorianAccess.AddRevisionValues, REVISION_MODE ∈ InsertLatest/UpdateSingle/ +/// UpdateMultiple). That trio has NO corresponding RPC in the gRPC contract (no "Revision" +/// message type exists in Archestra.Grpc.Contract) — it rides the native storage-engine +/// transaction channel only. So R4.2 revision edits remain unreachable over gRPC regardless of this +/// probe's outcome; this probe neither covers nor unblocks them. /// internal sealed class HistorianGrpcRevisionProbe {