Private/Get-DescendantProcessIds.ps1
|
#Requires -Version 5.1 <# .SYNOPSIS Returns all descendant process IDs for a given parent process. .DESCRIPTION Recursively queries Win32_Process via CIM to collect all child and grandchild PIDs for a given parent process ID. Enforces a maximum recursion depth to prevent runaway traversal on pathological process trees. Stale PIDs (processes that exit mid-traversal) are skipped individually without failing the whole traversal. This is a private helper used by the Window Manager (Phase 5) to find windows of Electron apps (Teams, ClickUp) that spawn child processes. .PARAMETER ParentId The process ID of the parent process whose descendants to collect. .PARAMETER Depth Current recursion depth. Internal use only -- do not pass this parameter externally. Defaults to 0. .PARAMETER MaxDepth Maximum recursion depth before traversal stops. Logs a WARNING when reached. Defaults to 8 (well above observed Electron app depth of 2-4 levels). .OUTPUTS [int[]] -- Array of descendant process IDs (does not include the parent PID). .NOTES Author: Aaron AlAnsari Created: 2026-02-25 #> # PSUseSingularNouns: intentionally plural -- this function returns a collection of # integer process IDs. The plural noun accurately describes the return value. [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')] param () function Get-DescendantProcessIds { [CmdletBinding()] [OutputType([int[]])] param ( [Parameter(Mandatory)] [int]$ParentId, [Parameter()] [int]$Depth = 0, [Parameter()] [int]$MaxDepth = 8 ) # Guard: depth limit -- log WARNING because this is abnormal in production. if ($Depth -ge $MaxDepth) { Write-EnvkLog -Level 'WARNING' -Message "Get-DescendantProcessIds: max depth $MaxDepth reached at PID $ParentId -- stopping traversal" return [int[]]@() } # Use List[int] to avoid O(n^2) array-concatenation on large process trees. $descendantIds = [System.Collections.Generic.List[int]]::new() # Query direct children. ErrorAction SilentlyContinue returns $null (not an # exception) when the parent PID has already exited. $children = Get-CimInstance -ClassName Win32_Process -Filter "ParentProcessId=$ParentId" -ErrorAction SilentlyContinue Write-EnvkLog -Level 'DEBUG' -Message "Get-DescendantProcessIds: found $(@($children).Count) children of PID $ParentId at depth $Depth" foreach ($child in $children) { $childPid = [int]$child.ProcessId $descendantIds.Add($childPid) # Wrap each child's recursion individually so a stale PID mid-traversal # does not stop collection of its siblings. try { $grandchildren = Get-DescendantProcessIds -ParentId $childPid -Depth ($Depth + 1) -MaxDepth $MaxDepth foreach ($gc in $grandchildren) { $descendantIds.Add($gc) } } catch { Write-EnvkLog -Level 'DEBUG' -Message "Get-DescendantProcessIds: skipped PID $childPid -- process exited during traversal ($($_.Exception.Message))" } } return $descendantIds.ToArray() } |