用 PowerShell 診斷卡住的 Windows Update:先唯讀檢查服務、待重新啟動狀態、更新快取及事件記錄錯誤
Diagnose a stuck Windows Update with PowerShell: services, pending restarts, update cache and event-log errors, read-only first
文章提供一套以唯讀檢查為先的 PowerShell 方法,診斷卡住的 Windows Update,檢查服務、待重新啟動訊號、更新快取、可用磁碟空間及近期錯誤。需要修復時,可使用要求確認並支援 -WhatIf 的快取重設;程式碼尚未在實際 Windows 或 Windows PowerShell 5.1 上執行測試。
AI disclosure: this post was written with AI assistance. The code was syntax-checked and linted, and its logic was tested with simulated services, registry values and event-log entries, but it has not yet been run on Windows: not on a real Windows 11 PC, and not on Windows PowerShell 5.1. Read it before you run it, and try the reset with
-WhatIffirst.
When an update sits at 0% for hours or fails with the same code every night, Microsoft's troubleshooting page has a long list of things to try. Most are quick, but it helps to know which one applies before you start renaming system folders. This post builds a small read-only health check in PowerShell that answers five questions:
- Are the update services able to run?
- Is a restart already pending?
- How big is the update cache?
- Is there enough free space?
- What errors has Windows Update logged recently, and with which codes?
Then, only if you need it, there's one repair: the cache reset from Microsoft's manual steps, wrapped so it asks first and supports -WhatIf.
Before any of it, try the built-in option: Settings > System > Troubleshoot > Other troubleshooters, then Run next to Windows Update.
Step 1: the services
Microsoft's manual reset steps stop and start two services, bits and wuauserv (Windows Update). Its broader reset guidance also involves cryptsvc. Get-Service gives you each one's Status and StartType:
function Get-UpdateServiceState {
[CmdletBinding()]
param([string[]] $Name = @('bits', 'wuauserv', 'cryptsvc'))
foreach ($n in $Name) {
$svc = Get-Service -Name $n -ErrorAction SilentlyContinue
if (-not $svc) {
[pscustomobject]@{ Name = $n; Status = 'NotFound'; StartType = $null; Note = 'Service not found.' }
continue
}
$note = if ("$($svc.StartType)" -eq 'Disabled') {
'Disabled: it cannot start. Microsoft lists error 0x80070422 for a disabled or stopped Windows Update service (on its Windows 10 troubleshooting tab).'
}
elseif ("$($svc.Status)" -eq 'Stopped') {
'Stopped. Not a problem by itself if the start type lets it start when needed.'
}
else { 'OK' }
[pscustomobject]@{ Name = $svc.Name; Status = "$($svc.Status)"; StartType = "$($svc.StartType)"; Note = $note }
}
}
Get-UpdateServiceState | Format-Table -AutoSize
The important distinction is Stopped versus Disabled. Set-Service's documentation describes a Manual service as one that's started by a user, the Service Control Manager or an application when needed, so seeing it stopped is normal. A Disabled service can't be started at all. For 0x80070422, Microsoft's fix is to open services.msc, set the Windows Update service's Startup type to Automatic, start it if it's stopped, then restart and check for updates again.
Step 2: is a restart pending?
There's no single "restart pending" flag, so check several signals and report each one separately:
function Get-WuaRebootRequired {
[CmdletBinding()]
[OutputType([bool])]
param()
try { [bool](New-Object -ComObject Microsoft.Update.SystemInfo).RebootRequired }
catch { $null }
}
function Test-PendingReboot {
[CmdletBinding()]
param()
$sm = Get-ItemProperty -LiteralPath 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager' `
-Name 'PendingFileRenameOperations' -ErrorAction SilentlyContinue
$signals = [ordered]@{
WindowsUpdateAgent = Get-WuaRebootRequired
ComponentBasedServicing = Test-Path -LiteralPath 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending'
WindowsUpdateAutoUpdate = Test-Path -LiteralPath 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired'
PendingFileRenameOperations = [bool]($sm -and $sm.PendingFileRenameOperations)
}
$out = [ordered]@{ RebootPending = @($signals.Values | Where-Object { $_ -eq $true }).Count -gt 0 }
foreach ($k in $signals.Keys) { $out[$k] = $signals[$k] }
[pscustomobject]$out
}
Test-PendingReboot
Where each signal comes from, and how much to trust it:
-
WindowsUpdateAgentis the Windows Update Agent's ownISystemInformation.RebootRequired. Microsoft documents it as "a restart is required to complete the installation or uninstallation of one or more updates". This is the most direct answer. -
PendingFileRenameOperationsis whereMoveFileExrecords files to be replaced at the next restart. Any installer can use it, not just Windows Update, soTruehere means "something wants a restart". - The two registry keys come from a Microsoft Scripting Blog post on detecting pending restarts, not from Microsoft's reference docs. They're useful, but treat them as hints.
If any of them is True, restart first and check for updates again before you try anything else. Microsoft's troubleshooting page has restarting as its own step, because many updates need one to finish.
Step 3: the update cache and free space
%SystemRoot%\SoftwareDistribution holds Windows Update's working files, and Download inside it holds what has been downloaded. The size alone doesn't tell you it's broken, but it's useful context before a reset, and it shows any .bak folders left over from earlier resets. The full script measures it with the same link-safe folder walk as the disk-space-audit repo.
Free space matters more. Microsoft says a device needs at least 16 GB free to upgrade a 32-bit OS, or 20 GB for a 64-bit OS, and lists 0x800f0922 as "unable to install due to insufficient disk space" (the Windows 11 table adds .NET Framework or other component issues):
function Get-SystemDriveSpace {
[CmdletBinding()]
param([string] $SystemDrive = $env:SystemDrive, [int] $RecommendedFreeGB = 20)
$d = Get-CimInstance -ClassName Win32_LogicalDisk -Filter "DeviceID = '$SystemDrive'" -ErrorAction Stop
$free = [math]::Round($d.FreeSpace / 1GB, 1)
[pscustomobject]@{ Drive = $SystemDrive; FreeGB = $free; Enough = $free -ge $RecommendedFreeGB }
}
Step 4: recent Windows Update errors
Microsoft's guidance is to look for Windows Update Agent events in the System log (Event Viewer also has a separate WindowsUpdateClient > Operational log). They come from the Microsoft-Windows-WindowsUpdateClient source, and its example of a failed install is an Error-level event with ID 20 and the error code in the message. Get-WinEvent -FilterHashtable filters on the log, the provider and the level (2 = Error) in one query:
function Get-RecentUpdateError {
[CmdletBinding()]
param([int] $Days = 30, [int] $MaxEvents = 20)
$filter = @{
LogName = 'System'
ProviderName = 'Microsoft-Windows-WindowsUpdateClient'
Level = 2
StartTime = (Get-Date).AddDays(-$Days)
}
try {
$events = Get-WinEvent -FilterHashtable $filter -MaxEvents $MaxEvents -ErrorAction Stop
}
catch {
if ($_.FullyQualifiedErrorId -like 'NoMatchingEventsFound*' -or $_.Exception.Message -like '*No events were found*') { return }
Write-Warning "Could not read the System event log: $($_.Exception.Message)"
return
}
foreach ($e in $events) {
$code = if ($e.Message -match '0x[0-9A-Fa-f]{8}') { $Matches[0] } else { $null }
[pscustomobject]@{ TimeCreated = $e.TimeCreated; Id = $e.Id; ErrorCode = $code; Message = $e.Message }
}
}
Get-RecentUpdateError -Days 30 | Format-List TimeCreated, Id, ErrorCode, Message
Two practical notes:
-
Get-WinEventreports "no events found" as an error. That's good news, not a failure, so the function returns nothing instead of a wall of red. - Pulling out the
0x...code lets you look it up straight away in Microsoft's error tables. Two from Microsoft's table:0x80070422(Windows Update service is disabled or stopped) and0x800f0922(unable to install due to insufficient disk space).
For more detail, Get-WindowsUpdateLog merges Windows Update's trace files into a readable WindowsUpdate.log. It's a static copy, so run it again for newer entries. Servicing details are in %systemroot%\Logs\CBS\CBS.log.
Step 5 (opt-in): reset the update cache
If the services are fine, you've restarted, there's enough space and updates still fail, Microsoft's manual steps are to stop bits and wuauserv, rename %systemroot%\SoftwareDistribution to SoftwareDistribution.bak, start the services again, and restart. Here's that sequence as a function that asks first:
function Reset-UpdateCache {
[CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')]
param([string] $WindowsPath = $env:SystemRoot)
$sd = Join-Path -Path $WindowsPath -ChildPath 'SoftwareDistribution'
if (-not (Test-Path -LiteralPath $sd -PathType Container)) { throw "Folder not found: $sd" }
$newName = 'SoftwareDistribution.bak-{0}' -f (Get-Date -Format 'yyyyMMdd-HHmmss')
if (-not $PSCmdlet.ShouldProcess($sd, "Stop bits and wuauserv, rename to $newName, start them again")) { return }
$id = [Security.Principal.WindowsIdentity]::GetCurrent()
if (-not ([Security.Principal.WindowsPrincipal]$id).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
throw 'Run PowerShell as administrator to use Reset-UpdateCache.'
}
try {
foreach ($s in 'wuauserv', 'bits') { Stop-Service -Name $s -ErrorAction Stop -Confirm:$false }
Rename-Item -LiteralPath $sd -NewName $newName -ErrorAction Stop -Confirm:$false
}
finally {
foreach ($s in 'bits', 'wuauserv') {
try { Start-Service -Name $s -ErrorAction Stop -Confirm:$false }
catch { Write-Warning "Could not start ${s}: $($_.Exception.Message)" }
}
}
[pscustomobject]@{ RenamedTo = Join-Path -Path $WindowsPath -ChildPath $newName }
}
What makes it safe to run:
- Rename, not delete. The old cache stays on disk until you remove it, so you can put it back.
-
ConfirmImpact = 'High'means it asks before doing anything, even without-Confirm.-WhatIfprints what it would do and touches nothing. -
-ErrorAction Stopplusfinally. If a service won't stop, the rename never happens. Whether the rename works or fails, the services are started again. -
A timestamp in the backup name, so a second reset doesn't collide with an older
.bak. -
No
-ForceonStop-Service, so it won't quietly stop other services that depend on these two.
In an elevated PowerShell window:
Reset-UpdateCache -WhatIf
Reset-UpdateCache
Then restart and go to Settings > Windows Update > Check for updates.
To undo, stop the two services, move the new folder aside, rename your backup back to SoftwareDistribution, and start them again:
Stop-Service -Name wuauserv, bits
Rename-Item -LiteralPath "$env:SystemRoot\SoftwareDistribution" -NewName 'SoftwareDistribution.new'
Rename-Item -LiteralPath "$env:SystemRoot\SoftwareDistribution.bak-20261030-101500" -NewName 'SoftwareDistribution'
Start-Service -Name bits, wuauserv
Use the folder name the function printed. Once updates work again, you can delete the .bak-... folder to get the space back.
If that doesn't fix it
Microsoft's next step for corrupted system files is to repair the image and then check the system files, in this order, in an elevated window:
DISM /Online /Cleanup-Image /RestoreHealth
sfc /scannow
Microsoft's manual steps also rename catroot2. This script leaves it alone on purpose to keep the change small. If you need it, follow the steps on Microsoft's page directly.
Limits, honestly
- The code was tested with PowerShell 7 on Linux, with every Windows command (services, registry, event log, the Windows Update COM object) replaced by fakes. PSScriptAnalyzer reported nothing, and Pester tests of each check, the
-WhatIfpath, the failure paths and the admin check passed. It has not been run on a real Windows PC or on Windows PowerShell 5.1. Try the reset with-WhatIffirst. - The two registry restart keys come from a blog post, not reference docs, and
PendingFileRenameOperationsisn't specific to Windows Update. - Event messages are localized. The
0x...code extraction doesn't depend on language, but the "no events found" text check is a fallback for the error ID. - The full script, with its Pester tests, is at https://github.com/fecklesspleb/windows-update-health (MIT).
Sources (Microsoft)
- Troubleshoot problems updating Windows (troubleshooter, restart, free space, error table, manual reset commands, DISM then SFC): https://support.microsoft.com/en-us/windows/deployment/updates-lifecycle/troubleshoot-problems-updating-windows
- Additional resources for Windows Update (manual reset steps): https://learn.microsoft.com/en-us/troubleshoot/windows-client/installing-updates-features-roles/additional-resources-for-windows-update
- Common Windows Update errors: https://learn.microsoft.com/en-us/troubleshoot/windows-client/installing-updates-features-roles/common-windows-update-errors
- Windows Update troubleshooting guidance (Windows Update Agent events in the System log): https://learn.microsoft.com/en-us/troubleshoot/windows-server/installing-updates-features-roles/troubleshoot-windows-server-update-guidance
- Windows Update error 0x80070002 (example System log event from Microsoft-Windows-WindowsUpdateClient): https://learn.microsoft.com/en-us/troubleshoot/windows-server/installing-updates-features-roles/troubleshoot-windows-update-error-0x80070002
- ISystemInformation interface and RebootRequired: https://learn.microsoft.com/en-us/windows/win32/api/wuapi/nn-wuapi-isysteminformation and https://learn.microsoft.com/en-us/windows/win32/api/wuapi/nf-wuapi-isysteminformation-get_rebootrequired
- MoveFileEx (
PendingFileRenameOperations): https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-movefileexa - Determine pending reboot status, PowerShell style (Microsoft Scripting Blog): https://devblogs.microsoft.com/scripting/determine-pending-reboot-statuspowershell-style-part-1/
- Windows Update log files: https://learn.microsoft.com/en-us/windows/deployment/update/windows-update-logs
- Creating Get-WinEvent queries with FilterHashtable: https://learn.microsoft.com/en-us/powershell/scripting/samples/creating-get-winevent-queries-with-filterhashtable
- Get-Service: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.management/get-service?view=powershell-7.5
- Set-Service (startup types): https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.management/set-service
- Win32_LogicalDisk class: https://learn.microsoft.com/en-us/windows/win32/cimwin32prov/win32-logicaldisk
- Everything you wanted to know about ShouldProcess: https://learn.microsoft.com/en-us/powershell/scripting/learn/deep-dives/everything-about-shouldprocess
This article was created with the help of AI. Not affiliated with or endorsed by Microsoft.
If you want the rest of the fixes (slow startup, full drive, stuck updates) in one place, I sell a 40-page guide with the scripts: Windows 11 Fix-It Guide + PowerShell Toolkit, $19.
來源:DEV Community · dev.to