Functions/GenXdev.Windows/Get-WindowsIsUpToDate.ps1

################################################################################
<#
.SYNOPSIS
Checks if Windows is up to date and optionally installs available updates.
 
.DESCRIPTION
This function checks for both Windows updates and winget package updates. It can
display available updates or automatically install them. The function requires
administrative privileges to install Windows updates and can optionally reboot
the system if updates require a restart.
 
.PARAMETER AutoInstall
Automatically install available Windows and winget updates instead of just
checking for their availability.
 
.PARAMETER AutoReboot
Automatically reboot the system if installed updates require a restart. This
parameter only has effect when AutoInstall is also specified.
 
.EXAMPLE
Get-WindowsIsUpToDate
 
Checks for available Windows and winget updates without installing them.
 
.EXAMPLE
Get-WindowsIsUpToDate -AutoInstall
 
Automatically installs all available Windows and winget updates.
 
.EXAMPLE
updatewindows -AutoInstall -AutoReboot
 
Installs all updates and reboots automatically if required using the alias.
#>

function Get-WindowsIsUpToDate {

    [CmdletBinding()]
    [OutputType([System.Boolean])]
    [Alias("updatewindows")]

    param(
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = "Automatically install available Windows updates"
        )]
        [switch] $AutoInstall,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = "Automatically reboot if updates require a restart"
        )]
        [switch] $AutoReboot
        ###############################################################################
    )

    begin {

        # initialize tracking variable for winget update availability
        [bool] $wingetHasUpdates = $false

        # process winget updates if auto-install is requested
        if ($AutoInstall) {

            # automatically install all available winget packages
            winget update --all --verbose `
                --accept-package-agreements `
                --accept-source-agreements `
                --authentication-mode silent `
                --disable-interactivity `
                --include-unknown

            # verify if any winget updates remain after installation
            try {

                # get list of packages with available upgrades
                $wingetOutput = winget list --upgrade-available `
                    --accept-source-agreements 2>&1

                # parse output to determine if updates are still available
                # filter out header lines and match package data rows
                $wingetHasUpdates = ($wingetOutput |
                    Microsoft.PowerShell.Core\Where-Object {
                        $_ -match "^\S+\s+\S+\s+\S+\s+\S+\s*$" -and
                        $_ -notmatch "^Name\s+Id\s+Version\s+Available"
                    }).Count -gt 0
            }
            catch {

                # assume no updates if winget check fails to avoid false positives
                $wingetHasUpdates = $false

                Microsoft.PowerShell.Utility\Write-Verbose (
                    "Failed to check winget updates: ${_}")
            }
        }
        else {

            # check for available winget updates without installing them
            try {

                # get list of packages with available upgrades
                $wingetOutput = winget list --upgrade-available `
                    --accept-source-agreements 2>&1

                # parse output to identify packages needing updates
                # filter out header lines and match package data rows
                $wingetUpdates = $wingetOutput |
                    Microsoft.PowerShell.Core\Where-Object {
                        $_ -match "^\S+\s+\S+\s+\S+\s+\S+\s*$" -and
                        $_ -notmatch "^Name\s+Id\s+Version\s+Available"
                    }

                $wingetHasUpdates = $wingetUpdates.Count -gt 0

                # display available winget updates when not auto-installing
                if ($wingetHasUpdates -and -not $AutoInstall) {

                    Microsoft.PowerShell.Utility\Write-Host (
                        "Available Winget Updates:") -ForegroundColor Yellow

                    Microsoft.PowerShell.Utility\Write-Host (
                        "=========================") -ForegroundColor Yellow

                    # show header and package information in formatted output
                    $wingetOutput |
                        Microsoft.PowerShell.Core\Where-Object {
                            $_ -match "^Name\s+Id\s+Version\s+Available" -or
                            $_ -match "^\S+\s+\S+\s+\S+\s+\S+\s*$"
                        } |
                        Microsoft.PowerShell.Core\ForEach-Object {
                            Microsoft.PowerShell.Utility\Write-Host $_ `
                                -ForegroundColor White
                        }

                    Microsoft.PowerShell.Utility\Write-Host ""
                }

                # continue execution to also check windows updates
            }
            catch {

                # assume no updates if winget check fails to avoid false positives
                $wingetHasUpdates = $false

                Microsoft.PowerShell.Utility\Write-Verbose (
                    "Failed to check winget updates: ${_}")
            }
        }

        # verify administrative privileges are available for windows updates
        if (-not (GenXdev.Windows\CurrentUserHasElevatedRights)) {

            Microsoft.PowerShell.Utility\Write-Error (
                "This cmdlet requires administrative privileges.")

            # return opposite of winget status to indicate overall system state
            return (-not $wingetHasUpdates)
        }

        # initialize com objects for windows update operations
        try {

            # create main session object for update operations
            $updateSession = Microsoft.PowerShell.Utility\New-Object `
                -ComObject Microsoft.Update.Session

            # create searcher object to find available updates
            $updateSearcher = $updateSession.CreateUpdateSearcher()
        }
        catch {

            Microsoft.PowerShell.Utility\Write-Error (
                "Failed to initialize Windows Update session: ${_}")

            # return opposite of winget status if windows update init fails
            return (-not $wingetHasUpdates)
        }
    }

    process {

        try {

            # search for updates that are not installed and not hidden
            Microsoft.PowerShell.Utility\Write-Verbose (
                "Searching for Windows updates...")

            $searchResult = $updateSearcher.Search("IsInstalled=0 and IsHidden=0")

            $updates = $searchResult.Updates

            # check if any updates were found
            if ($updates.Count -eq 0 -and -not $wingetHasUpdates) {

                Microsoft.PowerShell.Utility\Write-Verbose (
                    "No updates available. System is up to date.")

                return $true
            }

            # return false if updates exist but autoinstall is not requested
            if (-not $AutoInstall) {

                if ($updates.Count -gt 0) {
                    Microsoft.PowerShell.Utility\Write-Host "Available Windows Updates:" -ForegroundColor Cyan
                    Microsoft.PowerShell.Utility\Write-Host "==========================" -ForegroundColor Cyan

                    foreach ($update in $updates) {
                        if (-not $update.IsHidden) {
                            Microsoft.PowerShell.Utility\Write-Host "• $($update.Title)" -ForegroundColor White
                            Microsoft.PowerShell.Utility\Write-Host " Size: $([math]::Round($update.MaxDownloadSize / 1MB, 2)) MB" -ForegroundColor Gray
                            if ($update.Description) {
                                $description = $update.Description
                                if ($description.Length -gt 100) {
                                    $description = $description.Substring(0, 97) + "..."
                                }
                                Microsoft.PowerShell.Utility\Write-Host " $description" -ForegroundColor Gray
                            }
                            Microsoft.PowerShell.Utility\Write-Host ""
                        }
                    }

                    Microsoft.PowerShell.Utility\Write-Verbose (
                        "$($updates.Count) Windows updates are available but AutoInstall " +
                        "is not specified.")
                }

                if ($wingetHasUpdates) {
                    Microsoft.PowerShell.Utility\Write-Verbose (
                        "Winget updates are available but AutoInstall is not specified.")
                }

                # inform user how to install the available updates
                if ($updates.Count -gt 0 -or $wingetHasUpdates) {

                    Microsoft.PowerShell.Utility\Write-Host (
                        "To install these updates automatically, use:") `
                        -ForegroundColor Green

                    Microsoft.PowerShell.Utility\Write-Host (
                        " Get-WindowsIsUpToDate -AutoInstall") `
                        -ForegroundColor Cyan

                    Microsoft.PowerShell.Utility\Write-Host (
                        " updatewindows -AutoInstall") `
                        -ForegroundColor Cyan

                    Microsoft.PowerShell.Utility\Write-Host ""

                    Microsoft.PowerShell.Utility\Write-Host (
                        "To install and automatically reboot if needed, use:") `
                        -ForegroundColor Green

                    Microsoft.PowerShell.Utility\Write-Host (
                        " Get-WindowsIsUpToDate -AutoInstall -AutoReboot") `
                        -ForegroundColor Cyan

                    Microsoft.PowerShell.Utility\Write-Host (
                        " updatewindows -AutoInstall -AutoReboot") `
                        -ForegroundColor Cyan

                    Microsoft.PowerShell.Utility\Write-Host ""
                }

                return $false
            }

            # prepare to install the available updates
            Microsoft.PowerShell.Utility\Write-Verbose (
                "Found $($updates.Count) updates to install.")

            $updatesToInstall = Microsoft.PowerShell.Utility\New-Object -ComObject Microsoft.Update.UpdateColl

            # filter out hidden updates and add valid ones to install collection
            foreach ($update in $updates) {

                if ($update.IsHidden -eq $false) {

                    $null = $updatesToInstall.Add($update)
                }
            }

            # verify we have updates to install after filtering
            if ($updatesToInstall.Count -eq 0) {

                Microsoft.PowerShell.Utility\Write-Verbose (
                    "No valid updates to install after filtering.")

                return $true
            }

            # create downloader and configure it with the filtered updates
            $downloader = $updateSession.CreateUpdateDownloader()

            $downloader.Updates = $updatesToInstall

            # download the updates before installation
            Microsoft.PowerShell.Utility\Write-Verbose "Downloading updates..."

            $downloadResult = $downloader.Download()

            # check if download was successful (result code 2 = success)
            if ($downloadResult.ResultCode -ne 2) {

                Microsoft.PowerShell.Utility\Write-Error (
                    "Failed to download updates. Result code: " +
                    "$($downloadResult.ResultCode)")

                return $false
            }

            # create installer and configure it with the downloaded updates
            $installer = $updateSession.CreateUpdateInstaller()

            $installer.Updates = $updatesToInstall

            # install the downloaded updates
            Microsoft.PowerShell.Utility\Write-Verbose "Installing updates..."

            $installResult = $installer.Install()

            # check if installation was successful (result code 2 = success)
            if ($installResult.ResultCode -ne 2) {

                Microsoft.PowerShell.Utility\Write-Error (
                    "Failed to install updates. Result code: " +
                    "$($installResult.ResultCode)")

                return $false
            }

            # handle reboot requirement if updates need system restart
            if ($installResult.RebootRequired -and $AutoReboot) {

                Microsoft.PowerShell.Utility\Write-Verbose (
                    "Reboot required. Initiating reboot...")

                Microsoft.PowerShell.Management\Restart-Computer -Force

                # return false since system needs reboot to complete updates
                return $false
            }
            elseif ($installResult.RebootRequired) {

                Microsoft.PowerShell.Utility\Write-Verbose (
                    "Reboot required but AutoReboot not specified.")

                return $false
            }

            # search again for remaining updates after installation
            $newSearchResult = $updateSearcher.Search("IsInstalled=0 and IsHidden=0")

            # check if all updates have been successfully installed
            # also consider winget updates in the final determination
            if ($newSearchResult.Updates.Count -eq 0 -and -not $wingetHasUpdates) {

                Microsoft.PowerShell.Utility\Write-Verbose (
                    "No more updates available after installation.")

                return $true
            }
            else {

                if ($newSearchResult.Updates.Count -gt 0) {
                    Microsoft.PowerShell.Utility\Write-Verbose (
                        "Additional Windows updates found after installation.")
                }

                if ($wingetHasUpdates) {
                    Microsoft.PowerShell.Utility\Write-Verbose (
                        "Winget updates still available.")
                }

                return $false
            }
        }
        catch {

            Microsoft.PowerShell.Utility\Write-Error (
                "Error during update process: ${_}")

            return $false
        }
    }

    end {

        # release com objects to prevent memory leaks
        if ($updateSession) {

            $null = [System.Runtime.InteropServices.Marshal]::ReleaseComObject(
                $updateSession)
        }
    }
}
################################################################################