927 lines
35 KiB
PowerShell
927 lines
35 KiB
PowerShell
<#
|
|
.SYNOPSIS
|
|
Windows Update Notification System
|
|
.DESCRIPTION
|
|
Handles Windows 10/11 Action Center notifications and popup dialogs
|
|
showing detailed update information
|
|
.NOTES
|
|
Author: oxmc
|
|
Version: 2.1
|
|
#>
|
|
|
|
Add-Type -AssemblyName System.Windows.Forms
|
|
Add-Type -AssemblyName System.Drawing
|
|
Add-Type -AssemblyName PresentationFramework
|
|
|
|
# Load Windows Runtime for Toast Notifications
|
|
[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
|
|
[Windows.UI.Notifications.ToastNotification, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
|
|
[Windows.UI.Notifications.NotificationData, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
|
|
[Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] | Out-Null
|
|
|
|
$script:APP_ID = '{1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}\WindowsPowerShell\v1.0\powershell.exe'
|
|
$script:ProgressTag = "wum-progress"
|
|
$script:ProgressSeq = [uint32]1
|
|
|
|
#region Windows Toast Notifications
|
|
|
|
function Show-UpdateAvailableToast {
|
|
param(
|
|
[int]$UpdateCount,
|
|
[array]$Updates
|
|
)
|
|
|
|
$updateList = ""
|
|
$maxDisplay = [Math]::Min(3, $Updates.Count)
|
|
|
|
for ($i = 0; $i -lt $maxDisplay; $i++) {
|
|
$updateList += "* $($Updates[$i].Title)`n"
|
|
}
|
|
|
|
if ($Updates.Count -gt 3) {
|
|
$updateList += "* And $($Updates.Count - 3) more..."
|
|
}
|
|
|
|
$template = @"
|
|
<toast launch="action=viewUpdates" scenario="reminder">
|
|
<visual>
|
|
<binding template="ToastGeneric">
|
|
<text>Windows Updates Available</text>
|
|
<text>$UpdateCount update(s) ready to install</text>
|
|
<text>$updateList</text>
|
|
</binding>
|
|
</visual>
|
|
<actions>
|
|
<action content="View Details" arguments="action=viewDetails" activationType="foreground"/>
|
|
<action content="Install Now" arguments="action=installNow" activationType="foreground"/>
|
|
<action content="Remind Me Later" arguments="action=dismiss" activationType="background"/>
|
|
</actions>
|
|
<audio src="ms-winsoundevent:Notification.Default"/>
|
|
</toast>
|
|
"@
|
|
|
|
try {
|
|
$xml = New-Object Windows.Data.Xml.Dom.XmlDocument
|
|
$xml.LoadXml($template)
|
|
$toast = New-Object Windows.UI.Notifications.ToastNotification $xml
|
|
[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier($script:APP_ID).Show($toast)
|
|
return $true
|
|
}
|
|
catch {
|
|
Write-Host "Toast notification failed: $($_.Exception.Message)" -ForegroundColor Yellow
|
|
return $false
|
|
}
|
|
}
|
|
|
|
function Show-UpdateInstallingToast {
|
|
param([int]$UpdateCount)
|
|
|
|
$template = @"
|
|
<toast scenario="reminder">
|
|
<visual>
|
|
<binding template="ToastGeneric">
|
|
<text>Installing Windows Updates</text>
|
|
<text>Installing $UpdateCount update(s)...</text>
|
|
<text>This may take several minutes. Please do not turn off your computer.</text>
|
|
<progress value="indeterminate" status="Installing..." />
|
|
</binding>
|
|
</visual>
|
|
<audio src="ms-winsoundevent:Notification.Default"/>
|
|
</toast>
|
|
"@
|
|
|
|
try {
|
|
$xml = New-Object Windows.Data.Xml.Dom.XmlDocument
|
|
$xml.LoadXml($template)
|
|
$toast = New-Object Windows.UI.Notifications.ToastNotification $xml
|
|
[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier($script:APP_ID).Show($toast)
|
|
}
|
|
catch {
|
|
Write-Host "Toast notification failed: $($_.Exception.Message)" -ForegroundColor Yellow
|
|
}
|
|
}
|
|
|
|
function Show-UpdateCompleteToast {
|
|
param(
|
|
[int]$SuccessCount,
|
|
[bool]$RebootRequired,
|
|
[string]$AppId = $script:APP_ID
|
|
)
|
|
|
|
$rebootText = if ($RebootRequired) {
|
|
"A restart is required to complete installation."
|
|
} else {
|
|
"No restart required."
|
|
}
|
|
|
|
$template = @"
|
|
<toast launch="action=viewResults" scenario="reminder">
|
|
<visual>
|
|
<binding template="ToastGeneric">
|
|
<text>Updates Installed Successfully</text>
|
|
<text>$SuccessCount update(s) installed</text>
|
|
<text>$rebootText</text>
|
|
</binding>
|
|
</visual>
|
|
<actions>
|
|
<action content="View Details" arguments="action=viewResults" activationType="foreground"/>
|
|
<action content="Restart Now" arguments="action=restartNow" activationType="foreground"/>
|
|
<action content="Dismiss" arguments="action=dismiss" activationType="background"/>
|
|
</actions>
|
|
<audio src="ms-winsoundevent:Notification.Default"/>
|
|
</toast>
|
|
"@
|
|
|
|
if (-not $RebootRequired) {
|
|
$template = @"
|
|
<toast launch="action=viewResults">
|
|
<visual>
|
|
<binding template="ToastGeneric">
|
|
<text>Updates Installed Successfully</text>
|
|
<text>$SuccessCount update(s) installed</text>
|
|
<text>$rebootText</text>
|
|
</binding>
|
|
</visual>
|
|
<actions>
|
|
<action content="View Details" arguments="action=viewResults" activationType="foreground"/>
|
|
<action content="Dismiss" arguments="action=dismiss" activationType="background"/>
|
|
</actions>
|
|
<audio src="ms-winsoundevent:Notification.Default"/>
|
|
</toast>
|
|
"@
|
|
}
|
|
|
|
try {
|
|
$xml = New-Object Windows.Data.Xml.Dom.XmlDocument
|
|
$xml.LoadXml($template)
|
|
$toast = New-Object Windows.UI.Notifications.ToastNotification $xml
|
|
[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier($AppId).Show($toast)
|
|
}
|
|
catch {
|
|
Write-Host "Toast notification failed: $($_.Exception.Message)" -ForegroundColor Yellow
|
|
}
|
|
}
|
|
|
|
function Show-UpdateFailedToast {
|
|
param([string]$ErrorMessage, [string]$AppId = $script:APP_ID)
|
|
|
|
$template = @"
|
|
<toast>
|
|
<visual>
|
|
<binding template="ToastGeneric">
|
|
<text>Update Installation Failed</text>
|
|
<text>Some updates could not be installed</text>
|
|
<text>$ErrorMessage</text>
|
|
</binding>
|
|
</visual>
|
|
<actions>
|
|
<action content="View Logs" arguments="action=viewLogs" activationType="foreground"/>
|
|
<action content="Retry" arguments="action=retry" activationType="foreground"/>
|
|
<action content="Dismiss" arguments="action=dismiss" activationType="background"/>
|
|
</actions>
|
|
<audio src="ms-winsoundevent:Notification.Looping.Alarm"/>
|
|
</toast>
|
|
"@
|
|
|
|
try {
|
|
$xml = New-Object Windows.Data.Xml.Dom.XmlDocument
|
|
$xml.LoadXml($template)
|
|
$toast = New-Object Windows.UI.Notifications.ToastNotification $xml
|
|
[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier($AppId).Show($toast)
|
|
}
|
|
catch {
|
|
Write-Host "Toast notification failed: $($_.Exception.Message)" -ForegroundColor Yellow
|
|
}
|
|
}
|
|
|
|
function Show-RebootRequiredToast {
|
|
param([int]$MinutesUntilReboot = 60)
|
|
|
|
$template = @"
|
|
<toast scenario="urgent">
|
|
<visual>
|
|
<binding template="ToastGeneric">
|
|
<text>Restart Required</text>
|
|
<text>Your PC will restart in $MinutesUntilReboot minutes</text>
|
|
<text>Save your work and close all applications</text>
|
|
</binding>
|
|
</visual>
|
|
<actions>
|
|
<action content="Restart Now" arguments="action=restartNow" activationType="foreground"/>
|
|
<action content="Schedule Restart" arguments="action=scheduleRestart" activationType="foreground"/>
|
|
<action content="Postpone" arguments="action=postpone" activationType="background"/>
|
|
</actions>
|
|
<audio src="ms-winsoundevent:Notification.Looping.Alarm" loop="true"/>
|
|
</toast>
|
|
"@
|
|
|
|
try {
|
|
$xml = New-Object Windows.Data.Xml.Dom.XmlDocument
|
|
$xml.LoadXml($template)
|
|
$toast = New-Object Windows.UI.Notifications.ToastNotification $xml
|
|
[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier($script:APP_ID).Show($toast)
|
|
}
|
|
catch {
|
|
Write-Host "Toast notification failed: $($_.Exception.Message)" -ForegroundColor Yellow
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Balloon Tip Notifications (Fallback)
|
|
|
|
function Show-BalloonNotification {
|
|
param(
|
|
[string]$Title,
|
|
[string]$Message,
|
|
[ValidateSet("None", "Info", "Warning", "Error")]
|
|
[string]$Icon = "Info",
|
|
[int]$Duration = 10000
|
|
)
|
|
|
|
# Balloon tips are silently dropped on Windows 11 (build 22000+); toast already handles it
|
|
$build = [int](Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion").CurrentBuildNumber
|
|
if ($build -ge 22000) { return }
|
|
|
|
$balloon = New-Object System.Windows.Forms.NotifyIcon
|
|
|
|
switch ($Icon) {
|
|
"Info" { $balloon.Icon = [System.Drawing.SystemIcons]::Information }
|
|
"Warning" { $balloon.Icon = [System.Drawing.SystemIcons]::Warning }
|
|
"Error" { $balloon.Icon = [System.Drawing.SystemIcons]::Error }
|
|
default { $balloon.Icon = [System.Drawing.SystemIcons]::Application }
|
|
}
|
|
|
|
$balloon.BalloonTipIcon = $Icon
|
|
$balloon.BalloonTipText = $Message
|
|
$balloon.BalloonTipTitle = $Title
|
|
$balloon.Visible = $true
|
|
$balloon.ShowBalloonTip($Duration)
|
|
|
|
$cleanupTimer = [System.Windows.Forms.Timer]::new()
|
|
$cleanupTimer.Interval = $Duration + 500
|
|
$cleanupTimer.Add_Tick({
|
|
$balloon.Dispose()
|
|
$cleanupTimer.Stop()
|
|
$cleanupTimer.Dispose()
|
|
})
|
|
$cleanupTimer.Start()
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Updates Available Notification
|
|
|
|
function Show-UpdatesAvailableNotification {
|
|
param([int]$UpdateCount)
|
|
|
|
$form = New-Object System.Windows.Forms.Form
|
|
$form.Text = "Windows Update"
|
|
$form.Size = New-Object System.Drawing.Size(360, 140)
|
|
$form.StartPosition = "Manual"
|
|
$form.FormBorderStyle = "FixedDialog"
|
|
$form.MaximizeBox = $false
|
|
$form.MinimizeBox = $false
|
|
$form.TopMost = $true
|
|
|
|
# Position bottom-right above taskbar
|
|
$screen = [System.Windows.Forms.Screen]::PrimaryScreen.WorkingArea
|
|
$form.Location = New-Object System.Drawing.Point(
|
|
($screen.Right - $form.Width - 12),
|
|
($screen.Bottom - $form.Height - 12))
|
|
|
|
$icon = New-Object System.Windows.Forms.PictureBox
|
|
$icon.Image = [System.Drawing.SystemIcons]::Information.ToBitmap()
|
|
$icon.SizeMode = "StretchImage"
|
|
$icon.Size = New-Object System.Drawing.Size(32, 32)
|
|
$icon.Location = New-Object System.Drawing.Point(14, 20)
|
|
$form.Controls.Add($icon)
|
|
|
|
$label = New-Object System.Windows.Forms.Label
|
|
$label.Text = "$UpdateCount update(s) available"
|
|
$label.Font = New-Object System.Drawing.Font("Segoe UI", 10, [System.Drawing.FontStyle]::Bold)
|
|
$label.Location = New-Object System.Drawing.Point(56, 14)
|
|
$label.Size = New-Object System.Drawing.Size(280, 22)
|
|
$form.Controls.Add($label)
|
|
|
|
$sub = New-Object System.Windows.Forms.Label
|
|
$sub.Text = "Review and choose which updates to install."
|
|
$sub.Font = New-Object System.Drawing.Font("Segoe UI", 9)
|
|
$sub.Location = New-Object System.Drawing.Point(56, 36)
|
|
$sub.Size = New-Object System.Drawing.Size(280, 18)
|
|
$form.Controls.Add($sub)
|
|
|
|
$viewBtn = New-Object System.Windows.Forms.Button
|
|
$viewBtn.Text = "View Details"
|
|
$viewBtn.Size = New-Object System.Drawing.Size(100, 28)
|
|
$viewBtn.Location = New-Object System.Drawing.Point(140, 70)
|
|
$viewBtn.DialogResult = [System.Windows.Forms.DialogResult]::OK
|
|
$viewBtn.FlatStyle = "Flat"
|
|
$viewBtn.BackColor = [System.Drawing.Color]::FromArgb(0, 120, 215)
|
|
$viewBtn.ForeColor = [System.Drawing.Color]::White
|
|
$form.Controls.Add($viewBtn)
|
|
$form.AcceptButton = $viewBtn
|
|
|
|
$laterBtn = New-Object System.Windows.Forms.Button
|
|
$laterBtn.Text = "Later"
|
|
$laterBtn.Size = New-Object System.Drawing.Size(80, 28)
|
|
$laterBtn.Location = New-Object System.Drawing.Point(250, 70)
|
|
$laterBtn.DialogResult = [System.Windows.Forms.DialogResult]::Cancel
|
|
$laterBtn.FlatStyle = "Flat"
|
|
$form.Controls.Add($laterBtn)
|
|
$form.CancelButton = $laterBtn
|
|
|
|
return $form.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Loading Splash
|
|
|
|
function Show-LoadingSplash {
|
|
param([string]$Message = "Checking for updates...")
|
|
|
|
$splash = New-Object System.Windows.Forms.Form
|
|
$splash.Text = "Windows Update Manager"
|
|
$splash.Size = New-Object System.Drawing.Size(380, 110)
|
|
$splash.StartPosition = "CenterScreen"
|
|
$splash.FormBorderStyle = "FixedDialog"
|
|
$splash.MaximizeBox = $false
|
|
$splash.MinimizeBox = $false
|
|
$splash.ControlBox = $false
|
|
$splash.TopMost = $true
|
|
|
|
$label = New-Object System.Windows.Forms.Label
|
|
$label.Text = $Message
|
|
$label.Font = New-Object System.Drawing.Font("Segoe UI", 10)
|
|
$label.Location = New-Object System.Drawing.Point(20, 15)
|
|
$label.Size = New-Object System.Drawing.Size(340, 22)
|
|
$splash.Controls.Add($label)
|
|
|
|
$bar = New-Object System.Windows.Forms.ProgressBar
|
|
$bar.Style = "Marquee"
|
|
$bar.Location = New-Object System.Drawing.Point(20, 45)
|
|
$bar.Size = New-Object System.Drawing.Size(340, 20)
|
|
$splash.Controls.Add($bar)
|
|
|
|
$splash.Show()
|
|
[System.Windows.Forms.Application]::DoEvents()
|
|
return $splash
|
|
}
|
|
|
|
function Close-LoadingSplash {
|
|
param([System.Windows.Forms.Form]$Splash)
|
|
if ($Splash -and -not $Splash.IsDisposed) {
|
|
$Splash.Close()
|
|
$Splash.Dispose()
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Update Details Popup Window
|
|
|
|
function Show-UpdateDetailsWindow {
|
|
param([array]$Updates)
|
|
|
|
$form = New-Object System.Windows.Forms.Form
|
|
$form.Text = "Windows Updates Available - Details"
|
|
$form.Size = New-Object System.Drawing.Size(900, 750)
|
|
$form.StartPosition = "CenterScreen"
|
|
$form.FormBorderStyle = "FixedDialog"
|
|
$form.MaximizeBox = $false
|
|
$form.MinimizeBox = $true
|
|
$form.Icon = [System.Drawing.SystemIcons]::Information
|
|
$form.TopMost = $true
|
|
|
|
# Header Panel
|
|
$headerPanel = New-Object System.Windows.Forms.Panel
|
|
$headerPanel.Dock = "Top"
|
|
$headerPanel.Height = 80
|
|
$headerPanel.BackColor = [System.Drawing.Color]::FromArgb(0, 120, 215)
|
|
$form.Controls.Add($headerPanel)
|
|
|
|
# Icon
|
|
$iconBox = New-Object System.Windows.Forms.PictureBox
|
|
$iconBox.Location = New-Object System.Drawing.Point(20, 15)
|
|
$iconBox.Size = New-Object System.Drawing.Size(48, 48)
|
|
$iconBox.Image = [System.Drawing.SystemIcons]::Information.ToBitmap()
|
|
$iconBox.SizeMode = "StretchImage"
|
|
$headerPanel.Controls.Add($iconBox)
|
|
|
|
# Title
|
|
$titleLabel = New-Object System.Windows.Forms.Label
|
|
$titleLabel.Location = New-Object System.Drawing.Point(80, 15)
|
|
$titleLabel.Size = New-Object System.Drawing.Size(800, 30)
|
|
$titleLabel.Font = New-Object System.Drawing.Font("Segoe UI", 16, [System.Drawing.FontStyle]::Bold)
|
|
$titleLabel.ForeColor = [System.Drawing.Color]::White
|
|
$titleLabel.Text = "$($Updates.Count) Windows Update(s) Available"
|
|
$headerPanel.Controls.Add($titleLabel)
|
|
|
|
# Subtitle
|
|
$subtitleLabel = New-Object System.Windows.Forms.Label
|
|
$subtitleLabel.Location = New-Object System.Drawing.Point(80, 45)
|
|
$subtitleLabel.Size = New-Object System.Drawing.Size(800, 25)
|
|
$subtitleLabel.Font = New-Object System.Drawing.Font("Segoe UI", 10)
|
|
$subtitleLabel.ForeColor = [System.Drawing.Color]::White
|
|
$subtitleLabel.Text = "Review and install the following updates"
|
|
$headerPanel.Controls.Add($subtitleLabel)
|
|
|
|
# Info Panel
|
|
$infoPanel = New-Object System.Windows.Forms.Panel
|
|
$infoPanel.Location = New-Object System.Drawing.Point(20, 100)
|
|
$infoPanel.Size = New-Object System.Drawing.Size(840, 50)
|
|
$infoPanel.BackColor = [System.Drawing.Color]::FromArgb(245, 245, 245)
|
|
$infoPanel.BorderStyle = "FixedSingle"
|
|
$form.Controls.Add($infoPanel)
|
|
|
|
$infoLabel = New-Object System.Windows.Forms.Label
|
|
$infoLabel.Location = New-Object System.Drawing.Point(10, 12)
|
|
$infoLabel.Size = New-Object System.Drawing.Size(820, 25)
|
|
$infoLabel.Font = New-Object System.Drawing.Font("Segoe UI", 10)
|
|
$infoLabel.Text = "Total Size: Fetching... | Available: $($Updates.Count) update(s)"
|
|
$infoPanel.Controls.Add($infoLabel)
|
|
|
|
# ListView for updates
|
|
$listView = New-Object System.Windows.Forms.ListView
|
|
$listView.Location = New-Object System.Drawing.Point(20, 160)
|
|
$listView.Size = New-Object System.Drawing.Size(840, 320)
|
|
$listView.View = "Details"
|
|
$listView.FullRowSelect = $true
|
|
$listView.GridLines = $true
|
|
$listView.CheckBoxes = $true
|
|
$listView.MultiSelect = $false
|
|
$form.Controls.Add($listView)
|
|
|
|
# Add columns
|
|
$listView.Columns.Add("", 30) | Out-Null
|
|
$listView.Columns.Add("Update Name", 450) | Out-Null
|
|
$listView.Columns.Add("Type", 100) | Out-Null
|
|
$listView.Columns.Add("Size", 80) | Out-Null
|
|
$listView.Columns.Add("KB Article", 100) | Out-Null
|
|
$listView.Columns.Add("Severity", 80) | Out-Null
|
|
|
|
# Add updates to list — size column starts as "Fetching..." and is filled asynchronously
|
|
$urlsPerUpdate = @{}
|
|
for ($ui = 0; $ui -lt $Updates.Count; $ui++) {
|
|
$update = $Updates[$ui]
|
|
$item = New-Object System.Windows.Forms.ListViewItem("")
|
|
$item.Checked = $true
|
|
$item.SubItems.Add($update.Title) | Out-Null
|
|
|
|
$type = switch ($update.Type) {
|
|
1 { "Software" }
|
|
2 { "Driver" }
|
|
default { "Other" }
|
|
}
|
|
$item.SubItems.Add($type) | Out-Null
|
|
$item.SubItems.Add("Fetching...") | Out-Null
|
|
|
|
$kbNumber = "N/A"
|
|
if ($update.Title -match '(KB\d+)') { $kbNumber = $Matches[1] }
|
|
$item.SubItems.Add($kbNumber) | Out-Null
|
|
|
|
$severity = "Standard"
|
|
foreach ($category in $update.Categories) {
|
|
if ($category.Name -match "Critical") {
|
|
$severity = "Critical"
|
|
$item.ForeColor = [System.Drawing.Color]::Red
|
|
break
|
|
} elseif ($category.Name -match "Important|Security") {
|
|
$severity = "Important"
|
|
$item.ForeColor = [System.Drawing.Color]::OrangeRed
|
|
}
|
|
}
|
|
$item.SubItems.Add($severity) | Out-Null
|
|
$item.Tag = $update
|
|
$listView.Items.Add($item) | Out-Null
|
|
|
|
# Collect per-update metadata from COM now (COM objects are main-thread only)
|
|
$isDelta = ($update.DownloadContents | ForEach-Object { $_.IsDeltaCompressedContent }) -contains $true
|
|
$urls = @($update.DownloadContents |
|
|
ForEach-Object { $_.DownloadUrl } |
|
|
Where-Object { $_ -and $_ -match '^https?://' })
|
|
$urlsPerUpdate[$ui] = @{
|
|
IsDelta = $isDelta
|
|
MaxSize = [long]$update.MaxDownloadSize
|
|
Urls = $urls
|
|
}
|
|
}
|
|
|
|
# Background runspace: HEAD-request every URL to get real Content-Length
|
|
$sizeResults = [System.Collections.Concurrent.ConcurrentDictionary[int,long]]::new()
|
|
$fetchRS = [System.Management.Automation.Runspaces.RunspaceFactory]::CreateRunspace()
|
|
$fetchRS.Open()
|
|
$fetchRS.SessionStateProxy.SetVariable('urlsPerUpdate', $urlsPerUpdate)
|
|
$fetchRS.SessionStateProxy.SetVariable('sizeResults', $sizeResults)
|
|
$fetchRS.SessionStateProxy.SetVariable('updateCount', $Updates.Count)
|
|
$fetchPS = [System.Management.Automation.PowerShell]::Create()
|
|
$fetchPS.Runspace = $fetchRS
|
|
$fetchPS.AddScript({
|
|
for ($i = 0; $i -lt $updateCount; $i++) {
|
|
$info = $urlsPerUpdate[$i]
|
|
|
|
if (-not $info.IsDelta -and $info.MaxSize -gt 0) {
|
|
# Non-delta: MaxDownloadSize is the exact file size
|
|
$sizeResults.TryAdd($i, $info.MaxSize) | Out-Null
|
|
continue
|
|
}
|
|
|
|
# Delta-compressed: HEAD the actual CDN URL for real size
|
|
$total = 0L
|
|
foreach ($url in $info.Urls) {
|
|
try {
|
|
$req = [System.Net.HttpWebRequest]::Create([uri]$url)
|
|
$req.Method = "HEAD"
|
|
$req.Timeout = 8000
|
|
$req.AllowAutoRedirect = $true
|
|
$resp = $req.GetResponse()
|
|
if ($resp.ContentLength -gt 0) { $total += $resp.ContentLength }
|
|
$resp.Close()
|
|
} catch {}
|
|
}
|
|
$sizeResults.TryAdd($i, $total) | Out-Null
|
|
}
|
|
}) | Out-Null
|
|
$fetchPS.BeginInvoke() | Out-Null
|
|
|
|
# WinForms Timer: polls results on UI thread and updates rows live
|
|
$fetchDone = [System.Collections.Generic.HashSet[int]]::new()
|
|
$fetchTimer = [System.Windows.Forms.Timer]::new()
|
|
$fetchTimer.Interval = 300
|
|
$fetchTimer.Add_Tick({
|
|
$fetched = 0L
|
|
for ($ti = 0; $ti -lt $Updates.Count; $ti++) {
|
|
if ($fetchDone.Contains($ti)) { $v = 0L; $sizeResults.TryGetValue($ti, [ref]$v); $fetched += $v; continue }
|
|
$val = 0L
|
|
if (-not $sizeResults.TryGetValue($ti, [ref]$val)) { continue }
|
|
|
|
# Format real size or fall back to WU API estimate
|
|
$u = $Updates[$ti]
|
|
$isDefinition = $u.Categories | Where-Object { $_.Name -match "Definition" }
|
|
if ($isDefinition) {
|
|
# Delta-delivery means CDN size is full package, not what downloads — always Unknown
|
|
$szStr = "Unknown"
|
|
$val = 0
|
|
} elseif ($val -gt 0) {
|
|
$szStr = if ($val -ge 1GB) { "$([math]::Round($val/1GB,2)) GB" }
|
|
elseif ($val -ge 1MB) { "$([math]::Round($val/1MB,1)) MB" }
|
|
else { "$([math]::Round($val/1KB,0)) KB" }
|
|
} else {
|
|
$fb = if ($u.MinDownloadSize -gt 0) { $u.MinDownloadSize } else { $u.MaxDownloadSize }
|
|
$px = if ($u.MinDownloadSize -eq 0 -and $u.MaxDownloadSize -gt 0) { "<= " } else { "" }
|
|
$szStr = if ($fb -ge 1GB) { "$px$([math]::Round($fb/1GB,2)) GB" }
|
|
elseif ($fb -ge 1MB) { "$px$([math]::Round($fb/1MB,1)) MB" }
|
|
elseif ($fb -gt 0) { "$px$([math]::Round($fb/1KB,0)) KB" }
|
|
else { "Unknown" }
|
|
$val = $fb
|
|
}
|
|
$listView.Items[$ti].SubItems[3].Text = $szStr
|
|
$fetchDone.Add($ti) | Out-Null
|
|
$fetched += $val
|
|
}
|
|
|
|
if ($fetchDone.Count -ge $Updates.Count) {
|
|
$fetchTimer.Stop()
|
|
$fetchTimer.Dispose()
|
|
$totStr = if ($fetched -ge 1GB) { "$([math]::Round($fetched/1GB,2)) GB" }
|
|
elseif ($fetched -ge 1MB) { "$([math]::Round($fetched/1MB,1)) MB" }
|
|
elseif ($fetched -gt 0) { "$([math]::Round($fetched/1KB,0)) KB" }
|
|
else { "Unknown" }
|
|
$infoLabel.Text = "Total Size: $totStr | Available: $($Updates.Count) update(s)"
|
|
try { $fetchRS.Close(); $fetchRS.Dispose() } catch {}
|
|
}
|
|
})
|
|
$fetchTimer.Start()
|
|
|
|
$form.Add_FormClosed({
|
|
try { $fetchTimer.Stop(); $fetchTimer.Dispose() } catch {}
|
|
try { $fetchPS.Stop() } catch {}
|
|
try { $fetchRS.Close(); $fetchRS.Dispose() } catch {}
|
|
})
|
|
|
|
# Description TextBox
|
|
$descLabel = New-Object System.Windows.Forms.Label
|
|
$descLabel.Location = New-Object System.Drawing.Point(20, 490)
|
|
$descLabel.Size = New-Object System.Drawing.Size(200, 20)
|
|
$descLabel.Text = "Update Description:"
|
|
$descLabel.Font = New-Object System.Drawing.Font("Segoe UI", 9, [System.Drawing.FontStyle]::Bold)
|
|
$form.Controls.Add($descLabel)
|
|
|
|
$descBox = New-Object System.Windows.Forms.TextBox
|
|
$descBox.Location = New-Object System.Drawing.Point(20, 515)
|
|
$descBox.Size = New-Object System.Drawing.Size(840, 100)
|
|
$descBox.Multiline = $true
|
|
$descBox.ReadOnly = $true
|
|
$descBox.ScrollBars = "Vertical"
|
|
$descBox.BackColor = [System.Drawing.Color]::White
|
|
$descBox.Font = New-Object System.Drawing.Font("Segoe UI", 9)
|
|
$form.Controls.Add($descBox)
|
|
|
|
# Show description when item is selected
|
|
$listView.Add_SelectedIndexChanged({
|
|
if ($listView.SelectedItems.Count -gt 0) {
|
|
$selectedUpdate = $listView.SelectedItems[0].Tag
|
|
$descBox.Text = $selectedUpdate.Description
|
|
}
|
|
})
|
|
|
|
# Buttons Panel
|
|
$buttonPanel = New-Object System.Windows.Forms.Panel
|
|
$buttonPanel.Dock = "Bottom"
|
|
$buttonPanel.Height = 60
|
|
$buttonPanel.BackColor = [System.Drawing.Color]::FromArgb(240, 240, 240)
|
|
$form.Controls.Add($buttonPanel)
|
|
|
|
# Install Button
|
|
$installButton = New-Object System.Windows.Forms.Button
|
|
$installButton.Location = New-Object System.Drawing.Point(640, 15)
|
|
$installButton.Size = New-Object System.Drawing.Size(100, 30)
|
|
$installButton.Text = "Install Now"
|
|
$installButton.BackColor = [System.Drawing.Color]::FromArgb(0, 120, 215)
|
|
$installButton.ForeColor = [System.Drawing.Color]::White
|
|
$installButton.FlatStyle = "Flat"
|
|
$installButton.Font = New-Object System.Drawing.Font("Segoe UI", 9, [System.Drawing.FontStyle]::Bold)
|
|
$installButton.DialogResult = [System.Windows.Forms.DialogResult]::OK
|
|
$buttonPanel.Controls.Add($installButton)
|
|
|
|
# Later Button
|
|
$laterButton = New-Object System.Windows.Forms.Button
|
|
$laterButton.Location = New-Object System.Drawing.Point(750, 15)
|
|
$laterButton.Size = New-Object System.Drawing.Size(110, 30)
|
|
$laterButton.Text = "Remind Later"
|
|
$laterButton.BackColor = [System.Drawing.Color]::White
|
|
$laterButton.FlatStyle = "Flat"
|
|
$laterButton.DialogResult = [System.Windows.Forms.DialogResult]::Cancel
|
|
$buttonPanel.Controls.Add($laterButton)
|
|
|
|
# Select All / Deselect All
|
|
$selectAllButton = New-Object System.Windows.Forms.Button
|
|
$selectAllButton.Location = New-Object System.Drawing.Point(20, 15)
|
|
$selectAllButton.Size = New-Object System.Drawing.Size(90, 30)
|
|
$selectAllButton.Text = "Select All"
|
|
$selectAllButton.FlatStyle = "Flat"
|
|
$selectAllButton.Add_Click({
|
|
foreach ($item in $listView.Items) {
|
|
$item.Checked = $true
|
|
}
|
|
})
|
|
$buttonPanel.Controls.Add($selectAllButton)
|
|
|
|
$deselectAllButton = New-Object System.Windows.Forms.Button
|
|
$deselectAllButton.Location = New-Object System.Drawing.Point(120, 15)
|
|
$deselectAllButton.Size = New-Object System.Drawing.Size(100, 30)
|
|
$deselectAllButton.Text = "Deselect All"
|
|
$deselectAllButton.FlatStyle = "Flat"
|
|
$deselectAllButton.Add_Click({
|
|
foreach ($item in $listView.Items) {
|
|
$item.Checked = $false
|
|
}
|
|
})
|
|
$buttonPanel.Controls.Add($deselectAllButton)
|
|
|
|
# Show the form
|
|
$result = $form.ShowDialog()
|
|
|
|
if ($result -eq [System.Windows.Forms.DialogResult]::OK) {
|
|
# Return selected updates
|
|
$selectedUpdates = @()
|
|
foreach ($item in $listView.Items) {
|
|
if ($item.Checked) {
|
|
$selectedUpdates += $item.Tag
|
|
}
|
|
}
|
|
$form.Dispose()
|
|
return @{
|
|
Action = "Install"
|
|
Updates = $selectedUpdates
|
|
}
|
|
}
|
|
else {
|
|
$form.Dispose()
|
|
return @{
|
|
Action = "Postpone"
|
|
Updates = @()
|
|
}
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Installation Progress Window
|
|
|
|
function Show-InstallationProgressWindow {
|
|
param(
|
|
[array]$Updates,
|
|
[scriptblock]$InstallAction
|
|
)
|
|
|
|
$form = New-Object System.Windows.Forms.Form
|
|
$form.Text = "Installing Windows Updates"
|
|
$form.Size = New-Object System.Drawing.Size(600, 450)
|
|
$form.StartPosition = "CenterScreen"
|
|
$form.FormBorderStyle = "FixedDialog"
|
|
$form.MaximizeBox = $false
|
|
$form.MinimizeBox = $false
|
|
$form.ControlBox = $false
|
|
$form.TopMost = $true
|
|
|
|
# Header
|
|
$headerPanel = New-Object System.Windows.Forms.Panel
|
|
$headerPanel.Dock = "Top"
|
|
$headerPanel.Height = 80
|
|
$headerPanel.BackColor = [System.Drawing.Color]::FromArgb(0, 120, 215)
|
|
$form.Controls.Add($headerPanel)
|
|
|
|
$titleLabel = New-Object System.Windows.Forms.Label
|
|
$titleLabel.Location = New-Object System.Drawing.Point(20, 15)
|
|
$titleLabel.Size = New-Object System.Drawing.Size(560, 30)
|
|
$titleLabel.Font = New-Object System.Drawing.Font("Segoe UI", 14, [System.Drawing.FontStyle]::Bold)
|
|
$titleLabel.ForeColor = [System.Drawing.Color]::White
|
|
$titleLabel.Text = "Installing Windows Updates"
|
|
$titleLabel.Name = "TitleLabel"
|
|
$headerPanel.Controls.Add($titleLabel)
|
|
|
|
$subtitleLabel = New-Object System.Windows.Forms.Label
|
|
$subtitleLabel.Location = New-Object System.Drawing.Point(20, 45)
|
|
$subtitleLabel.Size = New-Object System.Drawing.Size(560, 25)
|
|
$subtitleLabel.Font = New-Object System.Drawing.Font("Segoe UI", 10)
|
|
$subtitleLabel.ForeColor = [System.Drawing.Color]::White
|
|
$subtitleLabel.Text = "Please wait while updates are being installed..."
|
|
$subtitleLabel.Name = "SubtitleLabel"
|
|
$headerPanel.Controls.Add($subtitleLabel)
|
|
|
|
# Status Label
|
|
$statusLabel = New-Object System.Windows.Forms.Label
|
|
$statusLabel.Location = New-Object System.Drawing.Point(20, 100)
|
|
$statusLabel.Size = New-Object System.Drawing.Size(560, 25)
|
|
$statusLabel.Font = New-Object System.Drawing.Font("Segoe UI", 10)
|
|
$statusLabel.Text = "Preparing to install $($Updates.Count) update(s)..."
|
|
$statusLabel.Name = "StatusLabel"
|
|
$form.Controls.Add($statusLabel)
|
|
|
|
# Main Progress Bar (for download/install)
|
|
$mainProgressBar = New-Object System.Windows.Forms.ProgressBar
|
|
$mainProgressBar.Location = New-Object System.Drawing.Point(20, 135)
|
|
$mainProgressBar.Size = New-Object System.Drawing.Size(540, 30)
|
|
$mainProgressBar.Style = "Continuous"
|
|
$mainProgressBar.Name = "MainProgressBar"
|
|
$form.Controls.Add($mainProgressBar)
|
|
|
|
# Progress Label
|
|
$progressLabel = New-Object System.Windows.Forms.Label
|
|
$progressLabel.Location = New-Object System.Drawing.Point(20, 170)
|
|
$progressLabel.Size = New-Object System.Drawing.Size(560, 20)
|
|
$progressLabel.Font = New-Object System.Drawing.Font("Segoe UI", 9)
|
|
$progressLabel.Text = "0%"
|
|
$progressLabel.Name = "ProgressLabel"
|
|
$form.Controls.Add($progressLabel)
|
|
|
|
# Update List
|
|
$listBox = New-Object System.Windows.Forms.ListBox
|
|
$listBox.Location = New-Object System.Drawing.Point(20, 200)
|
|
$listBox.Size = New-Object System.Drawing.Size(540, 150)
|
|
$listBox.Font = New-Object System.Drawing.Font("Segoe UI", 9)
|
|
$listBox.Name = "UpdateListBox"
|
|
$form.Controls.Add($listBox)
|
|
|
|
foreach ($update in $Updates) {
|
|
$listBox.Items.Add("[ ] $($update.Title)") | Out-Null
|
|
}
|
|
|
|
# Warning Label
|
|
$warningLabel = New-Object System.Windows.Forms.Label
|
|
$warningLabel.Location = New-Object System.Drawing.Point(20, 360)
|
|
$warningLabel.Size = New-Object System.Drawing.Size(560, 40)
|
|
$warningLabel.Font = New-Object System.Drawing.Font("Segoe UI", 9, [System.Drawing.FontStyle]::Italic)
|
|
$warningLabel.ForeColor = [System.Drawing.Color]::Red
|
|
$warningLabel.Text = "WARNING: Do not turn off your computer during installation"
|
|
$form.Controls.Add($warningLabel)
|
|
|
|
# Show form
|
|
$form.Show()
|
|
$form.Refresh()
|
|
|
|
return $form
|
|
}
|
|
|
|
function Update-InstallationProgress {
|
|
param(
|
|
[System.Windows.Forms.Form]$Form,
|
|
[int]$Current,
|
|
[int]$Total,
|
|
[string]$CurrentUpdate,
|
|
[string]$Status = "Installing"
|
|
)
|
|
|
|
if ($Form -and -not $Form.IsDisposed) {
|
|
$statusLabel = $Form.Controls.Find("StatusLabel", $false)[0]
|
|
if ($statusLabel) {
|
|
$statusLabel.Text = "$Status update $Current of $Total..."
|
|
}
|
|
$Form.Refresh()
|
|
[System.Windows.Forms.Application]::DoEvents()
|
|
}
|
|
}
|
|
|
|
function Update-InstallationProgressBar {
|
|
param(
|
|
[System.Windows.Forms.Form]$Form,
|
|
[int]$PercentComplete,
|
|
[string]$Status
|
|
)
|
|
|
|
if ($Form -and -not $Form.IsDisposed) {
|
|
$mainProgressBar = $Form.Controls.Find("MainProgressBar", $false)[0]
|
|
$progressLabel = $Form.Controls.Find("ProgressLabel", $false)[0]
|
|
$statusLabel = $Form.Controls.Find("StatusLabel", $false)[0]
|
|
|
|
if ($mainProgressBar) {
|
|
$mainProgressBar.Value = [Math]::Min(100, [Math]::Max(0, $PercentComplete))
|
|
}
|
|
|
|
if ($progressLabel) {
|
|
$progressLabel.Text = "$PercentComplete%"
|
|
}
|
|
|
|
if ($statusLabel -and $Status) {
|
|
$statusLabel.Text = $Status
|
|
}
|
|
|
|
$Form.Refresh()
|
|
[System.Windows.Forms.Application]::DoEvents()
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
# Export functions
|
|
#region Install Progress Toasts
|
|
|
|
function Show-InstallProgressToast {
|
|
param([int]$Total, [string]$AppId = $script:APP_ID)
|
|
|
|
$script:ProgressSeq = [uint32]1
|
|
$data = [Windows.UI.Notifications.NotificationData]::new()
|
|
$data.Values["pv"] = "0"
|
|
$data.Values["pvs"] = "0 of $Total"
|
|
$data.Values["status"] = "Starting..."
|
|
$data.SequenceNumber = $script:ProgressSeq
|
|
|
|
$xml = New-Object Windows.Data.Xml.Dom.XmlDocument
|
|
$xml.LoadXml(@"
|
|
<toast tag="wum-progress" scenario="incomingCall">
|
|
<visual>
|
|
<binding template="ToastGeneric">
|
|
<text>Installing Windows Updates</text>
|
|
<text>Please do not shut down your computer.</text>
|
|
<progress value="{pv}" valueStringOverride="{pvs}" title="" status="{status}"/>
|
|
</binding>
|
|
</visual>
|
|
<audio silent="true"/>
|
|
</toast>
|
|
"@)
|
|
$toast = [Windows.UI.Notifications.ToastNotification]::new($xml)
|
|
$toast.Tag = $script:ProgressTag
|
|
$toast.Data = $data
|
|
[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier($AppId).Show($toast)
|
|
}
|
|
|
|
function Update-InstallProgressToast {
|
|
param(
|
|
[string]$Status,
|
|
[int]$Index,
|
|
[int]$Total,
|
|
[double]$Percent,
|
|
[string]$AppId = $script:APP_ID
|
|
)
|
|
|
|
$script:ProgressSeq++
|
|
$data = [Windows.UI.Notifications.NotificationData]::new()
|
|
$data.Values["pv"] = "$([math]::Round([math]::Max(0, [math]::Min(1, $Percent)), 2))"
|
|
$data.Values["pvs"] = if ($Index -gt 0) { "$Index of $Total" } else { "" }
|
|
$data.Values["status"] = $Status
|
|
$data.SequenceNumber = $script:ProgressSeq
|
|
|
|
try {
|
|
[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier($AppId).Update(
|
|
$data, $script:ProgressTag) | Out-Null
|
|
} catch {}
|
|
}
|
|
|
|
function Remove-InstallProgressToast {
|
|
param([string]$AppId = $script:APP_ID)
|
|
try {
|
|
[Windows.UI.Notifications.ToastNotificationManager]::History.Remove(
|
|
$script:ProgressTag, "", $AppId)
|
|
} catch {}
|
|
}
|
|
|
|
#endregion
|
|
|
|
Export-ModuleMember -Function Show-UpdateAvailableToast, Show-UpdateInstallingToast, `
|
|
Show-UpdateCompleteToast, Show-UpdateFailedToast, Show-RebootRequiredToast, `
|
|
Show-BalloonNotification, Show-UpdateDetailsWindow, Show-InstallationProgressWindow, `
|
|
Update-InstallationProgress, Update-InstallationProgressBar, `
|
|
Show-LoadingSplash, Close-LoadingSplash, Show-UpdatesAvailableNotification, `
|
|
Show-InstallProgressToast, Update-InstallProgressToast, Remove-InstallProgressToast |