224 lines
8.5 KiB
PowerShell
224 lines
8.5 KiB
PowerShell
<#
|
|
LXS - Update Windows
|
|
Description: Install pending Windows updates and upgrade every winget
|
|
package. Mirror of linux/tools/update-server.sh.
|
|
Repo: https://git.hyko.cx/hykocx/lxs
|
|
|
|
Usage: update-windows.ps1 [-Yes] [-NoWinget] [-NoWindowsUpdate] [-Help]
|
|
#>
|
|
|
|
# Load LXS common library (colors, UI helpers, spinner, loggers, guards).
|
|
# Prefers the sibling ..\lib\common.ps1 (repo checkout or installed layout) and
|
|
# only hits the network when this script is run standalone.
|
|
$LxsRawBase = if ($env:LXS_RAW_BASE) { $env:LXS_RAW_BASE } else { 'https://git.hyko.cx/hykocx/lxs/raw/branch/main' }
|
|
if (-not $env:LXS_RAW_PLATFORM_BASE) { $env:LXS_RAW_PLATFORM_BASE = "$LxsRawBase/windows" }
|
|
$LxsLibPath = if ($PSScriptRoot) { Join-Path $PSScriptRoot '..\lib\common.ps1' } else { $null }
|
|
if ($LxsLibPath -and (Test-Path $LxsLibPath)) {
|
|
. $LxsLibPath
|
|
} else {
|
|
try {
|
|
$LxsLibSource = Invoke-RestMethod -Uri "$env:LXS_RAW_PLATFORM_BASE/lib/common.ps1" -UseBasicParsing -ErrorAction Stop
|
|
} catch {
|
|
Write-Error 'Failed to fetch lib/common.ps1'
|
|
exit 1
|
|
}
|
|
# The library ships with a UTF-8 BOM (Windows PowerShell 5.1 needs it to
|
|
# read the file as UTF-8); over HTTP that BOM arrives as a leading U+FEFF
|
|
# character, which the parser will not accept.
|
|
. ([scriptblock]::Create(($LxsLibSource -replace '^\uFEFF', '')))
|
|
}
|
|
|
|
$env:LXS_LOG_FILE = Join-Path (Get-LxsTempDir) 'lxs_update_windows.log'
|
|
|
|
$LxsOpts = Read-LxsFlags -Arguments $args -Known @(
|
|
'-Yes', '-y', '-NoWinget', '-NoWindowsUpdate', '-Help', '-h'
|
|
)
|
|
if (Show-LxsUnknownFlags $LxsOpts) { exit 1 }
|
|
$AssumeYes = Test-LxsFlag $LxsOpts @('-Yes', '-y')
|
|
$NoWinget = Test-LxsFlag $LxsOpts @('-NoWinget')
|
|
$NoWindowsUpdate = Test-LxsFlag $LxsOpts @('-NoWindowsUpdate')
|
|
|
|
if (Test-LxsFlag $LxsOpts @('-Help', '-h')) {
|
|
Write-Host @'
|
|
Usage: update-windows.ps1 [options]
|
|
|
|
Options:
|
|
-Yes, -y Skip the confirmation prompt
|
|
-NoWinget Skip `winget upgrade --all`
|
|
-NoWindowsUpdate Skip Windows Update
|
|
-Help, -h Show this help
|
|
'@
|
|
exit 0
|
|
}
|
|
|
|
if (-not (Assert-LxsWindows)) { exit 1 }
|
|
if (-not $NoWindowsUpdate) { Assert-LxsAdmin -ScriptPath $PSCommandPath -Arguments $args }
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
# Windows Update via the built-in COM API. Deliberately not PSWindowsUpdate:
|
|
# that module has to be pulled from the PowerShell Gallery first, which fails
|
|
# on locked-down or offline machines. The COM surface ships with Windows.
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
function Invoke-LxsWindowsUpdate {
|
|
Write-Host ''
|
|
Show-LxsBoxMid 'WINDOWS UPDATE'
|
|
Write-Host ''
|
|
|
|
try {
|
|
$session = New-Object -ComObject Microsoft.Update.Session
|
|
$searcher = $session.CreateUpdateSearcher()
|
|
} catch {
|
|
Write-LxsErr "Windows Update API unavailable: $($_.Exception.Message)"
|
|
return $false
|
|
}
|
|
|
|
Write-LxsInfo 'Searching for updates (this can take several minutes)...'
|
|
try {
|
|
$result = $searcher.Search("IsInstalled=0 and Type='Software' and IsHidden=0")
|
|
} catch {
|
|
Write-LxsErr "Search failed: $($_.Exception.Message)"
|
|
return $false
|
|
}
|
|
|
|
$updates = @($result.Updates)
|
|
if ($updates.Count -eq 0) {
|
|
Write-LxsOk 'No pending updates'
|
|
return $true
|
|
}
|
|
|
|
Write-LxsOk "$($updates.Count) update(s) available:"
|
|
Write-Host ''
|
|
foreach ($u in $updates) {
|
|
$sizeMB = [math]::Round($u.MaxDownloadSize / 1MB, 1)
|
|
Write-Host " $($script:Gray)-$($script:NC) $($u.Title) $($script:Gray)(${sizeMB} MB)$($script:NC)"
|
|
}
|
|
Write-Host ''
|
|
|
|
if (-not (Confirm-LxsAction -Question 'Download and install these updates?' -DefaultYes -AssumeYes:$AssumeYes)) {
|
|
Write-LxsInfo 'Skipped.'
|
|
return $true
|
|
}
|
|
|
|
# EULAs must be accepted per-update before an unattended install.
|
|
$toInstall = New-Object -ComObject Microsoft.Update.UpdateColl
|
|
foreach ($u in $updates) {
|
|
if (-not $u.EulaAccepted) {
|
|
try { $u.AcceptEula() } catch { Write-LxsWarn "Could not accept the EULA for: $($u.Title)" }
|
|
}
|
|
[void]$toInstall.Add($u)
|
|
}
|
|
|
|
Write-Host ''
|
|
Write-LxsInfo 'Downloading...'
|
|
try {
|
|
$downloader = $session.CreateUpdateDownloader()
|
|
$downloader.Updates = $toInstall
|
|
$dlResult = $downloader.Download()
|
|
if ($dlResult.ResultCode -ne 2) {
|
|
Write-LxsWarn "Download finished with result code $($dlResult.ResultCode) (2 = success)."
|
|
} else {
|
|
Write-LxsOk 'Download complete'
|
|
}
|
|
} catch {
|
|
Write-LxsErr "Download failed: $($_.Exception.Message)"
|
|
return $false
|
|
}
|
|
|
|
Write-Host ''
|
|
Write-LxsInfo 'Installing...'
|
|
try {
|
|
$installer = $session.CreateUpdateInstaller()
|
|
$installer.Updates = $toInstall
|
|
$instResult = $installer.Install()
|
|
} catch {
|
|
Write-LxsErr "Install failed: $($_.Exception.Message)"
|
|
return $false
|
|
}
|
|
|
|
for ($i = 0; $i -lt $toInstall.Count; $i++) {
|
|
$code = $instResult.GetUpdateResult($i).ResultCode
|
|
$title = $toInstall.Item($i).Title
|
|
if ($code -eq 2) { Write-LxsOk $title } else { Write-LxsWarn "$title (result code $code)" }
|
|
}
|
|
|
|
Write-Host ''
|
|
if ($instResult.RebootRequired) {
|
|
Write-LxsWarn 'A reboot is required to finish installing these updates.'
|
|
} else {
|
|
Write-LxsOk 'Windows Update finished'
|
|
}
|
|
return $true
|
|
}
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
# winget
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
function Invoke-LxsWingetUpgrade {
|
|
Write-Host ''
|
|
Show-LxsBoxMid 'WINGET'
|
|
Write-Host ''
|
|
|
|
if (-not (Assert-LxsWinget)) { return $false }
|
|
|
|
Write-LxsInfo 'Packages with an available upgrade:'
|
|
Write-Host ''
|
|
& winget upgrade --include-unknown --accept-source-agreements | Out-String | Write-Host
|
|
|
|
if (-not (Confirm-LxsAction -Question 'Upgrade all of them?' -DefaultYes -AssumeYes:$AssumeYes)) {
|
|
Write-LxsInfo 'Skipped.'
|
|
return $true
|
|
}
|
|
|
|
Write-Host ''
|
|
& winget upgrade --all --include-unknown --silent `
|
|
--accept-package-agreements --accept-source-agreements --disable-interactivity
|
|
$code = $LASTEXITCODE
|
|
|
|
Write-Host ''
|
|
if ($code -eq 0) {
|
|
Write-LxsOk 'winget upgrade completed'
|
|
return $true
|
|
}
|
|
# -1978335189 = no applicable upgrade found; nothing actually went wrong.
|
|
if ($code -eq -1978335189) {
|
|
Write-LxsOk 'Everything is already up to date'
|
|
return $true
|
|
}
|
|
Write-LxsWarn "winget exited with code $code"
|
|
return $false
|
|
}
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
# Run
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
Clear-Host
|
|
Show-LxsBoxTop -Title 'UPDATE WINDOWS'
|
|
Write-Host ''
|
|
Write-Host 'The following will run on this machine:'
|
|
if (-not $NoWindowsUpdate) { Write-Host ' - Windows Update: search, download and install pending updates' }
|
|
if (-not $NoWinget) { Write-Host ' - winget: upgrade every package with a newer version' }
|
|
Write-Host ''
|
|
Show-LxsSeparator
|
|
|
|
if (-not (Confirm-LxsAction -Question 'Continue?' -DefaultYes -AssumeYes:$AssumeYes)) {
|
|
Write-LxsInfo 'Cancelled.'
|
|
exit 0
|
|
}
|
|
|
|
$failed = $false
|
|
if (-not $NoWindowsUpdate) { if (-not (Invoke-LxsWindowsUpdate)) { $failed = $true } }
|
|
if (-not $NoWinget) { if (-not (Invoke-LxsWingetUpgrade)) { $failed = $true } }
|
|
|
|
Write-Host ''
|
|
Show-LxsSeparator
|
|
Write-Host ''
|
|
if ($failed) {
|
|
Write-LxsWarn 'Update pass finished with warnings.'
|
|
exit 1
|
|
}
|
|
Write-LxsOk 'Update pass finished'
|
|
exit 0
|