SCCM

Troubleshooting Windows Updates: Reboot Pending Loop

Introduction

Sometimes, after updating your Windows system, one of the updates may fail to install properly. In this blog post, we’ll discuss a common issue where the installation process gets interrupted, leading to a ‘Pending Reboot’ loop. We’ll look at how to identify if this is happening and how to fix it, especially if the computer doesn’t handle it automatically.

Verifying if your system still has a pending installation

After your system restarts, you can check whether the same update still has a pending status. You can use the following command to see if the same update still has a “Pending Reboot” status.

dism /online /get-packages /format:table | Select-String "Pending"

This PowerShell command uses the DISM to list installed packages and then filters the results to show only those with “Pending” in the status. If the update is still pending, it will be displayed in the output.
Please remember, not every “pending” status is an error. Only use the workaround below if you’re completely sure it applies to you.

Why does this happen?

After an update, sometimes the system can get messed up, especially if there’s a forced shutdown during the update. This can leave the update installation status as “Pending,” causing problems.

Workaround

I’ve written a powershell script that checks which packages have a “Pending” status and initiates a cleanup action.

⚠ Backup your registry before proceeding

Link to Github

<#
.SYNOPSIS
This script checks for pending Windows Update packages and performs actions based on the presence of stuck updates.

.DESCRIPTION
The script checks if there are pending Windows Update packages. If stuck updates are present, it proceeds to remove related registry entries and files to resolve the pending updates.

.PARAMETER
None required

.EXAMPLE
.\WindowsUpdateRemediation.ps1

.NOTES
Date Created: 2023-12-12
Author: Ditor Sahiti
#>

# Set log file path
$logFilePath = "C:\Windows\Temp\WindowsUpdateRemediation.log"

# Function to write log entries
function Write-Log {
    param(
        [string]$Message
    )

    $TimeStamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
    $LogEntry = "$TimeStamp - $Message"
    Add-Content -Path $logFilePath -Value $LogEntry
}

# Log script start
Write-Log -Message "Script execution started."

$pendingPackages = dism /online /get-packages /format:table | Select-String "Pending" | ForEach-Object { $_.ToString().Split('|')[0].Trim() }

# Check if $pendingPackages is not null
if ($pendingPackages -ne $null) {
    Write-Host "PendingPackages is present. Executing the script."
    Write-Log -Message "Pending Windows Update packages found. Script execution started."

    # Define registry paths to check
    $registryPaths = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing"

    # Loop through each registry path
    foreach ($path in $registryPaths) {
        # Check if the path exists
        if (Test-Path $path) {
            # Get all subkeys and values recursively
            $items = Get-ChildItem -Path $path -Recurse
            foreach ($item in $items) {
                # Check if the current item is a key
                if ($item.PSIsContainer) {
                    # Get all values in the key
                    $values = Get-ItemProperty -Path $item.PSPath -ErrorAction SilentlyContinue
                    foreach ($valueName in $values.PSObject.Properties.Name) {
                        foreach ($package in $pendingPackages) {
                            # Check if the value name matches the package name
                            if ($valueName -like "*$package*") {
                                # Remove the value
                                Remove-ItemProperty -Path $item.PSPath -Name $valueName -Force -ErrorAction SilentlyContinue
                                Write-Host "Removed value $valueName in $($item.PSPath)" 
                                Write-Log -Message "Removed value $valueName in $($item.PSPath)"
                            }
                        }
                    }

                    # Additional check for the key itself
                    foreach ($package in $pendingPackages) {
                        if ($item.Name -like "*$package*") {
                            # Remove the key
                            Remove-Item -Path $item.PSPath -Recurse -Force -ErrorAction SilentlyContinue
                            Write-Host "Removed key $($item.PSPath)" 
                            Write-Log -Message "Removed key $($item.PSPath)"
                        }
                    }
                }
            }
        }
        else {
            Write-Host "Path $path does not exist."
            Write-Log -Message "Path $path does not exist."
        }
    }

    # Define the paths for the registry keys
    $registryPaths = @(
        "SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending",
        "SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\PendedSessionPackages",
        "SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\SessionsPending"
    )

    # Function to safely check and delete subkeys, then delete the main key
    function ModifyAndDeleteRegKey {
        param(
            [string]$regPath
        )

        try {
            $localMachineKey = [Microsoft.Win32.Registry]::LocalMachine
            $regKey = $localMachineKey.OpenSubKey($regPath, $true)

            if ($regKey -ne $null) {
                # Delete subkeys
                $subKeys = $regKey.GetSubKeyNames()
                foreach ($subKey in $subKeys) {
                    $subRegKeyPath = "$regPath\$subKey"
                    $localMachineKey.DeleteSubKeyTree($subRegKeyPath)
                    Write-Host "Successfully deleted subkey: $subRegKeyPath"
                    Write-Log -Message "Successfully deleted subkey: $subRegKeyPath"
                }

                # Delete the main key
                 $localMachineKey.DeleteSubKeyTree($regPath)
                 Write-Host "Successfully deleted main registry key: $regPath"
                 Write-Log -Message "Successfully deleted main registry key: $regPath"

                $regKey.Close()
            } else {
                Write-Host "Registry key does not exist or cannot be accessed: $regPath"
                Write-Log -Message "Registry key does not exist or cannot be accessed: $regPath"
            }
        } catch {
            Write-Host "Failed to delete registry key: $regPath. Error: $_"
            Write-Log -Message "Failed to delete registry key: $regPath. Error: $_"
        }
    }

    # Process each registry path
    foreach ($path in $registryPaths) {
        ModifyAndDeleteRegKey -regPath $path
    }

    #Check for RebootPendingfilePath Key and remove it if present.
    $RebootPendingfilePath = "C:\Windows\Winsxs\Pending.xml"
    if (Test-Path $RebootPendingfilePath) {
        Remove-Item -Path $RebootPendingfilePath -Force -ErrorAction SilentlyContinue
        Write-Host "File removed."
        Write-Log -Message "File $RebootPendingfilePath removed."
    } else {
        Write-Host "Pending.XML File does not exist, no action taken." -ForegroundColor green
        Write-Log -Message "Pending.XML File does not exist, no action taken."
    }

} else {
    write-Host "No Pending Windows Updates found. The script will not be executed."
    Write-Log -Message "No Pending Windows Updates found. The script will not be executed."
}

# Log script end
Write-Log -Message "Script execution completed."

Conclusion

While issues with Windows updates leading to a ‘Pending Reboot’ loop aren’t exceedingly common, they do occur regularly in enterprise environments. Addressing these problems typically involves using the workaround script to resolve the issue, followed by installing the latest Cumulative Updates and rebooting the system. Although this process may be somewhat complex, it efficiently restores system functionality.

2 Comments

Leave a Reply

Your email address will not be published. Required fields are marked *