chore: add isolated Tornado readiness probe
This commit is contained in:
218
scripts/Inspect-TornadoTestIsolation.ps1
Normal file
218
scripts/Inspect-TornadoTestIsolation.ps1
Normal file
@@ -0,0 +1,218 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[ValidateRange(1, 65535)]
|
||||
[int]$ExpectedTcpPort,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[ValidateLength(1, 128)]
|
||||
[string]$ExpectedTestWindowTitle
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
function Write-SafeResult {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[object]$Value,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[int]$ExitCode
|
||||
)
|
||||
|
||||
$Value | ConvertTo-Json -Depth 6
|
||||
exit $ExitCode
|
||||
}
|
||||
|
||||
function Test-ProgramWindowTitle {
|
||||
param([AllowEmptyString()][string]$Title)
|
||||
|
||||
return $Title -match '(?i)(^|[^A-Z0-9])(PGM|PROGRAM)([^A-Z0-9]|$)'
|
||||
}
|
||||
|
||||
try {
|
||||
$expectedTitle = $ExpectedTestWindowTitle.Trim()
|
||||
if (-not $expectedTitle.StartsWith('Tornado2', [StringComparison]::OrdinalIgnoreCase) -or
|
||||
$expectedTitle -notmatch '(?i)(^|[^A-Z0-9])TEST([^A-Z0-9]|$)' -or
|
||||
(Test-ProgramWindowTitle $expectedTitle)) {
|
||||
Write-SafeResult -ExitCode 64 -Value ([ordered]@{
|
||||
command = 'test-isolation-probe'
|
||||
ready = $false
|
||||
errorCode = 'invalid-test-window-title'
|
||||
connectRequestIssued = $false
|
||||
comActivationAttempted = $false
|
||||
ktapConnectAttempted = $false
|
||||
})
|
||||
}
|
||||
|
||||
$processInspectionAvailable = $true
|
||||
$tornadoProcesses = @()
|
||||
try {
|
||||
foreach ($process in Get-Process -ErrorAction Stop) {
|
||||
try {
|
||||
if (-not $process.ProcessName.StartsWith(
|
||||
'Tornado2',
|
||||
[StringComparison]::OrdinalIgnoreCase)) {
|
||||
continue
|
||||
}
|
||||
|
||||
$title = $process.MainWindowTitle
|
||||
$tornadoProcesses += [pscustomobject]@{
|
||||
Id = $process.Id
|
||||
IsExpectedTest = [string]::Equals(
|
||||
$title,
|
||||
$expectedTitle,
|
||||
[StringComparison]::OrdinalIgnoreCase)
|
||||
IsProgram = Test-ProgramWindowTitle $title
|
||||
}
|
||||
}
|
||||
catch {
|
||||
$processInspectionAvailable = $false
|
||||
}
|
||||
finally {
|
||||
$process.Dispose()
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
$processInspectionAvailable = $false
|
||||
$tornadoProcesses = @()
|
||||
}
|
||||
|
||||
$eligibleProcesses = @($tornadoProcesses | Where-Object IsExpectedTest)
|
||||
$programProcesses = @($tornadoProcesses | Where-Object IsProgram)
|
||||
|
||||
$registryAvailable = $false
|
||||
$configuredPortMatches = $false
|
||||
$networkAddressIsLoopback = $false
|
||||
$networkAutoStart = $false
|
||||
try {
|
||||
$controlKey = Get-Item -LiteralPath (
|
||||
'Registry::HKEY_CURRENT_USER\Software\Visual Research Inc\Tornado2\Option\Control')
|
||||
$configuredPort = [int]$controlKey.GetValue('Network TCP Port', 0)
|
||||
$configuredAddress = [string]$controlKey.GetValue('Network Address', '')
|
||||
$networkAutoStart = [int]$controlKey.GetValue('Network Auto Start', 0) -ne 0
|
||||
$parsedAddress = $null
|
||||
$addressParsed = [Net.IPAddress]::TryParse($configuredAddress, [ref]$parsedAddress)
|
||||
$networkAddressIsLoopback = $addressParsed -and
|
||||
[Net.IPAddress]::IsLoopback($parsedAddress)
|
||||
$configuredPortMatches = $configuredPort -eq $ExpectedTcpPort
|
||||
$registryAvailable = $true
|
||||
}
|
||||
catch {
|
||||
$registryAvailable = $false
|
||||
}
|
||||
|
||||
$listenerInspectionAvailable = $true
|
||||
$listeners = @()
|
||||
try {
|
||||
$listeners = @(Get-NetTCPConnection `
|
||||
-State Listen `
|
||||
-LocalPort $ExpectedTcpPort `
|
||||
-ErrorAction Stop)
|
||||
}
|
||||
catch [Microsoft.PowerShell.Cmdletization.Cim.CimJobException] {
|
||||
# Get-NetTCPConnection uses this exception for an empty query on some Windows builds.
|
||||
$listeners = @()
|
||||
}
|
||||
catch {
|
||||
$listenerInspectionAvailable = $false
|
||||
$listeners = @()
|
||||
}
|
||||
|
||||
$listenerAddressesAreLoopback = $listeners.Count -gt 0
|
||||
foreach ($listener in $listeners) {
|
||||
$parsedListenerAddress = $null
|
||||
$listenerAddressParsed = [Net.IPAddress]::TryParse(
|
||||
[string]$listener.LocalAddress,
|
||||
[ref]$parsedListenerAddress)
|
||||
if (-not $listenerAddressParsed -or
|
||||
-not [Net.IPAddress]::IsLoopback($parsedListenerAddress)) {
|
||||
$listenerAddressesAreLoopback = $false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
$eligibleProcessIds = @($eligibleProcesses | ForEach-Object Id)
|
||||
$listenerOwnerIds = @($listeners | ForEach-Object OwningProcess | Sort-Object -Unique)
|
||||
$listenerOwnedOnlyByTest = $listeners.Count -gt 0 -and
|
||||
$listenerOwnerIds.Count -gt 0 -and
|
||||
@($listenerOwnerIds | Where-Object { $_ -notin $eligibleProcessIds }).Count -eq 0
|
||||
|
||||
$reasons = [Collections.Generic.List[string]]::new()
|
||||
if (-not $processInspectionAvailable) {
|
||||
$reasons.Add('process-inspection-unavailable')
|
||||
}
|
||||
if ($tornadoProcesses.Count -ne 1) {
|
||||
$reasons.Add('tornado-process-count-not-one')
|
||||
}
|
||||
if ($eligibleProcesses.Count -ne 1) {
|
||||
$reasons.Add('test-window-not-exact-match')
|
||||
}
|
||||
if ($programProcesses.Count -gt 0) {
|
||||
$reasons.Add('program-window-detected')
|
||||
}
|
||||
if (-not $registryAvailable) {
|
||||
$reasons.Add('network-settings-unavailable')
|
||||
}
|
||||
elseif (-not $configuredPortMatches) {
|
||||
$reasons.Add('network-tcp-port-mismatch')
|
||||
}
|
||||
if ($registryAvailable -and -not $networkAddressIsLoopback) {
|
||||
$reasons.Add('network-address-not-loopback')
|
||||
}
|
||||
if (-not $listenerInspectionAvailable) {
|
||||
$reasons.Add('listener-inspection-unavailable')
|
||||
}
|
||||
elseif ($listeners.Count -eq 0) {
|
||||
$reasons.Add('network-server-not-listening')
|
||||
}
|
||||
else {
|
||||
if (-not $listenerAddressesAreLoopback) {
|
||||
$reasons.Add('listener-address-not-loopback')
|
||||
}
|
||||
if (-not $listenerOwnedOnlyByTest) {
|
||||
$reasons.Add('listener-not-owned-by-test-instance')
|
||||
}
|
||||
}
|
||||
|
||||
$ready = $reasons.Count -eq 0
|
||||
Write-SafeResult -ExitCode $(if ($ready) { 0 } else { 3 }) -Value ([ordered]@{
|
||||
command = 'test-isolation-probe'
|
||||
ready = $ready
|
||||
expectedTcpPort = $ExpectedTcpPort
|
||||
process = [ordered]@{
|
||||
inspectionAvailable = $processInspectionAvailable
|
||||
totalCount = $tornadoProcesses.Count
|
||||
eligibleTestCount = $eligibleProcesses.Count
|
||||
programWindowDetected = $programProcesses.Count -gt 0
|
||||
}
|
||||
networkServer = [ordered]@{
|
||||
settingsAvailable = $registryAvailable
|
||||
configuredPortMatches = $configuredPortMatches
|
||||
addressIsLoopback = $networkAddressIsLoopback
|
||||
autoStartEnabled = $networkAutoStart
|
||||
listenerInspectionAvailable = $listenerInspectionAvailable
|
||||
listenerEndpointCount = $listeners.Count
|
||||
listenerAddressesAreLoopback = $listenerAddressesAreLoopback
|
||||
listenerOwnedOnlyByTest = $listenerOwnedOnlyByTest
|
||||
}
|
||||
reasons = @($reasons)
|
||||
connectRequestIssued = $false
|
||||
comActivationAttempted = $false
|
||||
lastKtapConnectState = 'not-attempted'
|
||||
ktapConnectAttempted = $false
|
||||
networkMonitoringRecordExpected = $false
|
||||
networkMonitoringCheckRequired = $false
|
||||
})
|
||||
}
|
||||
catch {
|
||||
Write-SafeResult -ExitCode 70 -Value ([ordered]@{
|
||||
command = 'test-isolation-probe'
|
||||
ready = $false
|
||||
errorCode = 'internal-probe-failure'
|
||||
connectRequestIssued = $false
|
||||
comActivationAttempted = $false
|
||||
ktapConnectAttempted = $false
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user