1a7519c803
Captured the native StartQuery2 pRequestBuff and the GetNextQueryResultBuffer2 response (instrument-wcf-writemessage + chained instrument-wcf-readmessage) and decoded both against AnalogSummaryHistory SQL ground truth. Conclusion: the rich multi-aggregate analog/state summary struct is NOT delivered over the 2020 WCF binary protocol — the response is the ordinary version-9 row buffer the existing aggregate parser already handles, carrying one value per cycle selected by RetrievalMode (QueryType 5-8), not ValueSelector (inert on this path). So "analog summary" == the existing ReadAggregateAsync; no new src/ code warranted. Tooling (tools/ + scripts/ only, nothing in src/): - NativeTraceHarness: drive summary knobs via --value-selector / --aggregation-type / --max-states (uint16) / --filter - Capture-SummaryRequest.ps1: repeatable instrument+stage+matrix capture, -WithResponse chains the ReadMessage hook - decode-summary-capture.py: StartQuery2 request diff vs baseline - decode-summary-response.py: response decode vs SQL ground truth Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
151 lines
8.3 KiB
PowerShell
151 lines
8.3 KiB
PowerShell
<#
|
|
.SYNOPSIS
|
|
Captures the native AVEVA client's StartQuery2 request bytes for analog/state
|
|
summary queries (HCAL roadmap R1.8/R1.9) so the managed SDK's summary request
|
|
shape can be decoded against ground truth instead of guessed.
|
|
|
|
.DESCRIPTION
|
|
Drives the .NET-Framework NativeTraceHarness against the live Historian with an
|
|
IL-rewritten copy of aahClientManaged.dll whose ClientMessageEncoder.WriteMessage
|
|
is instrumented to log every outgoing MDAS body (the same pipeline that produced
|
|
every other proven request shape). For each candidate HistoryQueryArgs config it
|
|
writes a per-config NDJSON capture under
|
|
artifacts/reverse-engineering/instrumented-wcf-writemessage-summary/ (gitignored).
|
|
|
|
The default matrix is:
|
|
- baseline-full : RetrievalMode=Full (the known-good non-summary request)
|
|
- analog-avg : RetrievalMode=Cyclic + ValueSelector=Average + Resolution
|
|
- analog-min : RetrievalMode=Cyclic + ValueSelector=Minimum + Resolution
|
|
- analog-agg-avg : RetrievalMode=Cyclic + AggregationType=Average + Resolution
|
|
- state-summary : RetrievalMode=Cyclic + MaxStates>0 + Resolution
|
|
|
|
Diff any candidate against baseline-full (scripts/decode-summary-capture.py) to read
|
|
off the exact QueryType / SummaryType / AutoSummaryParameters bytes the native client
|
|
sets for a summary, then implement the managed request against that.
|
|
|
|
.NOTES
|
|
Artifacts are diagnostic. Sanitize before copying anything into docs/ — never commit
|
|
raw capture NDJSON, credentials, hostnames, or customer tag names.
|
|
#>
|
|
[CmdletBinding()]
|
|
param(
|
|
[string]$ServerName = "localhost",
|
|
[int]$TcpPort = 32568,
|
|
# SysTimeSec is the local data-bearing system tag (OtOpcUaParityTest_001.Counter is stale/empty).
|
|
[string]$TagName = "SysTimeSec",
|
|
[int]$LookbackMinutes = 240,
|
|
[int]$MaxRows = 4,
|
|
# 1-hour summary cycle in 100ns ticks (1h = 36,000,000,000 ticks).
|
|
[uint64]$ResolutionTicks = 36000000000,
|
|
[string]$Configuration = "Debug",
|
|
# Restrict the run to a single named config from the matrix (default: run all).
|
|
[string]$OnlyConfig = "",
|
|
# Also instrument ReadMessage so each capture includes the incoming WCF response bodies
|
|
# (the GetNextQueryResultBuffer2 pResultBuff summary rows). Decoded by decode-summary-response.py.
|
|
[switch]$WithResponse
|
|
)
|
|
|
|
$ErrorActionPreference = "Stop"
|
|
$repoRoot = Split-Path -Parent $PSScriptRoot
|
|
Set-Location $repoRoot
|
|
|
|
$reProj = Join-Path $repoRoot "tools\AVEVA.Historian.ReverseEngineering\AVEVA.Historian.ReverseEngineering.csproj"
|
|
$harnessProj = Join-Path $repoRoot "tools\AVEVA.Historian.NativeTraceHarness\AVEVA.Historian.NativeTraceHarness.csproj"
|
|
$instrProj = Join-Path $repoRoot "tools\AVEVA.Historian.ReverseInstrumentation\AVEVA.Historian.ReverseInstrumentation.csproj"
|
|
|
|
$captureDir = Join-Path $repoRoot "artifacts\reverse-engineering\instrumented-wcf-writemessage-summary"
|
|
$currentCopy = Join-Path $captureDir "current-copy"
|
|
$instrDll = Join-Path $captureDir "aahClientManaged.dll"
|
|
|
|
Write-Host "== Building tooling ($Configuration) ==" -ForegroundColor Cyan
|
|
dotnet build $reProj -c $Configuration --nologo -v q | Out-Null
|
|
dotnet build $instrProj -c $Configuration --nologo -v q | Out-Null
|
|
dotnet build $harnessProj -c $Configuration --nologo -v q | Out-Null
|
|
|
|
$instrSourceDll = Get-ChildItem -Recurse (Join-Path $repoRoot "tools\AVEVA.Historian.ReverseInstrumentation\bin\$Configuration") `
|
|
-Filter "AVEVA.Historian.ReverseInstrumentation.dll" | Select-Object -First 1 -ExpandProperty FullName
|
|
if (-not $instrSourceDll) { throw "ReverseInstrumentation.dll not found under bin\$Configuration." }
|
|
|
|
Write-Host "== Instrumenting WriteMessage$(if ($WithResponse) { ' + ReadMessage' }) ==" -ForegroundColor Cyan
|
|
New-Item -ItemType Directory -Force -Path $captureDir | Out-Null
|
|
if ($WithResponse) {
|
|
# Chain via a distinct intermediate file (reading+writing the same path drops the second
|
|
# hook on the mixed-mode native image). Final dll carries both hooks with distinct Phase
|
|
# strings: WCF.WriteMessage.Body and WCF.ReadMessage.Body.
|
|
$writeOnly = Join-Path $captureDir "aahClientManaged.write.dll"
|
|
dotnet run --no-build -c $Configuration --project $reProj -- `
|
|
instrument-wcf-writemessage (Join-Path $repoRoot "current\aahClientManaged.dll") $writeOnly | Out-Null
|
|
dotnet run --no-build -c $Configuration --project $reProj -- `
|
|
instrument-wcf-readmessage $writeOnly $instrDll | Out-Null
|
|
} else {
|
|
dotnet run --no-build -c $Configuration --project $reProj -- `
|
|
instrument-wcf-writemessage (Join-Path $repoRoot "current\aahClientManaged.dll") $instrDll | Out-Null
|
|
}
|
|
|
|
Write-Host "== Staging current-copy ==" -ForegroundColor Cyan
|
|
# Mirror current/ into current-copy, then overwrite the managed dll with the instrumented
|
|
# build and drop the strong-named logger assembly alongside it so the injected call binds.
|
|
robocopy (Join-Path $repoRoot "current") $currentCopy /MIR /NJH /NJS /NDL /NP /NC /NS | Out-Null
|
|
Copy-Item -Force $instrDll (Join-Path $currentCopy "aahClientManaged.dll")
|
|
Copy-Item -Force $instrSourceDll (Join-Path $currentCopy "AVEVA.Historian.ReverseInstrumentation.dll")
|
|
|
|
# Candidate matrix: name + harness arg list. Summary configs all use Cyclic + a resolution;
|
|
# the differentiator is which summary knob is set.
|
|
$matrix = @(
|
|
@{ Name = "baseline-full"; Args = @("--retrieval-mode", "Full") },
|
|
@{ Name = "analog-avg"; Args = @("--retrieval-mode", "Cyclic", "--value-selector", "Average", "--resolution-ticks", "$ResolutionTicks") },
|
|
@{ Name = "analog-min"; Args = @("--retrieval-mode", "Cyclic", "--value-selector", "Minimum", "--resolution-ticks", "$ResolutionTicks") },
|
|
@{ Name = "analog-max"; Args = @("--retrieval-mode", "Cyclic", "--value-selector", "Maximum", "--resolution-ticks", "$ResolutionTicks") },
|
|
@{ Name = "analog-integral"; Args = @("--retrieval-mode", "Cyclic", "--value-selector", "Integral", "--resolution-ticks", "$ResolutionTicks") },
|
|
@{ Name = "mode-integral"; Args = @("--retrieval-mode", "Integral", "--resolution-ticks", "$ResolutionTicks") },
|
|
@{ Name = "mode-twavg"; Args = @("--retrieval-mode", "TimeWeightedAverage", "--resolution-ticks", "$ResolutionTicks") },
|
|
@{ Name = "analog-agg-avg"; Args = @("--retrieval-mode", "Cyclic", "--aggregation-type", "Average", "--resolution-ticks", "$ResolutionTicks") },
|
|
@{ Name = "state-summary"; Args = @("--retrieval-mode", "Cyclic", "--max-states", "10", "--resolution-ticks", "$ResolutionTicks") }
|
|
)
|
|
|
|
if ($OnlyConfig) { $matrix = $matrix | Where-Object { $_.Name -eq $OnlyConfig } }
|
|
if (-not $matrix) { throw "No matrix entry named '$OnlyConfig'." }
|
|
|
|
$harnessDll = Join-Path $currentCopy "aahClientManaged.dll"
|
|
$summary = @()
|
|
|
|
foreach ($cfg in $matrix) {
|
|
$name = $cfg.Name
|
|
$capturePath = Join-Path $captureDir "summary-capture-$name-latest.ndjson"
|
|
if (Test-Path $capturePath) { Remove-Item -Force $capturePath }
|
|
$env:AVEVA_HISTORIAN_RE_CAPTURE = $capturePath
|
|
|
|
Write-Host "== Capturing: $name ==" -ForegroundColor Green
|
|
$harnessArgs = @(
|
|
"--scenario", "history",
|
|
"--server-name", $ServerName,
|
|
"--tcp-port", "$TcpPort",
|
|
"--tag", $TagName,
|
|
"--lookback-minutes", "$LookbackMinutes",
|
|
"--max-rows", "$MaxRows",
|
|
"--current-dir", $currentCopy,
|
|
"--managed-dll-path", $harnessDll
|
|
) + $cfg.Args
|
|
|
|
# Don't let a single config that errors (e.g. state summary on an analog tag) abort the
|
|
# whole matrix, and don't treat dotnet's stderr noise as a terminating error.
|
|
try {
|
|
$prevEap = $ErrorActionPreference
|
|
$ErrorActionPreference = "Continue"
|
|
& dotnet run --no-build -c $Configuration --project $harnessProj -- @harnessArgs 2>&1 | Out-Null
|
|
} catch {
|
|
Write-Host " (config '$name' raised: $($_.Exception.Message))" -ForegroundColor Yellow
|
|
} finally {
|
|
$ErrorActionPreference = $prevEap
|
|
}
|
|
$recCount = if (Test-Path $capturePath) { (Get-Content $capturePath | Where-Object { $_.Trim() }).Count } else { 0 }
|
|
Write-Host " -> $recCount records -> $capturePath"
|
|
$summary += [pscustomobject]@{ Config = $name; Records = $recCount; Capture = $capturePath }
|
|
}
|
|
|
|
Remove-Item Env:\AVEVA_HISTORIAN_RE_CAPTURE -ErrorAction SilentlyContinue
|
|
|
|
Write-Host "`n== Capture summary ==" -ForegroundColor Cyan
|
|
$summary | Format-Table -AutoSize
|
|
Write-Host "Decode with: python scripts\decode-summary-capture.py" -ForegroundColor Cyan
|