c95824a65d
Full read-only SDK (src/AVEVA.Historian.Client) implementing the CLAUDE.md required
surface against AVEVA Historian's binary WCF protocol — no native AVEVA runtime
dependency. All operations live-verified against a local Historian:
- ProbeAsync, ReadRawAsync, ReadAggregateAsync, ReadAtTimeAsync, ReadEventsAsync
- BrowseTagNamesAsync, GetTagMetadataAsync (17 native data-type codes mapped)
- GetConnectionStatusAsync, GetStoreForwardStatusAsync, GetSystemParameterAsync
- 108/108 unit + integration tests pass
Includes the reverse-engineering toolkit (tools/AVEVA.Historian.ReverseEngineering)
used to decode the protocol: WCF probes, IL inspection via dnlib, and IL-rewrite
instrumentation (instrument-wcf-{write,read}message etc.) plus the .NET Framework
trace harness (tools/AVEVA.Historian.NativeTraceHarness) for parity testing.
Sanitized handoff evidence under docs/reverse-engineering/. Native AVEVA binaries
(current/, aveva-install-x64/, aveva-install-x86/) are gitignored — fetch separately
from the AVEVA installer.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
177 lines
6.2 KiB
PowerShell
177 lines
6.2 KiB
PowerShell
param(
|
|
[string]$SshUser = "dohertj2",
|
|
[string]$SshHost = "10.100.0.35",
|
|
[string]$TargetHost = "10.100.0.48",
|
|
[int]$ListenPort = 32568,
|
|
[int]$TargetPort = 32568,
|
|
[ValidateSet("history", "event")]
|
|
[string]$Scenario = "history",
|
|
[string]$TagName = "OtOpcUaParityTest_001.Counter",
|
|
[int]$LookbackMinutes = 1440,
|
|
[int]$MaxRows = 1,
|
|
[string]$OutputPath = $null
|
|
)
|
|
|
|
$ErrorActionPreference = "Stop"
|
|
|
|
$repoRoot = Split-Path -Parent $PSScriptRoot
|
|
$stamp = Get-Date -Format "yyyyMMdd-HHmmss"
|
|
if ([string]::IsNullOrWhiteSpace($OutputPath)) {
|
|
$OutputPath = Join-Path $repoRoot "docs\reverse-engineering\system-boundary-via-debian-relay-$Scenario-$stamp.ndjson"
|
|
}
|
|
|
|
$localPy = Join-Path $env:TEMP "histsdk-simple-relay-$stamp.py"
|
|
$remotePy = "/tmp/histsdk-simple-relay-$stamp.py"
|
|
$remoteLog = "/tmp/histsdk-simple-relay-$stamp.log"
|
|
$sshTarget = "$SshUser@$SshHost"
|
|
$tcpOwnerLog = Join-Path $env:TEMP "histsdk-tcp-owner-$stamp.ndjson"
|
|
|
|
$relaySource = @'
|
|
import argparse, selectors, socket, sys, time
|
|
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--listen-port", type=int, default=32568)
|
|
parser.add_argument("--target", required=True)
|
|
parser.add_argument("--target-port", type=int, default=32568)
|
|
args = parser.parse_args()
|
|
|
|
sel = selectors.DefaultSelector()
|
|
peers = {}
|
|
|
|
def log(text):
|
|
print(time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), text, flush=True)
|
|
|
|
def close_pair(sock):
|
|
peer = peers.pop(sock, None)
|
|
if peer is None:
|
|
return
|
|
peers.pop(peer, None)
|
|
for item in (sock, peer):
|
|
try: sel.unregister(item)
|
|
except Exception: pass
|
|
try: item.close()
|
|
except Exception: pass
|
|
|
|
def relay(sock):
|
|
peer = peers.get(sock)
|
|
if peer is None:
|
|
return
|
|
try:
|
|
data = sock.recv(65536)
|
|
except Exception as exc:
|
|
log("recv_error " + repr(exc))
|
|
close_pair(sock)
|
|
return
|
|
if not data:
|
|
close_pair(sock)
|
|
return
|
|
try:
|
|
peer.sendall(data)
|
|
except Exception as exc:
|
|
log("send_error " + repr(exc))
|
|
close_pair(sock)
|
|
|
|
def accept(listener):
|
|
client, address = listener.accept()
|
|
client.setblocking(False)
|
|
upstream = socket.create_connection((args.target, args.target_port), timeout=8)
|
|
upstream.setblocking(False)
|
|
peers[client] = upstream
|
|
peers[upstream] = client
|
|
sel.register(client, selectors.EVENT_READ, relay)
|
|
sel.register(upstream, selectors.EVENT_READ, relay)
|
|
log("connect " + repr(address))
|
|
|
|
listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
listener.bind(("0.0.0.0", args.listen_port))
|
|
listener.listen(16)
|
|
listener.setblocking(False)
|
|
sel.register(listener, selectors.EVENT_READ, accept)
|
|
log("listening")
|
|
|
|
while True:
|
|
for key, _ in sel.select(timeout=1):
|
|
key.data(key.fileobj)
|
|
'@
|
|
|
|
Push-Location $repoRoot
|
|
try {
|
|
Set-Content -LiteralPath $localPy -Value $relaySource -Encoding UTF8
|
|
scp -q $localPy "${sshTarget}:$remotePy"
|
|
$remoteCommand = "nohup python3 $remotePy --listen-port $ListenPort --target $TargetHost --target-port $TargetPort > $remoteLog 2>&1 < /dev/null & echo `$!"
|
|
$relayPid = (ssh -o BatchMode=yes -o ConnectTimeout=8 $sshTarget $remoteCommand).Trim()
|
|
Start-Sleep -Seconds 1
|
|
"--- relay startup log ---" | Tee-Object -FilePath $OutputPath -Append | Out-Host
|
|
ssh -o BatchMode=yes -o ConnectTimeout=8 $sshTarget "cat $remoteLog 2>/dev/null || true" |
|
|
Tee-Object -FilePath $OutputPath -Append |
|
|
Out-Host
|
|
|
|
$monitorJob = Start-Job -ScriptBlock {
|
|
param($RemoteAddress, $RemotePort, $LogPath)
|
|
$deadline = (Get-Date).AddSeconds(45)
|
|
while ((Get-Date) -lt $deadline) {
|
|
Get-NetTCPConnection -RemoteAddress $RemoteAddress -RemotePort $RemotePort -ErrorAction SilentlyContinue |
|
|
ForEach-Object {
|
|
$processName = $null
|
|
try {
|
|
$processName = (Get-Process -Id $_.OwningProcess -ErrorAction Stop).ProcessName
|
|
}
|
|
catch {
|
|
$processName = $null
|
|
}
|
|
|
|
[pscustomobject]@{
|
|
TimestampUtc = (Get-Date).ToUniversalTime().ToString("O")
|
|
LocalAddress = $_.LocalAddress
|
|
LocalPort = $_.LocalPort
|
|
RemoteAddress = $_.RemoteAddress
|
|
RemotePort = $_.RemotePort
|
|
State = $_.State.ToString()
|
|
OwningProcess = $_.OwningProcess
|
|
ProcessName = $processName
|
|
} | ConvertTo-Json -Compress
|
|
} | Add-Content -LiteralPath $LogPath
|
|
Start-Sleep -Milliseconds 50
|
|
}
|
|
} -ArgumentList $SshHost, $ListenPort, $tcpOwnerLog
|
|
|
|
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\scripts\Attach-NativeTraceHarnessSystemBoundaryCapture.ps1 `
|
|
-Scenario $Scenario `
|
|
-ServerName $SshHost `
|
|
-TagName $TagName `
|
|
-LookbackMinutes $LookbackMinutes `
|
|
-MaxRows $MaxRows `
|
|
-PreLoadSleepSeconds 10 `
|
|
-OutputPath $OutputPath
|
|
|
|
Stop-Job -Job $monitorJob -ErrorAction SilentlyContinue
|
|
Receive-Job -Job $monitorJob -ErrorAction SilentlyContinue | Out-Null
|
|
Remove-Job -Job $monitorJob -Force -ErrorAction SilentlyContinue
|
|
|
|
"--- windows tcp owner monitor ---" | Tee-Object -FilePath $OutputPath -Append | Out-Host
|
|
Get-Content -LiteralPath $tcpOwnerLog -ErrorAction SilentlyContinue |
|
|
Sort-Object -Unique |
|
|
Tee-Object -FilePath $OutputPath -Append |
|
|
Out-Host
|
|
|
|
"--- relay final log ---" | Tee-Object -FilePath $OutputPath -Append | Out-Host
|
|
ssh -o BatchMode=yes -o ConnectTimeout=8 $sshTarget "cat $remoteLog 2>/dev/null || true" |
|
|
Tee-Object -FilePath $OutputPath -Append |
|
|
Out-Host
|
|
}
|
|
finally {
|
|
try {
|
|
if ($relayPid) {
|
|
ssh -o BatchMode=yes $sshTarget "kill $relayPid 2>/dev/null || true; rm -f $remotePy $remoteLog" | Out-Null
|
|
}
|
|
}
|
|
catch {
|
|
Write-Warning "Relay cleanup failed: $_"
|
|
}
|
|
|
|
Remove-Item -LiteralPath $localPy -ErrorAction SilentlyContinue
|
|
Remove-Item -LiteralPath $tcpOwnerLog -ErrorAction SilentlyContinue
|
|
Pop-Location
|
|
}
|