253 lines
9.8 KiB
PowerShell
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
|