# 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 "" } 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(@" $iconElem Windows Updates Available $count update(s) ready to install $([System.Security.SecurityElement]::Escape($listText)) "@) 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