Private/Watch-SACProcessTree.ps1
|
function Watch-SACProcessTree { <# .SYNOPSIS Monitors a process and its entire descendant process tree until all processes have exited. .DESCRIPTION Acts as a "Supervisor" for uninstallation tasks. It tracks a root Process ID and recursively finds all child processes. It monitors the aggregate CPU and Memory usage of the entire tree. Includes safeguards against PID recycling and enforces hard/idle timeouts. .PARAMETER RootPID The Process ID of the root process to monitor. .PARAMETER DisplayName A friendly name for the process being monitored, used in UI feedback. .PARAMETER TimeoutMinutes The maximum total time (in minutes) the process tree is allowed to run before being force-killed. .PARAMETER IdleTimeoutMinutes The maximum time (in minutes) the aggregate CPU time can remain unchanged before the tree is considered hung and force-killed. #> [CmdletBinding()] param( [Parameter(Mandatory=$true)] [int]$RootPID, [string]$DisplayName = "Process", [int]$TimeoutMinutes = 20, [int]$IdleTimeoutMinutes = 5 ) $StartTime = Get-Date $ZeroCpuTime = $null $LastTotalCpu = $null # Capture root creation time to prevent monitoring recycled PIDs $RootCreationTime = $null try { $rootCim = Get-CimInstance Win32_Process -Filter "ProcessId = $RootPID" -ErrorAction Stop if ($rootCim) { $RootCreationTime = $rootCim.CreationDate } } catch { # If we can't get the root creation time (e.g. process exited too fast), we just continue. } # Helper function to recursively find all descendant processes function Get-ProcessTree { param([int]$RootId, [datetime]$RootTime) # Fetch all processes once per loop iteration to minimize WMI overhead $allProcs = Get-CimInstance Win32_Process -ErrorAction SilentlyContinue if (-not $allProcs) { return @{} } $tree = @{} $queue = [System.Collections.Generic.Queue[int]]::new() $queue.Enqueue($RootId) while ($queue.Count -gt 0) { $currentId = $queue.Dequeue() # Find direct children of the current ID $children = $allProcs | Where-Object { $_.ParentProcessId -eq $currentId } foreach ($child in $children) { # Safeguard: PID Recycling. A child cannot be older than the root process. if ($null -ne $RootTime -and $child.CreationDate -lt $RootTime) { continue } # Prevent infinite loops in case of bizarre circular PID relationships if (-not $tree.ContainsKey($child.ProcessId)) { $tree[$child.ProcessId] = $child $queue.Enqueue($child.ProcessId) } } } return $tree } $isRemote = $false if (Get-Command Test-SACRemoteSession -ErrorAction SilentlyContinue) { $isRemote = Test-SACRemoteSession } Write-Host "`n Monitoring process tree for $DisplayName..." -ForegroundColor Cyan while ($true) { $elapsed = (Get-Date) - $StartTime # 1. Check Hard Timeout if ($elapsed.TotalMinutes -ge $TimeoutMinutes) { Write-Host "`n [!] Hard timeout ($TimeoutMinutes m) reached for $DisplayName. Terminating tree." -ForegroundColor Red try { Stop-Process -Id $RootPID -Force -ErrorAction SilentlyContinue } catch {} $currentTree = Get-ProcessTree -RootId $RootPID -RootTime $RootCreationTime foreach ($childId in $currentTree.Keys) { try { Stop-Process -Id $childId -Force -ErrorAction SilentlyContinue } catch {} } break } # 2. Check if processes are alive $rootAlive = [bool](Get-Process -Id $RootPID -ErrorAction SilentlyContinue) $currentTree = Get-ProcessTree -RootId $RootPID -RootTime $RootCreationTime if (-not $rootAlive -and $currentTree.Count -eq 0) { Write-Host "`n [*] Process tree for $DisplayName has exited cleanly." -ForegroundColor Green break } # 3. Calculate Aggregate Resource Usage $totalCpu = 0 $totalMemMB = 0 $activeCount = 0 if ($rootAlive) { try { $p = Get-Process -Id $RootPID -ErrorAction Stop $totalCpu += $p.CPU $totalMemMB += ($p.WorkingSet64 / 1MB) $activeCount++ } catch {} } foreach ($childId in $currentTree.Keys) { try { $p = Get-Process -Id $childId -ErrorAction Stop $totalCpu += $p.CPU $totalMemMB += ($p.WorkingSet64 / 1MB) $activeCount++ } catch {} } # 4. Check Idle Timeout if ($null -ne $LastTotalCpu -and $totalCpu -eq $LastTotalCpu) { if ($null -eq $ZeroCpuTime) { $ZeroCpuTime = Get-Date } elseif (((Get-Date) - $ZeroCpuTime).TotalMinutes -ge $IdleTimeoutMinutes) { Write-Host "`n [!] Process tree idle timeout ($IdleTimeoutMinutes m) reached for $DisplayName. Terminating tree." -ForegroundColor Yellow try { Stop-Process -Id $RootPID -Force -ErrorAction SilentlyContinue } catch {} foreach ($childId in $currentTree.Keys) { try { Stop-Process -Id $childId -Force -ErrorAction SilentlyContinue } catch {} } break } } else { $ZeroCpuTime = $null $LastTotalCpu = $totalCpu } # 5. UI Feedback if (-not $isRemote) { $elapsedStr = "{0:mm\:ss}" -f $elapsed $memStr = [math]::Round($totalMemMB, 1) $statusLine = " [Elapsed: $elapsedStr] | [Active Procs: $activeCount] | [Tree Mem: ${memStr}MB]" # Pad with spaces to overwrite previous line completely $statusLine = $statusLine.PadRight(80) Write-Host "`r$statusLine" -NoNewline -ForegroundColor DarkGray } Start-Sleep -Milliseconds 1500 } if (-not $isRemote) { Write-Host "" } # Newline cleanup } |