#Requires -RunAsAdministrator <# .SYNOPSIS Nerdy Neighbor - Force Windows Update .DESCRIPTION Installs every pending Windows Update right now: security and cumulative updates, .NET, drivers, Defender definitions, and Microsoft Update items (Office and other Microsoft products). Keeps searching and installing until nothing is left, or a reboot is needed to continue. Uses the built-in Windows Update Agent (no modules to download). It goes straight to Microsoft's servers, bypassing a stale WSUS setting left behind by an old domain or a "debloat" tool. If the Windows Update search is broken, it resets the Windows Update components once and retries. .NOTES Run: irm winupdate.nerdyneighbor.net | iex (elevated Windows PowerShell) Log: C:\ProgramData\NerdyNeighbor\updates.log Options (set BEFORE the irm line, since iex can't take parameters): $env:NN_REBOOT = 'yes' # reboot automatically when finished, if needed # 'no' = never reboot. Default: ask (interactive), no (RMM) $env:NN_DRIVERS = 'no' # skip driver updates $env:NN_OPTIONAL = 'yes' # also install optional updates (preview CUs, optional drivers) #> $ErrorActionPreference = 'Stop' [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 # When run via `irm ... | iex` the #Requires line is NOT enforced (that only # works for a real .ps1 file), so check for elevation ourselves. $isAdmin = ([Security.Principal.WindowsPrincipal] ` [Security.Principal.WindowsIdentity]::GetCurrent() ).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) if (-not $isAdmin) { Write-Host "" Write-Host " This needs an ELEVATED PowerShell (Run as Administrator)." -ForegroundColor Red Write-Host " Close this window, reopen PowerShell as Administrator, and run again." -ForegroundColor Yellow Write-Host "" return } # --- Settings ------------------------------------------------------------------ $ScriptUrl = 'https://winupdate.nerdyneighbor.net' $MicrosoftUpdate = '7971f918-a847-4430-9279-4a52d1efe18d' # "Microsoft Update" service (Office etc.) $MaxPasses = 4 $me = [Security.Principal.WindowsIdentity]::GetCurrent() $script:Interactive = [Environment]::UserInteractive -and -not $me.IsSystem -and -not [Console]::IsInputRedirected $RebootMode = "$env:NN_REBOOT".Trim().ToLower() $WantDrivers = "$env:NN_DRIVERS".Trim().ToLower() -ne 'no' $WantOptional = "$env:NN_OPTIONAL".Trim().ToLower() -eq 'yes' # --- Logging ------------------------------------------------------------------- $LogDir = Join-Path $env:ProgramData 'NerdyNeighbor' $LogFile = Join-Path $LogDir 'updates.log' if (-not (Test-Path $LogDir)) { New-Item -ItemType Directory -Path $LogDir -Force | Out-Null } function Write-Log { param([string]$Message, [string]$Level = 'INFO') $line = '{0} [{1}] {2}' -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $Level, $Message Add-Content -Path $LogFile -Value $line -ErrorAction SilentlyContinue switch ($Level) { 'ERROR' { Write-Host " $Message" -ForegroundColor Red } 'WARN' { Write-Host " $Message" -ForegroundColor Yellow } 'OK' { Write-Host " $Message" -ForegroundColor Green } 'STEP' { Write-Host " $Message" -ForegroundColor White } default { Write-Host " $Message" -ForegroundColor Gray } } } function Format-HResult($e) { $h = $e.Exception.HResult if ($e.Exception.InnerException) { $h = $e.Exception.InnerException.HResult } '0x{0:X8}' -f $h } # --- Windows Update plumbing --------------------------------------------------------- function Enable-UpdateServices { # Debloat tools sometimes disable these, and then every search fails. foreach ($svc in 'wuauserv', 'bits', 'cryptsvc', 'UsoSvc') { $s = Get-Service $svc -ErrorAction SilentlyContinue if (-not $s) { continue } if ($s.StartType -eq 'Disabled') { Set-Service $svc -StartupType Manual Write-Log "Service $svc was DISABLED - set to Manual." 'WARN' } if ($s.Status -ne 'Running') { Start-Service $svc -ErrorAction SilentlyContinue } } } function Reset-UpdateComponents { Write-Log "Resetting Windows Update components (clears the download cache)..." 'STEP' $svcs = 'wuauserv', 'bits', 'cryptsvc', 'UsoSvc' foreach ($s in $svcs) { Stop-Service $s -Force -ErrorAction SilentlyContinue } $stamp = Get-Date -Format 'yyyyMMddHHmmss' foreach ($d in "$env:SystemRoot\SoftwareDistribution", "$env:SystemRoot\System32\catroot2") { if (Test-Path $d) { Rename-Item $d "$(Split-Path $d -Leaf).nn-$stamp" -ErrorAction SilentlyContinue } } foreach ($s in $svcs) { Start-Service $s -ErrorAction SilentlyContinue } Write-Log "Windows Update components reset." 'OK' } function Register-MicrosoftUpdate { try { $sm = New-Object -ComObject Microsoft.Update.ServiceManager $already = $sm.Services | Where-Object { $_.ServiceID -eq $MicrosoftUpdate } if (-not $already) { $sm.AddService2($MicrosoftUpdate, 7, '') | Out-Null Write-Log "Opted in to Microsoft Update (Office and other Microsoft products)." } return $true } catch { Write-Log "Couldn't opt in to Microsoft Update ($(Format-HResult $_)) - using Windows Update only." 'WARN' return $false } } function Find-PendingUpdates($Session, [bool]$UseMU) { $searcher = $Session.CreateUpdateSearcher() if ($UseMU) { $searcher.ServerSelection = 3; $searcher.ServiceID = $MicrosoftUpdate } # skip WSUS else { $searcher.ServerSelection = 2 } # Windows Update $criteria = 'IsInstalled=0 and IsHidden=0' if (-not $WantDrivers) { $criteria += " and Type='Software'" } $result = $searcher.Search($criteria) $list = @() foreach ($u in $result.Updates) { if ($u.BrowseOnly -and -not $WantOptional) { continue } $list += $u } return , $list } function Get-UpdateLabel($u) { $kb = if ($u.KBArticleIDs.Count -gt 0) { "KB$($u.KBArticleIDs.Item(0))" } else { '' } $mb = [math]::Round($u.MaxDownloadSize / 1MB) $t = $u.Title if ($t.Length -gt 80) { $t = $t.Substring(0, 77) + '...' } if ($kb -and $t -notmatch $kb) { $t = "$t ($kb)" } if ($mb -gt 0) { $t = "$t - ${mb} MB" } return $t } # Windows Update refuses to download/install from a remote logon (SSH, WinRM): # 0x80070005 access denied. Re-run this script as SYSTEM through a scheduled # task and stream its log here instead. function Invoke-AsSystem { Write-Log "Remote session detected - handing off to a SYSTEM task." 'WARN' $envs = @() foreach ($n in 'NN_REBOOT', 'NN_DRIVERS', 'NN_OPTIONAL') { $v = [Environment]::GetEnvironmentVariable($n) if ($v) { $envs += "`$env:$n='$($v -replace "'", "''")';" } } $cmd = ($envs -join ' ') + " irm $ScriptUrl | iex" $task = 'NN-WindowsUpdate' $act = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument "-NoProfile -ExecutionPolicy Bypass -Command `"$cmd`"" $prin = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest Register-ScheduledTask -TaskName $task -Action $act -Principal $prin -Force | Out-Null $startLine = (Get-Content $LogFile -ErrorAction SilentlyContinue).Count Start-ScheduledTask -TaskName $task Start-Sleep -Seconds 3 while ((Get-ScheduledTask -TaskName $task).State -eq 'Running') { $lines = Get-Content $LogFile -ErrorAction SilentlyContinue if ($lines.Count -gt $startLine) { $lines[$startLine..($lines.Count - 1)] | ForEach-Object { Write-Host " [SYSTEM] $_" -ForegroundColor Gray } $startLine = $lines.Count } Start-Sleep -Seconds 5 } $lines = Get-Content $LogFile -ErrorAction SilentlyContinue if ($lines.Count -gt $startLine) { $lines[$startLine..($lines.Count - 1)] | ForEach-Object { Write-Host " [SYSTEM] $_" -ForegroundColor Gray } } Unregister-ScheduledTask -TaskName $task -Confirm:$false -ErrorAction SilentlyContinue } # --- Main ------------------------------------------------------------------------------- try { Write-Host "" Write-Host " Nerdy Neighbor - Force Windows Update" -ForegroundColor Cyan Write-Host "" Write-Log "=== Run started on $env:COMPUTERNAME (user: $env:USERNAME, interactive: $script:Interactive) ===" Write-Log ("Drivers: {0} Optional updates: {1}" -f $(if ($WantDrivers) { 'yes' } else { 'no' }), $(if ($WantOptional) { 'yes' } else { 'no' })) Enable-UpdateServices $useMU = Register-MicrosoftUpdate $session = New-Object -ComObject Microsoft.Update.Session $session.ClientApplicationID = 'Nerdy Neighbor updates' $installed = @(); $failed = @(); $reset = $false; $handedOff = $false $sysInfo = New-Object -ComObject Microsoft.Update.SystemInfo for ($pass = 1; $pass -le $MaxPasses; $pass++) { Write-Log "Checking for updates (pass $pass)... this can take a few minutes." 'STEP' try { $pending = Find-PendingUpdates $session $useMU } catch { $code = Format-HResult $_ if ($reset) { throw "Windows Update search failed again after a reset ($code)." } Write-Log "Update search failed ($code)." 'WARN' Reset-UpdateComponents $reset = $true $pass-- continue } if ($pending.Count -eq 0) { if ($pass -eq 1) { Write-Log "This PC is already up to date." 'OK' } else { Write-Log "No more updates to install." 'OK' } break } Write-Log ("Found {0} update(s):" -f $pending.Count) $pending | ForEach-Object { Write-Log (" - " + (Get-UpdateLabel $_)) } $i = 0 foreach ($u in $pending) { $i++ $label = Get-UpdateLabel $u if (-not $u.EulaAccepted) { $u.AcceptEula() } $one = New-Object -ComObject Microsoft.Update.UpdateColl $one.Add($u) | Out-Null try { Write-Log ("[{0}/{1}] Downloading: {2}" -f $i, $pending.Count, $label) 'STEP' $dl = $session.CreateUpdateDownloader(); $dl.Updates = $one $dr = $dl.Download() if ($dr.ResultCode -notin 2, 3) { throw "download result $($dr.ResultCode)" } Write-Log ("[{0}/{1}] Installing..." -f $i, $pending.Count) $inst = $session.CreateUpdateInstaller(); $inst.Updates = $one $inst.ForceQuiet = $true $ir = $inst.Install() if ($ir.ResultCode -in 2, 3) { $installed += $label Write-Log ("[{0}/{1}] Installed." -f $i, $pending.Count) 'OK' } else { $hr = '0x{0:X8}' -f $ir.GetUpdateResult(0).HResult $failed += "$label (result $($ir.ResultCode), $hr)" Write-Log ("[{0}/{1}] FAILED to install." -f $i, $pending.Count) 'ERROR' } } catch { $code = Format-HResult $_ if ($code -eq '0x80070005' -and -not $me.IsSystem) { $handedOff = $true; break } $failed += "$label ($code)" Write-Log ("[{0}/{1}] FAILED: {2}" -f $i, $pending.Count, $code) 'ERROR' } } if ($handedOff) { break } if ($sysInfo.RebootRequired) { Write-Log "A restart is needed before the remaining updates can install." 'WARN' break } } if ($handedOff) { Invoke-AsSystem; return } # --- Summary ----------------------------------------------------------------------- Write-Host "" Write-Log ("Installed: {0} Failed: {1}" -f $installed.Count, $failed.Count) $(if ($failed.Count) { 'WARN' } else { 'OK' }) foreach ($f in $failed) { Write-Log " failed: $f" 'WARN' } if ($sysInfo.RebootRequired) { $doReboot = $false if ($RebootMode -eq 'yes') { $doReboot = $true } elseif ($RebootMode -ne 'no' -and $script:Interactive) { $a = Read-Host " Restart now to finish installing? [Y/n]" $doReboot = $a.Trim().ToLower() -ne 'n' } if ($doReboot) { Write-Log "Restarting in 30 seconds. Run this again after the restart to catch any follow-up updates." 'WARN' shutdown.exe /r /t 30 /c "Nerdy Neighbor: restarting to finish Windows Updates" | Out-Null } else { Write-Log "Restart required to finish installing. Run this again after the restart to catch any follow-up updates." 'WARN' } } else { Write-Log "Done - no restart needed." 'OK' } Write-Host "" } catch { Write-Log "FAILED: $($_.Exception.Message)" 'ERROR' Write-Host " Log: $LogFile" -ForegroundColor Yellow Write-Host "" }