R1.2 GetRuntimeParameter + string-handle wall RESOLVED (handle-format bug)
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
This commit is contained in:
@@ -48,3 +48,26 @@ Interpretation:
|
||||
- **`GetServerTimeZoneAsync` (roadmap R1.3) is NOT a trivial WCF op on 2020** — it
|
||||
is a stub returning empty. Do not ship it over the 2020 WCF transport. Deliver
|
||||
it only against a live 2023 R2 gRPC server. Reclassified in `docs/plans/hcal-roadmap.md`.
|
||||
|
||||
## GETRP / GetRuntimeParameter (roadmap R1.2) — DONE, live-verified 2026-06-20
|
||||
|
||||
Captured the native `HistorianAccess.GetRuntimeParameter(List<string>, out List<object>)`
|
||||
WCF traffic with `scripts/Capture-RuntimeParam.ps1` (instrument-wcf-{write,read}message).
|
||||
Findings:
|
||||
|
||||
- The WCF op is **`aa/Stat/GETRP`** — `bool GETRP(string handle, byte[] pRequestBuff,
|
||||
out byte[] pResponseBuff, out byte[] errorBuffer)`, i.e. the **same string-handle +
|
||||
request/response-buffer shape as GETHI**, *not* the simple `GetSystemParameter(uint, string)`
|
||||
shape the roadmap originally assumed.
|
||||
- The `string handle` is the **Open2 storage-session GUID** (the value
|
||||
`ParseOpenConnectionResponse` reads from `outBuff[5..21]`), sent **UPPERCASE, dash-separated,
|
||||
no braces** (`ToString("D").ToUpperInvariant()`).
|
||||
- Unlike GETHI (which the earlier probe found blocked), **GETRP succeeds from the pure-managed
|
||||
client** with that handle: `GetRuntimeParameter("HistorianVersion")` → `20,0,000,000`.
|
||||
- `pRequestBuff` = `54 67 01 00` (sig+version) + uint nameCount + per name(uint charCount +
|
||||
UTF-16LE). `pResponseBuff` = version(1) + uint resultCount + CRetVariant(`0x43` VT_BSTR +
|
||||
uint16 payloadLen + uint16 charCount + UTF-16LE).
|
||||
|
||||
Shipped as `HistorianClient.GetRuntimeParameterAsync(name)`. See
|
||||
`HistorianRuntimeParameterProtocol`, golden `WcfRuntimeParameterProtocolTests`, and the
|
||||
handle-format lead in `wcf-string-handle-wall.md` §Update (retry GETHI/ExeC uppercased).
|
||||
|
||||
@@ -1,10 +1,35 @@
|
||||
# The 2020 WCF string-handle wall (2026-06-20)
|
||||
|
||||
> ## ✅✅ RESOLVED (2026-06-20): the "wall" was a handle-FORMAT bug, not a registration wall.
|
||||
>
|
||||
> The string-handle ops are reachable from the pure-managed client after all. The Open2
|
||||
> storage-session GUID must be passed as the `string handle` **UPPERCASE, dash-separated,
|
||||
> no braces** — `storageSessionId.ToString("D").ToUpperInvariant()`. The earlier probes that
|
||||
> "proved" the wall passed the GUID in .NET's default **lowercase** `ToString("D")`, which the
|
||||
> server's session table does not match. Live-verified end-to-end against the local 2020 server:
|
||||
> - **GETRP** (R1.2) → returns the runtime `HistorianVersion` (shipped).
|
||||
> - **GETHI** (R1.4) → `returned=True`, returns the version buffer (`0C000000` + UTF-16 "20,0,000,000").
|
||||
> - **ExeC** (R1.1) → `returned=True`, `Retr.GetV` prime + `ExeC("SELECT 1 AS ProbeValue", option=0)`
|
||||
> yields `queryHandle`, then `GetR(handle, queryHandle, sequence=0)` returns a 1232-byte result =
|
||||
> a **BinaryFormatter-serialized .NET DataTable** (stream header `…System.Data, Version=4.0.0.0…`).
|
||||
>
|
||||
> Probes: gated `StringHandleProbeDiagnosticTests` (GETHI + ExeC). Captures:
|
||||
> `scripts/Capture-RuntimeParam.ps1`, `scripts/Capture-ExecSql.ps1`. The handle for ExeC/GetR is the
|
||||
> **same** Open2 storage-session GUID (confirmed = `outBuff[5..21]`). The original analysis below is
|
||||
> retained for history; treat its "blocked" conclusions as **superseded** — the only missing piece
|
||||
> was the uppercase format. R1.5/R1.6 (GetTepByNm family) and QTB/QTG are very likely reachable the
|
||||
> same way but have not yet been individually re-probed.
|
||||
|
||||
---
|
||||
|
||||
Live-probing the local **Historian 2020** (WCF, port 32568) for HCAL roadmap M1
|
||||
surfaced a clean structural boundary on what the pure-managed client can call. It
|
||||
explains why R1.1/R1.4/R1.5 all fail and identifies the single RE target that
|
||||
unblocks the rest of the M1 read surface.
|
||||
|
||||
> ⚠️ **Superseded — see the RESOLVED banner above.** The boundary below is real *only* when the
|
||||
> handle is sent lowercase. With the uppercased storage GUID the string-handle ops succeed.
|
||||
|
||||
## The dichotomy
|
||||
|
||||
Retrieval/Status/History ops split by the **type of their first (handle) parameter**:
|
||||
@@ -56,3 +81,34 @@ once and the whole family unlocks. Until then, the alternatives are:
|
||||
|
||||
Do **not** ship any string-handle op via guessed calls (project discipline:
|
||||
"leave them throwing until evidence supports an implementation").
|
||||
|
||||
## ⚠️ Update (2026-06-20): GETRP punches through — the wall is not absolute
|
||||
|
||||
Roadmap **R1.2 `GetRuntimeParameterAsync`** turned out to be a **`string`-handle op**
|
||||
(`aa/Stat/GETRP(string handle, byte[] pRequestBuff) → (bool, byte[] pResponseBuff,
|
||||
byte[] errorBuffer)`) — the **same shape as GETHI**, and in the same native session it
|
||||
uses the **same handle GUID** as GETHI (confirmed: the GUID equals the Open2 `outBuff`
|
||||
storage-session id at `[5..21]`, the value the managed `ParseOpenConnectionResponse`
|
||||
already extracts as `StorageSessionId`).
|
||||
|
||||
Yet GETRP **works from the pure-managed client** — live-verified, returns the runtime
|
||||
`HistorianVersion` value `20,0,000,000`. The only material difference from the failed
|
||||
GETHI probe is the **handle string format**: the native client sends the GUID
|
||||
**UPPERCASE, dash-separated, no braces** (format example
|
||||
`XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX`, all hex upper), i.e.
|
||||
`storageSessionId.ToString("D").ToUpperInvariant()`. `.NET Guid.ToString("D")` is
|
||||
lowercase, so a probe that passed the GUID without upcasing would not byte-match what
|
||||
the server's session table is keyed on.
|
||||
|
||||
**Implication / open lead (not yet retested):** the GETHI/ExeC/QTB/QTG family failures
|
||||
may be (at least partly) a **handle-format** issue, not (only) a missing native
|
||||
registration step. The highest-value cheap follow-up is to **re-probe GETHI and ExeC
|
||||
with the uppercased storage-session GUID** before assuming the registration wall. If
|
||||
they also return data, the "wall" collapses to a formatting bug and R1.4/R1.5/R1.6/R1.1
|
||||
may be reachable without any new RE. This has **not** been done yet — do not reclassify
|
||||
those items until it is. GETRP is shipped because it was directly captured + live-verified
|
||||
end-to-end; the rest remain `ProtocolEvidenceMissingException`/unprobed until tested.
|
||||
|
||||
See `HistorianRuntimeParameterProtocol`, `IStatusServiceContract2.GetRuntimeParameter`,
|
||||
golden `WcfRuntimeParameterProtocolTests`, and capture tooling
|
||||
`scripts/Capture-RuntimeParam.ps1` + `scripts/decode-runtime-param-capture.py`.
|
||||
|
||||
Reference in New Issue
Block a user