Check for Needed Reboot with PowerShell

29 Mar

Test-PendingReboot

About half of the time when Windows is behaving badly, all it needs is reboot.  The other half of the time restarting fixes the problem 😉 .  Okay, that’s an exaggeration, but all too often we are asked to fix problems caused by a failure to properly reboot the machine.

Today we are going to take a look at how we can use PowerShell to check the pending / required reboot status of a machine.

Just give me the script already

You can download the most current version of the code from my github site.  If you want to see how the function works or review a few usage examples, keep on reading.

How this function works

For the sake of brevity, I am saving the basics of a function, comment-based-help, CmdletBinding, and parameters for their own blog posts.

Parameters

The function supports the following parameters

  • ComputerName (IPAddress, __Server, CN) – This parameter will take a string, string[] or pipeline with property name and aliases.
  • Credential – Accepts a PSCredential object.
  • RequiredOnly – Switch that returns only required reboot results.
  • PassThru – Switch that returns the entire result object.
The “Begin”

To get started, I only need a few variable set up front.

# Custom object to store and format my results
[PSCustomObject[]]$rebootResultCollection = @()
# The localhost must be handled a little differently
# so I need to know which host is the localhost
$localHost = [System.Net.Dns]::GetHostByName(($env:computerName)).HostName
# The scriptblock lets me use this code on local and remote machines.
[ScriptBlock] $scriptBlock = { # Removed for demonstration }

The working code is all contained in the scriptblock. All it really does in check a few registry values, and one WMI value and stores a true or false value in a hash table.

# A simple hash (similar to an array) to store the results
$result = @{
    CBSRebootPending = $false
    FileRenamePending = $false
    SCCMRebootPending = $false
    WURebootRequired = $false
}
#Check CBS Registry
# Try to get the key
$cbsKey = Get-ChildItem "HKLM:Software\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending" -ErrorAction Ignore
# See if it exists
if ($cbsKey -ne $null)
{
    # Mark the result true if it exists
    $result.CBSRebootPending = $true
}

# Check PendingFileRenameOperations
# This key is always present with a list of properties for pending renames
$pendProp = Get-ItemProperty "HKLM:SYSTEM\CurrentControlSet\Control\Session Manager" -Name "PendingFileRenameOperations" -ErrorAction Ignore
# See if any properties were found
if($pendProp -ne $null)
{
    # Mark the result true if any properties were found
    $result.FileRenamePending = $true
}
else
{
    # Just a little extra output for troubleshooting with the -verbose parameter
    Write-Verbose "No Pending File Renames found."
}

# Check SCCM Client 
# Since this is a .NET call I can't use the ErrorAction Parameter
# to handle the error
try
{
    # This will fail in an error if SCCM isn't used
    # otherwise the result is stored
    $sccmStatus = ([wmiclass]"\\.\root\ccm\clientsdk:CCM_ClientUtilities").DetermineIfRebootPending()
}
catch
{
    # Just a little extra output for troubleshooting with the -verbose parameter
    Write-Verbose "SCCM not found."
}
# See if the variable has a value and then call the method contained
if(($sccmStatus -ne $null) -and $sccmStatus.RebootPending)
{
    $result.SCCMRebootPending = $true
}
#Check Windows Update
# Try to get the key
$wuKey = Get-Item "HKLM:SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired" -ErrorAction Ignore
# See if anything was found
if($wuKey -ne $null)
{
    # Mark the result true if the key is present
    $result.WURebootRequired = $true
}
# Return the result back to the caller of the scriptblock
return $result
The “Process”

Now that we know how to gather the data, we need to handle the execution of the scriptblock and the output of the results. Aforeach($name in $ComputerName) loops through each computer name passed through the parameter or pipeline.  Here is the section of code, from inside of that loop, that runs the scriptblock remotely.  Remote connections are not always dependable, so we need to make sure to catch errors anytime something could go wrong.

# The first step is to resolve the hostname. 
# We will use the same process we did for the localhost.
# The line calling the Resolve method is the only thing doing any work here.  
# The rest is error handling. We want to catch any DNS errors and display 
# in verbose output only.
try
{
    # Get the DNS address of the host
    $resolvedName = [System.Net.Dns]::Resolve(($name)).HostName
    Write-Verbose ("{0} resolved to {1}" -f $name, $resolvedName)
}
catch
{
    Write-Verbose $_.Exception.Message
}
# We need to clear the results from the Invoke-Command in each loop.
# If it persists and the Invoke-Command fails, this variable will contain the result from the 
# previous loop.  
$commandResult = $null
# We need to make sure we don't try to use Credentials on the local host 
# unless credentials are provided and the session is being run with administrative permissions.
if($resolvedName -ne $localHost -and $Credential -ne [System.Management.Automation.PSCredential]::Empty -and [bool](([System.Security.Principal.WindowsIdentity]::GetCurrent()).groups -match "S-1-5-32-544"))
{
    # Attempt Remote connection and Execution
    # I want to give the option of seeing errors with the -verbose parameter.
    Write-Verbose "Attempting remote connection."
    try
    {
        # Attempt a connection, execute the scriptblock, and store the results
        $commandResult = Invoke-Command -Session $session -ComputerName $resolvedName -ScriptBlock $scriptBlock -Credential $Credential -ErrorAction Stop
    }
    catch
    {
    # Write any error data to the screen 
    Write-Verbose $_.Exception.Message
    Write-Verbose "No Data Collected"
    }
}

The local session execution is has its own issues to sort through.  Connection issues are not likely, but there is this little security feature that will not allow you to execute a command with alternate credentials unless you are running an administrative session.

# Just a helpful line for the verbose option
Write-Verbose "Connecting using local session."
# Special Handling for localhost.
# Checks to see if this is the local host and to see if we are running as Admin.
if($resolvedName -eq $localHost -and !([bool](([System.Security.Principal.WindowsIdentity]::GetCurrent()).groups -match "S-1-5-32-544")))
{
    # If the user provided credentials but did not execute in an
    # administrative console, we need to let them know that we are stripping
    # off the credentials.
    if($Credential -ne [System.Management.Automation.PSCredential]::Empty)
    {
        Write-Verbose "Session is not run as administrator. Provided credentials will be ignored for localhost."
    }
    # Because this can fail, I want to make sure I catch the error.
    try
    {
        # This is the same execution I used for the remote machines
        # When run without a computername parameter, it runs the command
        # on the localhost.
        $commandResult = Invoke-Command -ScriptBlock $scriptBlock -ErrorAction Stop
    }
    catch
    {
         Write-Verbose $_.Exception.Message
    }
}
# The remote machines will use this command if credentials were not provided
else
{
    try
    {
        # The same command used elsewhere with the ComputerName Parameter added.
        $commandResult = Invoke-Command -ScriptBlock $scriptBlock -ComputerName $resolvedName -ErrorAction Stop
    }
    catch
    {
        Write-Verbose $_.Exception.Message
    }
 }

Once we’ve gathered the data for this machine, we need to store it before we move to the next machine.

# Process the results
# Store the values in the hash.
# This could be done directly in the New-Object command, but the hash looks better to me.
$rebootResult = [ordered]@{
    ComputerName = $resolvedName
    CBSRebootPending = $commandResult.CBSRebootPending
    FileRenamePending = $commandResult.FileRenamePending
    SCCMRebootPending = $commandResult.SCCMRebootPending
    WURebootRequired = $commandResult.WURebootRequired
}
# Convert the hash to a PSCustomObject and store in the array we created at the beginning. 
# This provides the output object we need in the last section.
$rebootResultCollection += New-Object  PSCustomObject -Property $rebootResult
The “End”

The end of this function is all about presenting the data.

# This if statement just checks to make sure we collected some data 
# before trying to display it.
if($rebootResultCollection -ne $null)
 {
    # To get those simple one-off results on the local host, 
    # I need to know if this is the localhost and if it is the 
    # only host in the list.
    [bool]$localOnly = ($colCount -eq 1 -and [System.Net.Dns]::Resolve(($rebootResultCollection[0].ComputerName)).HostName -eq $localHost) 
    # These next few lines are just for adding verbose output.
    $colCount = $rebootResultCollection.Count
    Write-Verbose "$colCount results returned."
    if($localOnly)
    {
        Write-Verbose "Returning results in Local Only format."
    }    
# We'll discuss the chunk I cut out here in a moment
 }
 else
 {
    # The error when nothing is collected.
    Write-Output "Failed to collect any results"
 }

The last two parameters both control the output, and, don’t forget, I wanted a very simple $true or $false output when run in the local session.

# The required only switch.
if($requiredOnly)
{
    # This statement removes the items from the collection if they 
    # only pending a reboot.
    $rebootResultCollection = $rebootResultCollection | Where-Object {$_.WURebootRequired -eq $true}
    # Just a little verbose status update
    $curCount = $rebootResultCollection.Count
    Write-Verbose "$($colCount - $curCount) pending results were filtered out."
}
# We still want the PassThru option to be available for the localhost.
# I didn't need or want the machine name for just the localhost.
if($PassThru -and $localOnly)
{
    # Returns the object with the ComputerName property removed.
    # It's worth noting that the ExcludeProperty parameter used
    # here is counter intuitive. You have to include before you
    # can exclude.
    return $rebootResultCollection | Select-Object -Property * -ExcludeProperty ComputerName
}
# The straight Passthru
elseif($PassThru)
{
    # Just send the object as it is. 
    return $rebootResultCollection
}
# And then that simple local host option I wanted.
elseif($localOnly)
{
    # The match here just returns true if the collection has any
    # true value.
    if($rebootResultCollection -match $true)
    {
        return $true
    }
    else
    {
        return $false
    }
}
# This is the simple version for a list of machines.
# Instead of a simple true or false, here I just return a list
# of computer names that are found to be true.
else
{
    # Returns any of the objects that have a true value then 
    # outputs only the computer name.
    return $rebootResultCollection | Where-Object {$_ -match $true} | Select-Object ComputerName
}

Usage

This function is designed to work with a variety of other functions.  Here are some example usages.

# Basic execution.
Test-PendingReboot -RequiredOnly
# returns $true if a reboot is required.
Test-PendingReboot
# returns $true if a changes are pending a reboot.

# Pulling computer names from a text file (server1,server2,server3).
Test-PendingReboot -ComputerName (Get-Content .\Servers.txt) -RequiredReboot
# returns a list of computer names that require a Reboot.
Test-PendingReboot -ComputerName (Get-Content .\Servers.txt) -PassThru
# returns an object listing the full details of all of the machines.

# Query AD for computer names and pipe to restart.
Get-ADComputer -Filter { name -like "FAT*"} | Select-Object Name -ExpandProperty Name| Test-PendingReboot -RequiredReboot | Restart-Computer -force
# Attempts a reboot of every domain PC with a name beginning with FAT, 
# that requires a reboot (best run when user are not at work).

That about does it.  Now you can test you machines to see if they need a reboot and make sure it is happening in a timely fashion.  Enjoy and God bless!

Joshua

See https://joshuaallenshaw.com/about-me/ See https://joshuaallenshaw.com/kiss/bio/