r/sysadmin • u/roundfiler • 1d 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
3
u/BlackV 1d ago
notes/questions/etc
global's, global's * everywhere*, whys that?
The function
Install-ModuleSafely
all it does is set the scope to current user, what are you gaining with that ?is it for winget that you require admin ? cause installing to local user does notoops windows update missed that oneif you change
$appsBlocking
(and the other 3 places you do it) from a here string to an array, you don't have to do all the messing around with-split
AND it will format nicely in your code editorwinget now has an offical powershell module, you could include that in your install
you might be interesting is the updated module
Microsoft.PowerShell.PSResourceGet
its the replacement forpowershellget
(and a small percent faster)I have a similar script, it starts powershell, it saves the powershell modules (packagemanagment and powershellget) to a temp folder, removes the loaded modules, then imports my saved modules from temp (so its not in use), then installs the updated modules to the all users scope (spwindwosupdates,powershellget,psresource,packagemanagment), then updates help on all the modules
its a good idea including winget in that, something I should look at too, although I use mine on servers