<# .SYNOPSIS Windows Update Manager with Rich Notifications .DESCRIPTION Automated Windows Update manager with Windows 10/11 Action Center notifications and detailed update popup windows .NOTES Author: oxmc Version: 2.5 Requires: PowerShell 5.1+, Administrator privileges #> #Requires -RunAsAdministrator param( [switch]$Silent, [switch]$AutoInstall, [switch]$CheckOnly ) # Import notification module Import-Module "$PSScriptRoot\UpdateNotifications.psm1" -Force $script:DataDir = "$env:ProgramData\WindowsUpdateManager" $script:LogPath = "$script:DataDir\WindowsUpdate.log" $script:ConfigPath = "$PSScriptRoot\UpdateConfig.json" #region Configuration function Get-UpdateConfiguration { if (Test-Path $script:ConfigPath) { try { $config = Get-Content $script:ConfigPath -Raw | ConvertFrom-Json Write-Log "Configuration loaded from $script:ConfigPath" -Level "INFO" return $config } catch { Write-Log "Failed to load config, using defaults" -Level "WARNING" } } # Create default config $defaultConfig = @{ AutoInstall = $false AutoReboot = $false IncludeDrivers = $false IncludeOptional = $false ShowNotifications = $true CheckIntervalHours = 12 LogPath = "C:\Logs\WindowsUpdate.log" } # Save default config to file try { $defaultConfig | ConvertTo-Json -Depth 5 | Out-File $script:ConfigPath -Encoding UTF8 Write-Log "Created default configuration at $script:ConfigPath" -Level "SUCCESS" } catch { Write-Log "Could not create config file: $($_.Exception.Message)" -Level "WARNING" } return $defaultConfig } #endregion #region Logging function Write-Log { param( [string]$Message, [ValidateSet("INFO", "WARNING", "ERROR", "SUCCESS")] [string]$Level = "INFO" ) $logDir = Split-Path $script:LogPath -Parent if (!(Test-Path $logDir)) { New-Item -ItemType Directory -Path $logDir -Force | Out-Null } $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" $logEntry = "[$timestamp] [$Level] $Message" Add-Content -Path $script:LogPath -Value $logEntry $color = switch ($Level) { "ERROR" { "Red" } "WARNING" { "Yellow" } "SUCCESS" { "Green" } default { "White" } } Write-Host $logEntry -ForegroundColor $color } #endregion #region Update Functions function Start-UpdateService { param([string]$Name) $svc = Get-Service -Name $Name -ErrorAction SilentlyContinue if (-not $svc) { Write-Log "Service $Name not found" "ERROR"; return $false } if ($svc.Status -eq 'Running') { return $true } try { Start-Service -Name $Name -ErrorAction Stop return $true } catch { Write-Log "Could not start $Name`: $($_.Exception.Message)" "ERROR" return $false } } function Test-UpdateServices { Write-Log "Ensuring Windows Update services are available..." -Level "INFO" $ok = $true foreach ($svc in @("wuauserv", "BITS", "cryptsvc")) { if (-not (Start-UpdateService $svc)) { $ok = $false } } return $ok } function Repair-UpdateServices { Write-Log "Repair: stopping services and clearing corrupt cache..." -Level "WARNING" foreach ($svc in @("BITS", "wuauserv", "cryptsvc")) { Stop-Service -Name $svc -Force -ErrorAction SilentlyContinue } if (Test-Path "C:\Windows\SoftwareDistribution\Download") { Remove-Item "C:\Windows\SoftwareDistribution\Download\*" -Recurse -Force -ErrorAction SilentlyContinue Write-Log "Cleared SoftwareDistribution download cache" -Level "SUCCESS" } $ok = $true foreach ($svc in @("BITS", "wuauserv", "cryptsvc")) { if (-not (Start-UpdateService $svc)) { $ok = $false } } return $ok } function Get-AvailableUpdates { param([PSCustomObject]$Config) Write-Log "Searching for available updates..." -Level "INFO" try { $session = New-Object -ComObject "Microsoft.Update.Session" $searcher = $session.CreateUpdateSearcher() $criteria = "IsInstalled=0" Write-Log "Search criteria: $criteria" -Level "INFO" $searchResult = $searcher.Search($criteria) Write-Log "Found $($searchResult.Updates.Count) total updates" -Level "INFO" if ($searchResult.Updates.Count -eq 0) { return @() } # Filter updates based on config $filteredUpdates = @() foreach ($update in $searchResult.Updates) { $shouldInclude = $true # Debug: Show update type Write-Log "Evaluating: $($update.Title) - Type: $($update.Type)" -Level "INFO" # Filter drivers if (-not $Config.IncludeDrivers -and $update.Type -eq 2) { Write-Log "FILTERED: $($update.Title) - Driver update excluded by config" -Level "WARNING" $shouldInclude = $false } # Filter optional updates if ($shouldInclude -and -not $Config.IncludeOptional -and -not $update.IsMandatory) { # Check if it's a critical/security/important update $isImportant = $false foreach ($category in $update.Categories) { if ($category.Name -match "Critical|Security|Important|Definition") { $isImportant = $true break } } if (-not $isImportant) { Write-Log "FILTERED: $($update.Title) - Optional update excluded by config" -Level "WARNING" $shouldInclude = $false } } if ($shouldInclude) { Write-Log "INCLUDED: $($update.Title)" -Level "SUCCESS" $filteredUpdates += $update } } Write-Log "After filtering: $($filteredUpdates.Count) updates remaining" -Level "SUCCESS" return $filteredUpdates } catch { Write-Log "Error searching for updates: $($_.Exception.Message)" -Level "ERROR" return @() } } function Send-PipeMessage { param([System.IO.StreamWriter]$Writer, [hashtable]$Msg) try { $Writer.WriteLine(($Msg | ConvertTo-Json -Compress -Depth 4)) } catch {} } function Install-WindowsUpdates { param( [array]$Updates, [scriptblock]$OnMessage = $null # if set, progress goes to pipe instead of WinForms ) if (@($Updates).Count -eq 0) { Write-Log "No updates to install" -Level "INFO" return [PSCustomObject]@{ Succeeded = 0; Failed = 0; RebootRequired = $false } } Write-Log "Installing $($Updates.Count) update(s)..." -Level "INFO" $useWinForms = ($null -eq $OnMessage) $progressForm = $null if ($useWinForms) { Show-UpdateInstallingToast -UpdateCount $Updates.Count $progressForm = Show-InstallationProgressWindow -Updates $Updates } $emit = { param($msg) if ($OnMessage) { & $OnMessage $msg } } try { $session = New-Object -ComObject "Microsoft.Update.Session" $collection = New-Object -ComObject "Microsoft.Update.UpdateColl" $i = 1 foreach ($u in $Updates) { Write-Log "Queuing: $($u.Title)" -Level "INFO" if ($useWinForms) { Update-InstallationProgress -Form $progressForm -Current $i -Total $Updates.Count -CurrentUpdate $u.Title -Status "Preparing" } & $emit @{ cmd = "progress"; phase = "Preparing"; index = $i; total = $Updates.Count; title = $u.Title; percent = 0 } $collection.Add($u) | Out-Null $i++ } # Download Write-Log "Downloading..." -Level "INFO" if ($useWinForms) { Update-InstallationProgress -Form $progressForm -Current 0 -Total $Updates.Count -CurrentUpdate "Downloading..." -Status "Downloading" } & $emit @{ cmd = "progress"; phase = "Downloading"; index = 0; total = $Updates.Count; title = "Downloading updates..."; percent = 0.1 } $downloader = $session.CreateUpdateDownloader() $downloader.Updates = $collection $dlResult = $downloader.Download() if ($dlResult.ResultCode -ne 2) { Write-Log "Download failed (code $($dlResult.ResultCode))" -Level "ERROR" & $emit @{ cmd = "progress"; phase = "Error"; index = 0; total = $Updates.Count; title = "Download failed"; percent = 0 } if ($useWinForms) { if ($progressForm -and -not $progressForm.IsDisposed) { $progressForm.Close(); $progressForm.Dispose() } } return [PSCustomObject]@{ Succeeded = 0; Failed = $Updates.Count; RebootRequired = $false } } Write-Log "Download complete. Installing..." -Level "SUCCESS" if ($useWinForms) { Update-InstallationProgressBar -Form $progressForm -PercentComplete 50 -Status "Installing..." } & $emit @{ cmd = "progress"; phase = "Installing"; index = 0; total = $Updates.Count; title = "Installing updates..."; percent = 0.5 } $installer = $session.CreateUpdateInstaller() $installer.Updates = $collection $instResult = $installer.Install() if ($useWinForms -and $progressForm -and -not $progressForm.IsDisposed) { $progressForm.Close(); $progressForm.Dispose() } Write-Log "Install result code: $($instResult.ResultCode)" -Level "INFO" $succeeded = 0; $failed = 0 for ($j = 0; $j -lt $Updates.Count; $j++) { $ur = $instResult.GetUpdateResult($j) $u = $Updates[$j] $ok = $ur.ResultCode -in @(2, 3) if ($ok) { $succeeded++ } else { $failed++ } Write-Log "$(if ($ok) { 'OK' } else { 'FAIL' }): $($u.Title)" -Level $(if ($ok) { "SUCCESS" } else { "ERROR" }) & $emit @{ cmd = "progress" phase = if ($ok) { "Installed" } else { "Failed" } index = $j + 1 total = $Updates.Count title = $u.Title percent = ($j + 1) / $Updates.Count } } if ($instResult.RebootRequired) { Write-Log "Reboot required." -Level "WARNING" } return [PSCustomObject]@{ Succeeded = $succeeded; Failed = $failed; RebootRequired = $instResult.RebootRequired } } catch { if ($useWinForms -and $progressForm -and -not $progressForm.IsDisposed) { $progressForm.Close(); $progressForm.Dispose() } Write-Log "Installation error: $($_.Exception.Message)" -Level "ERROR" return [PSCustomObject]@{ Succeeded = 0; Failed = $Updates.Count; RebootRequired = $false } } } function Get-UpdateErrorMessage { param([int]$HResult) $errorMessages = @{ 0x80240001 = "WU_E_NO_SERVICE - Windows Update Agent was unable to provide the service" 0x80240002 = "WU_E_MAX_CAPACITY_REACHED - The maximum capacity of the service was exceeded" 0x80240016 = "WU_E_INSTALL_NOT_ALLOWED - Operation tried to install while another installation was in progress" 0x80240017 = "WU_E_NOT_APPLICABLE - Operation was not performed because there are no applicable updates" 0x80240022 = "WU_E_REBOOT_REQUIRED - The operation could not be completed because a reboot is required" 0x80246007 = "WU_E_DM_NOTDOWNLOADED - The update has not been downloaded (driver updates may require manual download)" 0x80070BC9 = "ERROR_FAIL_REBOOT_REQUIRED - The requested operation failed. A system reboot is required" 0x80070643 = "ERROR_INSTALL_FAILURE - Fatal error during installation" 0x80070490 = "ERROR_NOT_FOUND - Element not found" 0x800F0922 = "CBS_E_INSTALLERS_FAILED - Processing advanced installers failed" 0x80242014 = "WU_E_UH_POSTREBOOTSTILLPENDING - The post-reboot operation for the update is still in progress" 0x80242006 = "WU_E_UH_INVALIDMETADATA - Update contains invalid metadata" } if ($errorMessages.ContainsKey($HResult)) { return $errorMessages[$HResult] } else { $hexCode = "0x{0:X8}" -f $HResult return "Unknown error ($hexCode)" } } function Invoke-RebootRequest { param([bool]$RebootRequired) if (-not $RebootRequired) { return } $config = Get-UpdateConfiguration if ($config.AutoReboot -or $AutoInstall) { Write-Log "Auto-reboot enabled. System will restart in 5 minutes..." -Level "WARNING" # Show 5 minute warning Show-RebootRequiredToast -MinutesUntilReboot 5 Show-BalloonNotification -Title "Restart Required" ` -Message "Your computer will restart in 5 minutes to complete update installation" ` -Icon "Warning" Start-Sleep -Seconds 300 Restart-Computer -Force } else { # Show reboot notification without auto-restart Show-RebootRequiredToast -MinutesUntilReboot 0 Show-BalloonNotification -Title "Restart Required" ` -Message "Please restart your computer to complete update installation" ` -Icon "Warning" } } #endregion #region Main Logic function Start-UpdateCheck { Write-Log "=== Windows Update Manager v2.5 Starting ===" -Level "INFO" # Check services if (-not (Test-UpdateServices)) { Write-Log "Update services not running. Attempting repair..." -Level "WARNING" if (-not (Repair-UpdateServices)) { Write-Log "Failed to repair services. Exiting." -Level "ERROR" Show-BalloonNotification -Title "Service Error" ` -Message "Windows Update services could not be started" ` -Icon "Error" exit 1 } } # Load configuration $config = Get-UpdateConfiguration # Get available updates - force PS array so .Count is always reliable $updates = @(Get-AvailableUpdates -Config $config) if (@($updates).Count -eq 0) { Write-Log "No updates available" -Level "SUCCESS" if (-not $Silent) { Show-BalloonNotification -Title "Windows Update" ` -Message "Your system is up to date. No updates available." ` -Icon "Info" } return } Write-Log "Found $($updates.Count) available updates" -Level "SUCCESS" if ($CheckOnly) { Write-Log "Check-only mode. Exiting." -Level "INFO" return } $config = Get-UpdateConfiguration $pipeName = "WUM_IPC_$(Get-Date -Format 'yyyyMMddHHmmss')" if ($AutoInstall -or $config.AutoInstall) { $userLoggedIn = [bool](Get-Process explorer -ErrorAction SilentlyContinue) if ($userLoggedIn) { Write-Log "Auto-install: notifying user and installing via pipe..." -Level "INFO" Invoke-UpdatePipe -Updates $updates -PipeName $pipeName -AutoInstall } else { Write-Log "Auto-install: no user session, installing silently..." -Level "INFO" $result = Install-WindowsUpdates -Updates $updates if ($result.RebootRequired) { Invoke-RebootRequest -RebootRequired $true } } } else { Write-Log "Waiting for user approval via pipe..." -Level "INFO" Invoke-UpdatePipe -Updates $updates -PipeName $pipeName } } function Invoke-UpdatePipe { param( [array]$Updates, [string]$PipeName, [switch]$AutoInstall # skip approval, go straight to install+progress ) $pipeSecurity = [System.IO.Pipes.PipeSecurity]::new() $sid = [System.Security.Principal.SecurityIdentifier]::new( [System.Security.Principal.WellKnownSidType]::AuthenticatedUserSid, $null) $rule = [System.IO.Pipes.PipeAccessRule]::new( $sid, [System.IO.Pipes.PipeAccessRights]::ReadWrite, [System.Security.AccessControl.AccessControlType]::Allow) $pipeSecurity.AddAccessRule($rule) $pipe = [System.IO.Pipes.NamedPipeServerStream]::new( $PipeName, [System.IO.Pipes.PipeDirection]::InOut, 1, [System.IO.Pipes.PipeTransmissionMode]::Byte, [System.IO.Pipes.PipeOptions]::None, 65536, 65536, $pipeSecurity) $reader = $null $writer = $null try { $spawned = Invoke-UserInteractiveSession -PipeName $PipeName if (-not $spawned) { return } Write-Log "Pipe server listening on $PipeName ..." $cts = [System.Threading.CancellationTokenSource]::new([System.TimeSpan]::FromHours(2)) $task = $pipe.WaitForConnectionAsync($cts.Token) while (-not $task.IsCompleted) { Start-Sleep -Milliseconds 200 } if ($task.IsFaulted -or $task.IsCanceled) { Write-Log "Pipe wait cancelled." "WARN"; return } Write-Log "UI connected." $enc = [System.Text.UTF8Encoding]::new($false) $writer = [System.IO.StreamWriter]::new($pipe, $enc, 65536, $true) $reader = [System.IO.StreamReader]::new($pipe, $enc, $false, 65536, $true) $writer.AutoFlush = $true # Build the update metadata payload (IDs + titles for toast) $meta = @($Updates | ForEach-Object { [PSCustomObject]@{ UpdateID = $_.Identity.UpdateID; Title = $_.Title } }) $toInstall = $Updates if ($AutoInstall) { # Tell UI to show "installing" notification - no approval needed Send-PipeMessage $writer @{ cmd = "auto_install"; count = $Updates.Count; updates = @($meta | ForEach-Object { $_.Title }) } } else { # Send for user approval $writer.WriteLine(($meta | ConvertTo-Json -Compress)) Write-Log "Waiting for user approval..." $response = $reader.ReadLine() Write-Log "UI response received." if ([string]::IsNullOrEmpty($response) -or $response -eq "[]") { Write-Log "User dismissed - nothing to install." return } $approvedIds = @($response | ConvertFrom-Json) Write-Log "User approved $($approvedIds.Count) update(s). Re-querying WU..." $wuSession = New-Object -ComObject "Microsoft.Update.Session" $wuSearcher = $wuSession.CreateUpdateSearcher() $wuResult = $wuSearcher.Search("IsInstalled=0") $toInstall = @($wuResult.Updates | Where-Object { $_.Identity.UpdateID -in @($approvedIds) }) if ($toInstall.Count -eq 0) { Write-Log "Approved updates no longer in WU." "WARN"; return } } # Notify UI that install is starting Send-PipeMessage $writer @{ cmd = "install_start"; count = $toInstall.Count } # Install with progress pushed back to UI via pipe $progressCb = { param($msg) Send-PipeMessage $writer $msg } $result = Install-WindowsUpdates -Updates $toInstall -OnMessage $progressCb # Send completion Send-PipeMessage $writer @{ cmd = "complete" succeeded = $result.Succeeded failed = $result.Failed rebootRequired = $result.RebootRequired } Write-Log "Done. Succeeded: $($result.Succeeded) Failed: $($result.Failed) Reboot: $($result.RebootRequired)" if ($result.RebootRequired) { Invoke-RebootRequest -RebootRequired $true } } catch { Write-Log "Pipe error: $($_.Exception.Message)" "ERROR" try { Send-PipeMessage $writer @{ cmd = "error"; message = $_.Exception.Message } } catch {} } finally { if ($reader) { try { $reader.Dispose() } catch {} } if ($writer) { try { $writer.Dispose() } catch {} } try { $pipe.Close(); $pipe.Dispose() } catch {} } } Add-Type -TypeDefinition @' using System; using System.Runtime.InteropServices; public class WumSession { [DllImport("wtsapi32.dll", SetLastError=true)] static extern bool WTSQueryUserToken(uint sessionId, out IntPtr token); [DllImport("advapi32.dll", SetLastError=true, CharSet=CharSet.Auto)] static extern bool CreateProcessAsUser( IntPtr hToken, string app, string cmdLine, IntPtr procAttr, IntPtr threadAttr, bool inherit, uint flags, IntPtr env, string dir, ref STARTUPINFO si, out PROCESS_INFORMATION pi); [DllImport("userenv.dll", SetLastError=true)] static extern bool CreateEnvironmentBlock(out IntPtr env, IntPtr token, bool inherit); [DllImport("userenv.dll", SetLastError=true)] static extern bool DestroyEnvironmentBlock(IntPtr env); [DllImport("kernel32.dll")] static extern bool CloseHandle(IntPtr h); [StructLayout(LayoutKind.Sequential, CharSet=CharSet.Auto)] struct STARTUPINFO { public int cb, _r1; public string lpReserved, lpDesktop, lpTitle; public int dwX, dwY, dwXSize, dwYSize, dwXCountChars, dwYCountChars; public int dwFillAttribute, dwFlags; public short wShowWindow, cbReserved2; public IntPtr lpReserved2, hStdInput, hStdOutput, hStdError; } [StructLayout(LayoutKind.Sequential)] struct PROCESS_INFORMATION { public IntPtr hProcess, hThread; public int dwProcessId, dwThreadId; } public static bool SpawnInUserSession(uint sessionId, string commandLine) { IntPtr token = IntPtr.Zero, env = IntPtr.Zero; if (!WTSQueryUserToken(sessionId, out token)) return false; try { CreateEnvironmentBlock(out env, token, false); var si = new STARTUPINFO(); si.cb = Marshal.SizeOf(si); si.lpDesktop = "winsta0\\default"; PROCESS_INFORMATION pi; // CREATE_UNICODE_ENVIRONMENT | CREATE_NEW_CONSOLE bool ok = CreateProcessAsUser(token, null, commandLine, IntPtr.Zero, IntPtr.Zero, false, 0x410, env, null, ref si, out pi); if (ok) { CloseHandle(pi.hProcess); CloseHandle(pi.hThread); } return ok; } finally { if (env != IntPtr.Zero) DestroyEnvironmentBlock(env); CloseHandle(token); } } } '@ function Invoke-UserInteractiveSession { param([string]$PipeName) $uiScript = Join-Path $PSScriptRoot "WindowsUpdateManager-UI.ps1" if (-not (Test-Path $uiScript)) { Write-Log "UI script not found: $uiScript" "ERROR" return $false } $cmdLine = "PowerShell.exe -NoProfile -ExecutionPolicy Bypass -WindowStyle Normal -File `"$uiScript`" -PipeName `"$PipeName`"" $isSystem = [System.Security.Principal.WindowsIdentity]::GetCurrent().IsSystem if ($isSystem) { # Running as SYSTEM (scheduled task) - spawn directly into the user's desktop session $explorerProc = Get-Process explorer -ErrorAction SilentlyContinue | Select-Object -First 1 if (-not $explorerProc) { Write-Log "No interactive user session found - updates will be offered at next scheduled run." "WARN" return $false } $sessionId = [uint32]$explorerProc.SessionId $ok = [WumSession]::SpawnInUserSession($sessionId, $cmdLine) if ($ok) { Write-Log "UI process spawned in session $sessionId (pipe: $PipeName)" } else { Write-Log "SpawnInUserSession failed (error $([System.Runtime.InteropServices.Marshal]::GetLastWin32Error()))" "ERROR" } return $ok } else { # Running as interactive admin (dev/test) - keep window open so errors are visible Write-Log "Not SYSTEM - spawning UI in current session for testing (pipe: $PipeName)" "INFO" Start-Process -FilePath "PowerShell.exe" ` -ArgumentList "-NoProfile -ExecutionPolicy Bypass -WindowStyle Normal -File `"$uiScript`" -PipeName `"$PipeName`"" return $true } } # Main entry point try { Start-UpdateCheck Write-Log "=== Windows Update Manager Completed ===" -Level "SUCCESS" } catch { Write-Log "Fatal error: $($_.Exception.Message)" -Level "ERROR" Show-BalloonNotification -Title "Update Manager Error" ` -Message "An error occurred: $($_.Exception.Message)" ` -Icon "Error" exit 1 } #endregion