Programmatically run Windows Update (as part of a broader patch and reboot process)

Articles in the "Programmatically Run Windows Update" series

  1. Programmatically run Windows Update (as part of a broader patch and reboot process) [This article]
  2. Update to script that runs Windows Update

Edit: This script has been updated and is explained here.

It’s been awhile since I have posted a script, so this is the first post of my process for patching and rebooting the Exchange servers at work. I needed a way to patch the environment while maintaining high availability and not resorting to just staggering reboots at, say, 30-minute intervals. The staggered approach doesn’t account for any issues that can occur if a server doesn’t come back up or services fail to start, etc. What I have done to maintain HA starts with this script.

I originally intended to have the Windows Update process as part of my bigger patch-and-reboot script, but I learned that you can’t make calls to the Windows Update API from remote systems. This means that I need to execute the script locally on each server, but my reboot script runs remotely from a management server. Therefore, this is just a standalone script for installing software updates from Windows Update (or, in my case, Microsoft Update, since that is installed). It is locally installed as an on-demand Scheduled Task that gets remotely called from my reboot script, but I will go into those details when I post about that script.

The first function in the script is for writing to the event log. This is how my reboot script monitors the progress. I create a custom event source so that there is no chance of conflicting with anything else. Any time something is to be written I pass the message, type (Information, Warning, etc.), and event ID.

function WriteEvent ($eventMessage,$eventType,$eventID)
	{
	$sourceName = 'UCTeam Scripts'
	if (-not([System.Diagnostics.EventLog]::SourceExists($sourceName)))
		{
		[System.Diagnostics.EventLog]::CreateEventSource($sourceName,'Application')
		}
	$EventLog = New-Object System.Diagnostics.EventLog('Application')
	$eventLogType = [System.Diagnostics.EventLogEntryType]::$eventType
	$EventLog.Source = $sourceName
	$EventLog.WriteEntry($eventMessage,$eventLogType,$EventID)
	}

The search criteria is for software updates (no drivers) that are not installed and are checked by default. If you want to control which updates to install, you can manipulate the search criteria a little bit (as documented) so only certain updates are returned, but to not install an update associated with a specific KB article you have to loop through the results and then not install the update that matches that property.

#Software updates only, selected by default, not already installed
$criteria="IsInstalled=0 and Type='Software' and AutoSelectOnWebSites=1"
$resultcode= @{0="Not Started"; 1="In Progress"; 2="Succeeded"; 3="Succeeded With Errors"; 4="Failed" ; 5="Aborted" }
$updateSession = New-Object -ComObject 'Microsoft.Update.Session'
WriteEvent 'Windows Update process is starting.' 'Information' '1000'
WriteEvent "Beginning check for available updates based on the following criteria: $criteria." 'Information' '1001'
$updates = $updateSession.CreateupdateSearcher().Search($criteria).Updates

I use the WriteEvent function to write status updates to the event log so that my remote script can watch for events that indicate when it is done or that something has gone wrong. After the search results are downloaded, you still have to actually download them. Since I am not filtering any specific updates, I specify the entire $updates object to download.

#Create download object
$downloader = $updateSession.CreateUpdateDownloader()
$downloader.Updates = $updates  
WriteEvent 'Beginning download of available updates.' 'Information' '1002'
$result= $downloader.Download()

When that is complete you loop through the collection and add them to a new installer object, which is finally used to install the updates.

$updatesToInstall = New-Object -ComObject 'Microsoft.Update.UpdateColl'
$updates | Where-Object {$_.isdownloaded} | Foreach-Object {$updatesToInstall.Add($_) | Out-Null}
#Create installer object
$installer = $updateSession.CreateUpdateInstaller()
$installer.Updates = $updatesToInstall
WriteEvent "Beginning installation of downloaded updates `($($installer.Updates.count)`)." 'Information' '1003'
#Run installation of downloaded files
$installationResult = $installer.Install()

Lastly, I loop through the results to log the name of every update that installed (or tried to install).

