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

Search for a command to run...

No comments yet. Be the first to comment.
Why Least Privilege Security Engineering Is Frequently Skipped or Done Loosely In a previous life, I was on a team that reviewed the IAM policies specified by developers when they created new Cloud applications or required additional permissions for ...

Obscuring sensitive information like AWS account IDs in screenshots and videos is tedious and error-prone. Even with video editing tools that simplify the process, I still have to repeatedly add and remove blurring boxes as the view changes. You know...

AWS CloudShell joins the ranks of hostless shells for operating in your cloud environment. Cloud shells are a huge help to training and enablement scenarios because they remove the pain of fussy configuration of a user-owned endpoint - which can have...

This article is the third and final of a series. Part 1 justified that human-performed DevOps checklists are essentially source code, and according to GitOps principles, belong in Git just like any other code required for successfully managing a soft...

There are always those who feel checklists are an unnecessary waste of time because they think they can always remember the basics of the steps involved to complete a task. Many are also not aware of the huge, cross-discipline benefits that can come ...

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.
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.
There are several attributes of wusa.exe execution that make attention to logging all the more valuable:
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.
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:
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.
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"
The command line parameter is fussy. Unlike the other parameters for wusa.exe it:
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`""
wusa.exe W2K12-KB3191565-x64.msu $wusaswitches
In addition to being a big advocate of logging of called processes, I am a big advocate of verbose logging for my own automation. It is simply mind boggling how many times I find this truncates the root cause determination phase. I will receive a log and immediately notice things like:
This makes me more productive :)
However, another big reason I want to log the fact that I am doing detailed logging on a sub-process is for other professionals to take notice, examine that log and frequently solve the problem on their own - which makes both of us more productive.
Whenever possible I log to system folder (non-user profile) dedicated to the automation task that is running and use a date time serialized string in the name, among other things this helps with:
A distinct downside to a custom folder rather than a temp folder is that that you usually have to check for the folder and create it before you start logging to it. A downside to serializing log folder names in and file names in a custom folder that logs can accumulate indefinitely.
$env:public is an environment variable that points to the public profile - which all users have write access to. Although this folder is under the user profile root on many systems, it is not a user folder - there is no “public” user. It is used for anything that all users should see and have access to. A subfolder of the $env:public folder can be a great standardized log location, for example $env:public\logs
As per the [Testable Reference Pattern Manifesto] (https://MissionImpossibleCode.io/post/back-to-basics-testable-reference-pattern-manifesto-with-testable-sample-code/) 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: [MissionImpossibleCode] (https://gitlab.com/missionimpossiblecode/MissionImpossibleCode)
$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
$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
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.
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
}
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)
}
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
}
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
}
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 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://gitlab.com/missionimpossiblecode/MissionImpossibleCode
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 Windows Update Error List