Public/Network/Test-NetConnectionAsync.ps1

function Test-ConnectionAsync {
  <#
    .SYNOPSIS
        Performs a ping test asynchronously
    .DESCRIPTION
        Performs a ping test asynchronously
    .PARAMETER ComputerName
        List of computers to test connection. Aliased to 'CN', 'Server'
    .PARAMETER Timeout
        Timeout in milliseconds. Default 2000 ms.
    .PARAMETER TimeToLive
        Sets a time to live on ping request. Default 128.
    .PARAMETER BufferSize
        How large you want the buffer to be. Valid range 32-65500, default of 32. If the buffer is 1501 bytes or greater then the ping can fragment.
    .PARAMETER IncludeSource
        A switch determining if you want the source computer name to appear in the output
    .PARAMETER Full
        A switch determining if full output appears
    .NOTES
        Inspired by Test-ConnectionAsync by 'Boe Prox'
        https://gallery.technet.microsoft.com/scriptcenter/Asynchronous-Network-Ping-abdf01aa
        * fixed logic around processing pipeline
        * removed $Buffer parameter
        * added $BufferSize parameter and dynamically create $Buffer from $BufferSize
        * added $IncludeSource so that source computer would be included in output
        * added $Full so that default output is brief
        * changed datatype of .IPAddress to [version] so that it can be sorted properly
    .OUTPUTS
        [pscustomobject] with output from Net.AsyncPingResult and optionally the source address
    .EXAMPLE
        Test-ConnectionAsync google.com, youtube.com, bing.com, github.com, alainQtec.com
 
        ComputerName IPAddress Result
        ------------ --------- ------
        google.com 172.217.170.206 Success
        youtube.com 172.217.170.174 Success
        bing.com 204.79.197.200 Success
        github.com 140.82.121.4 Success
        alainQtec.com 185.199.110.153 Success
 
 
        Description
        -----------
        Performs asynchronous ping test against listed systems and lists brief output.
    .EXAMPLE
        Test-ConnectionAsync google.com, youtube.com, bing.com, github.com, alainQtec.com -Full | ft
 
        ComputerName IPAddress Result BufferSize ResponseTime DontFragment Timeout TimeToLive
        ------------ --------- ------ ---------- ------------ ------------ ------- ----------
        google.com 172.217.170.206 Success 32 79 True 2000 128
        youtube.com 172.217.170.174 Success 32 79 True 2000 128
        bing.com 204.79.197.200 Success 32 137 True 2000 128
        github.com 140.82.121.3 Success 32 252 True 2000 128
        alainQtec.com 185.199.110.153 Success 32 136 True 2000 128
 
        Description
        -----------
        Performs asynchronous ping test against listed systems and lists full output.
    .EXAMPLE
        Test-ConnectionAsync -ComputerName server1,server2 -Full -BufferSize 1500
 
        ComputerName IPAddress BufferSize Result ResponseTime
        ------------ --------- ---------- ------ ------------
        server1 192.168.1.31 1500 Success 140
        server2 192.168.1.41 1500 Success 137
 
        Description
        -----------
        Performs asynchronous ping test against listed systems and lists full output with a buffersize of 1500 bytes.
    #>

  #Requires -Version 3.0
  [OutputType('Net.AsyncPingResult')]
  [CmdletBinding(ConfirmImpact = 'None')]
  Param (
    [parameter(ValueFromPipeline, Position = 0)]
    [string[]] $ComputerName,

    [parameter()]
    [int] $Timeout = 2000,

    [parameter()]
    [Alias('Ttl')]
    [int] $TimeToLive = 128,

    [parameter()]
    [validaterange(32, 65500)]
    [int] $BufferSize = 32,

    [parameter()]
    [switch] $IncludeSource,

    [parameter()]
    [switch] $Full
  )

  begin {
    Out-Verbose "Starting [$($MyInvocation.Mycommand)]"
    if ($IncludeSource) { $Source = $env:COMPUTERNAME }
    $Buffer = New-Object -TypeName System.Collections.ArrayList
    1..$BufferSize | ForEach-Object { $null = $Buffer.Add(([byte] [char] 'A')) }
    $PingOptions = New-Object -TypeName System.Net.NetworkInformation.PingOptions
    $PingOptions.Ttl = $TimeToLive
    If ($BufferSize -gt 1500) {
      $DontFragment = $false
    } else {
      $DontFragment = $true
    }
    Out-Verbose "ComputerName [$($ComputerName -join ', ')]"
    Out-Verbose "BufferSize [$BufferSize]"
    Out-Verbose "Timeout [$Timeout]"
    Out-Verbose "TimeToLive [$TimeToLive]"
    Out-Verbose "IncludeSource [$IncludeSource]"
    Out-Verbose "Full [$Full]"
    Out-Verbose "DontFragment [$DontFragment]"
    $PingOptions.DontFragment = $DontFragment
    $Computerlist = New-Object -TypeName System.Collections.ArrayList
  }

  process {
    foreach ($Computer in $ComputerName) {
      [void] $Computerlist.Add($Computer)
    }
  }

  end {
    $Task = foreach ($Computer in $ComputerList) {
      [pscustomobject] @{
        ComputerName = $Computer
        Task         = (New-Object -TypeName System.Net.NetworkInformation.Ping).SendPingAsync($Computer, $Timeout, $Buffer, $PingOptions)
      }
    }
    try {
      [void] [Threading.Tasks.Task]::WaitAll($Task.Task)
    } catch {
      Write-Error -Message "⛔ Error checking [$Computer]"
    }
    $Task | ForEach-Object {
      if ($_.Task.IsFaulted) {
        $Result = $_.Task.Exception.InnerException.InnerException.Message
        $IPAddress = $Null
        $ResponseTime = $Null
      } else {
        $Result = $_.Task.Result.Status
        $IPAddress = $_.task.Result.Address.ToString()
        $ResponseTime = $_.task.Result.RoundtripTime
      }
      $Layout = [ordered] @{
        ComputerName = $_.ComputerName
        IPAddress    = if ($IPAddress) { [version] $IPAddress } else { $Null }
        Result       = $Result
        BufferSize   = $BufferSize
        ResponseTime = $ResponseTime
        DontFragment = $DontFragment
        Timeout      = $Timeout
        TimeToLive   = $TimeToLive
      }
      if ($IncludeSource) {
        $Layout.Insert(0, 'Source', $Source)
      }
      $Object = New-Object -TypeName psobject -Property $Layout
      $Object.pstypenames.insert(0, 'Net.AsyncPingResult')
      if ($Full) {
        $Object
      } else {
        if ($IncludeSource) {
          $Object | Select-Object -Property Source, ComputerName, IPAddress, Result
        } else {
          $Object | Select-Object -Property ComputerName, IPAddress, Result
        }
      }
    }
    Out-Verbose $fxn "Complete."
  }
}
function Test-Connections {
  <#
        .Synopsis
            Test-Connection to multiple devices in parallel.
        .Description
            Test-Connection to multiple devcies in parallel with a color and "watch" feature.
        .Example
            Test-Connections -TargetName 1.1.1.1 -Watch
        .Example
            Test-Connections -TargetName 1.1.1.1, 1.0.0.1, 8.8.4.4, 8.8.8.8, 9.9.9.9 -Watch
        .Example
            Test-Connections 1.1.1.1, 1.0.0.1, 8.8.4.4, 8.8.8.8, 9.9.9.9 -Watch -Repeat
        .Example
            Test-Connections 1.1.1.1, 1.0.0.1, 8.8.4.4, 8.8.8.8, 9.9.9.9 -Count 10 -Watch
        .Example
            Test-Connections $(Get-Content servers.txt) -Watch
        .Example
            @("1.1.1.1", "1.0.0.1", "8.8.4.4", "8.8.8.8", "9.9.9.9") | Test-Connections -Watch
        .Example
            Connect-VIServer esxi.local
            Get-VM | Test-Connections -Watch
        .Example
            (1..10) | ForEach-Object { "192.168.0.$_" } | Test-Connections -Watch
        .Notes
            Name: Test-Connections
            Author: David Isaacson
            Last Edit: 2022-04-24
            Keywords: Test-Connection, ping, icmp
        .Link
            https://github.com/daisaacson/test-connections
        .Inputs
            TargetName[]
        .Outputs
            none
        #Requires -Version 2.0
    #>

  [CmdletBinding(SupportsShouldProcess = $True)]
  Param (
    [Parameter(Mandatory = $True, ValueFromPipeline = $True, HelpMessage = "Stop after sending Count pings")]
    [string[]]$TargetName,

    [Parameter(Mandatory = $False)]
    [Alias("c")]
    [int]$Count = 4,

    [Parameter(Mandatory = $False, HelpMessage = "Continjously send pings")]
    [Alias("Continuous", "t")]
    [switch]$Repeat,

    [Parameter(Mandatory = $False, HelpMessage = "Delay between pings")]
    [int]$Delay = 1,

    [Parameter(Mandatory = $False, HelpMessage = "Interval between pings")]
    [Alias("u")]
    [int]$Update = 1000,

    [Parameter(Mandatory = $False, HelpMessage = "Watch")]
    [Alias("w")]
    [Switch]$Watch
  )
  Begin {
    Write-Verbose -Message "Begin $($MyInvocation.MyCommand)"
    $Targets = @()
    # Destingwish between Windows PowerShell and PowerShell Core
    $WindowsPowerShell = $PSVersionTable.PSEdition -and $PSVersionTable.PSEdition -eq 'Desktop'
    $PowerShellCore = ! $WindowsPowerShell

    #region Target_Class
    class Target {
      [String]$TargetName
      [String]$DNS
      hidden [PSObject]$Job
      [Int]$PingCount
      [boolean]$Status
      [Int]$Latency
      [Int]$LatencySum
      [Int]$SuccessSum
      [DateTime]$LastSuccessTime

      Target() {}
      Target([String]$TargetName, [PSObject]$Job) {
        $this.TargetName = $TargetName
        $this.Job = $Job
        $this.PingCount = 0
        $this.Status = $null
        $this.Latency = 0
        $this.LatencySum = 0
        $this.SuccessSum = 0
      }

      [String]ToString() {
        Return ("[{0}] {1} {2}ms {3:0.0}ms (avg) {4} {5:0.0}%" -f $this.Status, $this.TargetName, $this.Latency, $this.AverageLatency(), $this.PingCount, $this.PercentSuccess() )
      }

      [PSCustomObject]ToTable() {
        If ($Global:PSVersionTable.PSEdition -and $Global:PSVersionTable.PSEdition -eq 'Core') {
          if ($this.PingCount -eq 0) {
            $s = "`e[1;38;5;0;48;5;226m PEND `e[0m" # Yellow
          } elseif ($this.status) {
            $s = "`e[1;38;5;0;48;5;46m OKAY `e[0m"  # Green
          } else {
            $s = "`e[1;38;5;0;48;5;196m FAIL `e[0m" # Red
          }
        } else {
          if ($this.PingCount -eq 0) {
            $s = " PEND "
          } elseif ($this.status) {
            $s = " OK "
          } else {
            $s = " FAIL "
          }
        }
        Return [PSCustomObject]@{
          Status      = $s
          TargetName  = $this.TargetName
          ms          = $this.Latency
          Avg         = [math]::Round($this.AverageLatency(), 1)
          Count       = $this.PingCount
          Loss        = $this.PingCount - $this.SuccessSum
          Success     = [math]::Round($this.PercentSuccess(), 1)
          LastSuccess = $this.LastSuccessTime
        }
      }

      [void]Update() {
        $Data = Receive-Job -Id $this.Job.Id

        # If data is newer than update attributes
        If ($Data.ping -gt $this.PingCount) {
          $last = $Data | Select-Object -Last 1
          $this.PingCount = $last.Ping
          $this.Status = $last.Status -eq "Success"
          $this.SuccessSum += ($Data.Status | Where-Object { $_ -eq "Success" } | Measure-Object).Count
          if ($this.Status) {
            $this.Latency = $last.Latency
            $this.LatencySum += ($Data.Latency | Measure-Object -Sum).Sum
            $this.LastSuccessTime = Get-Date
          }
        }
      }

      [int]Count() {
        Return $this.PingCount
      }

      [float]PercentSuccess() {
        If (! $this.PingCount -eq 0) {
          Return $this.SuccessSum / $this.PingCount * 100
        }
        Return 0
      }

      [float]AverageLatency() {
        If (! $this.SuccessSum -eq 0) {
          Return $this.LatencySum / $this.SuccessSum
        }
        Return 0
      }
    }
    #endregion Target_Class
  }

  Process {
    Write-Verbose -Message "Process $($MyInvocation.MyCommand)"
    If ($pscmdlet.ShouldProcess("$TargetName")) {
      ForEach ($Target in $TargetName) {
        Write-Verbose -Message "$Target, $Count, $Delay, $Repeat"
        Try {
          If ($WindowsPowerShell) {
            # Create new Target and Start-Job
            # Windows PowerShell 5.1 Test-Connection sucks, wrapper for Test-Connection to behave more like Test-Connection in PowerShell Core
            $Targets += [Target]::new($Target, $(Start-Job -ScriptBlock $([ScriptBlock]::Create({
                      Param ([String]$TargetName, [int]$Count = 4, [int]$Delay = 1, [bool]$Repeat)
                      $Ping = 0
                      While ($Repeat -or $Count -gt $Ping) {
                        Write-Verbose "$($Repeat) $($Count) $($Ping)"
                        $Ping++
                        $icmp = Test-Connection -ComputerName $TargetName -Count 1 -ErrorAction SilentlyContinue
                        If ($icmp) {
                          [PSCustomObject]@{
                            Ping    = $Ping;
                            Status  = "Success"
                            Latency = $icmp.ResponseTime
                          }
                        } else {
                          [PSCustomObject]@{
                            Ping    = $Ping
                            Status  = "Failed"
                            Latency = 9999
                          }
                        }
                        Start-Sleep -Seconds $Delay
                      }
                    }
                  )
                ) -ArgumentList $Target, $Count, $Delay, $Repeat
              )
            )
          } else {
            If ($Repeat) {
              $Targets += [Target]::new($Target, $(Start-Job -ScriptBlock $([ScriptBlock]::Create({ Param ($Target) Test-Connection -TargetName $Target -Ping -Repeat })) -ArgumentList $Target))
            } else {
              $Targets += [Target]::new($Target, $(Start-Job -ScriptBlock $([ScriptBlock]::Create({ Param ($Target, $Count) Test-Connection -TargetName $Target -Ping -Count $Count })) -ArgumentList $Target, $Count))
            }
          }
        } Catch { $_ }
      }
    }
  }

  End {
    Write-Verbose -Message "End $($MyInvocation.MyCommand)"
    If ($pscmdlet.ShouldProcess("$TargetName")) {
      # https://blog.sheehans.org/2018/10/27/powershell-taking-control-over-ctrl-c/
      # Change the default behavior of CTRL-C so that the script can intercept and use it versus just terminating the script.
      [Console]::TreatControlCAsInput = $True
      # Sleep for 1 second and then flush the key buffer so any previously pressed keys are discarded and the loop can monitor for the use of
      # CTRL-C. The sleep command ensures the buffer flushes correctly.
      Start-Sleep -Seconds 1
      $Host.UI.RawUI.FlushInputBuffer()
      # Continue to loop while there are pending or currently executing jobs.
      While ($Targets.Job.HasMoreData -contains "True") {
        # If a key was pressed during the loop execution, check to see if it was CTRL-C (aka "3"), and if so exit the script after clearing
        # out any running jobs and setting CTRL-C back to normal.
        If ($Host.UI.RawUI.KeyAvailable -and ($Key = $Host.UI.RawUI.ReadKey("AllowCtrlC,NoEcho,IncludeKeyUp"))) {
          If ([Int]$Key.Character -eq 3) {
            Write-Warning -Message "Removing Test-Connection Jobs"
            If ($PowerShellCore) { Write-Host "`e[2A" }
            $Targets.Job | Remove-Job -Force
            $killed = $True
            [Console]::TreatControlCAsInput = $False

            break
          }
          # Flush the key buffer again for the next loop.
          $Host.UI.RawUI.FlushInputBuffer()
        }
        # Perform other work here such as process pending jobs or process out current jobs.

        # Get Test-Connection updates
        $Targets.Update()

        # Print Output
        $Targets.ToTable() | Format-Table

        # Move cursor up to overwrite old output
        If ($Watch -and $PowerShellCore) {
          Write-Host "`e[$($Targets.length+5)A"
        }

        # Output update delay
        Start-Sleep -Milliseconds $Update
      }

      # Clean up jobs
      If (!$killed) {
        $Targets.Job | Remove-Job -Force
      }

      # If in "Watch" mode, print output one last time
      If ($Watch) {
        $Targets.ToTable() | Format-Table
      }
    }
  }
}