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