This script is an extract of a longer version with the features of simultaneous executions and dealing with computers that have Internet connectivity – click here
Current version
# updateWindowsInvokeScheduledTasks.ps1
# Version 0.0.2
# Set a list of remote machines
$computernames=@(
'TESTWINDOWS0001',
'TESTWINDOWS0002',
'TESTWINDOWS0003'
)
# Domain Admin credentials
$adminUsername='domain\admin'
$plaintextPassword='SOMEVERYCOMPLEXPASS'
$encryptedPassword=ConvertTo-securestring $plaintextPassword -AsPlainText -Force
$adminCredential=New-Object -TypeName System.Management.Automation.PSCredential -Args $adminUsername,$encryptedPassword
$autoReboot=$false
$directMicrosoftUpdates=$false
function updateWindowsInvokeScheduledTasks{
[CmdletBinding()]
param (
[parameter(Mandatory=$true,Position=0)][string[]]$computernames,
[parameter(Mandatory=$false,Position=1)][System.Management.Automation.PSCredential]$adminCredential,
[parameter(Mandatory=$false,Position=6)][bool]$autoReboot=$false,
[parameter(Mandatory=$false,Position=2)][bool]$directMicrosoftUpdates=$false,
[parameter(Mandatory=$false,Position=4)][int]$winRmPort=5985,
[parameter(Mandatory=$false,Position=5)][string]$logFileName='PSWindowsUpdate.log'
)
# $ErrorActionPreference='stop'
$results=[hashtable]@{}
function installPsWindowsUpdate{
$ErrorActionPreference='stop'
$psWindowsUpdateAvailable=Get-Module -ListAvailable -Name PSWindowsUpdate -EA SilentlyContinue;
if (!($psWindowsUpdateAvailable)){
try {
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
if(Get-PackageProvider 'nuget' -ea SilentlyContinue -Force){
Install-PackageProvider -Name Nuget -RequiredVersion 2.8.5.201 -Force
}
if((Get-PSRepository psgallery).InstallationPolicy -ne 'Trusted'){
$null=Set-PSRepository -Name 'PSGallery' -InstallationPolicy Trusted
}
$psWindowsUpdate=Get-Module -ListAvailable -Name 'PSWindowsUpdate' -EA Ignore
if(!$psWindowsUpdate){
$null=Install-Module PSWindowsUpdate -Confirm:$false -Force
}
# Register Microsoft Update Service if it has not been registered
$null=Import-Module PSWindowsUpdate -force
$microsoftUpdateId='7971f918-a847-4430-9279-4a52d1efe18d'
if (!($microsoftUpdateId -in (Get-WUServiceManager).ServiceID)){
Add-WUServiceManager -ServiceID $microsoftUpdateId -Confirm:$false
}
return $true;
}
catch{
write-host "Prerequisites not met on $ENV:COMPUTERNAME.";
return $false;
}
}else{
# Register Microsoft Update Service if it has not been registered
$microsoftUpdateId='7971f918-a847-4430-9279-4a52d1efe18d'
if (!($microsoftUpdateId -in (Get-WUServiceManager).ServiceID)){
Add-WUServiceManager -ServiceID $microsoftUpdateId -Confirm:$false
}
return $true
}
}
function connectWinRm($computer,$adminCredential,$winRmPort=5985){
if(!$computer){
write-warning "Computer name must be specified to initiate a WinRM connection."
return $false
}
# Legacy equivalent to Test-Netconnection
function checkNetConnection($computername,$port,$timeout=1000,$verbose=$false) {
$tcp = New-Object System.Net.Sockets.TcpClient;
try {
$connect=$tcp.BeginConnect($computername,$port,$null,$null)
$wait = $connect.AsyncWaitHandle.WaitOne($timeout,$false)
if(!$wait){
$null=$tcp.EndConnect($connect)
$tcp.Close()
if($verbose){
Write-Host "Connection Timeout" -ForegroundColor Red
}
Return $false
}else{
$error.Clear()
$null=$tcp.EndConnect($connect) # Dispose of the connection to release memory
if(!$?){
if($verbose){
write-host $error[0].Exception.Message -ForegroundColor Red
}
$tcp.Close()
return $false
}
$tcp.Close()
Return $true
}
} catch {
return $false
}
}
function enableWinRmRemotely($remoteComputer,$winRmPort,$adminCredential){
function Check-NetConnection($computername,$port,$timeout=1000,$verbose=$false) {
$tcp = New-Object System.Net.Sockets.TcpClient;
try {
$connect=$tcp.BeginConnect($computername,$port,$null,$null)
$wait = $connect.AsyncWaitHandle.WaitOne($timeout,$false)
if(!$wait){
$null=$tcp.EndConnect($connect)
$tcp.Close()
if($verbose){
Write-Host "Connection Timeout" -ForegroundColor Red
}
Return $false
}else{
$error.Clear()
$null=$tcp.EndConnect($connect) # Dispose of the connection to release memory
if(!$?){
if($verbose){
write-host $error[0].Exception.Message -ForegroundColor Red
}
$tcp.Close()
return $false
}
$tcp.Close()
Return $true
}
} catch {
return $false
}
}
if (!(get-command psexec)){
# Install Chocolatey
if (!(Get-Command choco.exe -ErrorAction SilentlyContinue)) {
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12;
Set-ExecutionPolicy Bypass -Scope Process -Force; iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))
}
choco install sysinternals -y;
}
$success=check-netconnection $remoteComputer $winRmPort
write-host 'Attempting to use psexec to enable WinRM remotely...'
if(!$adminCredential){ # Enable WinRM Remotely
$null=psexec.exe \\$remoteComputer -s C:\Windows\system32\winrm.cmd qc -quiet;
}else{
$username=$adminCredential.Username
$password=$adminCredential.GetNetworkCredential().Password
$null=psexec.exe \\$remoteComputer -u $username -p $password -s C:\Windows\system32\winrm.cmd qc -quiet
}
return check-netconnection $remoteComputer $winRmPort
}
# If machine is not pingable, wait five minutes
$fiveMinuteTimer=[System.Diagnostics.Stopwatch]::StartNew()
do{
$ping = Test-Connection $computer -quiet
if($ping -eq $false){sleep 1}
$pastFiveMinutes=$fiveMinuteTimer.Elapsed.TotalMinutes -ge 5
}until ($ping -eq $true -or $pastFiveMinutes)
$fiveMinuteTimer.stop()
$winRmAvailable=checkNetConnection $computer $winRmPort
if(!$winRmAvailable){
write-host "Attempting to enable WinRM on $computer" -ForegroundColor Yellow
$enableWinRmSuccessful=enableWinRmRemotely $computer
if($enableWinRmSuccessful){
write-host "WinRM enabled: $enableWinRmSuccessful"
}else{
write-warning "WinRM could not be enabled remotely. WinRM connection aborted."
return $false
}
}
# Wait for WinRm session prior to proceeding
if($session.state -eq 'Opened'){remove-pssession $session}
do{
$session=if($adminCredential){
try{
New-PSSession -ComputerName $computer -Credential $adminCredential -ea Stop
}catch{
New-PSSession -ComputerName $computer -Credential $adminCredential -SessionOption $(new-pssessionoption -IncludePortInSPN)
}
}else{
try{
New-PSSession -ComputerName $computer -ea Stop
}catch{
New-PSSession -ComputerName $computer -SessionOption $(new-pssessionoption -IncludePortInSPN)
}
}
sleep -seconds 1
if ($session){
write-host "Connected to $computer."
return $session
}
} until ($session.state -match "Opened")
}
function includePsWindowsUpdate($session){
$psWindowsUpdateIsAvailable=invoke-command -session $session -scriptblock {
param($installPsWindowsUpdate);
# Register Microsoft Update Service if it has not been registered
return [ScriptBlock]::Create($installPsWindowsUpdate).Invoke()
} -Args ${function:installPsWindowsUpdate}
if($psWindowsUpdateIsAvailable){
return $true
}else{
return $false
}
}
function cleanupWuJob($session,$logFile="C:\$logFileName"){
invoke-command -Session $session -ScriptBlock {
param($logFile)
if (Get-ScheduledTask -TaskName "PSWindowsUpdate" -ErrorAction SilentlyContinue){
Write-Host "Removing PSWindowsUpdate scheduled task from $env:computername..."
Unregister-ScheduledTask -TaskName 'PSWindowsUpdate' -Confirm:$false};
if (Test-Path $logFile -ErrorAction SilentlyContinue){
Write-Host "Removing $logFile..."
Remove-item $logFile -force
}
} -Args $logFile
}
function updateLocalWindowsUsingComObjects($autoReboot=$false,$logfile="C:\PSWindowsUpdate.log"){
# in case contents of function is called from scheduled tasks without any default argument values
if(!(test-path $logfile)){
new-item $logfile -type file -force
}
$status="$(get-date) => $env:computername patching STARTED"
write-host $status
Add-Content -Path $logfile -Value $status
$updateSession=New-Object -Com Microsoft.Update.Session
$updateSearcher=$updateSession.CreateUpdateSearcher()
$searchCriteria="IsInstalled=0 AND Type='Software'" # "IsHidden=0 AND IsInstalled=0 AND Type='Software'"
$availableUpdates=$updateSearcher.Search($searchCriteria)
$availableUpdatesCount=$availableUpdates.Updates.count
if($availableUpdatesCount -eq 0){
$status="$(get-date) => $env:computername has 0 available updates. Patching FINISHED"
write-host $status
Add-Content -Path $logfile -Value $status
return $true
}else{
$status="$(get-date) => $availableUpdatesCount available updates detected"
write-host $status
Add-Content -Path $logfile -Value $status
$updatesToDownload=New-Object -Com Microsoft.Update.UpdateColl
For ($i=0; $i -lt $availableUpdates.Updates.Count; $i++){
$item = $availableUpdates.Updates.Item($i)
$Null = $updatesToDownload.Add($item)
}
$updateSession=New-Object -Com Microsoft.Update.Session
$downloader=$updateSession.CreateUpdateDownloader()
$downloader.Updates=$updatesToDownload
try{
$null=$downloader.Download()
}catch{
write-warning $_
}
$downloadedUpdates=New-Object -Com Microsoft.Update.UpdateColl
For ($i=0; $i -lt $availableUpdates.Updates.Count; $i++){
$item = $availableUpdates.Updates.Item($i)
If ($item.IsDownloaded) {
$null=$downloadedUpdates.Add($item)
}
}
$downloadedCount=$downloadedUpdates.count
if($downloadedCount -eq 0){
$status="$(get-date) => Installation cannot proceed 0 successful downloads."
write-host $status
Add-Content -Path $logfile -Value $status
return $false
}else{
$status="$(get-date) => $downloadedCount of $availableUpdatesCount updates downloaded successfully"
write-host $status
Add-Content -Path $logfile -Value $status
$installCount=0
foreach($update in $downloadedUpdates){
$thisUpdate = New-object -com "Microsoft.Update.UpdateColl"
$thisCount=$installCount++ +1
$null=$thisUpdate.Add($update)
$installer = $updateSession.CreateUpdateInstaller()
$installer.Updates = $thisUpdate
$installResult = $installer.Install()
$translatedCode=switch ($installResult.ResultCode){
0 {'not started'}
1 {'in progress'}
2 {'succeeded'}
3 {'succeeded with errors'}
4 {'failed'}
5 {'aborted'}
}
$status="$(get-date) => $thisCount of $downloadedCount $translatedCode`: $($update.Title)"
write-host $status
Add-Content -Path $logfile -Value $status
}
}
}
$pendingReboot=.{
if (Get-ChildItem "HKLM:\Software\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending" -EA Ignore) { return $true }
if (Get-Item "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired" -EA Ignore) { return $true }
if (Get-ItemProperty "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager" -Name PendingFileRenameOperations -EA Ignore) { return $true }
try {
$util = [wmiclass]"\\.\root\ccm\clientsdk:CCM_ClientUtilities"
$status = $util.DetermineIfRebootPending()
if (($null -ne $status) -and $status.RebootPending) {
return $true
}
}catch{}
return $false
}
If($pendingReboot){
if($autoReboot){
$status="$(get-date) => $env:computername REBOOTED"
write-host $status
Add-Content -Path $logfile -Value $status
(Get-WMIObject -Class Win32_OperatingSystem).Reboot()
}else{
$status="$(get-date) => $env:computername required a REBOOT"
write-host $status
Add-Content -Path $logfile -Value $status
}
}else{
$status="$(get-date) => $env:computername patching FINISHED"
write-host $status
Add-Content -Path $logfile -Value $status
}
}
function invokeScheduledTaskCallPowerShellFunction{
param(
[string[]]$computernames,
[string]$scriptblock,
[string]$taskName,
[string]$description,
[string]$repeatIntervalMinutes,
[System.Management.Automation.PSCredential]$credential
)
$username=$credential.Username;
$plaintextPassword=$credential.GetNetworkCredential().password;
if ($credential){
foreach ($computer in $computernames){
write-host "Adding task on $computer..."
$thisSession=if($credential){
try{
New-PSSession -ComputerName $computer -Credential $credential -ea Stop
}catch{
New-PSSession -ComputerName $computer -Credential $credential -SessionOption $(new-pssessionoption -IncludePortInSPN)
}
}else{
try{
New-PSSession -ComputerName $computer -ea Stop
}catch{
New-PSSession -ComputerName $computer -SessionOption $(new-pssessionoption -IncludePortInSPN)
}
}
if ($thisSession.state -match "Opened"){
$scheduledTaskAdded=Invoke-Command -session $thisSession -ScriptBlock{
param($scriptblock,$taskName,$description,$repeatMinutes,$user,$password)
$windowsVersion=[Environment]::OSVersion.Version
$windowsUpdateScript='C:\Scripts\WindowsUpdates.ps1'
if(!(test-path $windowsUpdateScript)){
$null=new-item $windowsUpdateScript -type File -force
$null=Unblock-File -Path $windowsUpdateScript
}
$null=Set-Content -Path $windowsUpdateScript -Value $scriptblock
if($windowsVersion -ge [version]'6.2'){
$username=if($user -notmatch '\\'){"$env:USERDOMAIN`\$user"}else{$user}
#$encryptedPassword=ConvertTo-SecureString $password -AsPlainText -Force
#$adminCredential=New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $username,$encryptedPassword;
# Unrestrict this Domain Administrator from security prompts
Set-Executionpolicy -Scope CurrentUser -ExecutionPolicy UnRestricted -Force
$settingsCommand = New-ScheduledTaskSettingsSet -MultipleInstances IgnoreNew -ExecutionTimeLimit 0
$callPowerShell = New-ScheduledTaskAction -Execute "Powershell.exe" -Argument "-ExecutionPolicy Bypass $windowsUpdateScript"
$runNow=$false
$taskTrigger = if($repeatMinutes -gt 0){
New-ScheduledTaskTrigger -Once -At (Get-Date) -RepetitionInterval (New-TimeSpan -Minutes $repeatMinutes)
}else{
$runNow=$true;
New-ScheduledTaskTrigger -Once -At (get-date).AddSeconds(-1)
}
# Unregister the Scheduled task if it already exists
Get-ScheduledTask $taskName -ErrorAction SilentlyContinue | Unregister-ScheduledTask -Confirm:$false;
# Create new scheduled task
$null=Register-ScheduledTask -Action $callPowerShell -Trigger $taskTrigger `
-TaskName $taskName -Description $description `
-User $username -Password $password `
-Settings $settingsCommand -RunLevel Highest;
if($runNow){
Start-ScheduledTask -TaskName $taskname
$timer = [Diagnostics.Stopwatch]::StartNew()
while (((Get-ScheduledTask -TaskName $taskname).State -ne 'Running') -or $null -eq $(gc C:\PSWindowsUpdate.log -EA Ignore)) {
Write-Verbose -Message "Waiting on scheduled task..."
Start-Sleep -Seconds 2
}
Write-host "$taskname has taken $([math]::round(($timer.Elapsed.TotalSeconds),2)) seconds to initiate"
$timer.Stop()
start-sleep -seconds 5
}
return $true
}else{
write-host "$env:computername is too old. Just turn it off."
return $false
}
} -ArgumentList $scriptblock,$taskName,$description,$repeatIntervalMinutes,$username,$plainTextPassword
Remove-PSSession $thisSession
return $scheduledTaskAdded
}
}
}else{
write-host "Please run this program with a valid Administrator account."
return $false
}
}
$regexIP = [regex] "\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b"
$names=$(foreach($computername in $computernames){
if($computername -match $regexIp){
[System.Net.Dns]::GetHostByAddress($computername).hostname.toUpper()
}else{$computername.toUpper()}
})|select-Object -Unique
foreach($computer in $names){
write-warning "Now performing Windows updates on $computer...`r`nPress Ctrl+C at anytime to cancel."
$session=connectWinRm $computer $adminCredential
$taskName='windowsUpdate'
$taskDescription='Perform Windows Updates'
$taskAdded=invokeScheduledTaskCallPowerShellFunction `
-computernames $computer `
-scriptblock $function:updateLocalWindowsUsingComObjects `
-taskName $taskName `
-description $taskDescription `
-repeatIntervalMinutes 0 `
-credential $adminCredential
if($taskAdded){
$updatesLog="\\$computer\C$\$logfileName"
$localUpdatesLog="C:\$logfileName"
$logContent=$previousLine=''
$updatesCompleted=$false
write-host "Checking $updatesLog for Windows Update statuses..."
while(!$updatesCompleted){
$logContent=try{
get-content $updatesLog -ea Stop
}catch{
invoke-command -ComputerName $computer -Credential $adminCredential -ScriptBlock{
param($localUpdatesLog)
get-content $localUpdatesLog
} -Args $localUpdatesLog
}
if($logContent){
$currentLine=($logContent|select -last 1|out-string).trim()
$updatesCompleted=$currentLine -match 'FINISHED$'
$rebootRequired=$currentLine -match 'REBOOT$'
#$rebooted=$currentLine -match 'REBOOTED$'
if($currentLine -ne $previousLine){
write-host "`r`n$currentLine"
$previousLine=$currentLine
}else{
write-host '.' -nonewline
sleep 10
}
if($rebootRequired -and $autoReboot){
# remove-pssession $sesion
# $server=gwmi Win32_operatingsystem -computer $computer
# $status = $server.reboot()
# if ($status.ReturnValue = "0") {
# Write-Host "Reboot successful."
# }else{
# Write-Host "Reboot failed."
# }
# do {
# Start-Sleep -s 2
# }while (Test-Connection $computer -count 1 -EA Ignore)
write-warning "Reboot flag detected and `$autoReboot flag has been set to `$True...`r`nProceeding to restart $computer now."
$null=try{
Restart-Computer -ComputerName $computer -credential $adminCredential -Force -Wait -ea Stop
$currentLine="$(get-date) => $computer REBOOTED"
}catch{
invoke-command -ComputerName $computer -Credential $adminCredential -ScriptBlock{
write-host "Restarting $env:computername..."
Restart-Computer -Force -Wait
}
$computerIsOff=$computerIsOn=$computerRestarted=$False
do{
if(!$computerIsOff){
write-host "." -NoNewline
sleep 1
$computerIsOff=!(test-connection $computer -Count 1 -Quiet -EA Ignore)
}elseif(!$computerIsOn){
write-host "." -NoNewline
sleep 1
$computerIsOn=test-connection $computer -Count 1 -Quiet
if($computerIsOn){
$startUpTime=invoke-command -computername $computer -Credential $adminCredential -scriptblock{(Get-CimInstance -ClassName win32_OperatingSystem).lastbootuptime}
$computerRestarted=$True
$currentLine="$(($startupTime|out-string).trim()) => $computer REBOOTED"
}
}
}until($computerRestarted)
}
try{
Add-content $updatesLog $currentLine -credential $adminCredential
}catch{
invoke-command -ComputerName $computer -Credential $adminCredential -ScriptBlock{
param($localUpdatesLog,$currentLine)
Add-content $localUpdatesLog $currentLine
} -Args $localUpdatesLog,$currentLine
}
write-host $currentLine
do{
$session=if($adminCredential){
try{
New-PSSession -ComputerName $computer -Credential $adminCredential -ea Stop
}catch{
New-PSSession -ComputerName $computer -Credential $adminCredential -SessionOption $(new-pssessionoption -IncludePortInSPN)
}
}else{
try{
New-PSSession -ComputerName $computer -ea Stop
}catch{
New-PSSession -ComputerName $computer -SessionOption $(new-pssessionoption -IncludePortInSPN)
}
}
sleep 1
}until($session.State -eq 'Opened')
$currentLine="$(get-date) => $taskName re-triggered"
invoke-command -session $session -scriptblock {
param ($localUpdatesLog,$taskName,$currentLine)
$null=Add-content $localUpdatesLog $currentLine
Start-ScheduledTask -TaskName $taskname
} -Args $localUpdatesLog,$taskName,$currentLine
Remove-PSSession $session
write-host $currentLine
}elseif($updatesCompleted){
$currentLine="$(get-date) => $computer patching FINISHED"
$null=try{
Add-content $updatesLog $currentLine -ea Stop
}catch{
invoke-command -ComputerName $computer -Credential $adminCredential -ScriptBlock{
param($localUpdatesLog,$currentLine)
Add-content $localUpdatesLog $currentLine
} -Args $localUpdatesLog,$currentLine
}
write-host $currentLine -ForegroundColor Green
$results."$computer"=$currentLine
}elseif($rebootRequired -and !$autoReboot){
$currentStatus="$computer reboot required."
$results."$computer"=$currentStatus
write-warning $currentStatus
if($session){Remove-PSSession $session}
break
}
}else{
write-host '.' -nonewline
sleep 10
}
}
}else{
$currentStatus="Cannot add scheduled task onto $computer"
write-warning $currentStatus
$results."$computer"=$currentStatus
if($session){Remove-PSSession $session}
break
}
if($session){Remove-PSSession $session}
}
return $results
}
updateWindowsInvokeScheduledTasks $computernames $adminCredential $autoReboot $directMicrosoftUpdates
Older version
# updateWindowsNoInternet.ps1
# Version 0.01
# Specify computer name(s)
$computer='testWindows'
# Set credentials - to be injected by Jenkins
$adminUsername='domain\admin'
$plaintextPassword='PASSWORD'
$encryptedPassword=ConvertTo-securestring $plaintextPassword -AsPlainText -Force
$adminCredential=New-Object -TypeName System.Management.Automation.PSCredential -Args $adminUsername,$encryptedPassword
$targetHasInternet=invoke-command -ComputerName $computer -Credential $adminCredential -scriptblock{test-connection 8.8.8.8 -Count 1 -Quiet}
if(!$targetHasInternet){
write-host "$computer`: no internet connectivity detected"
function updateLocalWindowsUsingComObjects($autoReboot=$false,$logfile='c:\WindowsUpdate.log'){
# in case contents of function is called from scheduled tasks without any default argument values
$logFile=if($logFile){$logFile}else{'c:\WindowsUpdate.log'}
if(!(test-path $logfile)){
new-item $logfile -type file -force
}
$status="$(get-date) => $env:computername patching STARTED"
write-host $status
Add-Content -Path $logfile -Value $status
$updateSession=New-Object -Com Microsoft.Update.Session
$updateSearcher=$updateSession.CreateUpdateSearcher()
$searchCriteria="IsInstalled=0 AND Type='Software'" # "IsHidden=0 AND IsInstalled=0 AND Type='Software'"
$availableUpdates=$updateSearcher.Search($searchCriteria)
$availableUpdatesCount=$availableUpdates.Updates.count
if($availableUpdatesCount -eq 0){
$status="$(get-date) => $env:computername has 0 available updates. Patching FINISHED"
write-host $status
Add-Content -Path $logfile -Value $status
return $true
}else{
$status="$(get-date) => $availableUpdatesCount available updates detected"
write-host $status
Add-Content -Path $logfile -Value $status
$updatesToDownload=New-Object -Com Microsoft.Update.UpdateColl
For ($i=0; $i -lt $availableUpdates.Updates.Count; $i++){
$item = $availableUpdates.Updates.Item($i)
$Null = $updatesToDownload.Add($item)
}
$updateSession=New-Object -Com Microsoft.Update.Session
$downloader=$updateSession.CreateUpdateDownloader()
$downloader.Updates=$updatesToDownload
try{
$null=$downloader.Download()
}catch{
write-warning $_
}
$downloadedUpdates=New-Object -Com Microsoft.Update.UpdateColl
For ($i=0; $i -lt $availableUpdates.Updates.Count; $i++){
$item = $availableUpdates.Updates.Item($i)
If ($item.IsDownloaded) {
$null=$downloadedUpdates.Add($item)
}
}
$downloadedCount=$downloadedUpdates.count
if($downloadedCount -eq 0){
$status="$(get-date) => Installation cannot proceed 0 successful downloads."
write-host $status
Add-Content -Path $logfile -Value $status
return $false
}else{
$status="$(get-date) => $downloadedCount of $availableUpdatesCount updates downloaded successfully"
write-host $status
Add-Content -Path $logfile -Value $status
$installCount=0
foreach($update in $downloadedUpdates){
$thisUpdate = New-object -com "Microsoft.Update.UpdateColl"
$thisCount=$installCount++ +1
$null=$thisUpdate.Add($update)
$installer = $updateSession.CreateUpdateInstaller()
$installer.Updates = $thisUpdate
$installResult = $installer.Install()
$translatedCode=switch ($installResult.ResultCode){
0 {'not started'}
1 {'in progress'}
2 {'succeeded'}
3 {'succeeded with errors'}
4 {'failed'}
5 {'aborted'}
}
$status="$(get-date) => $thisCount of $downloadedCount $translatedCode`: $($update.Title)"
write-host $status
Add-Content -Path $logfile -Value $status
}
}
}
$pendingReboot=.{
if (Get-ChildItem "HKLM:\Software\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending" -EA Ignore) { return $true }
if (Get-Item "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired" -EA Ignore) { return $true }
if (Get-ItemProperty "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager" -Name PendingFileRenameOperations -EA Ignore) { return $true }
try {
$util = [wmiclass]"\\.\root\ccm\clientsdk:CCM_ClientUtilities"
$status = $util.DetermineIfRebootPending()
if (($null -ne $status) -and $status.RebootPending) {
return $true
}
}catch{}
return $false
}
If($pendingReboot){
if($autoReboot){
$status="$(get-date) => $env:computername REBOOTED"
write-host $status
Add-Content -Path $logfile -Value $status
(Get-WMIObject -Class Win32_OperatingSystem).Reboot()
}else{
$status="$(get-date) => $env:computername required a REBOOT"
write-host $status
Add-Content -Path $logfile -Value $status
}
}else{
$status="$(get-date) => $env:computername patching FINISHED"
write-host $status
Add-Content -Path $logfile -Value $status
}
}
function invokeScheduledTaskCallPowerShellFunction{
param(
[string[]]$computernames,
[string]$scriptblock,
[string]$taskName,
[string]$description,
[string]$repeatIntervalMinutes,
[System.Management.Automation.PSCredential]$credential
)
$username=$credential.Username;
$plaintextPassword=$credential.GetNetworkCredential().password;
if ($credential){
foreach ($computer in $computernames){
write-host "Adding task on $computer..."
$thisSession=if($credential){
try{
New-PSSession -ComputerName $computer -Credential $credential -ea Stop
}catch{
New-PSSession -ComputerName $computer -Credential $credential -SessionOption $(new-pssessionoption -IncludePortInSPN)
}
}else{
try{
New-PSSession -ComputerName $computer -ea Stop
}catch{
New-PSSession -ComputerName $computer -SessionOption $(new-pssessionoption -IncludePortInSPN)
}
}
if ($thisSession.state -match "Opened"){
$scheduledTaskAdded=Invoke-Command -session $thisSession -ScriptBlock{
param($scriptblock,$taskName,$description,$repeatMinutes,$user,$password)
$windowsVersion=[Environment]::OSVersion.Version
$windowsUpdateScript='C:\Scripts\WindowsUpdates.ps1'
if(!(test-path $windowsUpdateScript)){
$null=new-item $windowsUpdateScript -type File -force
$null=Unblock-File -Path $windowsUpdateScript
}
$null=Set-Content -Path $windowsUpdateScript -Value $scriptblock
if($windowsVersion -ge [version]'6.2'){
$username=if($user -notmatch '\\'){"$env:USERDOMAIN`\$user"}else{$user}
#$encryptedPassword=ConvertTo-SecureString $password -AsPlainText -Force
#$adminCredential=New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $username,$encryptedPassword;
# Unrestrict this Domain Administrator from security prompts
Set-Executionpolicy -Scope CurrentUser -ExecutionPolicy UnRestricted -Force
$settingsCommand = New-ScheduledTaskSettingsSet -MultipleInstances IgnoreNew -ExecutionTimeLimit 0
$callPowerShell = New-ScheduledTaskAction -Execute "Powershell.exe" -Argument "-ExecutionPolicy Bypass $windowsUpdateScript"
$runNow=$false
$taskTrigger = if($repeatMinutes -gt 0){
New-ScheduledTaskTrigger -Once -At (Get-Date) -RepetitionInterval (New-TimeSpan -Minutes $repeatMinutes)
}else{
$runNow=$true;
New-ScheduledTaskTrigger -Once -At (get-date).AddSeconds(-1)
}
# Unregister the Scheduled task if it already exists
Get-ScheduledTask $taskName -ErrorAction SilentlyContinue | Unregister-ScheduledTask -Confirm:$false;
# Create new scheduled task
$null=Register-ScheduledTask -Action $callPowerShell -Trigger $taskTrigger `
-TaskName $taskName -Description $description `
-User $username -Password $password `
-Settings $settingsCommand -RunLevel Highest;
if($runNow){
Start-ScheduledTask -TaskName $taskname
$timeout = 60 ## seconds
$timer = [Diagnostics.Stopwatch]::StartNew()
while (((Get-ScheduledTask -TaskName $taskname).State -ne 'Running') -and ($timer.Elapsed.TotalSeconds -lt $timeout)) {
Write-Verbose -Message "Waiting on scheduled task..."
Start-Sleep -Seconds 2
}
Write-host "$taskname has taken $([math]::round(($timer.Elapsed.TotalSeconds),2)) seconds to initiate"
$timer.Stop()
start-sleep -seconds 5
}
return $true
}else{
write-host "$env:computername is too old. Just turn it off."
return $false
}
} -ArgumentList $scriptblock,$taskName,$description,$repeatIntervalMinutes,$username,$plainTextPassword
Remove-PSSession $thisSession
return $scheduledTaskAdded
}
}
}else{
write-host "Please run this program with a valid Administrator account."
return $false
}
}
$taskName='windowsUpdate'
$taskDescription='Perform Windows Updates'
$taskAdded=invokeScheduledTaskCallPowerShellFunction `
-computernames $computer `
-scriptblock $function:updateLocalWindowsUsingComObjects `
-taskName $taskName `
-description $taskDescription `
-repeatIntervalMinutes 0 `
-credential $adminCredential
if($taskAdded){
$updatesLog="\\$computer\c$\WindowsUpdate.log"
$localUpdatesLog='c:\WindowsUpdate.log'
$logContent=$previousLine=''
$updatesCompleted=$false
write-host "Checking $updatesLog for Windows Update statuses..."
while(!$updatesCompleted){
# Testing variance in results of local vs invoke-command
# $computer='testWindows'
# $updatesLog="\\$computer\c$\windows\system32\drivers\etc\hosts"
# $localUpdatesLog='c:\windows\system32\drivers\etc\hosts'
# $x=get-content $updatesLog
# $y=invoke-command -ComputerName $computer -ScriptBlock{
# param($localUpdatesLog)
# get-content $localUpdatesLog
# } -Args $localUpdatesLog
$logContent=try{
get-content $updatesLog -credential $adminCredential -ea Stop
}catch{
invoke-command -ComputerName $computer -Credential $adminCredential -ScriptBlock{
param($localUpdatesLog)
get-content $localUpdatesLog
} -Args $localUpdatesLog
}
if($logContent){
$currentLine=($logContent|select -last 1|out-string).trim()
$updatesCompleted=$currentLine -match 'FINISHED$'
$rebootRequired=$currentLine -match 'REBOOT$'
if($currentLine -ne $previousLine){
write-host "`r`n$currentLine"
$previousLine=$currentLine
}else{
write-host '.' -nonewline
sleep 10
}
if($rebootRequired){
$null=try{
Restart-Computer -Force -ComputerName $computer -credential $adminCredential -Wait -ea Stop
$currentLine="$(get-date) => $computer REBOOTED"
}catch{
invoke-command -ComputerName $computer -Credential $adminCredential -ScriptBlock{
write-host "Restarting $env:computername..."
Restart-Computer -Force
}
$computerIsOff=$computerIsOn=$computerRestarted=$False
do{
if(!$computerIsOff){
write-host "." -NoNewline
sleep 1
$computerIsOff=!(test-connection $computer -Count 1 -Quiet)
}elseif(!$computerIsOn){
write-host "." -NoNewline
sleep 1
$computerIsOn=test-connection $computer -Count 1 -Quiet
if($computerIsOn){
$startUpTime=invoke-command -computername $computer -Credential $adminCredential -scriptblock{(Get-CimInstance -ClassName win32_OperatingSystem).lastbootuptime}
$computerRestarted=$True
$currentLine="$(($startupTime|out-string).trim()) => $computer REBOOTED"
}
}
}until($computerRestarted)
}
try{
Add-content $updatesLog $currentLine -credential $adminCredential
}catch{
invoke-command -ComputerName $computer -Credential $adminCredential -ScriptBlock{
param($localUpdatesLog,$currentLine)
Add-content $localUpdatesLog $currentLine
} -Args $localUpdatesLog,$currentLine
}
write-host $currentLine
do{
$newSession=if($adminCredential){
try{
New-PSSession -ComputerName $computer -Credential $adminCredential -ea Stop
}catch{
New-PSSession -ComputerName $computer -Credential $adminCredential -SessionOption $(new-pssessionoption -IncludePortInSPN)
}
}else{
try{
New-PSSession -ComputerName $computer -ea Stop
}catch{
New-PSSession -ComputerName $computer -SessionOption $(new-pssessionoption -IncludePortInSPN)
}
}
sleep 1
}until($newSession.State -eq 'Opened')
$currentLine="$(get-date) => $taskName re-triggered"
invoke-command -session $newSession -scriptblock {
param ($localUpdatesLog,$taskName,$currentLine)
Start-ScheduledTask -TaskName $taskname
$null=Add-content $localUpdatesLog $currentLine
} -Args $localUpdatesLog,$taskName,$currentLine
Remove-PSSession $newSession
write-host $currentLine
}elseif($updatesCompleted){
$currentLine="$(get-date) => $computer patching FINISHED"
$null=try{
Add-content $updatesLog $currentLine -ea Stop
}catch{
invoke-command -ComputerName $computer -Credential $adminCredential -ScriptBlock{
param($localUpdatesLog,$currentLine)
Add-content $localUpdatesLog $currentLine
} -Args $localUpdatesLog,$currentLine
}
write-host $currentLine -ForegroundColor Green
}
}else{
write-host '.' -nonewline
sleep 10
}
}
}
}else{
write-host "$computer currently has Internet access. Use a diffferent function to patch it."
}
# Sample Output
#
# testwindows: no internet connectivity detected
# Adding task on testwindows...
# -Message windowsUpdate has taken 0.5089243 seconds to initiate
# Checking \\testwindows\c$\WindowsUpdate.log for Windows Update statuses...
# 01/18/2021 18:51:36 => TESTWINDOWS patching STARTED
# .01/18/2021 18:51:51 => 1 of 3 succeeded: Security Update for Windows Server 2019 for x64-based Systems (KB4535680)
# ........................................................................................................................
# ............................01/18/2021 19:16:34 => 2 of 3 succeeded: 2021-01 Cumulative Update for Windows Server 2019 f
# or x64-based Systems (KB4598230)
# ......01/18/2021 19:17:38 => 3 of 3 succeeded: 2021-01 Servicing Stack Update for Windows Server 2019 for x64-based Syst
# ems (KB4598480)
# .01/18/2021 19:17:43 => TESTWINDOWS required a REBOOT
# ...................................................
# 01/18/2021 19:29:25 => testwindows REBOOTED
# 01/18/2021 19:34:12 => windowsUpdate re-triggered
# Checking \\testwindows\c$\WindowsUpdate.log for Windows Update statuses...
# 01/18/2021 19:36:09 => TESTWINDOWS has 0 available updates. Patching FINISHED
# 01/18/2021 19:37:52 => testwindows patching FINISHED
Categories:
Marc Hagemann
get-content : Access is denied
At line:217 char:17
+ get-content $updatesLog
+ ~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : PermissionDenied: (\\testWindows\c$\WindowsUpdate.log:String) [Get-Content], UnauthorizedAccessException
+ FullyQualifiedErrorId : ItemExistsUnauthorizedAccessError,Microsoft.PowerShell.Commands.GetContentCommand
get-content : Cannot find path ‘\\testWindows\c$\WindowsUpdate.log’ because it does not exist.
And if I define more then one computers in the $computerc variable, the scipt says:
“testWindows has Internet access. Use a diffferent function to patch it.”
Isn’t there a way for a simple script, that only recognized thge pending updates, windows shows in the update section, then it shoul d install these updates an make a reebot. maybe scan again for new updates.
Marc Hagemann
Ok… I figured out something. Every time, the servers reboot, they will lost the credentials of each other for the connection between them. Even, if I store them by activate the Checkbox, when connect: Seems a group policy from the custome is doing that.
So for that reason the error message from my above comment is shown up. I tried t access the file by hand from remote and It says “acces denied” too. I have no chance to enter credentials at this moment. So If I open only “\\\c$windows” without “\WindowsUpdate.log” , I can enter my credentials and connect to the drive on the remote Server.
When I now run the script again, It can open the logfile an work on the task. So I don’t know, why the script can’t access the file from beginning, because the scheduled task is generatet without any problem on the remote server.
Ok. So I was able to run the script with manual help , which should be fixed.
Now the WindowsUpdate.log says “<servername" has 0 available updates. Patching FINISHED
But when I look into the available windows Updates on the server, it says:
"Updates are available.
2020-12 Cumulative Update for Windows Server 2016 for x64-based Systems (KB4593226).
Updates are ready to install"
Why doesn't your script detect this outstanding update?
kimconnect
I’ve finally find some additional time to check this script out and update it.
The script didn’t detect that outstanding update because I’ve missed the “-ea Stop” part in the try-catch
$logContent=try{
get-content $updatesLog -ea Stop
}catch{
invoke-command -ComputerName $computer -Credential $adminCredential -ScriptBlock{
get-content ‘C:\WindowsUpdate.log’}}
It should work via WinRM now.
Marc Hagemann
Hi , and a happy new year. I’ve just tested your updated script.
The server itself says a reboot is necessary to install “2020-12 Cumulative Update for Windows Server 2016 for x64-based Systems (KB4593226)”.
When I run your script, the following output is shown:
“TESTWINDOWS: no internet connectivity detected
Adding task on TESTWINDOWS…
-Message windowsUpdate has taken 0.5000465 seconds to initiate
Checking \\TESTWINDOWS\c$\WindowsUpdate.log for Windows Update statuses…”
Next, this message is shown severel hundred times:
“01/07/2021 07:33:00 => 1 of 1 updates downloaded successfully”
After that, this message cames up, several hundred times, till I end the Scipt manually:
“01/07/2021 07:33:14 => 0 of 1 succeeded: Security Intelligence Update for Microsoft Defender Antivirus – KB2267602 (Version 1.329.1786.0)”
So the Script finds an update, Windows is not showing as outstanding. But why does it show these messages so many times? Will it show the messages as long, as the installation is in progress in the background? If, yes, is it possible to write these message only one time with the additional note “please wait” ?
kimconnect
Happy new year, Marc
The script has been updated – please give it another try. The next revision should have a feature to simultaneously execute on a list of Windows machine to reduce maintenance time window. Perhaps, the original script now meets your requirements for all Windows environments.
Marc Hagemann
Hi Kim,
Looks like the finish line is near. With this updated script, The server installs their found upadets. The output ays, that a reeboot is triggered, but this semms to be fail. Tghe output further says, patching finished, but with outstanding reeboot, the last update could’t be installed complete and is still outstanding.:
: no internet connectivity detected
Adding task on …
-Message windowsUpdate has taken 0.4799641 seconds to initiate
Checking \\\c$\WindowsUpdate.log for Windows Update statuses…
01/19/2021 07:55:55 => 3 available updates detected
……………………………………………………….01/19/2021 08:06:34 => 3 of 3 updates downloaded successfully
……………………………………………………………………………….01/19/2021 08:21:39 => 1 of 3 succeeded: 2021-01 Cumulative Update for Windows Server 2016 for x64-based Systems (KB4598243)
.01/19/2021 08:21:50 => required a REBOOT
Restart-Computer : The computer is skipped. Fail to retrieve its LastBootUpTime via the WMI service with the following error message: Access is denied. (Exception from
HRESULT: 0x80070005 (E_ACCESSDENIED)).
At line:254 char:29
+ … Restart-Computer -Force -ComputerName $computer -Wait
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : OperationStopped: (:String) [Restart-Computer], InvalidOperationException
+ FullyQualifiedErrorId : RestartComputerSkipped,Microsoft.PowerShell.Commands.RestartComputerCommand
01/19/2021 08:21:55 => REBOOTED
01/19/2021 08:21:57 => windowsUpdate re-triggered
01/19/2021 08:21:57 => windowsUpdate re-triggered
.01/19/2021 08:21:59 => patching STARTED
.01/19/2021 08:22:13 => has 0 available updates. Patching FINISHED
01/19/2021 08:22:18 => patching FINISHED
On some other servers, there is only one update ready to install:
– Security Intelligence Update for Microsoft Defender Antivirus – KB2267602 (Version 1.329.2448.0)
, but this script doesn’t detect it and outputs:
: no internet connectivity detected
Adding task on …
-Message windowsUpdate has taken 0.4548604 seconds to initiate
Checking \\\c$\WindowsUpdate.log for Windows Update statuses…
01/19/2021 08:42:40 => has 0 available updates. Patching FINISHED
01/19/2021 08:42:44 => patching FINISHED
Same with the “original” Script
Marc Hagemann
I just fount this script, that checks for pending updates in many registry path, and detects my outstanding reboot.
But for me it’s hard, to include it in your script. May you could check this out?
###############################################################################
# Check-PendingReboot.ps1
# Andres Bohren
# Version 1.0 / 03.06.202020 - Initial Version
###############################################################################
function Test-RegistryValue {
param (
[parameter(Mandatory=$true)][ValidateNotNullOrEmpty()]$Path,
[parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()]$Value
)
try {
Get-ItemProperty -Path $Path | Select-Object -ExpandProperty $Value -ErrorAction Stop | Out-Null
return $true
}
catch {
return $false
}
}
[bool]$PendingReboot = $false
#Check for Keys
If ((Test-Path -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired") -eq $true)
{
$PendingReboot = $true
}
If ((Test-Path -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\PostRebootReporting") -eq $true)
{
$PendingReboot = $true
}
If ((Test-Path -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired") -eq $true)
{
$PendingReboot = $true
}
If ((Test-Path -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending") -eq $true)
{
$PendingReboot = $true
}
If ((Test-Path -Path "HKLM:\SOFTWARE\Microsoft\ServerManager\CurrentRebootAttempts") -eq $true)
{
$PendingReboot = $true
}
#Check for Values
If ((Test-RegistryValue -Path "HKLM:\Software\Microsoft\Windows\CurrentVersion\Component Based Servicing" -Value "RebootInProgress") -eq $true)
{
$PendingReboot = $true
}
If ((Test-RegistryValue -Path "HKLM:\Software\Microsoft\Windows\CurrentVersion\Component Based Servicing" -Value "PackagesPending") -eq $true)
{
$PendingReboot = $true
}
If ((Test-RegistryValue -Path "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager" -Value "PendingFileRenameOperations") -eq $true)
{
$PendingReboot = $true
}
If ((Test-RegistryValue -Path "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager" -Value "PendingFileRenameOperations2") -eq $true)
{
$PendingReboot = $true
}
If ((Test-RegistryValue -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce" -Value "DVDRebootSignal") -eq $true)
{
$PendingReboot = $true
}
If ((Test-RegistryValue -Path "HKLM:\SYSTEM\CurrentControlSet\Services\Netlogon" -Value "JoinDomain") -eq $true)
{
$PendingReboot = $true
}
If ((Test-RegistryValue -Path "HKLM:\SYSTEM\CurrentControlSet\Services\Netlogon" -Value "AvoidSpnSet") -eq $true)
{
$PendingReboot = $true
}
Write-Host "Reboot pending: $PendingReboot"
kimconnect
This function by Andres Bohren is a nice one. The original script does have a limited subset of the check list being queried by this script. I’ll consider updating that original with some borrowed techniques from Andres Bohren, if necessary.
Marc Hagemann
Hey. Got it by myself with the additional regestry paths.
Now the reeboot was initiated. But one thing is now pending: It seems, the task is not waiting enough tine to reinitiate the update process:
.01/19/2021 11:48:07 => required a REBOOT
01/19/2021 11:48:14 => REBOOTED
01/19/2021 11:48:15 => windowsUpdate re-triggered
01/19/2021 11:48:15 => windowsUpdate re-triggered
[] Connecting to remote server failed with the following error message : WinRM cannot complete the operation. Verify that the specified computer name is
valid, that the computer is accessible over the network, and that a firewall exception for the WinRM service is enabled and allows access from this computer. By default, the WinRM
firewall exception for public profiles limits access to remote computers within the same local subnet. For more information, see the about_Remote_Troubleshooting Help topic.
+ CategoryInfo : OpenError: (:String) [], PSRemotingTransportException
+ FullyQualifiedErrorId : WinRMOperationTimeout,PSSessionStateBroken
……………………………………………………………………………………..
kimconnect
Immediately after reboot, script should wait for test-connection before initiating WinRM. Script is now updated to fix this.
Marc Hagemann
Another “bug” I found is, when one of the Servers in my List, habve an internet connection, the script will end immediately with “currently has Internet access. Use a diffferent function to patch it.”
It should install the founded updates in this case, too
kimconnect
The original script has been updated to handle computers with and without Internet access. Let’s give that other one a try: https://blog.kimconnect.com/powershell-update-a-list-of-multiple-windows-machines/
Marc Hagemann
Hi Kim,
I think you did it! The original script works nearly perfect now. I put in 10 server IP’s to the variable and all of them got their updates and makes a reboot successfully. No outstanding updates after that! Thank you very, very, very much for your hard work!
So only thing is, script runs further, after all is done. The minutes still counting. I stopped it at minute 152. Updates were all done. I’ve checked that . So I don’t know, what the script is waiting for all the time.
Another question: Would it be a hard work to summarize the installed updates to an html formatted table, so that the script can send it out per e-mail?
I only needt the html code for that:
IP | KB Number | Update description | reboot time
Would be a charm and takes this script to the next level.
kimconnect
Hi Marc,
Thanks for trying this out and giving me good ideas. When I have more time, I’ll definitely update the original script to include:
1. HTML/CSS report to be send via email
2. Function to email via SMTP or relay
Of course, the ‘minutes still counting’ bug will be fixed as well.
To be continued…
Marc Hagemann
Thank you for your great help till now!!!
That makes workflow a lot easier for me.
I will look to this site regulary!