f1e23a3a02
Roadmap Milestone 2 (event sending). Capture disproved the assumption that event delivery uses the non-WCF storage-engine pipe (which would block it like revision writes): a native AddStreamedValue(HistorianEvent) leaves over WCF as AddS2 (IHistoryServiceContract2.AddStreamValues2). CM_EVENT is a built-in registered tag, so the 129 TagNotFoundInCache gate that blocks AddS2 for user tags does not apply. - R2.1: NativeTraceHarness "event-send" scenario + Capture-EventSend.ps1; two captures diffed to separate constant framing from value-dependent fields. - R2.2: HistorianEventWriteProtocol serializes the AddS2 pBuf (storage sample buffer wrapping the event VTQ) — golden-byte tested. Decoded "OS" sig + length fields + CM_EVENT tag id + EventTime/ReceivedTime FILETIMEs + Opc 192 + 0x118D descriptor + event Id + Namespace + EventType + version 5 + typed property bag. - R2.3/R2.4: HistorianWcfEventOrchestrator.SendEventAsync (Open2 event-mode 0x501 -> reuse CM_EVENT RTag2/EnsT2 -> AddStreamValues2) + HistorianClient.SendEventAsync. - R2.5: gated live test; server accepts the AddS2 (success, empty error buffer). Server requires delivered byte[].Length == declared packet length (uint32@0x04); the native relies on the MDAS encoder adding a pad byte, so the SDK emits an explicit trailing 0x00 (else AddS2 rejects with "CValuStream buffer size vs packet length mismatch"). Original events only (RevisionVersion=0) with string properties; other property types + revision/update/delete throw ProtocolEvidenceMissingException. Caveat (documented): accepted events are not persisted on the local dev box; the native client behaves identically (event ingestion pipeline inactive) — not an SDK gap. 212 unit tests pass; 16/16 event tests pass live. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC
95 lines
4.0 KiB
Python
95 lines
4.0 KiB
Python
"""Decide whether native event-send (HCAL R2.1) rides WCF or the storage-engine pipe.
|
|
|
|
Reads the both-hooks capture produced by scripts/Capture-EventSend.ps1 and, for every
|
|
outgoing WCF.WriteMessage.Body, tries to recognise the SOAP action / operation name. It
|
|
then renders a verdict:
|
|
|
|
* If a storage/event delivery op (AddStreamValues / EnqueueEventDataPacket /
|
|
OpenEventConnection / StartStorage / AddS2 / AddStreamValues2) appears on the WRITE path,
|
|
event-send is a WCF op → M2 is implementable over WCF and that body carries the
|
|
PackToVtq event value blob to decode (R2.2).
|
|
* If NO such op appears on the WRITE path, the queued event was delivered via the
|
|
storage-engine shared-memory pipe (not WCF) — M2 is architecturally blocked as a
|
|
pure-managed-WCF SDK, the same conclusion as the revision-write path.
|
|
|
|
Output is diagnostic. Sanitize before copying into docs/.
|
|
"""
|
|
import base64
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
CAPTURE = (REPO_ROOT / "artifacts" / "reverse-engineering"
|
|
/ "instrumented-wcf-event-send" / "event-send-capture-latest.ndjson")
|
|
|
|
# Operation-name markers we care about. The MDAS binary SOAP body carries the action /
|
|
# operation name as readable text (ASCII and/or UTF-16LE). We scan for both encodings.
|
|
EVENT_OR_STORAGE_OPS = [
|
|
"AddStreamValues2", "AddStreamValues", "EnqueueEventDataPacket", "OpenEventConnection2",
|
|
"OpenEventConnection", "StartStorage", "AddS2", "ForwardEventSnapshot", "AddStreamedValue",
|
|
"AddNonStreamValues",
|
|
]
|
|
# Other ops we expect to see on a healthy event-send connection (auth/open/registration),
|
|
# printed for context so a "no event op" result is clearly "delivery left WCF", not "nothing ran".
|
|
KNOWN_OPS = [
|
|
"GetV", "ValCl", "Open2", "OpenConnection", "GETHI", "GetSystemParameter",
|
|
"UpdC3", "UpdateClientStatus3", "RTag2", "RegisterTags2", "EnsT2", "EnsureTags2",
|
|
"IsOriginalAllowed", "StartQuery", "GetInterfaceVersion",
|
|
]
|
|
|
|
|
|
def find_ops(body, candidates):
|
|
hits = []
|
|
for op in candidates:
|
|
a = op.encode("ascii")
|
|
u = op.encode("utf-16-le")
|
|
if a in body or u in body:
|
|
hits.append(op)
|
|
return hits
|
|
|
|
|
|
def main() -> int:
|
|
if not CAPTURE.exists():
|
|
print(f"Capture not found: {CAPTURE}")
|
|
print("Run: scripts/Capture-EventSend.ps1")
|
|
return 1
|
|
|
|
with CAPTURE.open(encoding="utf-8-sig") as fh:
|
|
records = [json.loads(line) for line in fh if line.strip()]
|
|
|
|
writes = [r for r in records if r.get("Phase") == "WCF.WriteMessage.Body"]
|
|
reads = [r for r in records if r.get("Phase") == "WCF.ReadMessage.Body"]
|
|
print(f"Records: {len(records)} (write={len(writes)} read={len(reads)})\n")
|
|
|
|
event_write_hits = []
|
|
print("== Outgoing WCF.WriteMessage.Body ops ==")
|
|
for i, r in enumerate(writes):
|
|
body = base64.b64decode(r["Base64"])
|
|
known = find_ops(body, KNOWN_OPS)
|
|
event = find_ops(body, EVENT_OR_STORAGE_OPS)
|
|
if event:
|
|
event_write_hits.extend(event)
|
|
label = ", ".join(event + known) or "<no recognized op>"
|
|
flag = " <<< EVENT/STORAGE OP" if event else ""
|
|
print(f" write[{i:02d}] {len(body):6d}B {label}{flag}")
|
|
|
|
print("\n== Verdict ==")
|
|
if event_write_hits:
|
|
uniq = sorted(set(event_write_hits))
|
|
print(f" EVENT/STORAGE op(s) on the WCF WRITE path: {uniq}")
|
|
print(" => event-send IS a WCF op. M2 viable over WCF; decode the PackToVtq value")
|
|
print(" blob in that body for R2.2.")
|
|
return 0
|
|
|
|
print(" NO event/storage delivery op on the WCF WRITE path.")
|
|
print(" => the queued event did NOT leave via WCF. If the native AddStreamedValue")
|
|
print(" returned success (see harness JSON), delivery used the storage-engine")
|
|
print(" shared-memory pipe — M2 is blocked as a pure-managed-WCF SDK, same as the")
|
|
print(" revision-write path (docs/plans/revision-write-path.md).")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|