PowerShell: Update a List of Multiple Windows Machines

Version 3:

# updateWindowsList.ps1
# Version 0.0.3
#
# Features:
#   - New feature over version 0.0.1: Simultaneous Executions
#   - New feature over version 0.0.2: Handle cases where target hosts have no Internet access
#
# Requirements:
#   - WinRM must be enabled on target computer(s)

# User input variables
$computernames='testWindows'
$directMicrosoftUpdates=$true
$csvFile='C:\updateResults.csv'

# Domain Admin 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

function invokeWindowsUpdate{ 
    [CmdletBinding()] 
    param ( 
        [parameter(Mandatory=$true,Position=0)][string]$computer,
        [parameter(Mandatory=$false,Position=1)][System.Management.Automation.PSCredential]$adminCredential,
        [parameter(Mandatory=$false,Position=2)][bool]$microsoftUpdates=$false,
        [parameter(Mandatory=$false,Position=3)][bool]$bypassWsus=$false, 
        [parameter(Mandatory=$false,Position=4)][int]$winRmPort=5985,
        [parameter(Mandatory=$false,Position=5)][string]$logFile='C:\PSWindowsUpdate.log'
        )    
    $ErrorActionPreference='stop'
    <#
    .SYNOPSIS
    This script will automatically install all avaialable windows updates on a device and will automatically reboot if needed.
    After reboots, windows updates will continue to run until all updates are installed.
    # Features: 
    # - Check WSUS settings (bypass if required by the boolean value)
    # - Prepare the targets by installing prerequisites prior to proceeding further to preemptively resolve dependency errors
    # - Include additional dedendencies such as TLS1.2, Nuget & PSGALLERY
    # - Check if server needs a reboot before issuing the reboot command, instead of just inadvertently trigger reboots
    # - Fixed the blank lines in output log causing bug in status query
    # - More thorough cleanup routine
    # Future developments:
    # - Detect and handle proxies
    #>

    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 checkPendingReboot([string]$computer=$ENV:computername,$session){ 
        function checkRegistry{
            if (Get-ChildItem "HKLM:\Software\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending" -EA Ignore) { return $true }
            if (Get-ChildItem "HKLM:\Software\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootInProgress" -EA Ignore) { return $true }
            if (Get-Item "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired" -EA Ignore) { return $true }
            if (Get-Item "HKLM:\Software\Microsoft\Windows\CurrentVersion\Component Based Servicing\PackagesPending" -EA Ignore) { return $true }
            if (Get-Item "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\PostRebootReporting" -EA Ignore) { return $true }
            if (Get-Item 'HKLM:\SOFTWARE\Microsoft\ServerManager\CurrentRebootAttemps' -EA Ignore) { return $true }
            if (Get-ItemProperty "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager" -Name 'PendingFileRenameOperations' -EA Ignore) { return $true }
            if (Get-ItemProperty "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager" -Name 'PendingFileRenameOperations2' -EA Ignore) { return $true }
            if (Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce' -Name 'DVDRebootSignal' -EA Ignore) { return $true }
            if (Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Services\Netlogon' -Name 'JoinDomain' -EA Ignore) { return $true }
            if (Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Services\Netlogon' -Name 'AvoidSpnSet' -EA Ignore) { return $true }
            try{ # This peruses CCM utility, if exists
                $util = [wmiclass]"\\.\root\ccm\clientsdk:CCM_ClientUtilities"
                $status = $util.DetermineIfRebootPending()
                if(($null -ne $status) -and $status.RebootPending){
                    $result.SCCMRebootPending = $true
                }
            }catch{
                return $false
            }                    
        }

        if ($ENV:computername -eq $computer){
            $result=checkRegistry;
        }else{
            $result=Invoke-Command -session $session -ScriptBlock{
                    param($importedFunc);
                    [ScriptBlock]::Create($importedFunc).Invoke();
                } -ArgumentList ${function:checkRegistry}
        }
        return $result;
    }

    function clearRebootFlags($computer,$session,$verbose=$false){
        if (checkPendingReboot $computer $session){                    
            $isLocalHost=$env:computername -eq $computer
            if($isLocalHost){
                write-warning "$computer is LOCALHOST. Please reboot manually to clear reboot flags"
                return $false
            }else{
                write-host "`nRestarting remote computer $computer to clear pending reboot flags..." 
                if($adminCredential){
                    Restart-Computer -Wait -ComputerName $computer -Credential $adminCredential -Force
                }else{
                    Restart-Computer -Wait -ComputerName $computer -Force
                    }
                write-host "$computer has been successfully restarted!" -ForegroundColor Yellow
                return $true
            }
        }else{
            if($verbose){write-host "There are no pending reboot flags to clear." -ForegroundColor Green}
            return $true
        }
    }   
    function checkWsus{
        # Check if this machine has WSUS settings configured
        $wuPath="Registry::HKLM\Software\Policies\Microsoft\Windows\WindowsUpdate\AU";
        $wuKey="UseWUServer";
        $wuIsOn=(Get-ItemProperty -path $wuPath -name $wuKey -ErrorAction SilentlyContinue).$wuKey;
        #if($wuIsOn){$GLOBAL:wsus=$True}else{$GLOBAL:wsus=$False}
        return $wuIsOn  
    }

    function turnoffWsus{
        # Turn WSUS settings OFF temporarily...
        $wuPath="Registry::HKLM\Software\Policies\Microsoft\Windows\WindowsUpdate\AU";
        $wuKey="UseWUServer";
        Set-Itemproperty -path $wuPath -Name $wuKey -value 0
        restart-service wuauserv;        
        }

    function turnonWsus{
        # Turning WSUS settings back to ON status
        $wuPath="Registry::HKLM\Software\Policies\Microsoft\Windows\WindowsUpdate\AU";
        $wuKey="UseWUServer";
        Set-Itemproperty -path $wuPath -Name $wuKey -value 1
        restart-service wuauserv;
        }
    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:\PSWindowsUpdate.log'){
        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
    }

    write-warning "$computer will go through Windows Update and will REBOOT AUTOMATICALLY (if necessary).`r`nPress Ctrl+C at anytime to cancel."
    $session=connectWinRm $computer $adminCredential
    $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{    
        if(!(includePsWindowsUpdate $session)){
            remove-pssession $session
            return $false
        }    
        if($bypassWsus){
            $wsusIsOn=Invoke-Command -session $session -scriptblock{
                param($checkWsus);
                [scriptblock]::create($checkWsus).invoke()
                } -Args ${function:checkWsus}
            if($wsusIsOn){
                Invoke-Command -session $session -scriptblock{
                param($turnoffWsus);
                [scriptblock]::create($turnoffWsus).invoke()
                } -args ${function:turnoffWsus}
            }
        }
        Do{
            #retrieves a list of available updates 
            write-host "Checking for new updates on $computer..."
            $updates=invoke-command -session $session -scriptblock {
                param($microsoftUpdates)
                $executionPolicy=Get-ExecutionPolicy
                if($executionPolicy -notmatch 'RemoteSigned|Unrestricted'){
                    Set-ExecutionPolicy RemoteSigned -force
                }
                $null=Import-Module PSWindowsUpdate
                $availableUpdates=if($microsoftUpdates){
                        Get-wulist -MicrosoftUpdate -verbose
                    }else{
                        Get-Wulist -verbose
                    }
                write-host $($availableUpdates|out-string).trim()
                Set-ExecutionPolicy $executionPolicy -force
                return $availableUpdates
            } -Args $microsoftUpdates
            $updatesCount=$updates.KB.count # $updates.count returns $null when count equals 1
            # If there are available updates, proceed with installing the updates and then reboot the remote machine if required
            if ($updatesCount){ 
                #Invoke-WUJob will insert a scheduled task on the remote target as a mean to bypass 2nd hop issues            
                invoke-command -Session $session {
                    param($microsoftUpdates)
                    $logFile='C:\PSWindowsUpdate.log'
                    if(test-path $logFile){remove-item $logFile -force}                
                    if($microsoftUpdates){
                        $invokeScript={
                            import-module PSWindowsUpdate;
                            Get-WindowsUpdate -AcceptAll -MicrosoftUpdate -Install | Out-File 'C:\PSWindowsUpdate.log'
                        }
                    }else{
                        $invokeScript={
                            import-module PSWindowsUpdate;
                            Get-WindowsUpdate -AcceptAll -Install | Out-File 'C:\PSWindowsUpdate.log'
                        }
                    }
                    Invoke-WUjob -ComputerName $env:computername -Script $invokeScript -Confirm:$false -RunNow
                    write-host "Windows Updates have been triggerred. Now checking for its log file...`r`n"
                    Do{
                        $logFileGenerated=if(test-path $logFile){get-content $logFile}else{$false}
                        if(!$logFileGenerated){
                            Start-Sleep -Seconds 1
                            write-host '.' -NoNewline
                        }
                    }until($logFileGenerated)
                } -Args $microsoftUpdates

                #Show update status until the amount of installed updates equals the same as the count of updates being reported 
                $dotLimit=10
                $dotCount=0
                $minute=0
                $lastActivity=$null;
                $installedCount=0;
                Write-Host "`r`nThere $(if($updatesCount -eq 1){'is'}else{'are'}) $updatesCount pending update(s).";
                do {                
                    $getWindowsUpdateLog={param($logFile);Get-Content $logFile}
                    $updatestatus=Invoke-Command -Session $session -ScriptBlock $getWindowsUpdateLog -Args $logFile
                    $currentActivity=.{
                        $index=$updatestatus.count-1
                        do{
                            $value=$updatestatus[$index--]
                        }until($value)
                        return $value
                        }
                    $installedCount=([regex]::Matches($updatestatus, "Installed")).count
                    $updatesCompleted=$installedCount -eq $updatesCount
                    $failuresCount=([regex]::Matches($updatestatus, "Failed")).count
                    if ($currentActivity -ne $lastActivity){
                        if($currentActivity -match 'Installed'){
                            Write-Host "`r`nSuccessfully installed $installedCount of $updatesCount updates: $currentActivity" -ForegroundColor Green
                        }elseif($currentActivity -match 'Failed'){
                            Write-Host "`r`nFailed item: $currentActivity" -ForegroundColor Yellow
                        }else{
                            Write-Host "`r`nCurrent activity: $currentActivity"
                            }
                        $lastActivity=$currentActivity;
                        }else{
                            if ($dotCount++ -le $dotLimit){
                                Write-Host -NoNewline "."
                            }else{
                                $minute++
                                Write-Host "`r`nMinute count: $minute"
                                $dotCount=0
                                }                                                   
                            }                
                    Start-Sleep -Seconds 6
                }until ($updatesCompleted -or $failuresCount -ge 2)
            }else{
                write-host "There are $updatesCount pending updates detected."
            }
            if(checkPendingReboot $computer $session){
                $localhostRebootFlag=!(clearRebootFlags $computer $session)
            }else{
                write-host "No pending reboots detected on $computer"
            }
            if($session.State -ne 'Opened'){
                write-host "Reconnecting to $computer..." -ForegroundColor Yellow
                $session=connectWinRm $computer $adminCredential
            }
            cleanupWuJob $session $logFile
            if($updatesCompleted -or !$updatesCount){
                Write-Host "`r`nWindows is now up to date on $computer" -ForegroundColor Green
            }
        }until(!$updatesCount -or $updatesCompleted -or $localhostRebootFlag)
        if($bypassWsus -and $wsusIsOn -and $targetHasInternet){
            write-host "Reverting WSUS registry edits"
            Invoke-Command -session $session -scriptblock{
                param($turnonWsus);
                [scriptblock]::create($turnonWsus).invoke()
            } -args ${function:turnonWsus} ;
        }        
        $localhostRebootFlag=!(clearRebootFlags $computer $session)
        if($session.State -eq 'Opened'){Remove-PSSession $session}
    }
    
    if($localhostRebootFlag){
        write-host "$computer requires a reboot to complete the updates"
        return $false
    }else{
        return $true
    }
}

function confirmation($content,$testValue="I confirm",$maxAttempts=3){
    $confirmed=$false;
    $attempts=0;        
    $content|write-host
    write-host "Please review this content for accuracy.`r`n"
    while ($attempts -le $maxAttempts){
        if($attempts++ -ge $maxAttempts){
            write-host "A maximum number of attempts have reached. No confirmations received!`r`n"
            break;
            }
        $userInput = Read-Host -Prompt "Please type in this value => $testValue <= to confirm. Input CANCEL to skip this item";
        if ($userInput.ToLower() -eq $testValue.ToLower()){
            $confirmed=$true;
            write-host "Confirmed!`r`n";
            break;                
        }elseif($userInput -like 'cancel'){
            write-host 'Cancel command received.'
            $confirmed=$false
            break
        }else{
            Clear-Host;
            $content|write-host
            write-host "Attempt number $attempts of $maxAttempts`: $userInput does not match $testValue. Try again or Input CANCEL to skip this item`r`n"
            }
        }
    return $confirmed;
}

function obtainDomainAdminCredentials{
    # Legacy domain binding function
    function isValidCred($u,$p){
        # Get current domain using logged-on user's credentials
        $domain = "LDAP://" + ([ADSI]"").distinguishedName
        $domainCred = New-Object System.DirectoryServices.DirectoryEntry($domain,$u,$p)
        if ($domainCred){
            return $True
        }else{
            return $False
        }
    }
    function isDomainAdmin($username){
        if (!(get-module activedirectory)){Install-WindowsFeature RSAT-AD-PowerShell -Confirm:$false}
        if((Get-ADUser $username -Properties MemberOf).MemberOf -match '^CN=Domain Admins'){
            return $True;
        }else{return $false;}
    }
 
    # Create a domain context
    function dotNetDomainBind{
        Add-Type -AssemblyName System.DirectoryServices.AccountManagement;  
        Try {
            $type = [System.DirectoryServices.AccountManagement.ContextType]::Domain;
            $context = New-Object System.DirectoryServices.AccountManagement.PrincipalContext $type,$env:USERDOMAIN;
            return $context;
        } Catch {
            If ($_.Exception.InnerException -like "*The server could not be contacted*") {
                write-host "Could not contact a server for the specified domain $env:USERDOMAIN via DotNet Method.";
            } Else {
                write-host "Unknown errors occured while attempting to contact $env:USERDOMAIN via DotNet Method.";
            }
            return $false;
        }
    }
 
    $attempt=0;
    $maxAttempts=3;
    $plainTextPassword='';   
    Do{
        $attempt++;
        $failureMessage = $null; 
        [string][ValidateNotNullOrEmpty()]$userName=Read-Host -Prompt "Please input a User ID";
        if($userName -notmatch '\\'){
            $username=$env:USERDOMAIN+'\'+$userName
        }
        $password = Read-Host -Prompt "Please type in the password for user $userName" -AsSecureString;
        $plainTextPassword=[Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($password))
     
        # Test bind to this credential
        try {
            # Test bind
            $context=dotNetDomainBind
            if($context){
                $validatedAccount = $context.ValidateCredentials($userName,$plainTextPassword)
            }else{
                $validatedAccount=isValidCred -u $userName -p $plainTextPassword
            }
             
            If ($validatedAccount) {
                $onlyUserName=$userName -replace '.+\\'
                $isDomainAdmin=isDomainAdmin -username $onlyUserName;
                if($isDomainAdmin){
                    $credentials = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $userName,$password;
                    $validAccount=$true;
                    }else{
                        $failureMessage += "$attempt out of $maxAttempts`: $userName account is valid, but it is not a Domain Admin";
                        }
                }else{
                    $failureMessage += "$attempt out of $maxAttempts`: username and/or password error";
                    }           
            }catch{
                write-warning $_
                $failureMessage += "Unable to bind to $env:USERDNSDOMAIN.";
                }
 
        # Depending on whether there are failures, proceed accordingly       
        if($failureMessage){
            If ($attempt -lt $maxAttempts-1) {
                $message = "$failureMessage`: Authentication error. Please Try again.";
                Write-Warning $message;
            }elseif($attempt -eq $maxAttempts-1){
                    $message = "$failureMessage`: Last attempt.";
                    Write-Warning $message;
                    $credentials= $false;
            }
        }
 
        } Until (($ValidAccount) -or ($Attempt -ge $MaxAttempts))
    if($credentials){return $credentials;}else{return $false}
}

function updateWindows($computerNames,$adminCredential,$microsoftUpdates=$false,$csvFile='C:\updateResults.csv'){
    get-job|stop-job|remove-job -force
    foreach ($computerName in $computerNames){ 
        Start-Job -name $computerName -ScriptBlock {
            param($invokeWindowsUpdate,$computerName,$adminCredential,$microsoftUpdates)
            [scriptblock]::create($invokeWindowsUpdate).invoke($computerName,$adminCredential,$microsoftUpdates)
        } -Args ${function:invokeWindowsUpdate},$computerName,$adminCredential,$microsoftUpdates
    }

    $lineBreak=30
    $dotCount=0
    $minute=0
    write-host "Minute`r`n$minute`:" -NoNewline -ForegroundColor Yellow
    $jobResults=@()
    $jobsCount=(get-job).count
    do{        
        $completedJobs=get-job|?{$_.State -eq 'Completed'}
        foreach ($job in $completedJobs){
            $computer=$job.Name
            write-host "`r`n===================================================`r`n$computer job completed with these messages:`r`n===================================================`r`n"
            $timeStamp=$job.PSBeginTime
            $minutesElapsed=[math]::round(($job.PSEndTime-$job.PSBeginTime).TotalMinutes,2)
            $computer=$job.Name
            $completed=receive-job -id $job.id
            $result=[pscustomobject][ordered]@{
                computerName=$computer                
                timeStamp=$timeStamp
                minutesElapsed=$minutesElapsed
                completed=$completed
            }
            $jobResults+=,$result
            remove-job -id $job.id
        }
        if($dotCount++ -lt $lineBreak){
            write-host '.' -NoNewline
        }else{
            $minute++
            write-host "`r`n$minute`t:" -ForegroundColor Yellow -NoNewline
            $dotCount=0
            }
        Start-Sleep -seconds 2
    }until($jobsCount -eq $jobResults.count -or !(get-job))
    write-host "`r`n$(($jobResults|ft|out-string).trim())"
    $jobResults | Export-Csv -Path $csvFile -NoTypeInformation -Append
    write-host "Windows Update results have been exported to: $csvFile"  
    $localMachineIsPendingReboot=($jobResults|?{$_.computername -eq $env:computername}).pendingReboot -eq $true
    if($localMachineIsPendingReboot){
        $confirmed=confirmation "I want to reboot this local machine $env:computername"
        if($confirmed){
            Restart-Computer $env:computerName -force
        }else{
            write-warning "Please reboot $env:computername manually to complete its updates."
        }
    }else{
        write-host 'Done.' -ForegroundColor Green
    }
}

if(!$adminCredential){$adminCredential=obtainDomainAdminCredentials}
updateWindows $computerNames $adminCredential $directMicrosoftUpdates $csvFile

Version 2:

# updateWindowsList.ps1
# Version 0.0.2
# New feature over version 0.0.1: Simultaneous Executions
# Requirement: WinRM and Internet Access must be enabled on target computer(s)
  
#$computernames='SHERVER01','SHERVER02'
$directMicrosoftUpdates=$true
$csvFile='C:\updateResults.csv'
$adminCredential=$false # Leave this as false to obtain Domain Admin cred during runtime

function invokeWindowsUpdate{ 
    [CmdletBinding()] 
    param ( 
        [parameter(Mandatory=$true,Position=0)][string]$computer,
        [parameter(Mandatory=$false,Position=1)][System.Management.Automation.PSCredential]$adminCredential,
        [parameter(Mandatory=$false,Position=2)][bool]$microsoftUpdates=$false,
        [parameter(Mandatory=$false,Position=3)][bool]$bypassWsus=$false, 
        [parameter(Mandatory=$false,Position=4)][int]$winRmPort=5985,
        [parameter(Mandatory=$false,Position=5)][string]$logFile='C:\PSWindowsUpdate.log'
        )    
    $ErrorActionPreference='stop'
    <#
    .SYNOPSIS
    This script will automatically install all avaialable windows updates on a device and will automatically reboot if needed.
    After reboots, windows updates will continue to run until all updates are installed.
    # Features: 
    # - Check WSUS settings (bypass if required by the boolean value)
    # - Prepare the targets by installing prerequisites prior to proceeding further to preemptively resolve dependency errors
    # - Include additional dedendencies such as TLS1.2, Nuget & PSGALLERY
    # - Check if server needs a reboot before issuing the reboot command, instead of just inadvertently trigger reboots
    # - Fixed the blank lines in output log causing bug in status query
    # - More thorough cleanup routine
    # Future developments:
    # - Detect and handle proxies
    #>
 
    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 -Force
                }
                $null=Set-PSRepository -Name 'PSGallery' -InstallationPolicy Trusted
                $null=Install-Module PSWindowsUpdate -Confirm:$false -Force
                $null=Import-Module PSWindowsUpdate -force
                # 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;
                }
            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 checkPendingReboot([string]$computer=$ENV:computername,$session){ 
        function checkRegistry{
            if (Get-ChildItem "HKLM:\Software\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending" -EA Ignore) { return $true }
            if (Get-ChildItem "HKLM:\Software\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootInProgress" -EA Ignore) { return $true }
            if (Get-Item "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired" -EA Ignore) { return $true }
            if (Get-Item "HKLM:\Software\Microsoft\Windows\CurrentVersion\Component Based Servicing\PackagesPending" -EA Ignore) { return $true }
            if (Get-Item "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\PostRebootReporting" -EA Ignore) { return $true }
            if (Get-Item 'HKLM:\SOFTWARE\Microsoft\ServerManager\CurrentRebootAttemps' -EA Ignore) { return $true }
            if (Get-ItemProperty "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager" -Name 'PendingFileRenameOperations' -EA Ignore) { return $true }
            if (Get-ItemProperty "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager" -Name 'PendingFileRenameOperations2' -EA Ignore) { return $true }
            if (Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce' -Name 'DVDRebootSignal' -EA Ignore) { return $true }
            if (Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Services\Netlogon' -Name 'JoinDomain' -EA Ignore) { return $true }
            if (Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Services\Netlogon' -Name 'AvoidSpnSet' -EA Ignore) { return $true }
            try{ # This peruses CCM utility, if exists
                $util = [wmiclass]"\\.\root\ccm\clientsdk:CCM_ClientUtilities"
                $status = $util.DetermineIfRebootPending()
                if(($null -ne $status) -and $status.RebootPending){
                    $result.SCCMRebootPending = $true
                }
            }catch{
                return $false
            }                    
        }

        if ($ENV:computername -eq $computer){
            $result=checkRegistry;
        }else{
            $result=Invoke-Command -session $session -ScriptBlock{
                    param($importedFunc);
                    [ScriptBlock]::Create($importedFunc).Invoke();
                } -ArgumentList ${function:checkRegistry}
        }
        return $result;
    }
 
    function clearRebootFlags($computer,$session,$verbose=$false){
        if (checkPendingReboot $computer $session){                    
            $isLocalHost=$env:computername -eq $computer
            if($isLocalHost){
                write-warning "$computer is LOCALHOST. Please reboot manually to clear reboot flags"
                return $false
            }else{
                write-host "`nRestarting remote computer $computer to clear pending reboot flags..." 
                if($adminCredential){
                    Restart-Computer -Wait -ComputerName $computer -Credential $adminCredential -Force
                }else{
                    Restart-Computer -Wait -ComputerName $computer -Force
                    }
                write-host "$computer has been successfully restarted!" -ForegroundColor Yellow
                return $true
            }
        }else{
            if($verbose){write-host "There are no pending reboot flags to clear." -ForegroundColor Green}
            return $true
        }
    }   
    function checkWsus{
        # Check if this machine has WSUS settings configured
        $wuPath="Registry::HKLM\Software\Policies\Microsoft\Windows\WindowsUpdate\AU";
        $wuKey="UseWUServer";
        $wuIsOn=(Get-ItemProperty -path $wuPath -name $wuKey -ErrorAction SilentlyContinue).$wuKey;
        #if($wuIsOn){$GLOBAL:wsus=$True}else{$GLOBAL:wsus=$False}
        return $wuIsOn  
    }
   
    function turnoffWsus{
        # Turn WSUS settings OFF temporarily...
        $wuPath="Registry::HKLM\Software\Policies\Microsoft\Windows\WindowsUpdate\AU";
        $wuKey="UseWUServer";
        Set-Itemproperty -path $wuPath -Name $wuKey -value 0
        restart-service wuauserv;        
        }
   
    function turnonWsus{
        # Turning WSUS settings back to ON status
        $wuPath="Registry::HKLM\Software\Policies\Microsoft\Windows\WindowsUpdate\AU";
        $wuKey="UseWUServer";
        Set-Itemproperty -path $wuPath -Name $wuKey -value 1
        restart-service wuauserv;
        }
    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:\PSWindowsUpdate.log'){
        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
    }

    write-warning "$computer will go through Windows Update and will REBOOT AUTOMATICALLY (if necessary).`r`nPress Ctrl+C at anytime to cancel."
    $session=connectWinRm $computer $adminCredential
    if(!(includePsWindowsUpdate $session)){
        remove-pssession $session
        return $false
    }    
    if($bypassWsus){
        $wsusIsOn=Invoke-Command -session $session -scriptblock{
            param($checkWsus);
            [scriptblock]::create($checkWsus).invoke()
            } -Args ${function:checkWsus}
        if($wsusIsOn){
            Invoke-Command -session $session -scriptblock{
            param($turnoffWsus);
            [scriptblock]::create($turnoffWsus).invoke()
            } -args ${function:turnoffWsus}
        }
    }
    Do{
        #retrieves a list of available updates 
        write-host "Checking for new updates on $computer..."
        $updates=invoke-command -session $session -scriptblock {
            param($microsoftUpdates)
            $executionPolicy=Get-ExecutionPolicy
            if($executionPolicy -notmatch 'RemoteSigned|Unrestricted'){
                Set-ExecutionPolicy RemoteSigned -force
            }
            $null=Import-Module PSWindowsUpdate
            $availableUpdates=if($microsoftUpdates){
                    Get-wulist -MicrosoftUpdate -verbose
                }else{
                    Get-Wulist -verbose
                }
            write-host $($availableUpdates|out-string).trim()
            Set-ExecutionPolicy $executionPolicy -force
            return $availableUpdates
        } -Args $microsoftUpdates
        $updatesCount=$updates.KB.count # $updates.count returns $null when count equals 1
        # If there are available updates, proceed with installing the updates and then reboot the remote machine if required
        if ($updatesCount){ 
            #Invoke-WUJob will insert a scheduled task on the remote target as a mean to bypass 2nd hop issues            
            invoke-command -Session $session {
                param($microsoftUpdates)
                $logFile='C:\PSWindowsUpdate.log'
                if(test-path $logFile){remove-item $logFile -force}                
                if($microsoftUpdates){
                    $invokeScript={
                        import-module PSWindowsUpdate;
                        Get-WindowsUpdate -AcceptAll -MicrosoftUpdate -Install | Out-File 'C:\PSWindowsUpdate.log'
                    }
                }else{
                    $invokeScript={
                        import-module PSWindowsUpdate;
                        Get-WindowsUpdate -AcceptAll -Install | Out-File 'C:\PSWindowsUpdate.log'
                    }
                }
                Invoke-WUjob -ComputerName $env:computername -Script $invokeScript -Confirm:$false -RunNow
                write-host "Windows Updates have been triggerred. Now checking for its log file...`r`n"
                Do{
                    $logFileGenerated=if(test-path $logFile){get-content $logFile}else{$false}
                    if(!$logFileGenerated){
                        Start-Sleep -Seconds 1
                        write-host '.' -NoNewline
                    }
                }until($logFileGenerated)
            } -Args $microsoftUpdates

            #Show update status until the amount of installed updates equals the same as the count of updates being reported 
            $dotLimit=10
            $dotCount=0
            $minute=0
            $lastActivity=$null;
            $installedCount=0;
            Write-Host "`r`nThere $(if($updatesCount -eq 1){'is'}else{'are'}) $updatesCount pending update(s).";
            do {                
                $getWindowsUpdateLog={param($logFile);Get-Content $logFile}
                $updatestatus=Invoke-Command -Session $session -ScriptBlock $getWindowsUpdateLog -Args $logFile
                $currentActivity=.{
                    $index=$updatestatus.count-1
                    do{
                        $value=$updatestatus[$index--]
                    }until($value)
                    return $value
                    }
                $installedCount=([regex]::Matches($updatestatus, "Installed")).count
                $updatesCompleted=$installedCount -eq $updatesCount
                $failuresCount=([regex]::Matches($updatestatus, "Failed")).count
                if ($currentActivity -ne $lastActivity){
                    if($currentActivity -match 'Installed'){
                        Write-Host "`r`nSuccessfully installed $installedCount of $updatesCount updates: $currentActivity" -ForegroundColor Green
                    }elseif($currentActivity -match 'Failed'){
                        Write-Host "`r`nFailed item: $currentActivity" -ForegroundColor Yellow
                    }else{
                        Write-Host "`r`nCurrent activity: $currentActivity"
                        }
                    $lastActivity=$currentActivity;
                    }else{
                        if ($dotCount++ -le $dotLimit){
                            Write-Host -NoNewline "."
                        }else{
                            $minute++
                            Write-Host "`r`nMinute count: $minute"
                            $dotCount=0
                            }                                                   
                        }                
                Start-Sleep -Seconds 6
            }until ($updatesCompleted -or $failuresCount -ge 2)
        }else{
            write-host "There are $updatesCount pending updates detected."
        }
        if(checkPendingReboot $computer $session){
            $localhostRebootFlag=!(clearRebootFlags $computer $session)
        }else{
            write-host "No pending reboots detected on $computer"
        }
        if($session.State -ne 'Opened'){
            write-host "Reconnecting to $computer..." -ForegroundColor Yellow
            $session=connectWinRm $computer $adminCredential
        }
        cleanupWuJob $session $logFile
        if($updatesCompleted -or !$updatesCount){
            Write-Host "`r`nWindows is now up to date on $computer" -ForegroundColor Green
        }
    }until(!$updatesCount -or $updatesCompleted -or $localhostRebootFlag)    
    if($bypassWsus -and $wsusIsOn){
        write-host "Reverting WSUS registry edits"
        Invoke-Command -session $session -scriptblock{
            param($turnonWsus);
            [scriptblock]::create($turnonWsus).invoke()
        } -args ${function:turnonWsus} ;
    }    
    if($session.State -eq 'Opened'){Remove-PSSession $session}
    if($localhostRebootFlag){
        write-host "$computer requires a reboot to complete the updates"
        return $false
    }else{
        return $true
    }
}

function confirmation($content,$testValue="I confirm",$maxAttempts=3){
    $confirmed=$false;
    $attempts=0;        
    $content|write-host
    write-host "Please review this content for accuracy.`r`n"
    while ($attempts -le $maxAttempts){
        if($attempts++ -ge $maxAttempts){
            write-host "A maximum number of attempts have reached. No confirmations received!`r`n"
            break;
            }
        $userInput = Read-Host -Prompt "Please type in this value => $testValue <= to confirm. Input CANCEL to skip this item";
        if ($userInput.ToLower() -eq $testValue.ToLower()){
            $confirmed=$true;
            write-host "Confirmed!`r`n";
            break;                
        }elseif($userInput -like 'cancel'){
            write-host 'Cancel command received.'
            $confirmed=$false
            break
        }else{
            Clear-Host;
            $content|write-host
            write-host "Attempt number $attempts of $maxAttempts`: $userInput does not match $testValue. Try again or Input CANCEL to skip this item`r`n"
            }
        }
    return $confirmed;
}

function obtainDomainAdminCredentials{
    # Legacy domain binding function
    function isValidCred($u,$p){
        # Get current domain using logged-on user's credentials
        $domain = "LDAP://" + ([ADSI]"").distinguishedName
        $domainCred = New-Object System.DirectoryServices.DirectoryEntry($domain,$u,$p)
        if ($domainCred){
            return $True
        }else{
            return $False
        }
    }
    function isDomainAdmin($username){
        if (!(get-module activedirectory)){Install-WindowsFeature RSAT-AD-PowerShell -Confirm:$false}
        if((Get-ADUser $username -Properties MemberOf).MemberOf -match '^CN=Domain Admins'){
            return $True;
        }else{return $false;}
    }
 
    # Create a domain context
    function dotNetDomainBind{
        Add-Type -AssemblyName System.DirectoryServices.AccountManagement;  
        Try {
            $type = [System.DirectoryServices.AccountManagement.ContextType]::Domain;
            $context = New-Object System.DirectoryServices.AccountManagement.PrincipalContext $type,$env:USERDOMAIN;
            return $context;
        } Catch {
            If ($_.Exception.InnerException -like "*The server could not be contacted*") {
                write-host "Could not contact a server for the specified domain $env:USERDOMAIN via DotNet Method.";
            } Else {
                write-host "Unknown errors occured while attempting to contact $env:USERDOMAIN via DotNet Method.";
            }
            return $false;
        }
    }
 
    $attempt=0;
    $maxAttempts=3;
    $plainTextPassword='';   
    Do{
        $attempt++;
        $failureMessage = $null; 
        [string][ValidateNotNullOrEmpty()]$userName=Read-Host -Prompt "Please input a User ID";
        if($userName -notmatch '\\'){
            $username=$env:USERDOMAIN+'\'+$userName
        }
        $password = Read-Host -Prompt "Please type in the password for user $userName" -AsSecureString;
        $plainTextPassword=[Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($password))
     
        # Test bind to this credential
        try {
            # Test bind
            $context=dotNetDomainBind
            if($context){
                $validatedAccount = $context.ValidateCredentials($userName,$plainTextPassword)
            }else{
                $validatedAccount=isValidCred -u $userName -p $plainTextPassword
            }
             
            If ($validatedAccount) {
                $onlyUserName=$userName -replace '.+\\'
                $isDomainAdmin=isDomainAdmin -username $onlyUserName;
                if($isDomainAdmin){
                    $credentials = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $userName,$password;
                    $validAccount=$true;
                    }else{
                        $failureMessage += "$attempt out of $maxAttempts`: $userName account is valid, but it is not a Domain Admin";
                        }
                }else{
                    $failureMessage += "$attempt out of $maxAttempts`: username and/or password error";
                    }           
            }catch{
                write-warning $_
                $failureMessage += "Unable to bind to $env:USERDNSDOMAIN.";
                }
 
        # Depending on whether there are failures, proceed accordingly       
        if($failureMessage){
            If ($attempt -lt $maxAttempts-1) {
                $message = "$failureMessage`: Authentication error. Please Try again.";
                Write-Warning $message;
            }elseif($attempt -eq $maxAttempts-1){
                    $message = "$failureMessage`: Last attempt.";
                    Write-Warning $message;
                    $credentials= $false;
            }
        }
 
        } Until (($ValidAccount) -or ($Attempt -ge $MaxAttempts))
    if($credentials){return $credentials;}else{return $false}
}

function updateWindows($computerNames,$adminCredential,$microsoftUpdates=$false,$csvFile='C:\updateResults.csv'){
    get-job|stop-job|remove-job -force
    foreach ($computerName in $computerNames){ 
        Start-Job -name $computerName -ScriptBlock {
            param($invokeWindowsUpdate,$computerName,$adminCredential,$microsoftUpdates)
            [scriptblock]::create($invokeWindowsUpdate).invoke($computerName,$adminCredential,$microsoftUpdates)
        } -Args ${function:invokeWindowsUpdate},$computerName,$adminCredential,$microsoftUpdates
    }

    $lineBreak=30
    $dotCount=0
    $minute=0
    write-host "Minute`r`n$minute`:" -NoNewline -ForegroundColor Yellow
    $jobResults=@()
    $jobsCount=(get-job).count
    do{        
        $completedJobs=get-job|?{$_.State -eq 'Completed'}
        foreach ($job in $completedJobs){
            $computer=$job.Name
            write-host "`r`n===================================================`r`n$computer job completed with these messages:`r`n===================================================`r`n"
            $timeStamp=$job.PSBeginTime
            $minutesElapsed=[math]::round(($job.PSEndTime-$job.PSBeginTime).TotalMinutes,2)
            $computer=$job.Name
            $completed=receive-job -id $job.id
            $result=[pscustomobject][ordered]@{
                computerName=$computer                
                timeStamp=$timeStamp
                minutesElapsed=$minutesElapsed
                completed=$completed
            }
            $jobResults+=,$result
            remove-job -id $job.id
        }
        if($dotCount++ -lt $lineBreak){
            write-host '.' -NoNewline
        }else{
            $minute++
            write-host "`r`n$minute`t:" -ForegroundColor Yellow -NoNewline
            $dotCount=0
            }
        Start-Sleep -seconds 2
    }until($jobsCount -eq $jobResults.count -or !(get-job))
    write-host "`r`n$(($jobResults|ft|out-string).trim())"
    $jobResults | Export-Csv -Path $csvFile -NoTypeInformation -Append
    write-host "Windows Update results have been exported to: $csvFile"  
    $localMachineIsPendingReboot=($jobResults|?{$_.computername -eq $env:computername}).pendingReboot -eq $true
    if($localMachineIsPendingReboot){
        $confirmed=confirmation "I want to reboot this local machine $env:computername"
        if($confirmed){
            Restart-Computer $env:computerName -force
        }else{
            write-warning "Please reboot $env:computername manually to complete its updates."
        }
    }else{
        write-host 'Done.' -ForegroundColor Green
    }
}

if(!$adminCredential){$adminCredential=obtainDomainAdminCredentials}
updateWindows $computerNames $adminCredential $directMicrosoftUpdates $csvFile
# updateMultipleWindows.ps1
# Version 0.0.1
# Requirement: WinRM and Internet Access must be enabled on target computer(s)
  
$computernames='LAX-SERVER01','LAX-SERVER02'
$csvFile='C:\updateResults.csv'
$adminCredential=$false # Leave this as false to obtain Domain Admin cred during runtime
  
function updateRemoteWindows{ 
    [CmdletBinding()] 
    param ( 
        [parameter(Mandatory=$true,Position=0)][string]$computer,
        [parameter(Mandatory=$false,Position=1)][System.Management.Automation.PSCredential]$adminCredential,
        [parameter(Mandatory=$false,Position=2)][bool]$microsoftUpdates=$true,
        [parameter(Mandatory=$false,Position=3)][bool]$bypassWsus=$false,
        [parameter(Mandatory=$false,Position=4)][int]$winRmPort=5985,
        [parameter(Mandatory=$false,Position=5)][string]$logFile='C:\PSWindowsUpdate.log'
        )    
    $ErrorActionPreference='stop'
    <#
    .SYNOPSIS
    This script will automatically install all avaialable windows updates on a device and will automatically reboot if needed.
    After reboots, windows updates will continue to run until all updates are installed.
    # Features: 
    # - Check WSUS settings (bypass if required by the boolean value)
    # - Prepare the targets by installing prerequisites prior to proceeding further to preemptively resolve dependency errors
    # - Include additional dedendencies such as TLS1.2, Nuget & PSGALLERY
    # - Check if server needs a reboot before issuing the reboot command, instead of just inadvertently trigger reboots
    # - Fixed the blank lines in output log causing bug in status query
    # - More thorough cleanup routine
    # Future developments:
    # - Detect and handle proxies
    #>
 
    function installPsWindowsUpdate{
        $ErrorActionPreference='stop'
        $psWindowsUpdateAvailable=Get-Module -ListAvailable -Name PSWindowsUpdate -ErrorAction SilentlyContinue;
        if (!($psWindowsUpdateAvailable)){
            try {                
                [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12;          
                Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -Confirm:$false | Out-Null;
                Set-PSRepository -Name 'PSGallery' -InstallationPolicy Trusted | Out-Null;
                Install-Module PSWindowsUpdate -Confirm:$false -Force | Out-Null;
                Import-Module PSWindowsUpdate -force | Out-Null;
                return $true;
                }
            catch{
                "Prerequisites not met on $ENV:COMPUTERNAME.";
                return $false;
                }
        }else{
            return $true
            }
    }
   
    function checkPendingReboot([string]$computer=$ENV:computername,$session){ 
        function checkRegistry{
                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 -eq $status) -and $status.RebootPending){
                        return $true
                    }else{
                        return $false 
                    }
                }catch{
                    write-warning $_
                    return $false
                }
                        
        }

        if ($ENV:computername -eq $computer){
            $result=checkRegistry;
        }else{
            $result=Invoke-Command -session $session -ScriptBlock{
                    param($importedFunc);
                    [ScriptBlock]::Create($importedFunc).Invoke();
                } -ArgumentList ${function:checkRegistry}
        }
        return $result;
    }
   
    function checkWsus{
        # Check if this machine has WSUS settings configured
        $wuPath="Registry::HKLM\Software\Policies\Microsoft\Windows\WindowsUpdate\AU";
        $wuKey="UseWUServer";
        $wuIsOn=(Get-ItemProperty -path $wuPath -name $wuKey -ErrorAction SilentlyContinue).$wuKey;
        #if($wuIsOn){$GLOBAL:wsus=$True}else{$GLOBAL:wsus=$False}
        return $wuIsOn  
    }
   
    function turnoffWsus{
        # Turn WSUS settings OFF temporarily...
        $wuPath="Registry::HKLM\Software\Policies\Microsoft\Windows\WindowsUpdate\AU";
        $wuKey="UseWUServer";
        Set-Itemproperty -path $wuPath -Name $wuKey -value 0
        restart-service wuauserv;        
        }
   
    function turnonWsus{
        # Turning WSUS settings back to ON status
        $wuPath="Registry::HKLM\Software\Policies\Microsoft\Windows\WindowsUpdate\AU";
        $wuKey="UseWUServer";
        Set-Itemproperty -path $wuPath -Name $wuKey -value 1
        restart-service wuauserv;
        }
    function connectWinRm($computer,$adminCredential,$winRmPort=5985){
        # 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
        }

        do{
            $ping = Test-Connection $computer -quiet
            if($ping -eq $false){sleep 1}
        }until ($ping -eq $true)
        $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
        do{
            if($adminCredential){
                $session = New-PSSession -ComputerName $computer -Credential $adminCredential
            }else{
                $session = New-PSSession -ComputerName $computer
                }
            write-host "Connecting to remote computer $computer..."
            sleep -seconds 1
            if ($session){
                write-host "Connected."
                return $session
            }
        } until ($session.state -match "Opened")
    }

    # Ensure that this function does not execute on the localhost
    if($env:computername,'localhost'|?{$_ -like "$computer*"}){
        write-warning "$computer is detected as the localhost where this program is invoked. This is out of scope of this function."
        return $false
        }
    # Advisories
    write-warning "$computer will go through Windows Update and will REBOOT AUTOMATICALLY (if necessary). Press Ctrl+C at anytime to cancel."
   
    $session=connectWinRm $computer $adminCredential
    Do{        
        # Install prerequisites
        $psWindowsUpdateAvailable=invoke-command -session $session -scriptblock {
            param($installPsWindowsUpdate);
            [ScriptBlock]::Create($installPsWindowsUpdate).Invoke();
            } -Args ${function:installPsWindowsUpdate}
           
        if(!$psWindowsUpdateAvailable){
            write-warning "$computername`t: PSWindowsUpdate installation failed."
            return $false
            }
   
        #retrieves a list of available updates 
        write-host "Checking for new updates on $computer..."
        $updates=invoke-command -session $session -scriptblock {
            param($microsoftUpdates)
            if($(Get-ExecutionPolicy) -ne 'RemoteSigned'){
                Set-ExecutionPolicy RemoteSigned -force
            }
            $null=Import-Module PSWindowsUpdate
            if($microsoftUpdates){
                Get-wulist -MicrosoftUpdate -verbose
            }else{
                Get-wulist -WindowsUpdate -verbose
            }
        } -Args $microsoftUpdates
   
        # Count how many updates are available 
        $updatesCount = ($updates.kb).count                
   
        # If there are available updates proceed with installing the updates and then reboot the remote machine if required
        if ($null -ne $updates){ 
            if($bypassWsus){
                $wsusIsOn=Invoke-Command -session $session -scriptblock{
                    param($checkWsus);
                    [scriptblock]::create($checkWsus).invoke()
                    } -Args ${function:checkWsus}
                if($wsusIsOn){
                    Invoke-Command -session $session -scriptblock{
                    param($turnoffWsus);
                    [scriptblock]::create($turnoffWsus).invoke()
                    } -args ${function:turnoffWsus}
                }
            }
               
            #Invoke-WUJob will insert a scheduled task on the remote target as a mean to bypass 2nd hop issues            
            invoke-command -Session $session {
                    $invokeScript={
                        param($microsoftUpdates)
                        import-module PSWindowsUpdate;
                        if($microsoftUpdates){
                            # 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
                                }
                            Get-WindowsUpdate -AcceptAll -MicrosoftUpdate -Install | Out-File C:\PSWindowsUpdate.log
                        }else{
                            Get-WindowsUpdate -AcceptAll -WindowsUpdate -Install | Out-File C:\PSWindowsUpdate.log
                        }
                    }
                    Invoke-WUjob -ComputerName $env:computername -Script $invokeScript -Confirm:$false -RunNow
                } -Args $microsoftUpdates
   
            #Show update status until the amount of installed updates equals the same as the count of updates being reported 
            sleep -Seconds 30 # Wait for the log file to generate
            $dots=80
            $dotCount=0
            $lastActivity="";
            $installedCount=0;
            Write-Host "There $(if($updatesCount -eq 1){'is'}else{'are'}) $updatesCount pending update(s)`r`n";
            do {                
                $getWindowsUpdateLog={Get-Content "C:\PSWindowsUpdate.log"}
                $updatestatus=Invoke-Command -Session $session -ScriptBlock $getWindowsUpdateLog
                $currentActivity=$updatestatus | select-object -last 1
                if (($currentActivity -ne $lastActivity) -AND ($Null -ne $currentActivity)){
                    Write-Host "Procesing $($installedCount+1) of $updatesCount updates."
                    Write-Host "`n$currentActivity";
                    $lastActivity=$currentActivity;
                    }else{
                        if ($dotCount++ -le $dots){
                            Write-Host -NoNewline ".";
                            if($installedCount -eq $updatesCount){Write-Host "Processing last update: $installedCount of $updatesCount."}
                        }else{
                            Write-Host ".";
                            $dotCount=0;
                            }                                                   
                        }                
                sleep -Seconds 10
                $installedCount = ([regex]::Matches($updatestatus, "Installed")).count
                }until ($installedCount -eq $updatesCount)
    
                #restarts the remote computer and waits till it starts up again
                if (checkPendingReboot $computer $session){                    
                    write-host "`nReboots required.`nRestarting remote computer $computer to clear pending reboot flags." 
                    if($adminCredential){
                        Restart-Computer -Wait -ComputerName $computer -Credential $adminCredential -Force
                    }else{
                        Restart-Computer -Wait -ComputerName $computer -Force
                        }
                    write-host "$computer has been successfully restarted!" -ForegroundColor Yellow
                    if($session.State -ne 'Opened'){
                        write-host "Reconnecting to $computer..." -ForegroundColor Yellow
                        $session=connectWinRm $computer $adminCredential
                    }                    
                }else{
                    write-host "No reboots required." -ForegroundColor Green
                    }                        
            }else{
                write-host "There are no available patches detected on $computer"
                }
    }until(($null -eq $updates) -OR ($installedCount -eq $updatesCount))   
    if($session.State -ne 'Opened'){
        write-host "Reconnecting to $computer..." -ForegroundColor Yellow
        $session=connectWinRm $computer $adminCredential
    }
    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 -Credential $adminCredential -ErrorAction SilentlyContinue){
            Write-Host "Deleting log to prevent collisions with subsequent runs."                    
            Write-Host "Removing $logFile."
            Remove-item $logFile
            }
        } -Args $logFile
    if($bypassWsus -and $wsusIsOn){
        write-host "Reverting WSUS registry edits"
        Invoke-Command -session $session -scriptblock{
            param($turnonWsus);
            [scriptblock]::create($turnonWsus).invoke()
        } -args ${function:turnonWsus} ;
    }
    Write-Host "Windows is now up to date on $computer" -ForegroundColor Green
    if($session.State -eq 'Opened'){Remove-PSSession $session}
    return $true
}

