04ea0b9a1f
Live-probed both R1.3 and R1.4 against a real 2023 R2 server over the gRPC StatusService; implemented the one that carries an evidence-backed value. R1.3 GetServerTimeZoneAsync — SHIPPED: - StatusService.GetSystemTimeZoneName(uiHandle) returns the real server zone over RemoteGrpc (the 2020 WCF op is a client-side stub returning empty). - HistorianGrpcStatusClient.GetSystemTimeZoneNameAsync -> dialect routing -> public HistorianClient.GetServerTimeZoneAsync. Non-gRPC transports fail closed with ProtocolEvidenceMissingException (no empty-string lie). - Golden message-shape unit test + non-gRPC guardrail unit test + gated live test. 271 unit tests pass. R1.4 GetHistorianInfoAsync (EventStorageMode) — bounded out on gRPC too: - gRPC GetHistorianInfo is the same named-value query as 2020 WCF (only HistorianVersion resolves); EventStorageMode + 7 variants fail on both GetHistorianInfo and GetSystemParameter. The 518-byte struct is filled by a native vtable+648 HCAL call, not the gRPC op (per the 2023 R2 decompile), so the field is never on the wire. Not shipped on any transport. Closes the roadmap's open "build against a live 2023 R2 server" caveat. Also correct the stale M3 roadmap section: D2 already proved Transaction.AddNonStreamValues* rides the storage-engine pipe (STransactPipeClient2 -> aaStorageEngine), not WCF — same wall as R4.2 — so M3-over-WCF is blocked, not "the path that is NOT the gated cache push". Docs: hcal-roadmap.md, wcf-historian-info.md, wcf-status-localhost.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC
82 lines
5.3 KiB
Markdown
82 lines
5.3 KiB
Markdown
# GetHistorianInfo over 2020 WCF — GETHI is named-value-only (HCAL R1.4)
|
|
|
|
**Status: ⛔ Bounded out on BOTH 2020 WCF and 2023 R2 gRPC (2026-06-20; gRPC live-confirmed
|
|
2026-06-21).** `GetHistorianInfoAsync` is **not shipped on any transport**: the one field that
|
|
motivates it — `EventStorageMode` — is **not on the wire** on either transport (it lives only in
|
|
the C++ HCAL's in-memory 518-byte struct, filled via a native vtable+648 call — see the §gRPC
|
|
conclusion below). The version field GETHI *does* return is already exposed (`ProbeAsync`,
|
|
`GetRuntimeParameterAsync("HistorianVersion")`), so there is nothing new to ship. Note: R1.3
|
|
(`GetServerTimeZone`) — once paired with this as "2023R2-only" — **diverged**: it returns a real
|
|
value over gRPC and **shipped** 2026-06-21 (`GetServerTimeZoneAsync`); R1.4 did not.
|
|
|
|
## What the capture showed
|
|
|
|
`scripts/Capture-HistorianInfo.ps1` drives the native `HistorianAccess.GetHistorianInfo(out
|
|
HistorianInfo, out error)` through the instrumented (`instrument-wcf-{write,read}message`)
|
|
`current/aahClientManaged.dll`. The native call **succeeds** and returns
|
|
`EventStorageMode = Blocks`, `ServerVersion = 20,0,000,000`, no error.
|
|
|
|
But the wire tells a different story (`scripts/decode-historian-info-capture.py`):
|
|
|
|
- The only `GETHI` op on the wire is **`aa/Stat/GETHI(handle, pRequestBuff)`** with
|
|
`pRequestBuff = 53 67 02 00` (sig `0x6753` + version `2`) `+ uint charCount(16) + UTF-16
|
|
"HistorianVersion"` — i.e. the **named-value request**, identical to the GETRP/version shape.
|
|
- Its response `pResponseBuff` is **~30 bytes**: `uint charCount(12) + UTF-16 "20,0,000,000"`
|
|
(+ a `02 00 01 00` trailer). **Just the version** — not a 518-byte struct.
|
|
- The post-GETHI ops in the same capture are `Hist/UpdC3` + a run of `Stat/GetSystemParameter`
|
|
(`AllowOriginals`, `HistorianPartner`, `HistorianVersion`, `MaxCyclicStorageTimeout`,
|
|
`RealTimeWindow`, `FutureTimeThreshold`, `AllowRenameTags`). **None carries a storage-mode
|
|
value.** So the native wrapper's `EventStorageMode` is derived by the C++ HCAL **outside the
|
|
WCF wire**, not fetched over it.
|
|
|
|
## Probe: does GETHI expose storage mode under any name?
|
|
|
|
`StringHandleProbeDiagnosticTests.GETHI_CandidateInfoNames_AgainstLocalHistorian` (gated on
|
|
`HISTORIAN_HOST=localhost`) issues GETHI for `HistorianVersion` plus seven storage-mode name
|
|
guesses. Result on the live 2020 server:
|
|
|
|
| GETHI parameter name | result |
|
|
|---|---|
|
|
| `HistorianVersion` | **ok=True**, respLen=32 (version) |
|
|
| `EventStorageMode`, `EventStorageType`, `StorageType`, `HistorianEventStorageMode`, `EventStorage`, `StorageMode`, `HistorianInfo` | **ok=False**, errLen=5, empty |
|
|
|
|
So GETHI on 2020 WCF is a strict named-value lookup with exactly one known-good key
|
|
(`HistorianVersion`). There is no storage-mode key, no full-struct request.
|
|
|
|
## Why the 518-byte struct doesn't apply here
|
|
|
|
The 2023 R2 decompiled `ArchestrA.HistorianAccess.GetHistorianInfo` (analysis folder) allocates
|
|
a **518-byte `HISTORIAN_INFO`** struct, pre-inits `int32 @514` to `-1`, calls native HCAL
|
|
(vtable+648) which fills it, then reads version (UTF-16 @0) + `EventStorageMode` (`@514`:
|
|
`-1`=Unsupported, `0`=Database, else=Blocks). That is the **HCAL-native / 2023R2 gRPC**
|
|
front-door model (`StatusService.GetHistorianInfo` returns `bytes btHistorianInfo`). On **2020
|
|
WCF** that struct is never marshaled across the wire — only the version named-value is. The
|
|
native client's `EventStorageMode` therefore comes from C++-internal state the managed WCF
|
|
replay cannot observe or reproduce.
|
|
|
|
## Conclusion / where it lands
|
|
|
|
- **2020 WCF:** `GetHistorianInfoAsync` would add nothing over existing surface (version only) and
|
|
could not report a real `EventStorageMode` — so it is intentionally **not shipped** (no hollow
|
|
`Unsupported`-returning API; project discipline: don't ship misleading behavior).
|
|
- **2023 R2 gRPC — LIVE-PROBED 2026-06-21, also bounded out.** The earlier expectation that
|
|
`Status.GetHistorianInfo` returns the full 518-byte `btHistorianInfo` over gRPC was **wrong**. On
|
|
the real 2023 R2 server (History iface 12), the gRPC `GetHistorianInfo` is the
|
|
**same named-value query** as 2020 WCF: only `HistorianVersion` resolves (→ `"23,1,000,000"` +
|
|
`02 00 01 00` trailer); `EventStorageMode` and seven name variants return `success=false` on
|
|
**both** `GetHistorianInfo` and `GetSystemParameter`. The 518-byte struct is **not on the gRPC
|
|
wire** — the 2023 R2 decompile confirms managed `HistorianAccess.GetHistorianInfo` fills it via a
|
|
**native vtable+648 HCAL call** (`IClientCommon*` + offset 648), not the gRPC op, so
|
|
`EventStorageMode` is derived inside the C++ HCAL outside the wire on gRPC exactly as on WCF.
|
|
**Conclusion: `GetHistorianInfoAsync` is not shipped on any transport** (the only wire-reachable
|
|
field, version, is already exposed). No `HistorianInfo` / `HistorianEventStorageMode` public type
|
|
was added. Probe: the (now-deleted) `GrpcStatusInfoProbeTests`; raw dump under
|
|
`artifacts/reverse-engineering/grpc-status-info-probe/` (gitignored).
|
|
|
|
## Tooling kept as RE aids
|
|
|
|
- `tools/AVEVA.Historian.NativeTraceHarness` `historian-info` scenario (drives the native call).
|
|
- `scripts/Capture-HistorianInfo.ps1` + `scripts/decode-historian-info-capture.py`.
|
|
- `StringHandleProbeDiagnosticTests.GETHI_CandidateInfoNames_AgainstLocalHistorian` (locks the
|
|
named-value-only finding; gated).
|