[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 }) }