function updateLocalWindows{
    param(
        $microsoftUpdates=$true,
        $bypassWsus=$false,        
        $notCategory='Drivers',
        $verbose=$false
    )
    if($verbose){$localTimer=[System.Diagnostics.Stopwatch]::StartNew()}
    $tempDir='C:\temp\'
    if(Test-Path $tempDir){$null=mkdir $tempDir -force}
 
    function checkPendingReboot{
            function checkRegistryPendingReboot{
                    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(($status -ne $null) -and $status.RebootPending){
                        return $true
                    }
                    }catch{}
                    return $false          
            }
            return checkRegistryPendingReboot
        }
  
    function updateWindows($microsoftUpdates,$notCategory){
        #Install the PowerShell Windows Update module
        $checkModule=Get-Module -ListAvailable -Name PSWindowsUpdate
        if(!($checkModule)){
            [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
            # Set PowerShell Gallery as Trusted to bypass prompts
            #$trustPSGallery=(Get-psrepository -Name 'PSGallery').InstallationPolicy
            If($trustPSGallery -ne 'Trusted'){
                Install-PackageProvider -Name Nuget -Force
                #Set-PSRepository -Name 'PSGallery' -InstallationPolicy Trusted
                }        
            Install-Module PSWindowsUpdate -Confirm:$false
            }
   
        # Perform Updates
        set-executionpolicy bypass -force
        if($bypassWsus){
            }
        # -MicrosoftUpdate: include other Microsoft products (Office, Silverlight, Visual C++, etc.)
        # -WindowsUpdate: include only Windows updates
        if($microsoftUpdates){
            # 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
                }
            $updateCommand='Get-WindowsUpdate -AcceptAll -MicrosoftUpdate -Install -IgnoreReboot'+$(if($notCategory){' -NotCategory '+$notCategory})
        }else{
            $updateCommand='Get-WindowsUpdate -AcceptAll -WindowsUpdate -Install -IgnoreReboot'+$(if($notCategory){' -NotCategory '+$notCategory})
        }
        Invoke-Expression $updateCommand
             
        $pendingReboot=checkPendingReboot $computer
        if(!$pendingReboot){
            write-host 'Updates are completed.' -ForegroundColor Green
            return $true
        }else{
            write-host 'Please reboot and trigger updates again to complete the process.' -ForegroundColor Yellow
            return $false
            }        
        }
 
    function checkUpdates($microsoftUpdates,$notCategory='drivers',$verbose=$false){
        ## This runs into the constraints of unelevated process being prevented from reading stdout of an elevated session
        ## Source: https:// docs.microsoft.com/en-us/dotnet/api/system.diagnostics.process?view=netcore-3.1 
        #$startInfo = New-Object System.Diagnostics.ProcessStartInfo
        #$startInfo.FileName = $exe
        #$startInfo.RedirectStandardError = $true
        #$startInfo.RedirectStandardOutput = $true    
        #$startInfo.Arguments = "(Get-wulist -verbose).kb.count"
        #$startInfo.CreateNoWindow = $false
        #$startInfo.UseShellExecute = $false
        ##$startInfo.Verb = "runas"
        ##$startInfo.UseShellExecute = $true
        ##Error when trying to run as Administrator: Exception calling 'Start' with "0" argument(s):
        ## 'The Process object must have the UseShellExecute property set to false in order to redirect IO streams.'
        #$process = New-Object System.Diagnostics.Process
        #$process.StartInfo = $startInfo
        #$process.Start()
        #$process.WaitForExit()
        #$stdout = $process.StandardOutput.ReadToEnd()
        #$stderr = $process.StandardError.ReadToEnd()
        #Write-Host 'stdout: '+ $stdout
        #if($stderr){Write-Host 'stderr: '+$stderr -ForegroundColor Red}
        #Write-Host 'exit code: '+$($process.ExitCode)
        #return $stdout
 
        # This is the workaround
        $tempFile='C:\temp\availableUpdatesCount.txt'
        $checkUpdates=@"
            `$x=(Get-wulist -verbose $(if($microsoftUpdates){'-MicrosoftUpdate '}) $(if($notCategory){'-NotCategory '+$notCategory})).kb.count
            write-host `$x
            New-Item '$tempFile' -ItemType File -Value `$x -Force
"@
        Start-Process -verb Runas powershell.exe $checkUpdates -Wait -WindowStyle $(if($verbose){'normal'}else{'minimized'})
        $result=Get-Content $tempFile
        Start-Process -verb Runas powershell.exe "remove-item '$tempFile' -Force" -WindowStyle hidden -Wait 
        return $result
    }
 
    $functionScript = @"
        `$Host.UI.RawUI.BackgroundColor = 'Black'
        `$myWindowsID=[System.Security.Principal.WindowsIdentity]::GetCurrent()
        `$myWindowsPrincipal=new-object System.Security.Principal.WindowsPrincipal(`$myWindowsID)
        `$adminRole=[System.Security.Principal.WindowsBuiltInRole]::Administrator
        if (`$myWindowsPrincipal.IsInRole(`$adminRole)){
            write-host "This session is running under the context of the system 'Administrator'" -ForegroundColor Green
        }else{
            write-warning "This session is NOT in the context of 'Administrator'"
            }
        Function checkPendingReboot{
            $(Get-Command checkPendingReboot|Select -expand Definition)
        }
        Function updateWindows{
            $(Get-Command updateWindows|Select -expand Definition)
        }
        updateWindows $microsoftUpdates $notCategory
"@
    
    write-host "Spawning a background process to update Windows as Administrator..."
    do{
        $process=Start-Process -verb Runas powershell $functionScript -WindowStyle $(if($verbose){'normal'}else{'minimized'}) -PassThru
        $lineBreak=60
        $dotCount=0
        $minute=0
        write-host "Minute`r`n$minute`t:" -NoNewline -ForegroundColor Yellow
        do {
            if($dotCount++ -lt $lineBreak){
                write-host '.' -NoNewline
            }else{
                $minute++
                write-host "`r`n$minute`t:" -ForegroundColor Yellow -NoNewline
                $dotCount=0
                }
            Start-Sleep -s 1
        }until (!$process.Responding)
 
        [bool]$isPendingReboot=checkPendingReboot
        [int]$updatesAvailable=checkUpdates $microsoftUpdates $notCategory $verbose
        if(!$updatesAvailable){
            write-host "`r`nWindows updates completed" -ForegroundColor Green        
            $completed=$true
        }else{ 
            write-host "`r`nMissing $updatesAvailable updates." -ForegroundColor Yellow
            $completed=$false
            }
    }until($completed -or $isPendingReboot)
    
    if($verbose){
        $minutes=[math]::round($localTimer.Elapsed.TotalMinutes,2)
        write-host "$minutes minutes elapsed"
    }
    if($isPendingReboot){
        write-host "`r`nPending reboots detected." -ForegroundColor Yellow
        return $false
    }elseif($completed){
        return $true
    }
}

