Public/Test-PendingReboot.ps1

function Test-PendingReboot {
    # Reports whether Windows is waiting on a restart, and why. Every probe is a
    # Test-Path or Get-ItemProperty call, so a test can mock the registry instead
    # of needing a machine that genuinely owes a reboot.
    [CmdletBinding()]
    param()

    $pendingReboot  = $false
    $rebootReasons  = [System.Collections.Generic.List[string]]::new()

$rebootKeys = [ordered]@{
    'Component Based Servicing'  = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending'
    'CBS reboot in progress'     = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootInProgress'
    'Windows Update'             = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired'
    'Windows Update post-reboot' = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\PostRebootReporting'
}
foreach ($entry in $rebootKeys.GetEnumerator()) {
    if (Test-Path -LiteralPath $entry.Value) {
        $pendingReboot = $true
        $rebootReasons.Add($entry.Key)
    }
}

# PendingFileRenameOperations exists as an empty value on plenty of healthy
# machines, so test the value rather than the presence of the property.
# Read the key and look for the value, rather than asking for the value and
# catching the failure. -Name with -ErrorAction Stop throws when the property is
# absent -- the normal, healthy case -- and Start-Transcript dutifully records
# that as "TerminatingError(Get-ItemProperty)" in the run log, where it reads
# like something went wrong on a machine where nothing did.
$sessionManager = Get-ItemProperty -LiteralPath 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager' `
    -ErrorAction SilentlyContinue
if ($sessionManager -and
    $sessionManager.PSObject.Properties.Name -contains 'PendingFileRenameOperations') {
    # The value is a REG_MULTI_SZ of [source, destination] pairs. A pair with a
    # destination is a real rename: Windows is holding a replacement for a file
    # that is in use, and the restart is what completes it. A pair with an empty
    # destination is a scheduled *deletion*, which installers queue constantly
    # for their own temp files and which needs no restart to be meaningful.
    # Counting every entry would report a reboot as pending for a single leftover
    # temp-file deletion such as *1\??\C:\...\Temp\DEL396A.tmp.
    $entries     = @($sessionManager.PendingFileRenameOperations)
    $renamePaths = [System.Collections.Generic.List[string]]::new()
    $deletions   = 0

    for ($i = 0; $i -lt $entries.Count; $i += 2) {
        $source = $entries[$i]
        if (-not $source) { continue }

        # A trailing source with no partner is malformed; treat it as a deletion
        # rather than inventing a rename out of it.
        $destination = if ($i + 1 -lt $entries.Count) { $entries[$i + 1] } else { '' }

        if (-not $destination) {
            $deletions++
            continue
        }

        # Report which file, not just how many. The queue is consumed at boot and
        # nothing else records it, so a bare count cannot be resolved back into a
        # reason after a restart.
        #
        # Sources carry NT object-manager syntax and MoveFileEx markers --
        # "\??\C:\x", "*1\??\C:\x" and "!\??\C:\x" all mean C:\x. Trim them so
        # the reason reads as a path someone recognises.
        $path = $source
        foreach ($prefix in '*1\??\', '*\??\', '!\??\', '\??\') {
            if ($path.StartsWith($prefix)) {
                $path = $path.Substring($prefix.Length)
                break
            }
        }

        $renamePaths.Add($path)
    }

    if ($renamePaths.Count -gt 0) {
        $pendingReboot = $true

        # The summary prints one line per reason and the toast joins the first
        # two, so a long queue must not turn either into a wall of text. Three
        # paths is enough to recognise what is happening; the count still says
        # how many there are.
        $shown = [System.Collections.Generic.List[string]]::new()
        for ($j = 0; $j -lt [Math]::Min(3, $renamePaths.Count); $j++) {
            $p = $renamePaths[$j]
            if ($p.Length -gt 70) { $p = '...' + $p.Substring($p.Length - 67) }
            $shown.Add($p)
        }

        $detail = $shown -join '; '
        if ($renamePaths.Count -gt 3) {
            $detail = "$detail; and $($renamePaths.Count - 3) more"
        }

        $rebootReasons.Add("Pending file renames ($($renamePaths.Count)): $detail")
    }

    if ($deletions -gt 0) {
        Write-Verbose "$deletions scheduled file deletion(s) are queued; those do not require a restart."
    }
}

# A queued computer rename also needs a restart to take effect.
try {
    $activeName = (Get-ItemProperty -LiteralPath 'HKLM:\SYSTEM\CurrentControlSet\Control\ComputerName\ActiveComputerName' -Name ComputerName -ErrorAction Stop).ComputerName
    $targetName = (Get-ItemProperty -LiteralPath 'HKLM:\SYSTEM\CurrentControlSet\Control\ComputerName\ComputerName'       -Name ComputerName -ErrorAction Stop).ComputerName
    if ($activeName -ne $targetName) {
        $pendingReboot = $true
        $rebootReasons.Add("Computer rename pending ($activeName -> $targetName)")
    }
} catch { Write-Verbose "Could not read the computer name keys: $($_.Exception.Message)" }

    [pscustomobject]@{
        IsPending = $pendingReboot
        Reasons   = $rebootReasons.ToArray()
    }
}