$global:counter=-1
$installResults = $installer.updates | Select-Object -property Title,EulaAccepted,@{label='Result'; `
	expression={$resultCode[$installationResult.GetUpdateResult($($global:counter++)).resultCode]}}
WriteEvent ($installResults | Format-Table -Wrap | Out-String) 'Information' '1002'

While you can have the script issue a reboot locally, I just log when the updates are complete so that my reboot script can trigger that and know precisely when that occurs. The entire script is below (since the code snippets above don’t include every line), but you can also download it.

  Run-WindowsUpdate.zip (1.8 KiB)

#Download and install software updates from WU that are selected by default
#v1.0 11/4/2010

function WriteEvent ($eventMessage,$eventType,$eventID)
	{
	$sourceName = 'UCTeam Scripts'
	if (-not([System.Diagnostics.EventLog]::SourceExists($sourceName)))
		{
		[System.Diagnostics.EventLog]::CreateEventSource($sourceName,'Application')
		}
	$EventLog = New-Object System.Diagnostics.EventLog('Application')
	$eventLogType = [System.Diagnostics.EventLogEntryType]::$eventType
	$EventLog.Source = $sourceName
	$EventLog.WriteEntry($eventMessage,$eventLogType,$EventID)
	}

#Software updates only, selected by default, not already installed
$criteria="IsInstalled=0 and Type='Software' and AutoSelectOnWebSites=1"
$resultcode= @{0="Not Started"; 1="In Progress"; 2="Succeeded"; 3="Succeeded With Errors"; 4="Failed" ; 5="Aborted" }
$updateSession = New-Object -ComObject 'Microsoft.Update.Session'
WriteEvent 'Windows Update process is starting.' 'Information' '1000'
WriteEvent "Beginning check for available updates based on the following criteria: $criteria." 'Information' '1001'
$updates = $updateSession.CreateupdateSearcher().Search($criteria).Updates
if ($updates.Count -eq 0)
	{
	WriteEvent 'Check for available updates is complete.  There are no updates to apply.' 'Information' '1001'
	}   
else
	{
	WriteEvent "Check for available updates is complete.  There are $($updates.Count) updates to apply." 'Information' '1001'
	#Create download object
	$downloader = $updateSession.CreateUpdateDownloader()
	$downloader.Updates = $Updates  
	WriteEvent 'Beginning download of available updates.' 'Information' '1002'
	$result= $downloader.Download()  
	if (($result.Hresult -eq 0) –and (($result.resultCode –eq 2) -or ($result.resultCode –eq 3)))
		{
		WriteEvent 'Download of available updates has completed.' 'Information' '1002'
		$updatesToInstall = New-Object -ComObject 'Microsoft.Update.UpdateColl'
		$updates | Where-Object {$_.isdownloaded} | Foreach-Object {$updatesToInstall.Add($_) | Out-Null}
		#Create installer object
		$installer = $updateSession.CreateUpdateInstaller()
		$installer.Updates = $updatesToInstall
		WriteEvent "Beginning installation of downloaded updates `($($installer.Updates.count)`)." 'Information' '1003'
        #Run installation of downloaded files
		$installationResult = $installer.Install()
        $global:counter=-1
        $installResults = $installer.updates | Select-Object -property Title,EulaAccepted,@{label='Result'; `
			expression={$resultCode[$installationResult.GetUpdateResult($($global:counter++)).resultCode]}}
		WriteEvent ($installResults | Format-Table -Wrap | Out-String) 'Information' '1002'
		}
	else
		{
		WriteEvent 'Error downloading updates.' 'Warning' '1001'
		}
	}
WriteEvent 'Windows Update process is complete.' 'Information' '1010'

#Reboot
#Restart-Computer -Force -Confirm:$false

One thought on “Programmatically run Windows Update (as part of a broader patch and reboot process)

  1. Thanks for this. I ported it to IronRuby and used it to automate applying windows updates to a bunch of VM’s (we have automated tests which run IronRuby code inside VM’s), it really saved me lots of time.

Leave a Reply

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

*