# Logging and Error Handling Best Practices for Automating Windows Update Installs (MSU) with wusa.exe (And For Logging Any Called Process)

Automation of Windows Updates is generally done by calling wusa.exe against a .MSU file. When wusa.exe experiences a failure the messages are typically obscure and hard to diagnose. The underlying error messages, however, are frequently easy to understand and resolve.

The secret is to have wusa.exe do verbose logging in it’s standard exported windows event format and then use the PowerShell event message CMDLets to query that log - automatically, everytime you have an error.

Although focused on applying updates with wusa.exe, this article contains most of my logging best practices for automation coding when calling automated sub-processes.

# Toolsmithing Perspective

Everything in this post is helpful even if you are the only one who will ever examine the logs of the automation you build. However, you will hear a persistent theme in this post of how it helps “others”. Since I can remember, I have built utility tools that are leveraged by others or supported by a separate support team. In these cases, investments like the ones discussed in this article, have a very high “Team ROI” as well as help to guard the automation developer’s time against unnecessary escalation incidents.

# Windows Updates Error Trapping is Not Straight Forward

There are several attributes of wusa.exe execution that make attention to logging all the more valuable:

1. wusa.exe critical errors do not generate terminating errors in PowerShell
2. For the same root cause, wusa.exe exit codes are different from logged error codes
3. For the same root cause, logged error codes are in decimal while the ones that might display interactively when running Windows Update GUI are in hex (but at least the same number when converted).

# Examples Of Challenging Messages

0x80240017 / 2149842967 / -2145124329 (Wusa.exe exit code) Log: “Windows update could not be installed because of error 2149842967” Interactive “The update is not applicable to your computer” => Painful - first the logged message is not very helpful. Second, you can get this message if you simply do not have the correct prerequisites - if you can’t find the common causes of not meeting prerequisites for the specific update you are applying, then you have to dissect the windows update to read it’s prerequisites section. See “Dissecting Windows Update Packages” below.

0x80070422 / 2147943458 / 1058 (Wusa.exe exit code) - Windows Update Cannot Check For Updates => Usually means the Windows Update Service is stopped - be careful that it is not stopped for a valid reason (e.g. other long running deployment automation is currently underway.)

0x80070002 / 2147942402 / 2 (Wusa.exe exit code) - The system cannot find the file specified. => The .MSU file is not available at the location passed to wusa.exe. Also occurs in other circumstances.

# Logging - The Foundation Of Good Exception Handling and Quicker Resolution of Escalations

When coding automation I a huge advocate of verbose logging for everything my automation invokes (as well as having the automation do it’s own logging) - and to a task specific location when logs can be directed to a custom location.

This is for multiple strategic reasons:

1. Verbose sub-process logging is super useful in production environemnts, however, it is jaw dropping how often verbose logging helps me while developing automation code.
2. Once the solution is in production, I get less calls because those who run into problems with my work can frequently solve the problem on their own when the right logging data is in front of them. That makes me, my team and other teams faster and more productive.
3. When I get calls, I can ask for the logs that I already know exist. That makes me, my team and other teams faster and more productive :)
4. In production (or test or dev) there is no need to attempt to reproduce the problem AFTER logging has been enabled - so if you have a rare transient problem, you get maximum information on every occurence. That makes me, my team and other teams faster and more productive :)
5. I have come across many, many interesting edge cases simply by having verbose logging on for development. That allows me to bake in handling of specific issues when merited or to at least document them and their resolution.

# Always Run msu.exe With Verbose Logging

While wusa.exe logging is very useful, there a couple nuances about it that are challenging to navigate - especially if you are starting to use the parameter for the first time.

1. The log output format is “Exported Event Log” - in other words the same type of file as if you exported windows event logs using the event log viewer. Great confusion results if you give it a .log extension because you and others will try to open it in a text editor. A bunch of the productivity benefits I seek are lost if individuals besides myself can’t view the contents without contacting me. To get around this I simply name the log file with the exported event file extension. This causes it to open in the event log viewer if double-clicked and it also cues other people that the file is not a simple text file. I also like to use a file name that communicates a lot of meta data.

The below example tells exactly what update and exactly when the install happened. If people might send me these in email, I’d probably also embed the computer name, because having the wrong log sent to me is a frequent experience that makes us all less productive:

