diff --git a/data/wsl/UpdateInstall.ps1 b/data/wsl/UpdateInstall.ps1 index 7ee17e20931f..30324d823b7b 100644 --- a/data/wsl/UpdateInstall.ps1 +++ b/data/wsl/UpdateInstall.ps1 @@ -1,84 +1,202 @@ -# Define a function to log messages with timestamps -function LogMessage { - param ( - [string]$Message, - [string]$Color = "White" - ) - $Timestamp = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss") - Write-Host "$Timestamp - $Message" -ForegroundColor $Color -} - -# Start logging -LogMessage "Starting Windows Update process..." +# Automated Windows Update Tester +# Features: Non-interactive, JSON reporting, Idempotent operations, CI/CD integration -# Create Update Session -$Session = New-Object -ComObject Microsoft.Update.Session -$Searcher = $Session.CreateUpdateSearcher() +param( + [switch]$NonInteractive, + [switch]$AutoReboot, + [string]$StatePath = "$env:TEMP\UpdateTestState.json" +) -# Search for Updates -LogMessage "Searching for updates..." -$SearchResult = $Searcher.Search("IsInstalled=0") +### Configuration +$MAX_RETRIES = 3 +$RETRY_DELAY = 30 # seconds -# Check if updates are available -if ($SearchResult.Updates.Count -eq 0) { - LogMessage "No updates found." -Color "Yellow" - return 0 +### Unified Logging System +$GLOBAL_LOG = @() +function LogEvent { + param($Message, $Level="INFO", $Data=$null) + $entry = @{ + Timestamp = [datetime]::UtcNow.ToString("o") + Level = $Level + Message = $Message + Data = $Data + } + $GLOBAL_LOG += $entry + Write-Host "[$($entry.Level)] $($entry.Message)" } -# Display updates found -LogMessage "$($SearchResult.Updates.Count) update(s) found." -$UpdatesToInstall = New-Object -ComObject Microsoft.Update.UpdateColl -foreach ($Update in $SearchResult.Updates) { - LogMessage "Update found: $($Update.Title)" - $UpdatesToInstall.Add($Update) | Out-Null +### State Manager +class UpdateState { + [string]$RunId = [guid]::NewGuid().ToString() + [datetime]$StartTime = [datetime]::UtcNow + [array]$DiscoveredUpdates = @() + [array]$InstalledUpdates = @() + [array]$PendingReboots = @() + [int]$RetryCount = 0 + [bool]$RebootPerformed = $false } -# Download updates individually -LogMessage "Downloading updates..." -foreach ($Update in $UpdatesToInstall) { - $Downloader = $Session.CreateUpdateDownloader() - $SingleUpdateColl = New-Object -ComObject Microsoft.Update.UpdateColl - $SingleUpdateColl.Add($Update) | Out-Null - $Downloader.Updates = $SingleUpdateColl - - LogMessage "Downloading: $($Update.Title)..." +function Get-State { + param($Path) try { - $DownloadResult = $Downloader.Download() - if ($DownloadResult.ResultCode -eq 2) { - LogMessage "Downloaded: $($Update.Title) successfully." -Color "Green" - } else { - LogMessage "Download failed for: $($Update.Title)" -Color "Yellow" - return 1 + if (Test-Path $Path) { + $content = Get-Content $Path -Raw + return $content | ConvertFrom-Json } } catch { - LogMessage "Error downloading $($Update.Title): $_" -Color "Red" - return 1 + $port.WriteLine(LogEvent "State load failed: $_" "ERROR") } + return [UpdateState]::new() } -# Install updates individually -LogMessage "Installing updates..." -foreach ($Update in $UpdatesToInstall) { - $Installer = $Session.CreateUpdateInstaller() - $SingleUpdateColl = New-Object -ComObject Microsoft.Update.UpdateColl - $SingleUpdateColl.Add($Update) | Out-Null - $Installer.Updates = $SingleUpdateColl +function Save-State { + param($State, $Path) + $State.EndTime = [datetime]::UtcNow + $State | ConvertTo-Json -Depth 5 | Out-File $Path +} + +### Core Update Operations +function Get-WindowsUpdates { + $session = New-Object -ComObject Microsoft.Update.Session + $searcher = $session.CreateUpdateSearcher() + $result = $searcher.Search("IsInstalled=0 and IsHidden=0") - LogMessage "Installing: $($Update.Title)..." - try { - $InstallResult = $Installer.Install() - if ($InstallResult.ResultCode -eq 2) { - LogMessage "Successfully installed: $($Update.Title)" -Color "Green" - } else { - LogMessage "Installation failed for: $($Update.Title). Result code: $($InstallResult.ResultCode)" -Color "Yellow" - return 1 + $updates = foreach ($u in $result.Updates) { + @{ + Id = $u.Identity.UpdateID + Title = $u.Title + KB = ($u.KBArticleIDs -join ', ') + RebootRequired = $u.InstallationBehavior.RebootBehavior -gt 0 + Categories = $u.Categories.Name } - } catch { - LogMessage "Error occurred during installation of $($Update.Title): $_" -Color "Red" - return 1 + } + + return $updates +} + +function Install-UpdateBatch { + param($Updates, $Session) + + $installer = $Session.CreateUpdateInstaller() + $installer.Updates = $Updates + $installer.AllowRestarts = $false + + $result = $installer.Install() + return @{ + ResultCode = $result.ResultCode + RebootRequired = $result.RebootRequired + FailedUpdates = if ($result.ResultCode -ne 2) { $Updates } else { @() } + } +} + +### Verification Systems +function Test-UpdateSuccess { + param($Update) + $session = New-Object -ComObject Microsoft.Update.Session + $history = $session.CreateUpdateSearcher().QueryHistory(0, 100) | + Where-Object { $_.Title -eq $Update.Title -and $_.Date -gt (Get-Date).AddDays(-1) } + + return [PSCustomObject]@{ + Installed = $history.ResultCode -contains 2 + PendingReboot = Test-RebootRequired + VerificationTime = [datetime]::UtcNow } } -# Completion message -LogMessage "Windows Update process completed." -return 0 \ No newline at end of file +### Main Workflow +try { + # Load state + $state = Get-State -Path $StatePath + + # Phase 1: Discovery + if (-not $state.DiscoveredUpdates) { + $port.WriteLine(LogEvent "Starting update discovery" "INFO") + $state.DiscoveredUpdates = Get-WindowsUpdates + Save-State $state $StatePath + } + + # Phase 2: Installation + while ($state.RetryCount -lt $MAX_RETRIES) { + try { + $session = New-Object -ComObject Microsoft.Update.Session + $updatesToInstall = $state.DiscoveredUpdates | + Where-Object { $_.Id -notin $state.InstalledUpdates.Id } + + if (-not $updatesToInstall) { + $port.WriteLine(LogEvent "All updates installed" "INFO") + break + } + + $port.WriteLine(LogEvent "Installing batch of updates" "INFO" @{Count=$updatesToInstall.Count}) + $result = Install-UpdateBatch -Updates $updatesToInstall -Session $session + + if ($result.ResultCode -eq 2) { + $state.InstalledUpdates += $updatesToInstall + $state.PendingReboots += $updatesToInstall | Where-Object RebootRequired + Save-State $state $StatePath + break + } else { + $state.RetryCount++ + $port.WriteLine(LogEvent "Installation failed, retry $($state.RetryCount)/$MAX_RETRIES" "WARN") + Start-Sleep -Seconds $RETRY_DELAY + } + } catch { + $port.WriteLine(LogEvent "Installation error: $_" "ERROR") + $port.WriteLine('2') + return + } + } + + # Phase 3: Reboot Management + if ($state.PendingReboots -and $AutoReboot) { + $port.WriteLine(LogEvent "Automated reboot initiated" "INFO") + Save-State $state $StatePath + if (-not $NonInteractive) { + Restart-Computer -Force + } + $port.WriteLine('0') + return + } + + # Phase 4: Post-Reboot Verification + if ($state.RebootPerformed) { + $port.WriteLine(LogEvent "Post-reboot verification started" "INFO") + $verificationResults = foreach ($update in $state.InstalledUpdates) { + $status = Test-UpdateSuccess $update + [PSCustomObject]@{ + Update = $update.Title + KB = $update.KB + Verified = $status.Installed + PendingReboot = $status.PendingReboot + } + } + + $state | Add-Member -NotePropertyName Verification -NotePropertyValue $verificationResults + Save-State $state $StatePath + } + + # Generate CI Report + $report = @{ + RunId = $state.RunId + TotalUpdates = $state.DiscoveredUpdates.Count + InstalledUpdates = $state.InstalledUpdates.Count + FailedUpdates = $state.DiscoveredUpdates.Count - $state.InstalledUpdates.Count + PendingReboot = [bool]$state.PendingReboots + VerificationStatus = if ($state.Verification) { + @{ + Verified = ($state.Verification | Where-Object Verified).Count + Failed = ($state.Verification | Where-Object { -not $_.Verified }).Count + } + } else { $null } + } + + $report | ConvertTo-Json -Depth 3 | Out-File "$env:TEMP\UpdateTestReport.json" + $port.WriteLine(LogEvent "Test cycle completed" "INFO" $report) + $port.WriteLine('0') + return + +} catch { + $port.WriteLine(LogEvent "Critical error: $_" "ERROR") + $port.WriteLine('1') + return +} \ No newline at end of file diff --git a/tests/wsl/install/update_windows.pm b/tests/wsl/install/update_windows.pm index a5ef318cd23b..bd16497f630a 100644 --- a/tests/wsl/install/update_windows.pm +++ b/tests/wsl/install/update_windows.pm @@ -14,10 +14,10 @@ sub run { my $vbs_url = data_url("wsl/UpdateInstall.ps1"); $self->open_powershell_as_admin; - $self->run_in_powershell(cmd => "Invoke-WebRequest -Uri \"$vbs_url\" -OutFile \"C:\\UpdateInstall.ps1\""); + $self->run_in_powershell(cmd => "Invoke-WebRequest -Uri \"$vbs_url\" -OutFile \"\$env:TEMP\\UpdateInstall.ps1\""); $self->run_in_powershell(cmd => "Set-ExecutionPolicy Bypass -Scope CurrentUser -Force"); $self->run_in_powershell( - cmd => 'cd \\; $port.WriteLine($(.\\UpdateInstall.ps1))', + cmd => "\$env:TEMP\\UpdateInstall.ps1 -NonInteractive", code => sub { die("Update script finished unespectedly or timed out...") unless wait_serial('0', timeout => 3600);