最近把公司一批 Windows 11 From 22H2 用 ISO 就地升級到 26H2,升級本身都很順利,但有幾台升級完後,使用者回報記事本、小畫家、計算機、螢幕擷取工具都打不開,部分電腦連 App 都無法安裝或移除。
不是每一台都會發生,網路上也有人在 24H2、25H2 更新后遇到类似情况。这次一样是搭配 Claude Code 分析事件记录,最后发现问题出在 Windows 登录时的‘应用程序整备’卡住,把过程简单记录一下。
【症状】
- 升级方式:Windows 11 22H2 → 26H2,ISO 就地升级(保留文件与应用程序)
- 无法打开:記事本、小畫家、計算機、屏幕截图工具(Microsoft Store 也会转圈)
- 事件记录:TWinUI 5961,启用阶段
Deployment超时 - 手动
Add-AppxPackage -Register也会卡住,最后出现 Failed to reach state DeQueued
【原因:AppReadiness 卡在移除旧套件】
升级后用户第一次登录时,Windows 会在后台执行 AppReadiness(应用程序整备)of ART:UserLogon 工作,先把 22H2 留在用户配置文件里的旧版 App 移除,再注册新版 App。这个工作只要卡住,后面所有 App 的注册都会一直排队,App 自然就打不开。
這次有問題的電腦,卡的位置都很一致:
- 先卡在移除 Microsoft.XboxGameCallableUI,一卡就是幾十分鐘到幾小時
- 重開機後這個過了,接著出現找不到 WindowsAppRuntime 的错误
STUCK uninstall Microsoft.XboxGameCallableUI_cw5n1h2txyewy
FAIL uninstall Microsoft.WindowsAppRuntime.1.4_8wekyb3d8bbwe
【解法:多重開幾次】
我們升級完剛好 WSUS 也推了 26H2 的累積更新及 .NET 累積更新,但從記錄來看,真正有效的應該是多次重開機:每次開機登入,AppReadiness 會重跑一次,上次卡住的套件通常就過了,再往下處理下一個。目前大多數電腦重開約 3 次就恢復正常。
理論上「登出再登入」也會重跑這個工作,或許也能解決,不過我還沒實測過。另外要注意,Windows 預設開啟「快速啟動」,按「關機」不等於完整重開,要選「重新啟動」。
【快速查看 AppReadiness 狀態】
為了知道每次重開到底卡在哪、解決了哪個,寫了一支唯讀的 PowerShell,會依每次開機列出 AppReadiness 的處理結果,用來判斷還需不需要再重開。存成 Show-AppReadinessLog.ps1:
# =======================================================================
# Show-AppReadinessLog.ps1
# Read-only report of the per-user "App Readiness" logon tasks (ART:UserLogon /
# ART:UserFirstLogon) grouped by boot, so you can see on each restart:
# - which package removals/installs finished (OK),
# - which one was stuck / aborted (FAIL with long duration),
# - what was still pending when the task was cancelled,
# - which previously stuck package got resolved in a later boot.
# Also lists AppX deployment operations that started but never finished in the
# latest boot (best effort, correlated by ActivityId).
#
# Usage on the affected PC:
# Right-click the file -> "Run with PowerShell". It asks for admin rights (UAC) if
# needed, shows the report and waits for Enter before closing.
# Command line:
# powershell -ExecutionPolicy Bypass -File .\Show-AppReadinessLog.ps1 -Days 5
# add -ShowAll to also list every package operation that finished OK
# save to a file (no pause):
# powershell -ExecutionPolicy Bypass -File .\Show-AppReadinessLog.ps1 -NoPause *> $env:USERPROFILE\Desktop\AppReadiness.txt
# Offline (folder containing copied .evtx files: System, AppReadiness Admin/Operational,
# AppXDeploymentServer Operational):
# .\Show-AppReadinessLog.ps1 -LogFolder D:\evidence\PC01
#
# This script only reads event logs. It changes nothing.
# =======================================================================
param(
[int]$Days = 3,
[string]$LogFolder = '',
[int]$StuckSeconds = 300,
[switch]$ShowAll,
[switch]$NoPause
)
# Keep the window open at the end (for right-click "Run with PowerShell").
function Wait-Close {
if (-not $NoPause) { Write-Host ''; Read-Host 'Done. Press Enter to close' | Out-Null }
}
trap { Write-Host "ERROR: $_" -ForegroundColor Red; Wait-Close; break }
# Event logs need admin rights: relaunch elevated if needed (live mode only).
$isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
if (-not $isAdmin -and -not $LogFolder) {
Write-Host 'Not running as administrator. Relaunching elevated (please accept the UAC prompt)...'
$relaunch = @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', "`"$PSCommandPath`"", '-Days', $Days, '-StuckSeconds', $StuckSeconds)
if ($ShowAll) { $relaunch += '-ShowAll' }
try { Start-Process powershell.exe -Verb RunAs -ArgumentList $relaunch -ErrorAction Stop }
catch { Write-Host "Elevation cancelled or failed: $($_.Exception.Message)" -ForegroundColor Red; Wait-Close }
return
}
# Wider console so long lines do not wrap (ignored where not supported).
try { $raw = $Host.UI.RawUI; if ($raw.BufferSize.Width -lt 220) { $raw.BufferSize = New-Object Management.Automation.Host.Size(220, [Math]::Max($raw.BufferSize.Height, 3000)) } } catch { }
$start = (Get-Date).AddDays(-$Days)
function Get-Ev([string]$LogName, [string]$File, [hashtable]$Extra) {
$f = @{ StartTime = $start }
if ($Extra) { foreach ($k in $Extra.Keys) { $f[$k] = $Extra[$k] } }
if ($LogFolder) { $f['Path'] = (Join-Path $LogFolder $File) } else { $f['LogName'] = $LogName }
try { Get-WinEvent -FilterHashtable $f -ErrorAction Stop } catch { @() }
}
function Get-UserName([string]$Sid) {
try { ([System.Security.Principal.SecurityIdentifier]$Sid).Translate([System.Security.Principal.NTAccount]).Value } catch { $Sid }
}
# Quoted values in the message, excluding the user SID (word order differs by OS language).
function Get-Quoted([string]$Msg) {
$r = @(); foreach ($m in [regex]::Matches($Msg, "'([^']*)'")) { $v = $m.Groups[1].Value; if ($v -notmatch '^S-1-5-') { $r += $v } }; return ,$r
}
# "remove::X" / "removePackage:X" -> "remove X" so duplicates collapse.
function Get-Item2([string]$Raw) {
$m = [regex]::Match($Raw, '^(install|remove|register|update|uninstall)[A-Za-z]*:{1,2}(.+)$')
if ($m.Success) { '{0} {1}' -f $m.Groups[1].Value, $m.Groups[2].Value } else { $Raw }
}
function Get-Secs([string]$Msg) {
$m = [regex]::Match($Msg, '\(([\d\.]+)\s*[^\)]*\)\s*$')
if ($m.Success) { [double]$m.Groups[1].Value } else { $null }
}
function Get-Op([string]$Msg, [string]$Pkg) {
$m = [regex]::Match($Msg, [regex]::Escape("'$Pkg'") + '\s*([A-Za-z]+)')
if ($m.Success) { $m.Groups[1].Value } else { '' }
}
# ---- boots -------------------------------------------------------------
$boots = @(Get-Ev 'System' 'System.evtx' @{ ProviderName = 'Microsoft-Windows-Kernel-General'; Id = 12 } | Sort-Object TimeCreated)
$restarts = @(Get-Ev 'System' 'System.evtx' @{ ProviderName = 'User32'; Id = 1074 })
$powerLoss = @(Get-Ev 'System' 'System.evtx' @{ ProviderName = 'Microsoft-Windows-Kernel-Power'; Id = 41 })
if ($boots.Count -eq 0) { Write-Host "No boot events found in the last $Days days."; Wait-Close; return }
# ---- App Readiness events ---------------------------------------------
$ids = 213,214,218,220,225,240,313
$ar = @()
$ar += Get-Ev 'Microsoft-Windows-AppReadiness/Admin' 'Microsoft-Windows-AppReadiness%4Admin.evtx' @{ Id = $ids }
$ar += Get-Ev 'Microsoft-Windows-AppReadiness/Operational' 'Microsoft-Windows-AppReadiness%4Operational.evtx' @{ Id = $ids }
$seen = @{}
$ar = @($ar | Sort-Object TimeCreated, RecordId | Where-Object {
$k = '{0:o}|{1}|{2}' -f $_.TimeCreated, $_.Id, $_.Message
if ($seen.ContainsKey($k)) { $false } else { $seen[$k] = 1; $true }
})
$stuckEver = @{} # pkg -> "boot n"
$resolved = @()
for ($i = 0; $i -lt $boots.Count; $i++) {
$bStart = $boots[$i].TimeCreated
$bEnd = if ($i + 1 -lt $boots.Count) { $boots[$i + 1].TimeCreated } else { Get-Date }
$isLast = ($i + 1 -ge $boots.Count)
Write-Host ''
Write-Host ('=' * 90)
Write-Host ("BOOT #{0} started {1:yyyy-MM-dd HH:mm:ss} -> {2}" -f ($i + 1), $bStart, $(if ($isLast) { 'now (current boot)' } else { '{0:yyyy-MM-dd HH:mm:ss}' -f $bEnd }))
# how this boot ended
foreach ($r in ($restarts | Where-Object { $_.TimeCreated -ge $bStart -and $_.TimeCreated -lt $bEnd } | Sort-Object TimeCreated)) {
$p = $r.Properties
$proc = Split-Path -Leaf ([string]$p[0].Value)
$usr = if ($p.Count -ge 7) { [string]$p[6].Value } else { '' }
Write-Host (" Restart/shutdown requested {0:HH:mm:ss} by {1} ({2}) type: {3}" -f $r.TimeCreated, $proc, $usr, [string]$p[4].Value)
}
if (-not $isLast) {
foreach ($pl in ($powerLoss | Where-Object { $_.TimeCreated -ge $bEnd -and $_.TimeCreated -lt $bEnd.AddMinutes(5) })) {
$x = [xml]$pl.ToXml(); $d = @{}; foreach ($n in $x.Event.EventData.Data) { $d[$n.Name] = $n.'#text' }
$how = if ($d['LongPowerButtonPressDetected'] -eq 'true') { 'FORCED POWER OFF (long power-button press)' } else { 'unexpected power loss / hang' }
$pt = ''
if ($d['PowerButtonTimestamp'] -and $d['PowerButtonTimestamp'] -ne '0') { $pt = ' at {0:HH:mm:ss}' -f [datetime]::FromFileTime([int64]$d['PowerButtonTimestamp']) }
Write-Host (" !! This boot ended by {0}{1}" -f $how, $pt)
}
}
$evs = @($ar | Where-Object { $_.TimeCreated -ge $bStart -and $_.TimeCreated -lt $bEnd })
$sids = @($evs | ForEach-Object { $m = [regex]::Match($_.Message, 'S-1-5-21-[\d-]+'); if ($m.Success) { $m.Value } } | Select-Object -Unique)
if ($sids.Count -eq 0) { Write-Host ' (no user App Readiness activity in this boot)'; continue }
foreach ($sid in $sids) {
Write-Host ''
Write-Host (" USER {0}" -f (Get-UserName $sid))
$running = $false; $lastAct = $null; $pending = @(); $okCount = 0
foreach ($e in ($evs | Where-Object { $_.Message -match [regex]::Escape($sid) })) {
$q = Get-Quoted $e.Message
$t = '{0:HH:mm:ss}' -f $e.TimeCreated
switch ($e.Id) {
240 { if ($q.Count -ge 1 -and $q[0] -match '^ART:User') { Write-Host (" START {0} {1}" -f $t, $q[0]); $running = $true; $pending = @() } }
213 {
$pkg = if ($q.Count -ge 1) { $q[0] } else { '?' }; $s = Get-Secs $e.Message; $op = Get-Op $e.Message $pkg
$okCount++; $lastAct = $e.TimeCreated
$tag = if ($s -ne $null -and $s -ge $StuckSeconds) { 'OK-SLOW' } else { 'OK' }
if ($ShowAll -or $tag -eq 'OK-SLOW' -or $stuckEver.ContainsKey($pkg)) { Write-Host (" {0,-7}{1} {2,-9} {3} ({4:N1}s)" -f $tag, $t, $op, $pkg, $s) }
if ($stuckEver.ContainsKey($pkg)) { $resolved += ("{0} stuck in {1}, resolved in BOOT #{2} at {3}" -f $pkg, $stuckEver[$pkg], ($i + 1), $t); $stuckEver.Remove($pkg) }
}
214 {
$pkg = if ($q.Count -ge 1) { $q[0] } else { '?' }; $err = if ($q.Count -ge 2) { $q[1] } else { '' }
$s = Get-Secs $e.Message; $op = Get-Op $e.Message $pkg; $lastAct = $e.TimeCreated
$tag = if ($s -ne $null -and $s -ge $StuckSeconds) { 'STUCK' } else { 'FAIL' }
Write-Host (" {0,-7}{1} {2,-9} {3} ({4:N1}s) error: {5}" -f $tag, $t, $op, $pkg, $s, $err)
if ($tag -eq 'STUCK') { $stuckEver[$pkg] = "BOOT #$($i + 1)" }
}
225 {
$task = @($q | Where-Object { $_ -match '^ART:' })
if ($task.Count -gt 0) { Write-Host (" CANCEL {0} {1} cancelled (shutdown / sign-out)" -f $t, $task[0]); $running = $false }
}
313 { $it = @($q | Where-Object { $_ -notmatch '^ART:' }); if ($it.Count -gt 0) { $pending += (Get-Item2 $it[0]) } }
220 {
if ($q.Count -ge 1 -and $q[0] -match '^ART:User') {
Write-Host (" DONE {0} {1} finished OK ({2:N1}s)" -f $t, $q[0], (Get-Secs $e.Message)); $running = $false
}
}
}
}
if ($ShowAll) { Write-Host (" ({0} package operations finished OK in this boot)" -f $okCount) }
else { Write-Host (" ({0} package operations finished OK in this boot; fast OK lines hidden, use -ShowAll)" -f $okCount) }
if ($pending.Count -gt 0) {
Write-Host ' Still pending when cancelled:'
$pending | Select-Object -Unique | ForEach-Object { Write-Host (" - {0}" -f $_) }
}
if ($running) {
$since = if ($lastAct) { $lastAct } else { $bStart }
$mins = [int](($bEnd - $since).TotalMinutes)
Write-Host (" RUNNING / NO END in this boot. Last finished item at {0:HH:mm:ss} ({1} min before boot end/now)." -f $since, $mins)
Write-Host ' -> The item right after the last finished one is most likely the stuck one (see AppX section below for the current boot).'
}
}
}
Write-Host ''
Write-Host ('=' * 90)
Write-Host 'PREVIOUSLY STUCK PACKAGES THAT LATER SUCCEEDED'
if ($resolved.Count -gt 0) { $resolved | ForEach-Object { Write-Host " $_" } } else { Write-Host ' (none)' }
Write-Host 'STUCK PACKAGES NOT YET SEEN SUCCEEDING'
if ($stuckEver.Count -gt 0) { $stuckEver.GetEnumerator() | ForEach-Object { Write-Host (" {0} (stuck in {1})" -f $_.Key, $_.Value) } } else { Write-Host ' (none)' }
# ---- AppX operations started but not finished in the latest boot ---------
Write-Host ''
Write-Host ('=' * 90)
$lb = $boots[-1].TimeCreated
Write-Host ("APPX OPERATIONS STARTED BUT NOT FINISHED SINCE LATEST BOOT ({0:yyyy-MM-dd HH:mm:ss}) - best effort" -f $lb)
$ax = @(Get-Ev 'Microsoft-Windows-AppXDeploymentServer/Operational' 'Microsoft-Windows-AppXDeploymentServer%4Operational.evtx' @{ Id = 603,613,400,401,404 } | Where-Object { $_.TimeCreated -ge $lb })
$ended = @{}; foreach ($e in ($ax | Where-Object { $_.Id -ne 603 -and $_.ActivityId })) { $ended[[string]$e.ActivityId] = 1 }
# Only package Add/Register/Remove/Stage/Update style operations; helper operations
# such as SetDefaultVolume / StageInContainer never log an end event and are skipped.
$open = @($ax | Where-Object {
$_.Id -eq 603 -and $_.ActivityId -and -not $ended.ContainsKey([string]$_.ActivityId) -and
$_.Message -match '\b(Add|Register|Remove|Stage|Update|Deprovision)' -and
$_.Message -notmatch 'SetDefaultVolume|StageInContainer|InContainer'
} | Sort-Object TimeCreated)
if ($open.Count -eq 0) { Write-Host ' (none)' } else {
foreach ($e in $open) {
$m = ($e.Message -replace '\s+', ' '); if ($m.Length -gt 200) { $m = $m.Substring(0, 200) }
Write-Host (" {0:HH:mm:ss} {1}" -f $e.TimeCreated, $m)
}
}
Wait-Close
在檔案上按右鍵「以 PowerShell 執行」即可,沒有系統管理員權限時會自動跳 UAC。如果出現「已停用指令碼執行」,改用指令執行:
powershell -ExecutionPolicy Bypass -File .\Show-AppReadinessLog.ps1
結果判讀:
- STUCK:這次開機卡住的套件(處理超過 300 秒後被中止)
- FAIL:失敗但沒有卡住,通常下次會再試
- DONE:該使用者的整理工作已完整跑完,App 通常就恢復了
- RUNNING / NO END:目前還在跑,或正卡著
- 最下方會列出「之前卡住、後來成功」及「卡住後還沒成功過」的套件
簡單說,最後一次開機有看到 DONE 就不用再重開;還是 STUCK 或 RUNNING,就再重開一次。
【结论】
這次問題不是升級失敗,而是升級後第一次登入時 AppReadiness 移除舊版 22H2 套件卡住。遇到內建 App 打不開時,不用急著重設 App 或重裝 Windows App Runtime,先確認更新都裝完,再多重開幾次,搭配上面的腳本看 AppReadiness 有沒有跑完,大部分就能解決。
批次升級的話,也建議升級完先由 IT 重開到更新都裝好、App 能正常開啟,再交給使用者。
《相关链接》
- GitHub:Multiple Microsoft WinUI apps crash with Microsoft.UI.Xaml.dll 3.2.3.0 – 0xC000027B / 0x8000FFFF at Parser::CreateXamlBinaryFileReader(多個內建 App 無法開啟)
- Windows 11 论坛:在 Windows 上使用截图工具遇到烦人的问题 11(截图工具、计算机在 26H2 无法打开的讨论)








留下回复