5ce62a5900
ApplyScaling (HistorianTagDefinition.ApplyScaling): The EnsT2 trailer's second byte controls server-side scaling — `FE 00` mirrors MinRaw to MinEU and sets AnalogTag.Scaling=0; `FE 01` persists distinct MinRaw/MaxRaw and sets Scaling=1. Decoded by toggling set_ApplyScaling on the native harness and capturing the wire bytes for both values with identical inputs. The earlier docs claimed EnsureTagAsync needed a follow-up "UpdateTags" call; the WCF surface has no such operation — toggling that one byte is the whole fix. StorageRate (HistorianTagDefinition.StorageRateMs): Serializer accepts a non-default rate, validated empirically against the live server which only accepts quantized values (1000/5000/10000/60000/300000 ms). EnsureTagAsync upsert semantics: Second call on the same tag name with different fields succeeds and updates Description, MinEU, MaxEU, MinRaw, MaxRaw, Scaling in place (verified by direct SQL inspection in a live test). Plan + doc closeout: write-commands-reverse-engineering.md rewritten as a current-state plan with three workstreams (A doc closeout / B idempotency / C1 StorageRate) and a parallelism table; prior phase notes preserved as appendix. handoff.md, implementation-status.md, wcf-contract-evidence.md, README.md updated to remove "writes are out of scope" / non-existent UpdateTags references and document the actual EnsT2 wire format including the `FE xx` trailer. Reverse-engineering harness gains --write-apply-scaling and a SQL post-check that prints the persisted AnalogTag bounds so future RE sessions can verify wire→DB causality without leaving the harness. 169/169 tests pass (was 165; +4 new tests covering ApplyScaling, StorageRate golden bytes, StorageRate live persistence, and EnsureTagAsync upsert semantics). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
192 lines
11 KiB
Markdown
192 lines
11 KiB
Markdown
# WCF Contract Evidence
|
|
|
|
## Local run evidence
|
|
|
|
- `current\aahClient.dll` export inventory ran successfully. SHA256:
|
|
`77a778988e2d8f2d0e88113f8c8b0788a0ef34fa5134938a353976778144dc83`.
|
|
- `ArchestrA.HistorianAccess.OpenConnection` succeeded against `localhost:32568`
|
|
using `HistorianConnectionArgs` with `ConnectionType=Process` and `ReadOnly=true`.
|
|
- Holding that native connection open produced established TCP sessions from the
|
|
native PowerShell process to `127.0.0.1:32568`; the server-side listener was
|
|
owned by `SMSvcHost.exe`, consistent with WCF Net.TCP port sharing.
|
|
- The managed harness command
|
|
`dotnet run --no-build --project tools\AVEVA.Historian.ReverseEngineering -- wcf-probe localhost 32568`
|
|
successfully called `GetV` through fully managed WCF/MDAS:
|
|
- `net.tcp://localhost:32568/Hist` returned version `11`
|
|
- `net.tcp://localhost:32568/Retr` returned version `4`
|
|
- `net.tcp://localhost:32568/Stat` returned version `0`
|
|
- `net.tcp://localhost:32568/Trx` returned version `2`
|
|
- `net.tcp://localhost:32568/Storage` did not listen on this local install
|
|
- `net.tcp://localhost:32568/HistCert` and `/Hist-Integrated` reset when
|
|
called with the plain managed `GetV` contract, while prefixed variants such
|
|
as `/HCAP/HistCert` returned `EndpointNotFound`
|
|
Sanitized output is stored in `docs\reverse-engineering\wcf-probe-localhost.json`.
|
|
- `netsh trace` and `pktmon` produced ETL files under
|
|
`%TEMP%\histsdk-captures`, but their converted PCAPNG files contained zero
|
|
packets. Built-in Windows packet capture is not sufficient for loopback
|
|
evidence on this machine.
|
|
- A dedicated managed certificate-binding probe now reaches `HistCert.GetV`
|
|
through MDAS over WCF Net.TCP transport security:
|
|
- `net.tcp://localhost:32568/HistCert` returned version `11`
|
|
- `net.tcp://10.100.0.48:32568/HistCert` initially failed endpoint identity
|
|
validation because the server certificate presented DNS identity
|
|
`localhost`
|
|
- the same remote endpoint returned version `11` when the client supplied
|
|
endpoint DNS identity `localhost`
|
|
Sanitized outputs are stored in
|
|
`docs\reverse-engineering\wcf-cert-probe-localhost-latest.json`,
|
|
`docs\reverse-engineering\wcf-cert-probe-remote-latest.json`, and
|
|
`docs\reverse-engineering\wcf-cert-probe-remote-localhost-identity-latest.json`.
|
|
- The same remote server also accepts the plain managed WCF/MDAS probe on the
|
|
expected non-security-specific service paths:
|
|
- `net.tcp://10.100.0.48:32568/Hist` returned version `11`
|
|
- `net.tcp://10.100.0.48:32568/Retr` returned version `4`
|
|
- `net.tcp://10.100.0.48:32568/Stat` returned version `0`
|
|
- `net.tcp://10.100.0.48:32568/Trx` returned version `2`
|
|
Sanitized output is stored in
|
|
`docs\reverse-engineering\wcf-probe-remote-latest.json`.
|
|
- Managed remote `Open2` evidence matches localhost: integrated Windows auth
|
|
succeeds on `net.tcp://10.100.0.48:32568/Hist-Integrated`, while the same
|
|
Windows transport binding fails on plain `/Hist` before dispatch. The
|
|
successful returned handle is accepted by `Retr.IsOriginalAllowed`. Session
|
|
output bytes and transient handle values are redacted in
|
|
`docs\reverse-engineering\wcf-open2-remote-latest.json`.
|
|
- Managed remote `StartQuery2` evidence is still negative but sharper: all 22
|
|
reconstructed `DataQueryRequest` variants successfully open the integrated
|
|
session and pass `Retr.IsOriginalAllowed`, then `StartQuery2` returns `false`
|
|
with zero response and error sizes. The legacy `StartQuery` call returns code
|
|
`238` for each request and also returns zero response size/no response buffer.
|
|
Sanitized request hashes are stored in
|
|
`docs\reverse-engineering\wcf-start-query-remote-latest.json`.
|
|
- A later bounded managed replay of the first byte-matched full-history
|
|
candidate used the same integrated open and `Retr.IsOriginalAllowed` path;
|
|
`StartQuery2` still returned `false` with zero response/error sizes, while
|
|
legacy `StartQuery` faulted with a server null-reference. This keeps
|
|
`Open2` as useful connection evidence, but not as a viable replacement for
|
|
the native `OpenConnection3` session state required by query reads.
|
|
- Managed wildcard tag browse remains positive evidence for an `Open2`-backed
|
|
operation: `Retr.StartLikeTagNameSearch` returned `0`, and one
|
|
`GetLikeTagnames` batch returned the deterministic 66-byte single-tag buffer
|
|
with SHA-256
|
|
`2d450a55f392aed0026e9a957fefa3b116aab6ec81912c5d824c6b9a1ff5a4a1`.
|
|
- Managed remote scalar tag calls also accept the integrated session handle:
|
|
`Retr.GetTagTypeFromName` returns code `0` and tag type `1` for
|
|
`OtOpcUaParityTest_001.Counter`; `Retr.IsManualTag` returns code `0` and
|
|
`false`; legacy `Retr.GetTagInfoFromName` returns `238` with zero metadata
|
|
bytes. Five `GetTgByNm` tag-name buffer variants and five `GetTg` tag-id
|
|
buffer variants all return `238`, sequence `0`, and zero output bytes. This
|
|
suggests the calls are reaching server logic but the metadata-returning
|
|
contract shape or request buffer is still incomplete. Sanitized output is
|
|
stored in
|
|
`docs\reverse-engineering\wcf-tag-info-remote-latest.json`.
|
|
|
|
## Decompiled service contracts
|
|
|
|
`current\aahClientManaged.dll` contains WCF contracts with namespace `aa`:
|
|
|
|
- `HistoryServiceContract.IHistoryServiceContract`
|
|
- `[ServiceContract(Name = "Hist", Namespace = "aa")]`
|
|
- `GetInterfaceVersion` as operation `GetV`
|
|
- `OpenConnection` as operation `Open`
|
|
- `CloseConnection` as operation `Close`
|
|
- `ValidateClient` as operation `VldC`
|
|
- `UpdateClientStatus` as operation `UpdC`
|
|
- `AddTags` as `AddT`, `RegisterTags` as `RTag`
|
|
- `AddStreamValues` as `AddS`, `SetClientTimeOut` as `SetT`
|
|
- `HistoryServiceContract.IHistoryServiceContract2`
|
|
- `[ServiceContract(Name = "Hist", Namespace = "aa")]`
|
|
- byte-buffer session open uses `OpenConnection2` as operation `Open2`
|
|
- extended client status uses `UpdC2` / `UpdC3`
|
|
- extended write and maintenance calls include `EnsT`, `DelT`, `AddS2`,
|
|
`ExKey`, `ValCl`, and `GetI`
|
|
- `RetrievalServiceContract.IRetrievalServiceContract`
|
|
- `[ServiceContract(Name = "Retr", Namespace = "aa")]`
|
|
- `StartQuery`, `GetNextQueryResultBuffer`, `EndQuery` use default
|
|
operation names
|
|
- tag type/name helpers and tag info calls use default operation names
|
|
- `RetrievalServiceContract.IRetrievalServiceContract2/3/4`
|
|
- extended bool/error-buffer variants
|
|
- SQL/recordset byte stream calls
|
|
- tag query calls `QTB`, `QTG`, `QTE`
|
|
- event query calls use default operation names
|
|
- extended property calls include `GetTgByNm2` and `GetTepByNm`
|
|
- `StorageServiceContract.IStorageServiceContract`
|
|
- `[ServiceContract(Name = "Storage", Namespace = "aa")]`
|
|
- storage/session, metadata, streamed-value, block, snapshot, and delete-tag
|
|
calls
|
|
- `StatusServiceContract.IStatusServiceContract`
|
|
- `[ServiceContract(Name = "Stat", Namespace = "aa")]`
|
|
- `GetInterfaceVersion` as `GetV`
|
|
- server time, timezone, DB case sensitivity, and logging use default
|
|
operation names
|
|
- `StatusServiceContract.IStatusServiceContract2`
|
|
- extended status operations include `GetSystemParameter`, `GetTimeZoneNames`,
|
|
license checks, historian info, and process/ping helpers
|
|
- ping and historian-info helpers use aliases `PNGS`, `PNGP`, and `GETHI`
|
|
- `TransactionServiceContract.ITransactionServiceContract`
|
|
- `[ServiceContract(Name = "Trx", Namespace = "aa")]`
|
|
- snapshot forwarding and non-streamed value transactions
|
|
|
|
`aahMDASEncoder.ClientMessageEncoder` wraps an inner WCF encoder and exposes
|
|
media/content type `application/x-mdas`. This means the first managed driver
|
|
transport target should be WCF Net.TCP plus the MDAS content-type encoder, not
|
|
the earlier speculative raw-frame layer.
|
|
|
|
## Current unknowns
|
|
|
|
- Endpoint URI paths `net.tcp://{host}:32568/Hist`, `/Retr`, `/Stat`, and
|
|
`/Trx` are confirmed for `GetV` calls on the local 2020 install.
|
|
- Relay and local WCF probe evidence also identify security-specific history
|
|
endpoints `/HistCert` and `/Hist-Integrated`. `/HistCert` is confirmed as a
|
|
`Hist` contract endpoint when called with MDAS over TLS transport security;
|
|
`/Hist-Integrated` remains the Windows negotiate endpoint for integrated
|
|
session open.
|
|
- Managed `Open2` evidence confirms `/Hist-Integrated` is the correct endpoint
|
|
for integrated Windows auth. The plain `/Hist` endpoint rejects the Windows
|
|
transport-security upgrade before dispatching the operation.
|
|
- The storage contract is confirmed statically, but `/Storage` was not a
|
|
listening endpoint in the local probe; storage may be routed through
|
|
session-specific storage/shard endpoints.
|
|
- `Hist.OpenConnection` reaches server logic, but the native password/session
|
|
packet encoding is not decoded yet. See `wcf-open-localhost.md`.
|
|
- `Hist.Open2` is confirmed reachable with a managed version-1 byte buffer.
|
|
Empty credentials return custom native error `171` (`AuthenticationFailed`),
|
|
and the harness decodes observed five-byte error buffers as type plus
|
|
little-endian error code. This confirms framing has progressed past
|
|
packet-version rejection. See `wcf-open2-localhost.md`.
|
|
- `Stat` is reachable, but `CStatusConnectionWCF.GetServerTime` is a no-op stub
|
|
in the decompiled native WCF path and handle-dependent status calls fail with
|
|
handle `0`. See `wcf-status-localhost.md`.
|
|
- Query request and response byte-buffer layouts are still proprietary payloads
|
|
inside WCF operations such as `StartQuery` and `GetNextQueryResultBuffer`.
|
|
- Write payload layouts decoded for the two supported ops:
|
|
- `Hist.EnsT2(analog)` 144-byte `CTagMetadata` `InBuff` payload —
|
|
leading `0x4E` marker, fixed 10-byte signature, 1-byte CDataType
|
|
discriminator (`0x01` Float / `0x21` Double / `0x09` UInt2 / `0x11`
|
|
UInt4 / `0x29` Int2 / `0x31` Int4), 16 zero placeholder bytes,
|
|
compact-ASCII tag name, 16 bytes of `0xFF`, compact-ASCII description,
|
|
compact-ASCII `MDAS`, 7-byte flag block, uint32 storage rate,
|
|
int64 FILETIME, scaling block (compact `1A 03` for default
|
|
0/100/0/100 ranges OR `1F 00` + 4 doubles MinEU/MaxEU/MinRaw/MaxRaw
|
|
for explicit), compact-ASCII engineering unit, uint32 `0x2710`
|
|
constant, double 1.0 (IntegralDivisor), 2-byte trailer `FE xx`
|
|
where `xx` is the ApplyScaling flag (`0x00` false / `0x01` true).
|
|
Live-verified: with `0x01` the server persists distinct
|
|
MinRaw/MaxRaw and sets `AnalogTag.Scaling=1`; with `0x00` it
|
|
mirrors MinRaw to MinEU. Captured fixtures live at
|
|
`artifacts/reverse-engineering/instrumented-wcf-writemessage-writes/`
|
|
(default ranges) and
|
|
`artifacts/reverse-engineering/apply-scaling-experiment/` (both
|
|
ApplyScaling values for the same input ranges). Connection mode is
|
|
`0x401` (Process | Write | IntegratedSecurity) — the read-mode
|
|
`0x402` makes the server return err 132 silently.
|
|
- `Hist.DelT` `tagNames` byte buffer — `ushort 0x6751`, `ushort 1`,
|
|
`uint32 tagCount`, then per tag `uint32 charCount + UTF-16-LE chars`.
|
|
Decoded via wire capture against the sandbox tag.
|
|
- `Hist.AddS2` (write samples) is architecturally blocked — server
|
|
runtime cache requires IOServer / Application Server pipeline
|
|
registration, not just a `Tag` row in `Runtime.dbo`. Three
|
|
reproduction attempts (real wwTagKey, fresh session, 8s settle
|
|
wait) confirmed `129 "Tag not found in cache"` is the gate. No
|
|
AddS2 wire bytes leave the client.
|