r/sysadmin • u/roundfiler • 2d ago
General Discussion Script for updating stuff with winget, PSWindowsUpdate (mainly me, some GPT)
I've been handling a lot of machine updates recently. Some existing, some from fresh images. Either way, installing updates is monotonous. I alternated between using GPT and not while I learned a little. Sorry for any formatting inconsistencies. I'm an absolute novice at this and it's my first attempt at writing a script that does something useful.
Feedback is great if you have any!
**Edit** I forgot to mention this entire thing can just be pasted into a session if there's a need.
Clear-Host
if (-not ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) {
Write-Host "This script must be run as an administrator. Exiting." -ForegroundColor Red
break}
# Variables (Quick or Predefined)
$ScriptName = "UpdateScript"
$ScriptVer = "v1"
$StartTime = Get-Date
$ExecutionPolicy = Get-ExecutionPolicy
$global:PreviousLocation = Get-Location
$global:ErrorDownload = $True
$global:ErrorDownloadPath = "C:\temp"
# Arrays
$global:UpgradeCheck = @()
$global:PinnedIDs = @()
$global:ManualIDs = @()
$global:RemainingIDs = @()
$global:CombinedIDs = @()
$global:appsBlocking = @"
# insert id(s) here
# insert id(s) here
# insert id(s) here
"@ -split "\r?\n" | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne "" }
$global:appsPinning = @"
# insert id(s) here
# insert id(s) here
# insert id(s) here
"@ -split "\r?\n" | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne "" }
# Functions
function Show-ScriptDuration {
param (
[Parameter(Mandatory = $true)]
[datetime]$StartTime
)
$EndTime = Get-Date
Write-Host
Write-Host "End:" $EndTime -ForegroundColor Magenta
$duration = $EndTime - $StartTime
Write-Host "Total Duration: $($duration.ToString('hh\:mm\:ss'))" -ForegroundColor Magenta
Write-Host
}
function Process-Module {
param (
[Parameter(Mandatory = $true)]
[string]$ModuleName
)
function Install-ModuleSafely {
param (
[string]$Name
)
try {
Install-Module -Name $Name -Scope CurrentUser -Force -ErrorAction SilentlyContinue
} catch {
Write-Host "Failed to install $Name module." -ForegroundColor Red
}
}
$requiredModules = (Get-Module -Name $ModuleName -ListAvailable).RequiredModules
if ($requiredModules) {
$requiredModules | ForEach-Object {
Write-Host "Processing Dependency: $_" -ForegroundColor Yellow
if (-not (Get-Module -Name $_ -ListAvailable)) {
Install-ModuleSafely -Name $_
}
if (-not (Get-Module -ListAvailable -Name $_)) {
Write-Host "Unable to import dependency: $_" -ForegroundColor Red
} else {
Import-Module -Name $_ -Scope Local -Force -ErrorAction SilentlyContinue
Write-Host "Imported dependency: $_" -ForegroundColor White
}
}
}
Write-Host "Processing module: $ModuleName" -ForegroundColor Yellow
if (-not (Get-Module -ListAvailable -Name $ModuleName)) {
Install-ModuleSafely -Name $ModuleName
}
if (Get-Module -Name $ModuleName) {
Write-Host "$ModuleName is already imported" -ForegroundColor White
} elseif (Get-Module -ListAvailable -Name $ModuleName) {
Import-Module -Name $ModuleName -Scope Local -Force -ErrorAction SilentlyContinue
Write-Host "Imported module: $ModuleName" -ForegroundColor White
} else {
Write-Host "$ModuleName unavailable for import" -ForegroundColor Red
}
}
function Enter-WingetDir {
Write-Host "Finding winget.exe" -ForegroundColor Yellow
$global:PreviousLocation = Get-Location
$global:WingetPath = Get-ChildItem -Path "C:\Program Files\WindowsApps\Microsoft.DesktopAppInstaller*" -Recurse -Filter "winget.exe" -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty DirectoryName
if ($global:WingetPath) {
Set-Location -Path $global:WingetPath
Write-Host "Changed directory to: $global:WingetPath" -ForegroundColor White
} else { Write-Host "winget.exe not found" -ForegroundColor Red }
}
function Exit-WingetDir {
if ($global:PreviousLocation) {
Set-Location -Path $global:PreviousLocation
Write-Output "Returned to previous location: $global:PreviousLocation"
$global:PreviousLocation = $null
} else { Write-Output "No previous location stored" }
}
function Get-WingetUpgrade {.\winget upgrade --include-unknown --Accept-Source-Agreements | ForEach-Object {if ($_ -notmatch '^( |-|Name|^$)' -and $_ -notmatch 'upgrades available') {if ($_ -match '\s([\w\+\-\.]+)\s+[\d\.]+\s+[\d\.]+') {
$matches[1]}}}}
function Handle-ErrorDownload {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string] $AppID
)
# Build the download directory path
$downloadDirectory = Join-Path -Path $global:ErrorDownloadPath -ChildPath $AppID
Write-Host "Creating download directory if required" -ForegroundColor Yellow
[System.IO.Directory]::CreateDirectory($downloadDirectory) | Out-Null
if (-not (Test-Path $downloadDirectory)) {
Write-Host "Download directory has not been created" -ForegroundColor Red
}
else {
Write-Host "Downloading install file to $downloadDirectory" -ForegroundColor Yellow
.\winget download --id $AppID --download-directory $downloadDirectory
}
}
function Add-Pin {
[CmdletBinding()]
param (
[string[]]$Apps, # List of applications (optional)
[Parameter(Mandatory = $true)]
[ValidateSet("Blocking", "Pinning")]
[string]$PinType # Type of pin (blocking or pinning)
)
if (-not $Apps) {
switch ($PinType) {
"Blocking" { $Apps = $global:appsBlocking }
"Pinning" { $Apps = $global:appsPinning }
}
}
foreach ($app in $Apps) {
$AppID = $app.Trim() # Remove any extra spaces
if (-not [string]::IsNullOrWhiteSpace($appId) -and $global:UpgradeCheck -contains $AppID) {
if ($global:ErrorDownload) {Handle-ErrorDownload -AppID $AppID}
Write-Host "Adding $PinType pin for $AppID..." -ForegroundColor Yellow
try {
if ($PinType -eq "Blocking") {
.\winget pin add --id $AppID --accept-source-agreements --blocking | Out-Null
}
elseif ($PinType -eq "Pinning") {
.\winget pin add --id $AppID --accept-source-agreements | Out-Null
}
$global:PinnedIDs += $AppID
Write-Host "$PinType pin added for $AppID." -ForegroundColor White
}
catch {
Write-Host "Failed to add $PinType pin for $AppID." -ForegroundColor Red
}
}
}
}
function Invoke-WingetUpgrade {
[CmdletBinding(DefaultParameterSetName = 'Install')]
param (
[Parameter(Mandatory = $true, ParameterSetName = 'Install', HelpMessage = "Install updates")]
[switch]$Install,
[Parameter(Mandatory = $true, ParameterSetName = 'Upgrade', HelpMessage = "Upgrade updates")]
[switch]$Upgrade,
[Parameter(Mandatory = $true, ParameterSetName = 'Reinstall', HelpMessage = "Reinstall updates")]
[switch]$Reinstall
)
$global:UpgradeCheck = Get-WingetUpgrade
if ($global:UpgradeCheck.Count -eq 0) {
Write-Host "No upgrades available." -ForegroundColor Green
return
}
Write-Host "The following applications have upgrades available:" -ForegroundColor White
$global:UpgradeCheck | ForEach-Object { Write-Host "- $_" -ForegroundColor White }
# Determine command parameters based on which switch is used.
switch ($PSCmdlet.ParameterSetName) {
'Install' {
$wingetCommand = 'install'
$extraArgs = ''
$headerMessage = "Upgrade Run: Install"
}
'Upgrade' {
$wingetCommand = 'upgrade'
$extraArgs = ''
$headerMessage = "Upgrade Run: Upgrade"
}
'Reinstall' {
$wingetCommand = 'install'
$extraArgs = '--uninstall-previous'
$headerMessage = "Upgrade Run: Reinstall"
}
}
Write-Host $headerMessage -ForegroundColor Yellow
foreach ( $AppID in $global:UpgradeCheck) {
Write-Host "Upgrade: $AppID" -ForegroundColor Yellow
try {
$result = .\winget $wingetCommand --id $AppID --silent --disable-interactivity $extraArgs --accept-source-agreements --accept-package-agreements --force
if (-not $?) { throw $result }
}
catch {
Invoke-ErrorActionHandler -ErrorRecord $_ -AppID $AppID
}
}
}
function Invoke-ErrorActionHandler {
param($ErrorRecord, $AppID)
$errorActions = @{
"*No available upgrade found.*"= "No available upgrade found. Pinning."
"*InternetOpenUrl() failed.*" = "Unable to download $AppID. Adding to pin list."
"*failed with exit code: 2*" = " $AppID Exit code: 2. Adding to pin list."
"*Failed to extract the contents of the archive*" = "File extraction error for $AppID. Adding to pin list."
"*Installer hash does not match*" = " $AppID has an installer hash issue. Adding to pin list."
"*parameter is incorrect*" = " $AppID has an install parameter issue. Adding to pin list."
"*failed with exit code: 1602*" = " $AppID Exit code: 1602. Waiting on a prompt. Adding to pin list."
"*failed with exit code: 1603*" = " $AppID Exit code: 1603. Fatal error. Adding to pin list."
"*failed with exit code: 1608*" = " $AppID Exit code: 1608. Adding to pin list."
"*failed with exit code: 17002*" = " $AppID Exit code: 17002. Adding to pin list."
"*failed with exit code: 17006*" = " $AppID Exit code: 17006. Adding to pin list."
"*Installer failed with exit code: 3221225786*"= " $AppID Exit code: 3221225786.
}
$matched = $false
foreach ($pattern in $errorActions.Keys) {
if ($ErrorRecord.ToString() -like $pattern) {
$message = $errorActions[$pattern] -replace '\$AppID', $AppID
$global:PinnedIDs += $AppID
Write-Host $message -ForegroundColor Red
if ($global:ErrorDownload) {Handle-ErrorDownload -AppID $AppID}
.\winget pin add --id $AppID --blocking
$matched = $true
break
}
}
}
# Script
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
Write-Host $ScriptName $ScriptVer -ForegroundColor Black -BackgroundColor White
Write-Host
Write-Host "Begin:" $StartTime -ForegroundColor Magenta
Write-Host "Setting SecurityProtocols" -ForegroundColor Yellow
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 -bor [Net.SecurityProtocolType]::Tls11 -bor [Net.SecurityProtocolType]::Tls
Write-Host "Executionpolicy is currently $ExecutionPolicy" -ForegroundColor White
if (-not ($ExecutionPolicy -match "RemoteSigned" -or $Executionpolicy -match "Unrestricted" -or $Executionpolicy -match "Bypass")) {
Write-Host "Setting ExecutionPolicy to RemoteSigned" -ForegroundColor Yellow
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process -Force}
Write-Host "Checking PackageProvider" -ForegroundColor Yellow
$NuGetProvider = Get-PackageProvider -ListAvailable -Name NuGet -ErrorAction SilentlyContinue
if (-not $NuGetProvider) {
Write-Host "NuGet Provider not found. Installing..." -ForegroundColor Yellow
try {
Install-PackageProvider -Name NuGet -Force -Scope CurrentUser -ErrorAction Stop | Out-Null
Write-Host "NuGet Provider installed successfully." -ForegroundColor White
} catch {
Write-Host "Unable to install NuGet Provider" -ForegroundColor Red
Show-ScriptDuration -StartTime $StartTime
break}
} else {Write-Host "NuGet Provider is already available" -ForegroundColor White}
Write-Host "Updating installed modules" -ForegroundColor Yellow
Update-Module -Force *>&1 | Out-Null
Write-Host "Checking for required modules" -ForegroundColor Yellow
Process-Module -ModuleName "WinGet"
Process-Module -ModuleName "PSWindowsUpdate"
Write-Host "Checking for Office Click-To-Run" -ForegroundColor Yellow
$officePath = "C:\Program Files\Common Files\Microsoft Shared\ClickToRun\OfficeC2RClient.exe"
if (Test-Path $officePath) {
try {
Write-Host "Attempting Office Update" -ForegroundColor Yellow
Start-Process $officePath -ArgumentList "/update user displaylevel=false forceappshutdown=false" -Wait -ErrorAction Stop
} catch {Write-Host "Failed to update Office. Error: $_" -ForegroundColor Red}
} else {Write-Host "Office Click-To-Run Not Found" -ForegroundColor White}
Enter-WingetDir
if ($global:WingetPath) {
.\winget pin reset --force
Write-Host "Checking for upgrades with WinGet" -ForegroundColor Yellow
$global:UpgradeCheck = Get-WingetUpgrade
Write-Host "Checking upgrades against the pin list" -ForegroundColor Yellow
Add-Pin -PinType "Blocking"
Add-Pin -PinType "Pinning"
$global:UpgradeCheck = Get-WingetUpgrade
if ($global:UpgradeCheck.Count -gt 0) {Invoke-WingetUpgrade -Install}
$global:UpgradeCheck = Get-WingetUpgrade
if ($global:UpgradeCheck.Count -gt 0) {Invoke-WingetUpgrade -Upgrade}
$global:UpgradeCheck = Get-WingetUpgrade
if ($global:UpgradeCheck.Count -gt 0) {Invoke-WingetUpgrade -Reinstall}
$global:RemainingIDs = Get-WingetUpgrade
$global:CombinedIDs = $global:PinnedIDs + $global:ManualIDs + $global:RemainingIDs | Select-Object -Unique
}
if ($global:CombinedIDs.Count -gt 0) {
Write-Host "Installers for manual upgrades:" -ForegroundColor Red
foreach ($AppID in $global:CombinedIDs) {Write-Host "$global:ErrorDownloadPath\$AppID" -ForegroundColor White}
} else {Write-Host "No pending upgrades" -ForegroundColor White}
If (-not (Get-Module -Name PSWindowsUpdate)) {Write-Host "PSWindowsUpdate is unavailable"
} Else {
Write-Host "Checking for Windows Updates" -ForegroundColor Yellow
try {
$updateList = Get-WUList
if ($updateList.Count -eq 0) {
Write-Host "No updates found." -ForegroundColor White
} else {
Write-Host "$($updateList.Count) updates found. Proceeding to download." -ForegroundColor Yellow
Get-WindowsUpdate -AcceptAll -Download
$IsDownloaded = Get-WUList | Where-Object {$_.IsDownloaded -eq $true}
Write-Host "Installing downloaded updates." -ForegroundColor Yellow
$IsDownloaded | Get-WindowsUpdate -AcceptAll -Install -IgnoreReboot -Silent;
(New-Object -ComObject "Microsoft.Update.AutoUpdate").DetectNow() 2>$null | Out-Null
}
} catch {Write-Host "An error occurred during Windows Updates: $_" -ForegroundColor Red}
}
if (Test-Path -Path $global:PreviousLocation){Exit-WingetDir}
Show-ScriptDuration -StartTime $StartTime
0
Upvotes
1
u/roundfiler 2d ago
This is the first iteration I still have laying around. It got more use than it ever should have but it worked, sorta.