function confirmation($content,$testValue="I confirm",$maxAttempts=3){
    $confirmed=$false;
    $attempts=0;        
    $content|write-host
    write-host "Please review this content for accuracy.`r`n"
    while ($attempts -le $maxAttempts){
        if($attempts++ -ge $maxAttempts){
            write-host "A maximum number of attempts have reached. No confirmations received!`r`n"
            break;
            }
        $userInput = Read-Host -Prompt "Please type in this value => $testValue <= to confirm. Input CANCEL to skip this item";
        if ($userInput.ToLower() -eq $testValue.ToLower()){
            $confirmed=$true;
            write-host "Confirmed!`r`n";
            break;                
        }elseif($userInput -like 'cancel'){
            write-host 'Cancel command received.'
            $confirmed=$false
            break
        }else{
            cls;
            $content|write-host
            write-host "Attempt number $attempts of $maxAttempts`: $userInput does not match $testValue. Try again or Input CANCEL to skip this item`r`n"
            }
        }
    return $confirmed;
}

# Obtain Domain Admin Credentials
function obtainDomainAdminCredentials{
    # Legacy domain binding function
    function isValidCred($u,$p){
        # Get current domain using logged-on user's credentials
        $domain = "LDAP://" + ([ADSI]"").distinguishedName
        $domainCred = New-Object System.DirectoryServices.DirectoryEntry($domain,$u,$p)
        if ($domainCred){
            return $True
        }else{
            return $False
        }
    }
    function isDomainAdmin($username){
        if (!(get-module activedirectory)){Install-WindowsFeature RSAT-AD-PowerShell -Confirm:$false}
        if((Get-ADUser $username -Properties MemberOf).MemberOf -match '^CN=Domain Admins'){
            return $True;
        }else{return $false;}
    }
 
    # Create a domain context
    function dotNetDomainBind{
        Add-Type -AssemblyName System.DirectoryServices.AccountManagement;  
        Try {
            $type = [System.DirectoryServices.AccountManagement.ContextType]::Domain;
            $context = New-Object System.DirectoryServices.AccountManagement.PrincipalContext $type,$env:USERDOMAIN;
            return $context;
        } Catch {
            If ($_.Exception.InnerException -like "*The server could not be contacted*") {
                write-host "Could not contact a server for the specified domain $env:USERDOMAIN via DotNet Method.";
            } Else {
                write-host "Unknown errors occured while attempting to contact $env:USERDOMAIN via DotNet Method.";
            }
            return $false;
        }
    }
 
    $attempt=0;
    $maxAttempts=3;
    $plainTextPassword='';   
    Do{
        $attempt++;
        $failureMessage = $null; 
        [string][ValidateNotNullOrEmpty()]$userName=Read-Host -Prompt "Please input a User ID";
        if($userName -notmatch '\\'){
            $username=$env:USERDOMAIN+'\'+$userName
        }
        $password = Read-Host -Prompt "Please type in the password for user $userName" -AsSecureString;
        $plainTextPassword=[Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($password))
     
        # Test bind to this credential
        try {
            # Test bind
            $context=dotNetDomainBind
            if($context){
                $validatedAccount = $context.ValidateCredentials($userName,$plainTextPassword)
            }else{
                $validatedAccount=isValidCred -u $userName -p $plainTextPassword
            }
             
            If ($validatedAccount) {
                $onlyUserName=$userName -replace '.+\\'
                $isDomainAdmin=isDomainAdmin -username $onlyUserName;
                if($isDomainAdmin){
                    $credentials = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $userName,$password;
                    $validAccount=$true;
                    }else{
                        $failureMessage += "$attempt out of $maxAttempts`: $userName account is valid, but it is not a Domain Admin";
                        }
                }else{
                    $failureMessage += "$attempt out of $maxAttempts`: username and/or password error";
                    }           
            }catch{
                write-warning $_
                $failureMessage += "Unable to bind to $env:USERDNSDOMAIN.";
                }
 
        # Depending on whether there are failures, proceed accordingly       
        if($failureMessage){
            If ($attempt -lt $maxAttempts-1) {
                $message = "$failureMessage`: Authentication error. Please Try again.";
                Write-Warning $message;
            }elseif($attempt -eq $maxAttempts-1){
                    $message = "$failureMessage`: Last attempt.";
                    Write-Warning $message;
                    $credentials= $false;
            }
        }
 
        } Until (($ValidAccount) -or ($Attempt -ge $MaxAttempts))
    if($credentials){return $credentials;}else{return $false}
}
function outputHashtableToCsv{
    param(
        $hashTable,
        $csvFile='C:\updateResults.csv',
        $headers=@('computerName','minutesToUpdate')
        )
    try{
        write-host $($hashTable|out-string)
        if(test-path $csvFile){        
            rename-item $csvFile "$csvFile.bak"
            write-warning "$csvFile currently exists. Hence, that previous file has been renamed to $csvFile.bak"
        }
        $hashTable.GetEnumerator() | `
            Select-Object -Property @{N=$headers[0];E={$_.Key}}, @{N=$headers[1];E={$_.Value}} | `
            Export-Csv -NoTypeInformation -Path $csvFile
        write-host "CSV has been created successfully: $csvFile"
        return $true
    }catch{
        write-warning $_
        return $false
    }    
}
function updateWindows($computerNames,$adminCredential,$csvFile='C:\updateResults.csv'){
    #get-job|?{$_.HasMoreData -eq $false -and $_.State -match 'Completed|failed'}|remove-job
    get-job|remove-job
    foreach ($computerName in $computerNames){      
        if($env:computername -eq $computerName){
            #$timer=[System.Diagnostics.Stopwatch]::StartNew()
            #write-host "Updating localhost $computername..."
            Start-Job -name $computerName -ScriptBlock {
                param($updateLocalWindows)
                [scriptblock]::create($updateLocalWindows).invoke()
            } -Args ${function:updateLocalWindows}
            #$localMachineIsPendingReboot=!(updateLocalWindows)
        }else{
            Start-Job -name $computerName -ScriptBlock {
                param($updateRemoteWindows,$computerName,$adminCredential)
                [scriptblock]::create($updateRemoteWindows).invoke($computerName,$adminCredential)
            } -Args ${function:updateRemoteWindows},$computerName,$adminCredential
            #updateRemoteWindows -computer $computerName -adminCredential $adminCredential
        }        
        #$minutesElapsed=[math]::round($timer.Elapsed.TotalMinutes,2)
        #write-host "$computerName update minutes: $minutesElapsed"
        #$updateResults+=@{$computerName=$minutesElapsed}
    }

    $lineBreak=60
    $dotCount=0
    $minute=0
    write-host "Minute`r`n$minute`t:" -NoNewline -ForegroundColor Yellow
    $GLOBAL:jobResults=@()
    do{        
        $completedJobs=get-job|?{$_.HasMoreData -eq $true -and $_.State -eq 'Completed'}
        foreach ($job in $completedJobs){
            $minutesElapsed=[math]::round(($job.PSEndTime-$job.PSBeginTime).TotalMinutes,2)
            $computer=$job.Name
            write-host "Processing $computer results..."
            $result=receive-job -id $job.id
            $jobResults+=[pscustomobject]@{
                computerName=$computer
                minutesElapsed=$minutesElapsed
                completed=$result
            }
            remove-job -id $job.id
        }
        if($dotCount++ -lt $lineBreak){
            write-host '.' -NoNewline
        }else{
            $minute++
            write-host "`r`n$minute`t:" -ForegroundColor Yellow -NoNewline
            $dotCount=0
            }
        Start-Sleep -s 1
    }until(!(get-job))
    write-host "$(($jobResults|ft|out-string).trim())"
    
    $localMachineIsPendingReboot=($jobResults|?{$_.computername -eq $env:computername}).completed -eq $false
    if($localMachineIsPendingReboot){
        $confirmed=confirmation "I want to reboot this local machine $env:computername"
        if($confirmed){
            Restart-Computer $env:computerName -force
        }else{
            write-warning "Please reboot $env:computername manually to complete its updates."
        }
    }else{
        write-host 'Done.' -ForegroundColor Green
    }
}

