Initial commit: managed .NET 10 AVEVA Historian SDK + reverse-engineering toolkit
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>
This commit is contained in:
@@ -0,0 +1,279 @@
|
||||
param(
|
||||
[string]$HostName = "localhost",
|
||||
[UInt16]$Port = 32568,
|
||||
[string]$TagName = $env:HISTORIAN_TEST_TAG,
|
||||
[int]$LookbackMinutes = 60,
|
||||
[int]$MaxRows = 5,
|
||||
[int]$ConnectionWaitSeconds = 15,
|
||||
[int]$PreReadSleepSeconds = 0,
|
||||
[switch]$DumpLoadedModules
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
function Set-PropertyIfPresent {
|
||||
param(
|
||||
[object]$Object,
|
||||
[string]$Name,
|
||||
[object]$Value
|
||||
)
|
||||
|
||||
$property = $Object.GetType().GetProperty($Name)
|
||||
if ($null -ne $property -and $property.CanWrite) {
|
||||
$property.SetValue($Object, $Value, $null)
|
||||
}
|
||||
}
|
||||
|
||||
function Get-PropertyValue {
|
||||
param(
|
||||
[object]$Object,
|
||||
[string]$Name
|
||||
)
|
||||
|
||||
if ($null -eq $Object) {
|
||||
return $null
|
||||
}
|
||||
|
||||
$property = $Object.GetType().GetProperty($Name)
|
||||
if ($null -eq $property -or -not $property.CanRead) {
|
||||
return $null
|
||||
}
|
||||
|
||||
return $property.GetValue($Object, $null)
|
||||
}
|
||||
|
||||
function Get-PropertyText {
|
||||
param(
|
||||
[object]$Object,
|
||||
[string]$Name
|
||||
)
|
||||
|
||||
$value = Get-PropertyValue $Object $Name
|
||||
if ($null -eq $value) {
|
||||
return $null
|
||||
}
|
||||
|
||||
return $value.ToString()
|
||||
}
|
||||
|
||||
function Invoke-ByRefMethod {
|
||||
param(
|
||||
[object]$Target,
|
||||
[Reflection.MethodInfo]$Method,
|
||||
[object[]]$Arguments
|
||||
)
|
||||
|
||||
return $Method.Invoke($Target, $Arguments)
|
||||
}
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($TagName)) {
|
||||
$TagName = Read-Host "Historian test tag"
|
||||
}
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($TagName)) {
|
||||
throw "A tag name is required. Pass -TagName or set HISTORIAN_TEST_TAG."
|
||||
}
|
||||
|
||||
$repoRoot = Split-Path -Parent $PSScriptRoot
|
||||
$dllDir = Join-Path $repoRoot "current"
|
||||
$managedDll = Join-Path $dllDir "aahClientManaged.dll"
|
||||
|
||||
if (-not (Test-Path -LiteralPath $managedDll)) {
|
||||
throw "Missing aahClientManaged.dll at $managedDll"
|
||||
}
|
||||
|
||||
[AppDomain]::CurrentDomain.add_AssemblyResolve({
|
||||
param($sender, $eventArgs)
|
||||
|
||||
$assemblyName = New-Object Reflection.AssemblyName($eventArgs.Name)
|
||||
$candidate = Join-Path $script:dllDir ($assemblyName.Name + ".dll")
|
||||
if (Test-Path -LiteralPath $candidate) {
|
||||
return [Reflection.Assembly]::LoadFrom($candidate)
|
||||
}
|
||||
|
||||
return $null
|
||||
})
|
||||
|
||||
Push-Location $dllDir
|
||||
try {
|
||||
$assembly = [Reflection.Assembly]::LoadFrom($managedDll)
|
||||
if ($DumpLoadedModules) {
|
||||
[Diagnostics.Process]::GetCurrentProcess().Modules |
|
||||
Where-Object { $_.ModuleName -like '*aah*' -or $_.FileName -like '*histsdk*' -or $_.FileName -like '*AVEVA*' } |
|
||||
Select-Object ModuleName, BaseAddress, ModuleMemorySize, FileName |
|
||||
ConvertTo-Json -Depth 3
|
||||
}
|
||||
|
||||
if ($PreReadSleepSeconds -gt 0) {
|
||||
Start-Sleep -Seconds $PreReadSleepSeconds
|
||||
}
|
||||
|
||||
$accessType = $assembly.GetType("ArchestrA.HistorianAccess", $true)
|
||||
$connectionArgsType = $assembly.GetType("ArchestrA.HistorianConnectionArgs", $true)
|
||||
$connectionStatusType = $assembly.GetType("ArchestrA.HistorianConnectionStatus", $true)
|
||||
$connectionType = $assembly.GetType("ArchestrA.HistorianConnectionType", $true)
|
||||
$historyQueryArgsType = $assembly.GetType("ArchestrA.HistoryQueryArgs", $true)
|
||||
$errorType = $assembly.GetType("ArchestrA.HistorianAccessError", $true)
|
||||
$retrievalModeType = $assembly.GetType("ArchestrA.HistorianRetrievalMode", $true)
|
||||
|
||||
$access = [Activator]::CreateInstance($accessType)
|
||||
$connectionArgs = [Activator]::CreateInstance($connectionArgsType)
|
||||
Set-PropertyIfPresent $connectionArgs "ServerName" $HostName
|
||||
Set-PropertyIfPresent $connectionArgs "TcpPort" $Port
|
||||
Set-PropertyIfPresent $connectionArgs "ReadOnly" $true
|
||||
Set-PropertyIfPresent $connectionArgs "IntegratedSecurity" $true
|
||||
Set-PropertyIfPresent $connectionArgs "ConnectionType" ([Enum]::Parse($connectionType, "Process"))
|
||||
|
||||
$openError = [Activator]::CreateInstance($errorType)
|
||||
$openMethod = $accessType.GetMethod("OpenConnection", [Type[]]@($connectionArgsType, $errorType.MakeByRefType()))
|
||||
$openArgs = New-Object "object[]" 2
|
||||
$openArgs[0] = $connectionArgs
|
||||
$openArgs[1] = $openError
|
||||
$openSuccess = [bool](Invoke-ByRefMethod $access $openMethod $openArgs)
|
||||
$openError = $openArgs[1]
|
||||
|
||||
$connected = $false
|
||||
$pending = $false
|
||||
$connectionErrorOccurred = $false
|
||||
$connectionStatusError = $null
|
||||
$getStatusMethod = $accessType.GetMethod("GetConnectionStatus", [Type[]]@($connectionStatusType.MakeByRefType()))
|
||||
$deadline = [DateTime]::UtcNow.AddSeconds([Math]::Max($ConnectionWaitSeconds, 1))
|
||||
do {
|
||||
$status = [Activator]::CreateInstance($connectionStatusType)
|
||||
$statusArgs = New-Object "object[]" 1
|
||||
$statusArgs[0] = $status
|
||||
[void](Invoke-ByRefMethod $access $getStatusMethod $statusArgs)
|
||||
$status = $statusArgs[0]
|
||||
$connected = [bool](Get-PropertyValue $status "ConnectedToServer")
|
||||
$pending = [bool](Get-PropertyValue $status "Pending")
|
||||
$connectionErrorOccurred = [bool](Get-PropertyValue $status "ErrorOccurred")
|
||||
$connectionStatusError = Get-PropertyValue $status "Error"
|
||||
if (($connected -and -not $pending) -or $connectionErrorOccurred -or (-not $connected -and -not $pending)) {
|
||||
break
|
||||
}
|
||||
|
||||
Start-Sleep -Milliseconds 250
|
||||
} while ([DateTime]::UtcNow -lt $deadline)
|
||||
|
||||
$rows = New-Object System.Collections.Generic.List[object]
|
||||
$startSuccess = $false
|
||||
$moveErrorText = $null
|
||||
$startError = $null
|
||||
|
||||
if ($openSuccess -and $connected) {
|
||||
$query = $accessType.GetMethod("CreateHistoryQuery", [Type[]]@()).Invoke($access, @())
|
||||
$queryType = $query.GetType()
|
||||
$queryArgs = [Activator]::CreateInstance($historyQueryArgsType)
|
||||
|
||||
$tags = New-Object System.Collections.Specialized.StringCollection
|
||||
[void]$tags.Add($TagName)
|
||||
|
||||
Set-PropertyIfPresent $queryArgs "TagNames" $tags
|
||||
Set-PropertyIfPresent $queryArgs "StartDateTime" ([DateTime]::Now.AddMinutes(-1 * $LookbackMinutes))
|
||||
Set-PropertyIfPresent $queryArgs "EndDateTime" ([DateTime]::Now)
|
||||
Set-PropertyIfPresent $queryArgs "BatchSize" ([uint32][Math]::Max($MaxRows, 1))
|
||||
Set-PropertyIfPresent $queryArgs "RetrievalMode" ([Enum]::Parse($retrievalModeType, "Full"))
|
||||
|
||||
$queryError = [Activator]::CreateInstance($errorType)
|
||||
$startMethod = $queryType.GetMethod("StartQuery", [Type[]]@($historyQueryArgsType, $errorType.MakeByRefType()))
|
||||
$startArgs = New-Object "object[]" 2
|
||||
$startArgs[0] = $queryArgs
|
||||
$startArgs[1] = $queryError
|
||||
$startSuccess = [bool](Invoke-ByRefMethod $query $startMethod $startArgs)
|
||||
$startError = $startArgs[1]
|
||||
|
||||
if ($startSuccess) {
|
||||
$moveMethod = $queryType.GetMethod("MoveNext", [Type[]]@($errorType.MakeByRefType()))
|
||||
for ($i = 0; $i -lt $MaxRows; $i++) {
|
||||
$moveError = [Activator]::CreateInstance($errorType)
|
||||
$moveArgs = New-Object "object[]" 1
|
||||
$moveArgs[0] = $moveError
|
||||
$hasRow = [bool](Invoke-ByRefMethod $query $moveMethod $moveArgs)
|
||||
$moveError = $moveArgs[0]
|
||||
if (-not $hasRow) {
|
||||
$moveErrorText = Get-PropertyText $moveError "ErrorDescription"
|
||||
break
|
||||
}
|
||||
|
||||
$result = Get-PropertyValue $query "QueryResult"
|
||||
$row = [ordered]@{
|
||||
StartDateTime = (Get-PropertyValue $result "StartDateTime").ToString("O")
|
||||
EndDateTime = (Get-PropertyValue $result "EndDateTime").ToString("O")
|
||||
Quality = Get-PropertyValue $result "Quality"
|
||||
OpcQuality = Get-PropertyValue $result "OpcQuality"
|
||||
QualityDetail = Get-PropertyValue $result "QualityDetail"
|
||||
Value = Get-PropertyValue $result "Value"
|
||||
StringValuePresent = -not [string]::IsNullOrEmpty((Get-PropertyText $result "StringValue"))
|
||||
PercentGood = Get-PropertyValue $result "PercentGood"
|
||||
}
|
||||
$rows.Add($row)
|
||||
}
|
||||
}
|
||||
|
||||
$endMethod = $queryType.GetMethod("EndQuery", [Type[]]@($errorType.MakeByRefType()))
|
||||
if ($null -ne $endMethod) {
|
||||
$endError = [Activator]::CreateInstance($errorType)
|
||||
$endArgs = New-Object "object[]" 1
|
||||
$endArgs[0] = $endError
|
||||
[void](Invoke-ByRefMethod $query $endMethod $endArgs)
|
||||
}
|
||||
|
||||
$queryDispose = $query -as [IDisposable]
|
||||
if ($null -ne $queryDispose) {
|
||||
$queryDispose.Dispose()
|
||||
}
|
||||
}
|
||||
|
||||
$resultObject = [ordered]@{
|
||||
Operation = "aahClientManaged.HistoryQuery.Integrated"
|
||||
HostName = $HostName
|
||||
Port = $Port
|
||||
IntegratedSecurity = $true
|
||||
TagNameProvided = $true
|
||||
LookbackMinutes = $LookbackMinutes
|
||||
OpenSuccess = $openSuccess
|
||||
OpenErrorType = Get-PropertyText $openError "ErrorType"
|
||||
OpenErrorCode = Get-PropertyText $openError "ErrorCode"
|
||||
OpenErrorDescription = Get-PropertyText $openError "ErrorDescription"
|
||||
ConnectedToServer = $connected
|
||||
ConnectionPending = $pending
|
||||
ConnectionErrorOccurred = $connectionErrorOccurred
|
||||
ConnectionStatusErrorType = Get-PropertyText $connectionStatusError "ErrorType"
|
||||
ConnectionStatusErrorCode = Get-PropertyText $connectionStatusError "ErrorCode"
|
||||
ConnectionStatusErrorDescription = Get-PropertyText $connectionStatusError "ErrorDescription"
|
||||
StartQuerySuccess = $startSuccess
|
||||
StartQueryErrorType = Get-PropertyText $startError "ErrorType"
|
||||
StartQueryErrorCode = Get-PropertyText $startError "ErrorCode"
|
||||
StartQueryErrorDescription = Get-PropertyText $startError "ErrorDescription"
|
||||
MoveTerminalDescription = $moveErrorText
|
||||
RowCount = $rows.Count
|
||||
Rows = $rows
|
||||
}
|
||||
|
||||
$resultObject | ConvertTo-Json -Depth 6
|
||||
|
||||
if ($DumpLoadedModules) {
|
||||
[Diagnostics.Process]::GetCurrentProcess().Modules |
|
||||
Where-Object { $_.ModuleName -like '*aah*' -or $_.FileName -like '*histsdk*' -or $_.FileName -like '*AVEVA*' } |
|
||||
Select-Object ModuleName, BaseAddress, ModuleMemorySize, FileName |
|
||||
ConvertTo-Json -Depth 3
|
||||
}
|
||||
|
||||
if ($openSuccess) {
|
||||
$closeError = [Activator]::CreateInstance($errorType)
|
||||
$closeMethod = $accessType.GetMethod("CloseConnection", [Type[]]@($errorType.MakeByRefType()))
|
||||
if ($null -ne $closeMethod) {
|
||||
$closeArgs = New-Object "object[]" 1
|
||||
$closeArgs[0] = $closeError
|
||||
[void](Invoke-ByRefMethod $access $closeMethod $closeArgs)
|
||||
}
|
||||
}
|
||||
|
||||
$dispose = $access -as [IDisposable]
|
||||
if ($null -ne $dispose) {
|
||||
$dispose.Dispose()
|
||||
}
|
||||
}
|
||||
finally {
|
||||
Pop-Location
|
||||
}
|
||||
Reference in New Issue
Block a user