$logfilename = "$env:PUBLIC\Logs\ApplyPSH5\PowerShell-Install-W2K12-KB3191565-x64-$(Get-date -format 'yyyyMMddhhmmss')-Log.evtx"  2. The command line parameter is fussy. Unlike the other parameters for wusa.exe it: 1. MUST have a colon after it, 2. must not have whitespace after the colon, 3. must be enclosed in quotes if there are spaces in the file name 4. If you break any of these rules, windows update generally runs without errors, it just does not generate a log. The above list is an aggregate across multiple versions of Windows - so not all of these sensitivities might be present in a given version of Windows. Here is an example of what I use to avoid these problems: #Running Directly in CMD or PS1: wusa.exe W2K12-KB3191565-x64.msu /quiet /norestart /log:"$logfilename"

#Commandline stored in a variable (PS1):
$wusaswitches = "/quiet /norestart /log:"$logfilename""

# Example Code

As per the Testable Reference Pattern Manifesto all of the below code has been tested. It is also available in a repository to help avoid problems when copying and pasting from web pages: CloudyWindowsCode

## Fully Serialized Example of Log Naming (logs never overwritten - retains historical runs)

$LogRoot = "$env:PUBLIC\Logs" ## Non-serialized, globally consistent "logroot" on every machine.
$SerializedStringForThisRun =$(Get-date -format 'yyyyMMddhhmmss')  ## date based serialized string - allows all serialized items to have the same serialization.
$LogFolder = "$LogRoot\InstallPSH5-$SerializedStringForThisRun" ## date based serialized folder New-Item -ItemType Directory$LogFolder -Force -ErrorAction SilentlyContinue | Out-Null ## This will create the logroot and specific log folder in one shot
$logfilename = "$logfolder\PowerShell-Install-W2K12-KB3191565-x64-$SerializedStringForThisRun-Log.evtx" ##date based serialized log filename$wusaswitches = "/quiet /norestart /log:"$logfilename"" Write-Host "Running Windows Update with the following command which generate a verbose log at:$logfilename"  ##Logging the fact that a verbose log is available for the sub-process we are about to call

Write-Host "Initiating command: wusa.exe W2K12-KB3191565-x64.msu $wusaswitches"$ResultObject = start-process "wusa.exe" -ArgumentList "$pwd\W2K12-KB3191565-x64.msu$wusaswitches" -Wait -PassThru


## Non-serialized Example of Log Naming, reset logs to clean everytime (does not retain history)

$LogRoot = "$env:PUBLIC\Logs"
$LogFolder = "$LogRoot\InstallPSH5"
If (Test-Path $LogFolder) {Remove-Item$LogFolder -Recurse -Force -ErrorAction SilentlyContinue}
New-Item -ItemType Directory $LogFolder -Force -ErrorAction SilentlyContinue | Out-Null$logfilename = "$logfolder\PowerShell-Install-W2K12-KB3191565-x64-Log.evtx"$wusaswitches = "/quiet /norestart /log:"$logfilename"" Write-Host "Running Windows Update with the following command which generate a verbose log at:$logfilename"  ##Logging the fact that a verbose log is available for the sub-process we are about to call
Write-Host "Initiating command: wusa.exe W2K12-KB3191565-x64.msu $wusaswitches"$ResultObject = start-process "wusa.exe" -ArgumentList "$pwd\W2K12-KB3191565-x64.msu$wusaswitches" -Wait -PassThru


# Surface Error Messages (and Warnings If You Wish)

The below snippets show various ways of retrieving and potentially acting upon the messages generated by wusa.exe.

You can creatively mix some of these together - for instance, always re-logging found warnings and errors - but only taking action if ther are actually errors.

## Always Show All Messages in Log

This code always re-logs the detailed warnings and errors from the windows uupdate log - this is a good general practice for debugging related errors as well as helping with messages that are classfied as “Warnings” by windows update, which are essentially errors for your deployment scenario.

$WarningMsgs = 3$ErrorMsgs = 2

$LogRoot = "$env:PUBLIC\Logs"
$SerializedStringForThisRun =$(Get-date -format 'yyyyMMddhhmmss')
$LogFolder = "$LogRoot\InstallPSH5-$SerializedStringForThisRun" New-Item -ItemType Directory$LogFolder -Force -ErrorAction SilentlyContinue
$logfilename = "$logfolder\PowerShell-Install-W2K12-KB3191565-x64-$SerializedStringForThisRun-Log.evtx"$wusaswitches = "/quiet /norestart /log:"$logfilename"" Write-Host "Running Windows Update with the following command which generate a verbose log at:$logfilename"  ##Logging the fact that a verbose log is available for the sub-process we are about to call
Write-Host "Initiating command: wusa.exe W2K12-KB3191565-x64.msu $wusaswitches"$ResultObject = start-process "wusa.exe" -ArgumentList "$pwd\W2K12-KB3191565-x64.msu$wusaswitches" -Wait -PassThru

