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-04-23
    File: VSCode-Updater.psm1
    Version: 1.0.1
    Description: Module root for VSCode-Updater. Loads public functions, wires private helpers, and exposes the deterministic Update-VSCode entry point.
#>

# 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
    )

    # =====================================================================
    # Initialization + Metadata Banner
    # =====================================================================

    $scriptName    = "Update-VSCode"
    $scriptVersion = "2.0.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
    

    if (-not (Test-Path $cachedInstaller)) {
        Write-Log "[ERROR] Cached installer missing after update"
        Write-Log "----- $scriptName ended (exit 12) -----"
        return 12
    }

    # =====================================================================
    # Retry Loop
    # =====================================================================
    # NEW: Ensure no stale InnoSetup workers exist before launching installer
    Cleanup-InnoSetupWorkers
    Start-Sleep -Milliseconds 200

    $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)-----"
                    return 0
                }

                "FS-Stalled" {
                    Write-Log "[WATCHDOG] Filesystem stall detected — no writes for $IdleTimeout seconds"
                    Write-Log "----- $scriptName ended (exit 30) (FS-Stalled) -----"
                    return 30
                }

                "Idle-Stalled" {
                    Write-Log "[WATCHDOG] CPU/Disk idle stall — no activity for $IdleTimeout seconds"
                    Write-Log "----- $scriptName ended (exit 31) (Idle Stall) -----"
                    return 31
                }

                "Active-Stalled" {
                    Write-Log "[WATCHDOG] CPU/Disk active stall — metrics frozen for $IdleTimeout seconds"
                    Write-Log "----- $scriptName ended (exit 32) (Active Stall) -----"
                    return 32
                }

                default {
                    Write-Log "[WATCHDOG] Unexpected watchdog state: $result"
                    Write-Log "----- $scriptName ended (exit 99) (Unexpected Error) -----"
                    return 99
                }
            }
        }
        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
        }

        Write-Log "[STALL] Installer stalled on attempt $attempt"

        if ($attempt -ge $maxAttempts) {
            Write-Log "[FAIL] Installer stalled after $attempt attempts — aborting"
            Write-Log "----- $scriptName ended (exit 14) (Excessive Attempts) -----"
            return 14
        }

        Write-Log "[RETRY] Cleaning processes and artifacts before retry"

        Cleanup-VSCodeHelpers
        Cleanup-InnoSetupWorkers

        Get-Process Code, CodeHelper*, CodeSetup*, VSCodeSetup* -ErrorAction SilentlyContinue |
            Stop-Process -Force -ErrorAction SilentlyContinue
    }

    # =====================================================================
    # 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