Files
windows-builder/includes/$OEM$/$$/OEM/scripts/windowsupdate/WindowsUpdateManager-UI.ps1
2026-06-02 03:37:09 -07:00

253 lines
9.8 KiB
PowerShell

# WindowsUpdateManager-UI.ps1
# Runs in the user's interactive session (spawned by WindowsUpdateManager.ps1).
# Connects to the SYSTEM process via named pipe, shows a toast notification,
# and only opens the picker if the user clicks "View Details".
param([string]$PipeName = "WUM_IPC")
Import-Module "$PSScriptRoot\UpdateNotifications.psm1" -Force
$script:DataDir = "$env:ProgramData\WindowsUpdateManager"
$script:LogPath = "$script:DataDir\WindowsUpdateUI.log"
$script:AppId = "oxmc-servers.WindowsUpdateManager"
function Register-ToastAppId {
# HKLM is registered by first-time-setup.ps1 on OEM installs; HKCU is a dev/test fallback
$hklmKey = "HKLM:\SOFTWARE\Classes\AppUserModelId\$script:AppId"
if (Test-Path $hklmKey) { return }
$key = "HKCU:\SOFTWARE\Classes\AppUserModelId\$script:AppId"
$null = New-Item -Path $key -Force
Set-ItemProperty -Path $key -Name "DisplayName" -Value "Windows Update Manager"
Set-ItemProperty -Path $key -Name "ShowInSettings" -Value 1 -Type DWord
$iconPath = Join-Path $PSScriptRoot "wum-notify.png"
if (Test-Path $iconPath) { Set-ItemProperty -Path $key -Name "IconUri" -Value $iconPath }
}
function Get-ToastIconElement {
$iconPath = Join-Path $PSScriptRoot "wum-notify.png"
if (Test-Path $iconPath) {
$uri = "file:///" + $iconPath.Replace("\", "/")
return "<image placement=`"appLogoOverride`" src=`"$uri`" hint-crop=`"circle`"/>"
}
return ""
}
function Write-Log {
param([string]$Message, [string]$Level = "INFO")
$null = New-Item -ItemType Directory -Path $script:DataDir -Force
$line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message"
Write-Host $line
Add-Content -Path $script:LogPath -Value $line -ErrorAction SilentlyContinue
}
function Show-UpdateToast {
param([array]$Updates)
Register-ToastAppId
$count = $Updates.Count
$maxShow = [Math]::Min(3, $count)
$listText = ($Updates[0..($maxShow - 1)] | ForEach-Object { "* $($_.Title)" }) -join "`n"
if ($count -gt 3) { $listText += "`n* And $($count - 3) more..." }
$xml = New-Object Windows.Data.Xml.Dom.XmlDocument
$iconElem = Get-ToastIconElement
$xml.LoadXml(@"
<toast scenario="reminder">
<visual>
<binding template="ToastGeneric">
$iconElem
<text>Windows Updates Available</text>
<text>$count update(s) ready to install</text>
<text>$([System.Security.SecurityElement]::Escape($listText))</text>
</binding>
</visual>
<actions>
<action content="View Details" arguments="wum:viewDetails" activationType="protocol"/>
<action content="Install Now" arguments="wum:installNow" activationType="protocol"/>
<action content="Later" arguments="wum:dismiss" activationType="protocol"/>
</actions>
<audio src="ms-winsoundevent:Notification.Default"/>
</toast>
"@)
return [Windows.UI.Notifications.ToastNotification]::new($xml)
}
function Register-WumProtocol {
param([string]$SignalFile)
# Register wum: protocol in HKCU (user-level, no admin needed)
# When Windows activates the URI, cmd writes the action to the signal file
$base = "HKCU:\SOFTWARE\Classes\wum"
$null = New-Item -Path "$base\shell\open\command" -Force
Set-ItemProperty -Path $base -Name "(Default)" -Value "Windows Update Manager"
Set-ItemProperty -Path $base -Name "URL Protocol" -Value ""
Set-ItemProperty -Path "$base\shell\open\command" -Name "(Default)" `
-Value "cmd.exe /c echo %1 > `"$SignalFile`""
}
function Unregister-WumProtocol {
Remove-Item -Path "HKCU:\SOFTWARE\Classes\wum" -Recurse -Force -ErrorAction SilentlyContinue
}
function Show-ToastAndWait {
param([Windows.UI.Notifications.ToastNotification]$Toast)
$signalFile = "$env:TEMP\wum_action_$PipeName.txt"
Remove-Item $signalFile -Force -ErrorAction SilentlyContinue
Register-WumProtocol -SignalFile $signalFile
[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier($script:AppId).Show($Toast)
Write-Log "Toast shown. Waiting for user action..."
$deadline = [System.DateTime]::Now.AddHours(2)
while (-not (Test-Path $signalFile) -and [System.DateTime]::Now -lt $deadline) {
Start-Sleep -Milliseconds 300
}
$action = ""
if (Test-Path $signalFile) {
$action = (Get-Content $signalFile -Raw -ErrorAction SilentlyContinue).Trim()
Remove-Item $signalFile -Force -ErrorAction SilentlyContinue
}
Unregister-WumProtocol
return $action
}
$pipe = $null
$reader = $null
$writer = $null
try {
Write-Log "Connecting to SYSTEM pipe: $PipeName ..."
$pipe = [System.IO.Pipes.NamedPipeClientStream]::new(
".", $PipeName,
[System.IO.Pipes.PipeDirection]::InOut,
[System.IO.Pipes.PipeOptions]::None)
$pipe.Connect(15000)
Write-Log "Connected."
$enc = [System.Text.UTF8Encoding]::new($false)
$reader = [System.IO.StreamReader]::new($pipe, $enc, $false, 65536, $true)
$writer = [System.IO.StreamWriter]::new($pipe, $enc, 65536, $true)
$writer.AutoFlush = $true
$payload = $reader.ReadLine() | ConvertFrom-Json
$pendingList = @($payload)
$pendingIds = @($pendingList | ForEach-Object { $_.UpdateID })
Write-Log "Received $($pendingIds.Count) pending update ID(s)."
if ($pendingIds.Count -eq 0) {
$writer.WriteLine("[]")
exit 0
}
# Show toast and wait for user to click a button via wum: protocol handler
$toast = Show-UpdateToast -Updates $pendingList
$action = Show-ToastAndWait -Toast $toast
Write-Log "Toast action: $action"
if ($action -match "wum:installNow") {
Write-Log "User clicked Install Now - approving all $($pendingIds.Count) update(s)."
$writer.WriteLine(($pendingIds | ConvertTo-Json -Compress))
} elseif ($action -notmatch "wum:viewDetails") {
Write-Log "User dismissed or timed out."
$writer.WriteLine("[]")
exit 0
} else {
Write-Log "User clicked View Details."
$splash = Show-LoadingSplash -Message "Fetching update details, please wait..."
Write-Log "Querying Windows Update..."
$wuSession = New-Object -ComObject "Microsoft.Update.Session"
$wuSearch = $wuSession.CreateUpdateSearcher()
$wuResult = $wuSearch.Search("IsInstalled=0")
$available = @($wuResult.Updates | Where-Object { $_.Identity.UpdateID -in @($pendingIds) })
Close-LoadingSplash $splash
if ($available.Count -eq 0) {
Write-Log "Pending updates no longer available in WU." "WARN"
$writer.WriteLine("[]")
exit 0
}
Write-Log "Showing picker for $($available.Count) update(s)..."
$pickerResult = Show-UpdateDetailsWindow -Updates $available
if ($pickerResult.Action -eq "Install" -and $pickerResult.Updates.Count -gt 0) {
$approvedIds = @($pickerResult.Updates | ForEach-Object { $_.Identity.UpdateID })
$writer.WriteLine(($approvedIds | ConvertTo-Json -Compress))
Write-Log "Sent $($approvedIds.Count) approved ID(s) to SYSTEM."
} else {
$writer.WriteLine("[]")
Write-Log "User dismissed picker."
exit 0
}
}
# Pipe stays open - receive progress/complete messages from SYSTEM and show them
Write-Log "Waiting for installation progress..."
:msgloop while ($true) {
$line = try { $reader.ReadLine() } catch { break msgloop }
if ($null -eq $line) { break msgloop }
if ([string]::IsNullOrEmpty($line)) { continue }
try { $msg = $line | ConvertFrom-Json } catch { continue }
switch ($msg.cmd) {
"auto_install" {
Write-Log "Auto-install: $($msg.count) update(s)."
Show-InstallProgressToast -Total $msg.count -AppId $script:AppId
}
"install_start" {
Write-Log "Install starting: $($msg.count) update(s)."
Show-InstallProgressToast -Total $msg.count -AppId $script:AppId
}
"progress" {
$pct = if ($msg.percent) { [double]$msg.percent } else { 0 }
$stat = "$($msg.phase): $($msg.title)"
Write-Log "Progress: $stat"
Update-InstallProgressToast -Status $stat -Index $msg.index -Total $msg.total -Percent $pct -AppId $script:AppId
}
"complete" {
Write-Log "Complete. Succeeded: $($msg.succeeded) Failed: $($msg.failed) Reboot: $($msg.rebootRequired)"
Remove-InstallProgressToast -AppId $script:AppId
if ($msg.succeeded -gt 0) {
Show-UpdateCompleteToast -SuccessCount $msg.succeeded -RebootRequired $msg.rebootRequired -AppId $script:AppId
}
if ($msg.failed -gt 0) {
$failMsg = if ($msg.succeeded -eq 0) {
"All $($msg.failed) update(s) failed to install. Check logs for details."
} else {
"$($msg.failed) update(s) failed to install. $($msg.succeeded) succeeded."
}
Show-UpdateFailedToast -ErrorMessage $failMsg -AppId $script:AppId
}
break msgloop
}
"error" {
Write-Log "SYSTEM error: $($msg.message)" "ERROR"
Remove-InstallProgressToast -AppId $script:AppId
break msgloop
}
}
}
Write-Log "Message loop complete."
}
catch {
Write-Log "Fatal error: $($_.Exception.Message)" "ERROR"
try { if ($writer) { $writer.WriteLine("[]") } } catch {}
exit 1
}
finally {
if ($reader) { try { $reader.Dispose() } catch {} }
if ($writer) { try { $writer.Dispose() } catch {} }
if ($pipe) { try { $pipe.Close(); $pipe.Dispose() } catch {} }
}
exit 0