$LogMessagesOfConcern = @(Get-WinEvent -Path "$logfilename" -oldest | where {($_.level -ge$WarningMsgs) -AND ($_.level -le$ErrorMsgs)})
If ($LogMessagesOfConcern.count -gt 0) { Write-Host "Found the following concerning messages in the MSU log "$logfilename""
$LogMessagesOfConcern | Format-List ID, Message | out-string | write-host }  ## Using the Existence of an Error in the Verbose Log The code relies on the windows update to accurately report errors and then consider that there is a problem only if that is true. A downside to this code is that it will not capture malformed wusa.exe command lines or a non-existent .msu. $WarningMsgs = 3
$ErrorMsgs = 2$LogRoot = "$env:PUBLIC\Logs"$SerializedStringForThisRun = $(Get-date -format 'yyyyMMddhhmmss')$LogFolder = "$LogRoot\InstallPSH5-$SerializedStringForThisRun"
New-Item -ItemType Directory $LogFolder -Force -ErrorAction SilentlyContinue | Out-Null$logfilename = "$logfolder\PowerShell-Install-W2K12-KB3191565-x64-$SerializedStringForThisRun-Log.evtx"
$wusaswitches = "/quiet /norestart /log:"$logfilename""

Write-Host "Running Windows Update with the following command which generate a verbose log at: $logfilename" ##Logging the fact that a verbose log is available for the sub-process we are about to call Write-Host "Initiating command: wusa.exe W2K12-KB3191565-x64.msu$wusaswitches"

$ResultObject = start-process "wusa.exe" -ArgumentList "$pwd\W2K12-KB3191565-x64.msu $wusaswitches" -Wait -PassThru$LogMessagesOfConcern = @(Get-WinEvent -Path "$logfilename" -oldest | where {($_.level -ge $ErrorMsgs) -AND ($_.level -le $WarningMsgs)})$LogErrorsOnly = @(Get-WinEvent -Path "$logfilename" -oldest | where {$_.level -eq $ErrorMsgs}) If ($LogErrorsOnly.count -gt 0)
{
Write-Host "Found the following error(s) (and possibly some warnings) in the MSU log "$logfilename"" Throw ($LogMessagesOfConcern | Format-List ID, Message | out-string)
}


## Adding Try / Catch - Just Display Errors and Warnings

Try/Catch should be used to capture exceptions that happen because wusa.exe cannot be found or started at all.

$ErrorActionPreference = 'Stop'$LogRoot = "$env:PUBLIC\Logs"$SerializedStringForThisRun = $(Get-date -format 'yyyyMMddhhmmss')$LogFolder = "$LogRoot\Install PSH5-$SerializedStringForThisRun"
New-Item -ItemType Directory $LogFolder -Force -ErrorAction SilentlyContinue$logfilename = "$logfolder\PowerShell-Install-W2K12-KB3191565-x64-$SerializedStringForThisRun-Log.evtx"

$WarningMsgs = 3$ErrorMsgs = 2

$wusaswitches = "/quiet /norestart /log:"$logfilename""

Write-Host "Running Windows Update with the following command which generate a verbose log at: $logfilename" ##Logging the fact that a verbose log is available for the sub-process we are about to call Write-Host "Initiating command: wusa.exe W2K12-KB3191565-x64.msu$wusaswitches"

Try {
$ResultObject = start-process "wusa.exe" -ArgumentList "$pwd\W2K12-KB3191565-x64.msu $wusaswitches" -Wait -Passthru Write-Output "Return code is:$($ResultObject.ExitCode)" If (Test-Path "$logfilename")
{
$LogMessagesOfConcern = @(Get-WinEvent -Path "$logfilename" -oldest | where {($_.level -ge$ErrorMsgs) -AND ($_.level -le$WarningMsgs)})
If ($LogMessagesOfConcern.count -gt 0) { Write-Host "Found the following error(s) and warnings in the MSU log "$logfilename""
$LogMessagesOfConcern | Format-List ID, Message | out-string | write-host } } } catch { Throw$_.Exception
}


# Acting Upon Specific Messages

The following code shows how to take action depending on the CONTENT of the error in the verbose windows update / wusa.exe log. The action in this case is simply to give a more meaningful error - but if the error is recoverable, you can use the same code block to take action.