if(!$adminCredential){$adminCredential=obtainDomainAdminCredentials}
updateWindows $computerNames $adminCredential $csvFile

18 thoughts on “PowerShell: Update a List of Multiple Windows Machines”

  1. Hi,

    I’ve just tried out the script above to update severeal servers remotely. The servers have noc connection to the Internet. It’ seems like the scrit works. But if I run it a second time, it will end in :
    …………….
    1 :…………………………
    2 :…………………………
    3 :…………………………
    4 :…………………………
    5 :…………………………
    6 :…………………………
    7 :…………………………
    8 :…………………………
    9 :…………………………
    10 :…………………………
    11 :…………………………
    12 :…………………………
    13 :…………………………
    14 :…………………………
    15 :…………………………
    16 :…………………

    and ongoing. I have to abort it with ctrl+c.
    There is also a message for each server:

    ==================================================
    job completed with these messages:
    ===================================================

    WARNING: will go through Windows Update and will REBOOT AUTOMATICALLY (if necessary).
    Press Ctrl+C at anytime to cancel.
    Connected to .
    Checking for new updates on …
    VERBOSE: (08.12.2020 14:10:10): Connecting to Microsoft Update server. Please wait…

    Exception calling “Invoke” with “3” argument(s): “The running command stopped because the preference variable “ErrorActionPreference” or common parameter is set to Stop: An operation did not
    complete because the service is not in the data store. Probably you don’t have permission for remote connection to Windows Update Agent or used wrong ServiceID.”
    + CategoryInfo : NotSpecified: (:) [], MethodInvocationException
    + FullyQualifiedErrorId : ActionPreferenceStopException
    + PSComputerName : localhost

    The servers have already downloaded updates, which may have to be installed or a server restard is outstanding. can someone shorten the script for me, so that it only check if local downloaded updates, or wsus updates are available and restart the server?

    Regards

    Marc

    1. Hi Marc,

      Thanks for trying this out. Script does require that the target Windows have a connection to the Internet or WSUS. Let me find some time to modify this to account for pre-downloaded packages, where no Internet connections would be required. Meanwhile, can you give me more details about your remote computers: are they downloading packages from your local WSUS?

      Best regards,
      Kim

      1. Thank you,Kim, for your big efforts.

        We supply various customers with our own software and manage the servers on which our software is installed. However, many companies give Windows their own parameters through group policies. So does the current customer for whom I need the Powerschell script. With this customer, the updates are automatically downloaded to the server and you are asked to install them manually and to restart the server. So it’s really only a matter of displaying these already downloaded updates on the server, installing them and restarting the server (all remotely from a single server running this script).Since there are about 30 servers that we have to provide with Windows updates ourselves, such a script would be a great relief.
        I’ve tried a script like this before, but it never worked and didn’t work remotely. Yours, however, seems to be going in the right direction for my needs.

        I would like to have an output in the script that I can send by email when the updates have gone through successfully. I would like to have information on which server, which update was installed and when it was then re-evaluated.
        I can add the part by e-mail myself. I’ve already finished it. I would only need the listing as output.

        1. Marc,

          I’ve been preoccupied with things lately. I’ll take a closer look a the error message you’re showing and revise the script to account for that scenario. It does seem that your target machines do not have access to the Internet to contact Microsoft; hence, ‘service is not in the data store.’

          Will update this post when I make progress.

          Cheers,

          1. Thank you! Yes, servers have no comnnection to the internet and updates are predowanloaded by customers group policies settings, I cannot change. The customer only want us to check if updates for installation are available on the servers, install them and restart the servers, if necessary.
            Your main script is also very useful for me for other customers too.

      1. Thank you. I just tried out. I’m not that Powershell pro. So I entered Admin credentzials on the section on the top of the script. When I run the script, It asks for another credentials:

        “Please input a User ID: ”

        If I type in nothing, it says

        “Could not contact a server for the specified domain via DotNet Method.
        WARNING: Cannot validate argument on parameter ‘Identity’. The Identity property on the argument is null or empty.
        WARNING: Unable to bind to .: Authentication error. Please Try again.”

        If I type in admin credentials with domain, it says:

        “Could not contact a server for the specified domain via DotNet Method.
        WARNING: The server has rejected the client credentials.
        WARNING: Unable to bind to .: Last attempt.”

        Why I have to enter these User ID? For what case is it? Why not use the pre entered Admin credentials on the top? Can I bypass this? The scipt should run a scheduled Task, so that I cannot type in credentials manually, whe it runs

        1. I’ve been working on too many tasks at the same time, so I’ve made mistakes in updating the program. Please retry ‘version 3’ to see if it now works for ya.

          1. No problem. This is a long scipt.
            The updated version seems to run more smooth. The only thing , it stucks (even in the past versions ) is on this point:

            — —- ————- —– ———– ——– ——-
            59 testWindows BackgroundJob Running True localhost …
            Minute
            0:………
            ===================================================
            testWindows job completed with these messages:
            ===================================================

            WARNING: testWindows will go through Windows Update and will REBOOT AUTOMATICALLY (if necessary).
            Press Ctrl+C at anytime to cancel.
            Connected to testWindows.
            testWindows: no internet connectivity detected
            Adding task on testWindows…
            -Message windowsUpdate has taken 0.5019489 seconds to initiate
            Checking \\testWindows\c$\WindowsUpdate.log for Windows Update statuses…
            Exception calling “Invoke” with “3” argument(s): “The running command stopped because the preference variable “ErrorActionPreference” or common parameter is set to Stop: Access is denied”
            + CategoryInfo : NotSpecified: (:) [], MethodInvocationException
            + FullyQualifiedErrorId : ActionPreferenceStopException
            + PSComputerName : localhost

            …………………
            1 :…………………………
            2 :…………………………
            3 :…………………………
            4 :…………………………
            5 :…………………………
            6 :

            I only tested it with one server on the same time , but the warning:

            Exception calling “Invoke” with “3” argument(s): “The running command stopped because the preference variable “ErrorActionPreference” or common parameter is set to Stop: Access is denied”

            Appears on every server. If I comment it out. It runs without this message but in an andles retry where the dots are counting

          2. I just looked in the logfile of the servers, I’ll try to update. The write “2 updates detected” which is correct, but after that, the script seems to wait for something. no installation process, no reboot of the server.

          3. The server(s) was waiting for a reboot, which the script would perform. Afterward, updates would continue until done. I’ve misspelled the $adminCredential as $credential. That’s been fixed. Please give it another try.

  2. So, I tried again.
    An Additional Info: I have noc domain admin account. I only have an account with local admin rights on the target computers.
    ->I run the script
    -> The script finds the Update (can see it in the logfile on each server)
    -> Th log says, that server needs to be reboot.
    -> I see an scheduled task, generated
    -> lokked into the powershell script, which is opend due to the task
    ->I run it manually and it says ” has 0 available updates. Patching FINISHED”
    -> This is written on every server. If I look in the Windowes Updates, there are updates pending and renboot required
    -> On this part, there must be a little mistake in the script, I think
    -> Meanwhile the main script is running in an endless loop of the dots

    1. Hi Marc,

      I appreciate the opportunity to work with you on this. It appears that the script has ‘almost’ worked. It is suppose to detect whether the target is pending reboot and do that. However, that hasn’t happened for some reasons. I hope that it’s not firewall related as the computer and login credential where you’re triggering the script from (JumpBox) needs to be able to access the target’s \\$computer\c$\WindowsUpdate.log. If not, then I need to think of another way of detecting progress and set script to perform reboots and re-trigger updates…

      I’ve created another link to simply this script for the special situation here: https://kimconnect.com/update-windows-with-restricted-internet-access/

      Let’s continue our development talks there.

      Thanks,
      KimConnect

      1. Yeah. The thing with the access to the logfile could be the reason. When I run the schedulked task script manually, this happens:

        12/15/2020 11:23:23 => TESTWINDOWS has 0 available updates. Patching FINISHED
        Add-Content : Access to the path ‘C:\WindowsUpdate.log’ is denied.
        At C:\Scripts\WindowsUpdates.ps1:19 char:17
        + Add-Content -Path $logfile -Value $status
        + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        + CategoryInfo : PermissionDenied: (C:\WindowsUpdate.log:String) [Add-Content], UnauthorizedAccessException
        + FullyQualifiedErrorId : GetContentWriterUnauthorizedAccessError,Microsoft.PowerShell.Commands.AddContentCommand

        Don’t know, why access is denied to the Logfile, because the User I try with, has local admin rights

        So I’ll try the new scipt, you created.

  3. Awesome work- I’m getting an error when running this on PowerShell 5.1 and 7-

    Attempting to use psexec to enable WinRM remotely…
    Exception calling “Invoke” with “3” argument(s): “The running command stopped because the preference variable “ErrorActionPreference” or common parameter is set to Stop: The handle is invalid.”
    + CategoryInfo : NotSpecified: (:) [], MethodInvocationException
    + FullyQualifiedErrorId : ActionPreferenceStopException
    + PSComputerName : localhost

    I’ve run as a domain admin as well as a local admin without much luck.

    1. It appears that WinRM is unreachable at the Destination. Could be a firewall issue or WinRM isn’t setup/listening on remote machine. How about running this in a console or RDP session on that machine to see if it would make a difference: C:\Windows\system32\winrm.cmd qc -quiet

Leave a Reply

Your email address will not be published. Required fields are marked *