VSCode-Updater.psm1
|
<#
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-06-14 File: VSCode-Updater.psm1 Version: 2.1.0 Description: Module root for VSCode-Updater. Loads public functions, wires private helpers, and exposes the deterministic Update-VSCode entry point and related management commands. #> # Dot-source Public functions Get-ChildItem -Path $PSScriptRoot/Public -Filter *.ps1 | ForEach-Object { . $_.FullName } # Load private functions Get-ChildItem -Path "$PSScriptRoot/Private" -Filter *.ps1 | ForEach-Object { . $_.FullName } function Update-VSCode { [CmdletBinding()] param( [switch]$SkipUpdate, [switch]$SkipDownload, [switch]$ForceDownload, [int]$RetryCount = 3, [int]$IdleTimeout = 600 ) $MaxRetries = 5 # hard ceiling for safety # ===================================================================== # Initialization + Metadata Banner # ===================================================================== $scriptName = "Update-VSCode" $scriptVersion = "2.1.0" $codeExe = "$env:LOCALAPPDATA\Programs\Microsoft VS Code\Code.exe" $codeRoot = Split-Path $codeExe -Parent $cacheDir = "$PSScriptRoot/../Cache" $cachedInstaller = Join-Path $cacheDir "VSCodeSetup.exe" $tempInstaller = Join-Path $env:TEMP "VSCodeSetup.tmp" $installerUrl = "https://update.code.visualstudio.com/latest/win32-x64-user/stable" if (-not (Test-Path $cacheDir)) { New-Item -ItemType Directory -Force -Path $cacheDir | Out-Null } Write-Log "===============================================================================" Write-Log " $scriptName started — Version $scriptVersion" Write-Log " Host: $env:COMPUTERNAME" Write-Log " User: $env:USERNAME" Write-Log " RetryCount=$RetryCount IdleTimeout=$IdleTimeout" Write-Log "===============================================================================" # Guard: VS Code must not be running $running = Get-Process -Name "Code", "Code - Insiders" -ErrorAction SilentlyContinue if ($running) { Write-Host "VS Code is currently running." Write-Host "Updating requires closing VS Code." $choice = Read-Host "Close VS Code and continue? (Y/N)" if ($choice -notin @('Y','y')) { Write-Log -Level Warning -Message "User aborted update because VS Code was running." return 30 } Write-Log -Level Info -Message "Closing VS Code to continue update." $running | Stop-Process -Force } # ===================================================================== # Pre‑Cleanup # ===================================================================== Cleanup-SetupBootstrapper Cleanup-VSCodeHelpers Cleanup-InnoSetupWorkers Start-Sleep -Seconds 2 CleanCodePath Start-Sleep -Seconds 2 # ===================================================================== # Skip Mode # ===================================================================== if ($SkipUpdate) { Write-Log "[SKIP] SkipUpdate switch present — skipping update." Write-Log "----- $scriptName ended (exit 20) -----" return 20 } # ===================================================================== # Download + Cache Installer # ===================================================================== $Mode = if ($SkipDownload) { "Skip" } elseif ($ForceDownload) { "Force" } else { "Normal" } Write-Host "Installer URL: '$installerUrl'" Write-Host "Length: $($installerUrl.Length)" $installer = Get-Installer -Url $installerUrl -CachePath $cachedInstaller -DownloadMode $Mode # --- HARD FAILURE CHECK (keep this exactly as-is) --- if (-not (Test-Path $cachedInstaller)) { Write-Log "[ERROR] Cached installer missing after update" Write-Log "----- $scriptName ended (exit 12) -----" return 12 } # --- DIAGNOSTICS: Corrupted installer detection --- $size = (Get-Item $cachedInstaller).Length if ($size -lt 5MB) { Write-Log "[DETECT] Cached installer appears corrupted or incomplete (size: $size bytes)" } # --- DIAGNOSTICS: Stale cached installer detection --- $installerItem = Get-Item $cachedInstaller $age = (Get-Date) - $installerItem.LastWriteTime if ($age.TotalDays -gt 7) { Write-Log "[DETECT] Cached installer is stale (age: $([math]::Round($age.TotalDays,2)) days)" Write-Log "[DETECT] This may indicate a past failed or incomplete update attempt." } # --- DIAGNOSTICS: Stale install directory timestamp mismatch --- $installDir = "$env:LOCALAPPDATA\Programs\Microsoft VS Code" if (Test-Path $installDir) { $lastWrite = (Get-Item $installDir).LastWriteTime if ($lastWrite -lt (Get-Date).AddMinutes(-10)) { Write-Log "[DETECT] Install directory timestamp unchanged. Installer may have exited without updating." } } else { Write-Log "[DETECT] VS Code install directory not found. Installation may be incomplete or corrupted." } # ===================================================================== # Retry Loop # ===================================================================== # NEW: Ensure no stale InnoSetup workers exist before launching installer Cleanup-InnoSetupWorkers Start-Sleep -Milliseconds 200 if ($RetryCount -gt $MaxRetries) { Write-Log "[WARN] RetryCount ($RetryCount) exceeds maximum allowed ($MaxRetries). Clamping." $RetryCount = $MaxRetries } $attempt = 0 $maxAttempts = $RetryCount + 1 while ($attempt -lt $maxAttempts) { $attempt++ Write-Log "[ATTEMPT] Installer attempt $attempt of $maxAttempts" try { # Launch installer $p = Start-InstallerDetached -Path $cachedInstaller $parentPID = $p.Id Write-Log "[DETECT] Parent PID: $parentPID" # Detect child worker using Win32_Process (reliable parent PID) $child = $null $detectTimeout = 10 $elapsed = 0 while (-not $child -and $elapsed -lt $detectTimeout) { Start-Sleep -Milliseconds 500 $elapsed += 1 $child = Get-CimInstance Win32_Process -Filter "ParentProcessId = $parentPID" -ErrorAction SilentlyContinue | Sort-Object CreationDate | Select-Object -Last 1 } if ($child) { $childPID = $child.ProcessId Write-Log "[DETECT] Child worker PID: $childPID (found after ${elapsed}s)" } else { Write-Log "[DETECT] No child worker detected after ${detectTimeout}s — treating as installer failure" Cleanup-VSCodeHelpers Cleanup-InnoSetupWorkers continue } $childProcess = Get-Process -Id $childPID -ErrorAction SilentlyContinue $result = Watchdog-MonitorInstaller -ChildProcess $childProcess -ParentPID $parentPID -IdleTimeout $IdleTimeout switch ($result) { "Success" { Write-Log "[WATCHDOG] Installer exited normally" Write-Log "----- $scriptName ended (exit 0) (Normal)-----" $result="Success" break } "FS-Stalled" { Write-Log "[WATCHDOG] Filesystem stall detected — no writes for $IdleTimeout seconds" Write-Log "----- $scriptName ended (exit 30) (FS-Stalled) -----" $result="FS Stalled" break } "Idle-Stalled" { Write-Log "[WATCHDOG] CPU/Disk idle stall — no activity for $IdleTimeout seconds" Write-Log "----- $scriptName ended (exit 31) (Idle Stall) -----" $result="Idle Stall" break } "Active-Stalled" { Write-Log "[WATCHDOG] CPU/Disk active stall — metrics frozen for $IdleTimeout seconds" Write-Log "----- $scriptName ended (exit 32) (Active Stall) -----" $result="Active Stall" break } default { Write-Log "[WATCHDOG] Unexpected watchdog state: $result" Write-Log "----- $scriptName ended (exit 99) (Unexpected Error) -----" $result="Unexpected Error" break } } } catch { Write-Log "[ERROR] Installer start failure: $($_.Exception.Message)" if ($attempt -ge $maxAttempts) { Write-Log "----- $scriptName ended (exit 13) (Start Failure) -----" return 13 } Write-Log "[RETRY] Retrying due to start failure" continue } Cleanup-VSCodeHelpers Cleanup-InnoSetupWorkers if ($result -eq "Success") { Write-Log "[SUCCESS] Installer completed successfully on attempt $attempt" break } else { Write-Log "[STALL] Installer stalled on attempt $attempt" if ($result -like "*Stall*") { Write-Log "[STALL] Detected stall state '$result' — performing cleanup before fallback" Cleanup-VSCodeHelpers Cleanup-InnoSetupWorkers Write-Log "[STALL] Cleanup complete — invoking ZIP fallback" return Invoke-ZipFallback -Reason $result } if ($attempt -ge $maxAttempts) { Write-Log "[FAIL] Installer stalled after $attempt attempts — invoking ZIP fallback" return Invoke-ZipFallback -Reason "Installer stalled after $attempt attempts" } Write-Log "[RETRY] Cleaning processes and artifacts before retry" Cleanup-VSCodeHelpers Cleanup-InnoSetupWorkers continue } Get-Process Code, CodeHelper*, CodeSetup*, VSCodeSetup* -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue } # ===================================================================== # Post‑Install Health Check (Hybrid Trigger) # ===================================================================== Write-Log "[CHECK] Running post-install health validation" # 1. Ensure Code.exe exists if (-not (Test-Path $codeExe)) { Write-Log "[CHECK] Code.exe missing — triggering ZIP fallback" return Invoke-ZipFallback -Reason "Missing Code.exe" } # 2. Ensure Code.exe launches try { $proc = Start-Process $codeExe -PassThru -ErrorAction Stop Start-Sleep -Seconds 2 if ($proc.HasExited) { Write-Log "[CHECK] VS Code exited immediately — triggering ZIP fallback" return Invoke-ZipFallback -Reason "Launch failure" } $proc | Stop-Process -Force } catch { Write-Log "[CHECK] Exception launching VS Code: $($_.Exception.Message)" Write-Log "[CHECK] Triggering ZIP fallback" return Invoke-ZipFallback -Reason "Launch exception" } # 3. Detect leftover update debris $debris = @( "$codeRoot\update.exe", "$codeRoot\*.tmp", "$codeRoot\*.partial" ) foreach ($pattern in $debris) { if (Get-ChildItem $pattern -ErrorAction SilentlyContinue) { Write-Log "[CHECK] Detected leftover update debris ($pattern) — triggering ZIP fallback" return Invoke-ZipFallback -Reason "Debris detected" } } Write-Log "[CHECK] Health check passed — installer appears successful" # ===================================================================== # Finalization # ===================================================================== Write-Log "[FINAL] Waiting for cleanup to settle" Start-Sleep -Seconds 5 if ($attempt -lt $maxAttempts){ Write-Log "[FINAL] Update-VSCode completed successfully after $attempt attempts" } else { Write-Log "[FAIL] Errors encountered Updating VSCode" } Write-Log "===============================================================================" return 0 } Export-ModuleMember -Function Update-VSCode, ` Get-VSCodeVersions, ` Switch-VSCodeVersion, ` Invoke-VSCodeRollback, ` Test-VSCodeSymlink, ` Start-VSCodeSafeMode, ` Get-VSCodeDashboard, ` Invoke-ZipFallback |