$ErrorActionPreference = 'Stop'$LogRoot = "$env:PUBLIC\Logs"$SerializedStringForThisRun = $(Get-date -format 'yyyyMMddhhmmss')$LogFolder = "$LogRoot\Install PSH5-$SerializedStringForThisRun"
New-Item -ItemType Directory $LogFolder -Force -ErrorAction SilentlyContinue$logfilename = "$logfolder\PowerShell-Install-W2K12-KB3191565-x64-$SerializedStringForThisRun-Log.evtx"

$WarningMsgs = 3$ErrorMsgs = 2

$wusaswitches = "/quiet /norestart /log:"$logfilename""

Write-Host "Running Windows Update with the following command which generate a verbose log at: $logfilename" ##Logging the fact that a verbose log is available for the sub-process we are about to call Write-Host "Initiating command: wusa.exe W2K12-KB3191565-x64.msu$wusaswitches"

Try {
$ResultObject = start-process "wusa.exe" -ArgumentList "$pwd\W2K12-KB3191565-x64.msu $wusaswitches" -Wait -Passthru Write-Output "Return code is:$($ResultObject.ExitCode)" If (Test-Path "$logfilename")
{
$LogMessagesOfConcern = @(Get-WinEvent -Path "$logfilename" -oldest | where {($_.level -ge$ErrorMsgs) -AND ($_.level -le$WarningMsgs)})
If ($LogMessagesOfConcern.count -gt 0) { Write-Host "Found the following error(s) and warnings in the MSU log "$logfilename""
$LogMessagesOfConcern | Format-List ID, Message | out-string | write-host # Check a given substring against all messages to find and handle a known exception If ([bool]($LogMessagesOfConcern | where {$_.message -ilike "*error 2147943458*"})) { Write-warning "Service could not be started, attempting to fix this situation." If ((get-service wuauserv).Starttype -ieq 'Disabled') { Write-warning "The Windows Update Server was disabled, renenabling..." Set-Service wuauserv -StartupType 'Manual' #Place code to retry install here. } } If ([bool]($LogMessagesOfConcern | where {$_.message -ilike "*error 2149842967*"})) { Write-warning "The update is not applicable to this computer - possibilities reasons include: a prerequisite is missing or the update is not for this operating system." } } } } catch { Throw$_.Exception
}


# Dissecting Windows Update Packages

This is most frequently done to attempt to understand the prerequisites or OS targeting of a given update in order to understand why it gets the error 0x80240017 / 2149842967 / -2145124329.

New-Item $env:PUBLIC\Logs\ApplyPSH5\updatefiles -ItemType Directory expand W2K12-KB3191565-x64.msu -F:*$env:PUBLIC\Logs\ApplyPSH5\updatefiles
Get-Content \$env:PUBLIC\Logs\ApplyPSH5\updatefiles\*.txt


# This Code In Production

This code is used in the PowerShell for Windows Chocolatey package. It is used to surface better details about why the ugprade failed.

This package has been installed over 1.1 million times - so exposing the underlying errors for others to resolve on their own guards my time and keeps me a lot more productive ;)

Chocolate Package for PowerShell for Windows (see the ‘catch’ statement near the bottom of this script): https://github.com/DarwinJS/ChocoPackages/blob/master/PowerShell/v5.1/tools/ChocolateyInstall.ps1

The below code’s primary home is on the following repository - where it might be improved upon compared to the below. It is also safer to use the code from the repo rather than copy and paste from this post: https://github.com/DarwinJS/CloudyWindowsAutomationCode

0x80070003 / 2147942403 - The system cannot find the file specified. => The .MSU file is not available at the location passed to wusa.exe. Also occurs in other circumstances. Windows Update Troubleshooter

0x800705B4 / 2147943860 => Timeout Period Expired. Troubleshooting Guide, Windows Update Troubleshooter

0x8024200B / 2149851147 => A hardware update (driver) was not able to be installed. Windows Update Troubleshooter

0x80070020 / 2147942432 = The process cannot access the file because it is being used by another process => Windows update is unable to update specific files due to locking or antivirus. Troubleshooting Guide, Windows Update Troubleshooter

0x80073712 / 2147956498 - The Windows Update component manifest is corrupted. Troubleshooting Guide, Windows Update Troubleshooter

0x80004005 / 2147500037 - Corrupt or missing files in the OS.Reseting Windows Update Components, Windows Update Troubleshooter

0x8024402F / 2149859375 - windows update is having trouble contacting Microsoft servers - could be network settings.

0x80070643 / 2147944003 - problems installing .NET framework update. Troubleshooting Guide Windows Update Troubleshooter

# Comprehensive List of Windows Update Errors

Comprehensive Windows Update Error List