Private/Invoke-InstallerWatchdog.ps1
|
<#
SPDX-License-Identifier: MIT Copyright (c) 2026 Leon McClatchey, Linktech Engineering LLC Package: VSCode-Updater Author: Leon McClatchey Company: Linktech Engineering LLC Created: 2026-04-16 Modified: 2026-07-17 File: Private/Invoke-InstallerWatchdog.ps1 Version: 2.2.0 Description: Monitors the VS Code installer using deterministic module‑based progress detection. Tracks child‑process module loads, unloads, and phase transitions to identify real installer activity, replacing filesystem‑scanning heuristics with noise‑free, phase‑aware stall detection. Terminates the installer only when module state and CPU/disk activity indicate a true stall. #> # PSScriptAnalyzer SuppressMessage = PSUseApprovedVerbs "Intentional verb" # PSScriptAnalyzer SuppressMessage = PSUseConsistentWhitespace # PSScriptAnalyzer SuppressMessage = PSUseConsistentIndentation function Invoke-InstallerWatchdog { [OutputType([Int32])] param( $ChildProcess, [int]$ParentPID, [int]$IdleTimeout ) # Validate parent if ($ParentPID -eq 0 -or -not (Get-Process -Id $ParentPID -ErrorAction SilentlyContinue)) { Write-VSCodeUpdaterLog "[WATCHDOG] Invalid parent PID ($ParentPID) — aborting watchdog" return [WatchdogExitCode]::Unknown } # Safe mode bypass if ($script:SafeMode) { Write-VSCodeUpdaterLog "[WATCHDOG] SAFE MODE — watchdog disabled" return [WatchdogExitCode]::Success } # # v3.1 DIAGNOSTIC STATE # $phaseTimeline = @{ Bootstrap = $null Servicing = $null Extraction = $null Payload = $null Finalization = $null } $extractionStarted = $false $payloadLoaded = $false $finalizationReached = $false $stallReason = "Unknown" $pendingReboot = $false $componentStoreCorrupt = $false # Detect pending reboot (v3.1) if (Test-Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending') { $pendingReboot = $true Write-VSCodeUpdaterLog "[DIAGNOSTIC] Pending reboot detected — Windows servicing may be blocked" } # DISM component store health check (v3.1) try { dism.exe /online /cleanup-image /checkhealth | Out-Null if ($LASTEXITCODE -ne 0) { $componentStoreCorrupt = $true Write-VSCodeUpdaterLog "[DIAGNOSTIC] DISM checkhealth indicates component store corruption" } } catch { Write-VSCodeUpdaterLog "[DIAGNOSTIC] DISM checkhealth failed: $($_.Exception.Message)" } # v3.2 EARLY EXIT: Abort installer monitoring if servicing is blocked if ($pendingReboot -or $componentStoreCorrupt) { $reason = if ($pendingReboot) { "PendingReboot" } elseif ($componentStoreCorrupt) { "ComponentStoreCorrupt" } Write-VSCodeUpdaterLog "[WATCHDOG] Servicing blocked — aborting installer monitoring (Reason: $reason)" # Store diagnostics for main module $script:WatchdogDiagnostics = [PSCustomObject]@{ StallReason = $reason PendingReboot = $pendingReboot ComponentStoreCorrupt = $componentStoreCorrupt PhaseTimeline = $phaseTimeline ExtractionStarted = $false PayloadLoaded = $false FinalizationReached = $false LastModules = @() } return [WatchdogExitCode]::ServicingBlocked } # # Module tracking (v3.0 → v3.1) # $previousModules = @() $moduleInitialized = $false $currentPhase = "Bootstrap" $phaseTimeline["Bootstrap"] = Get-Date # Final install directory detection $finalInstallRoot = (Join-Path $env:LOCALAPPDATA "Programs\Microsoft VS Code") # Idle timer $idleSeconds = 0.0 $lastLoopTime = Get-Date Write-VSCodeUpdaterLog "[WATCHDOG] Monitoring child PID $($ChildProcess.Id), parent PID $ParentPID" # Grace period Start-Sleep -Seconds 3 $idleSeconds = 0.0 while ($true) { # Loop delta $now = Get-Date $delta = ($now - $lastLoopTime).TotalSeconds if ($delta -lt 0) { $delta = 0 } $lastLoopTime = $now $idleSeconds += $delta # Refresh child $child = Get-Process -Id $ChildProcess.Id -ErrorAction SilentlyContinue if (-not $child) { Write-VSCodeUpdaterLog "[WATCHDOG] Child exited — installer completed" $finalizationReached = $true $phaseTimeline["Finalization"] = Get-Date return [WatchdogExitCode]::Success } # # MODULE-BASED PROGRESS DETECTION (v3.1) # try { $currentModules = $child.Modules.FileName | ForEach-Object { $_.ToLowerInvariant() } if (-not $moduleInitialized) { Write-VSCodeUpdaterLog "[WATCHDOG] Module tracking initialized ($($currentModules.Count) modules)" $previousModules = $currentModules $moduleInitialized = $true } else { $moduleDelta = Compare-Object $previousModules $currentModules if ($moduleDelta) { Write-VSCodeUpdaterLog "[WATCHDOG] Module change detected" foreach ($d in $moduleDelta) { $side = if ($d.SideIndicator -eq '=>') { 'Added' } else { 'Removed' } Write-VSCodeUpdaterLog "[WATCHDOG] Module ${side}: $($d.InputObject)" } $previousModules = $currentModules $idleSeconds = 0.0 # # PHASE DETECTION (v3.1) # $newPhase = $currentPhase if ($currentModules -match '\\nsm.*\.tmp\\') { $newPhase = "Extraction" $extractionStarted = $true $phaseTimeline["Extraction"] = Get-Date } elseif ($currentModules -like '*chrome_elf.dll*' -or $currentModules -like '*node.dll*') { $newPhase = "Payload" $payloadLoaded = $true $phaseTimeline["Payload"] = Get-Date } elseif ($currentModules -like '*microsoft vs code*') { $newPhase = "Finalization" $finalizationReached = $true $phaseTimeline["Finalization"] = Get-Date } if ($newPhase -ne $currentPhase) { $currentPhase = $newPhase Write-VSCodeUpdaterLog "[WATCHDOG] Phase: $currentPhase" } } } } catch { Write-VSCodeUpdaterLog "[WATCHDOG] Module polling exception: $($_.Exception.Message)" } # # FINALIZATION SUCCESS DETECTION (v3.1) # $finalModulesLoaded = $currentModules | Where-Object { $_ -like "$($finalInstallRoot.ToLowerInvariant())\*" } if ($finalModulesLoaded.Count -gt 0 -and $currentPhase -eq "Finalization") { Write-VSCodeUpdaterLog "[WATCHDOG] Final modules loaded — installer completed" $finalizationReached = $true $phaseTimeline["Finalization"] = Get-Date return [WatchdogExitCode]::Success } # # UNIFIED STALL DETECTOR (v3.1) # $moduleDelta = Compare-Object $previousModules $currentModules if (-not $moduleDelta) { if ($idleSeconds -ge $IdleTimeout) { # # Stall reason classification (v3.1) # if ($currentModules -like '*setupapi.dll*') { $stallReason = "Windows servicing deadlock" } elseif (-not $extractionStarted) { $stallReason = "NSIS extraction never started" } elseif (-not $payloadLoaded) { $stallReason = "Payload modules never loaded" } Write-VSCodeUpdaterLog "[WATCHDOG] Deterministic stall — no module changes for $IdleTimeout seconds" Write-VSCodeUpdaterLog "[WATCHDOG] Stall reason: $stallReason" # # Kill installer # Stop-Process -Id $ChildProcess.Id -Force -ErrorAction SilentlyContinue Stop-Process -Id $ParentPID -Force -ErrorAction SilentlyContinue # # v3.1: Store diagnostic metadata for main module # $script:WatchdogDiagnostics = [PSCustomObject]@{ StallReason = $stallReason PendingReboot = $pendingReboot ComponentStoreCorrupt = $componentStoreCorrupt PhaseTimeline = $phaseTimeline ExtractionStarted = $extractionStarted PayloadLoaded = $payloadLoaded FinalizationReached = $finalizationReached LastModules = $currentModules } return [WatchdogExitCode]::IdleStalled } } else { $idleSeconds = 0.0 } Start-Sleep -Seconds 2 } } |