diff --git a/SRGAdminMenu.ps1 b/SRGAdminMenu.ps1 index 4be9466..7a7ddf0 100644 --- a/SRGAdminMenu.ps1 +++ b/SRGAdminMenu.ps1 @@ -122,6 +122,164 @@ function Remove-ProvisionedAndExisting { } } +function Invoke-WindowsUpdateRepair { + param([string]$Computer) + + $sb = { + function Test-Admin { + $id = [Security.Principal.WindowsIdentity]::GetCurrent() + $p = New-Object Security.Principal.WindowsPrincipal($id) + $p.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) + } + + if (-not (Test-Admin)) { throw "You must run this elevated (Administrator)." } + + # Local helper for progress (only available when running locally) + function Invoke-DismWithProgress { + param([string[]]$Arguments) + $ts = Get-Date -Format 'yyyyMMdd_HHmmss' + $outf = Join-Path $env:TEMP "dism_out_$ts.txt" + $errf = Join-Path $env:TEMP "dism_err_$ts.txt" + if (-not ($Arguments -match '/LogPath:')) { + $logFile = Join-Path $env:TEMP "dism_log_$ts.log" + $Arguments += "/LogPath:$logFile" + } + $p = Start-Process dism.exe -ArgumentList ($Arguments -join ' ') -NoNewWindow -RedirectStandardOutput $outf -RedirectStandardError $errf -PassThru + $lastPct = -1 + while (-not $p.HasExited) { + if (Test-Path $outf) { + $content = Get-Content $outf -Raw -ErrorAction SilentlyContinue + if ($content) { + $m = [regex]::Matches($content, '(\d{1,3}(?:\.\d+)?)%') + if ($m.Count -gt 0) { + $pct = [int][math]::Min(100, [double]$m[$m.Count-1].Value) + if ($pct -ne $lastPct) { + Write-Progress -Activity "DISM $($Arguments -join ' ')" -Status "$pct%" -PercentComplete $pct + $lastPct = $pct + } + } else { + Write-Progress -Activity "DISM $($Arguments -join ' ')" -Status "Working..." -PercentComplete 0 + } + } + } + Start-Sleep -Milliseconds 400 + } + Write-Progress -Activity "DISM $($Arguments -join ' ')" -Completed + if ($p.ExitCode -ne 0) { + Write-Warning "DISM exited with code $($p.ExitCode)" + if (Test-Path $errf) { Get-Content $errf -Tail 25 | Write-Warning } + } + return $p.ExitCode + } + + Write-Host "=== Step 1: DISM (ScanHealth) ===" -ForegroundColor Cyan + $e1 = Invoke-DismWithProgress -Arguments @('/Online','/Cleanup-Image','/ScanHealth') + if ($e1 -ne 0) { throw "DISM ScanHealth failed with code $e1" } + + Write-Host "`n=== Step 1b: DISM (RestoreHealth) ===" -ForegroundColor Cyan + $e2 = Invoke-DismWithProgress -Arguments @('/Online','/Cleanup-Image','/RestoreHealth','/NoRestart') + if ($e2 -ne 0) { throw "DISM RestoreHealth failed with code $e2" } + + Write-Host "`n=== Step 2: SFC ===" -ForegroundColor Cyan + & sfc.exe /scannow | Out-Host + + Write-Host "`n=== Step 3: Reset Windows Update components ===" -ForegroundColor Cyan + $services = 'wuauserv','bits','cryptsvc','msiserver' + foreach ($s in $services) { try { Stop-Service -Name $s -Force -ErrorAction Stop } catch { } } + + $sd = Join-Path $env:SystemRoot 'SoftwareDistribution' + $cat = Join-Path $env:SystemRoot 'System32\catroot2' + $ts = Get-Date -Format 'yyyyMMdd_HHmmss' + if (Test-Path $sd) { try { Rename-Item $sd ("SoftwareDistribution.bak_$ts") -ErrorAction Stop } catch { } } + if (Test-Path $cat) { try { Rename-Item $cat ("catroot2.bak_$ts") -ErrorAction Stop } catch { } } + foreach ($s in $services) { try { Start-Service -Name $s -ErrorAction Stop } catch { } } + + Write-Host "`n=== Step 4: Trigger update scan ===" -ForegroundColor Cyan + foreach ($cmd in @( + { & usoclient.exe StartScan }, + { & usoclient.exe StartDownload }, + { & usoclient.exe StartInstall }, + { & wuauclt.exe /detectnow }, + { & wuauclt.exe /reportnow } + )) { try { & $cmd | Out-Null } catch { } } + + Write-Host "`nWindows Update repair sequence completed." -ForegroundColor Green + } + + if ([string]::IsNullOrWhiteSpace($Computer) -or $Computer -in @('localhost','127.0.0.1','.')) { + & $sb + } else { + # Over remoting, Write-Progress doesn't render well—still run the repair, + # but you'll see normal text output. (If you want, we can add a remote-side + # log tail that writes periodic progress messages.) + Invoke-Remote -Computer $Computer -ScriptBlock $sb + } +} + +function Invoke-DismWithProgress { + <# + Runs DISM with a real progress bar. + Returns: DISM exit code (0 = success) + Note: Progress display is local-only (remoting doesn't surface Write-Progress well). + #> + param( + [Parameter(Mandatory)][string[]]$Arguments + ) + + $ts = Get-Date -Format 'yyyyMMdd_HHmmss' + $outf = Join-Path $env:TEMP "dism_out_$ts.txt" + $errf = Join-Path $env:TEMP "dism_err_$ts.txt" + + # Ensure we get a log file too (helps debugging) + if (-not ($Arguments -match '/LogPath:')) { + $logFile = Join-Path $env:TEMP "dism_log_$ts.log" + $Arguments += "/LogPath:$logFile" + } + + # Launch DISM and redirect output so we can parse percentages safely + $p = Start-Process -FilePath dism.exe ` + -ArgumentList ($Arguments -join ' ') ` + -NoNewWindow ` + -RedirectStandardOutput $outf ` + -RedirectStandardError $errf ` + -PassThru + + $lastPct = -1 + while (-not $p.HasExited) { + if (Test-Path $outf) { + # Read current output and grab the latest percentage like "63.8%" or "63%" + $content = Get-Content -Path $outf -Raw -ErrorAction SilentlyContinue + if ($content) { + $m = [regex]::Matches($content, '(\d{1,3}(?:\.\d+)?)%') + if ($m.Count -gt 0) { + $pct = [int][math]::Min(100, [double]$m[$m.Count-1].Groups[1].Value) + if ($pct -ne $lastPct) { + Write-Progress -Activity "DISM $($Arguments -join ' ')" -Status "$pct%" -PercentComplete $pct + $lastPct = $pct + } + } else { + # No explicit percent yet—show a spinner + Write-Progress -Activity "DISM $($Arguments -join ' ')" -Status "Working..." -PercentComplete 0 + } + } + } + Start-Sleep -Milliseconds 400 + } + + # Complete the bar cleanly + Write-Progress -Activity "DISM $($Arguments -join ' ')" -Completed + + $exit = $p.ExitCode + + # Show a quick summary if it failed + if ($exit -ne 0) { + Write-Warning "DISM exited with code $exit" + if (Test-Path $errf) { Get-Content $errf -Tail 25 | Write-Warning } + } + + return $exit +} + # ------- task functions (menu sections) ------- function Do-ConnectRemote { @@ -263,6 +421,40 @@ function do-wingetupgrades { winget upgrade --all --silent --accept-source-agreements --accept-package-agreements } +function do-winupdatefix { + do { + Clear-Host + Write-Host "=== Windows Update Repair ===" -ForegroundColor Cyan + Write-Host " 1) Local machine" + Write-Host " 2) Remote machine" + Write-Host " 0) Back`n" + $t = Read-Host "Choose target" + + switch ($t) { + '1' { + $go = Read-Host "This will run DISM/SFC and reset WU components on THIS machine. Proceed? (Y/N)" + if ($go -in @('Y','y')) { + try { Invoke-WindowsUpdateRepair } catch { Write-Warning $_ } + } + Read-Host "`nPress Enter to continue" | Out-Null + } + '2' { + $comp = Read-Host "Enter remote computer name" + if ([string]::IsNullOrWhiteSpace($comp)) { break } + $go = Read-Host "Run repair on '$comp'? This requires admin on that machine. Proceed? (Y/N)" + if ($go -in @('Y','y')) { + try { Invoke-WindowsUpdateRepair -Computer $comp } catch { Write-Warning $_ } + } + Read-Host "`nPress Enter to continue" | Out-Null + } + '0' { return } # leave submenu back to main menu + default { + Write-Host "Invalid choice." -ForegroundColor Yellow + Start-Sleep -Milliseconds 800 + } + } + } while ($true) +} # ------- Main Menu ------- do { @@ -272,6 +464,7 @@ do { Write-Host " 2) Remove Windows apps (local or remote)" Write-Host " 3) Restart a service (submenu)" write-Host " 4) Run Winget Upgrades" + write-Host " 5) Run Windows Update Repair" Write-Host " 0) Quit`n" $choice = Read-Host "Choose an option" @@ -280,6 +473,7 @@ do { '2' { Do-RemoveAppxMenu } '3' { Do-RestartServiceMenu } '4' { do-wingetupgrades } + '5' { do-winupdatefix } '0' { exit } default { Write-Host "Invalid choice." -ForegroundColor Yellow