functions/Ping-IpList.ps1
|
function Ping-IpList { <# .SYNOPSIS Advanced parallel ping utility for multiple IP addresses with history tracking and statistics. .DESCRIPTION Ping-IpList provides powerful network connectivity testing capabilities with features like: - Parallel ping execution for multiple targets - Continuous monitoring mode - Ping history visualization - DNS resolution - Customizable ping parameters - Support for IP ranges and CIDR notation - Downtime tracking By default (table view, no -ShowHistory) only hosts that have responded at least once during the run are displayed - a host appears the first time it answers a ping and then stays visible (sticky) even if it later starts timing out, so a flapping host doesn't blink in and out of the table. Hosts that have never responded stay hidden, which keeps the console readable when scanning large ranges (e.g. a /24 CIDR) where most addresses time out. Use -ShowAll to display every target instead, including hosts that have never responded. -ShowHistory is a monitor-mode view: it always shows every target regardless of -ShowAll, because the point of watching the symbol history (! / .) is to see hosts go down and come back, not to have them disappear while down. Regardless of display mode, a summary is printed once the function stops - either because it ran its configured -Count of sequences or because it was interrupted with Ctrl+C (useful with -Continuous). .PARAMETER FromClipBoard Reads IP addresses from clipboard. .PARAMETER ipList Array of IP addresses or hostnames to ping. Accepts pipeline input, one address per pipeline object (e.g. "8.8.8.8","1.1.1.1" | Ping-IpList). .PARAMETER range IP address range in format "192.168.1.1-192.168.1.254". .PARAMETER cidr CIDR notation subnet like "192.168.1.0/24". .PARAMETER Count Number of ping attempts (default: 4, use -Continuous for endless). .PARAMETER BufferSize Size of ping packet in bytes (default: 32). .PARAMETER DontFragment Sets the Don't Fragment flag in ping packet. .PARAMETER Ttl Time to live value (default: 128). .PARAMETER Timeout Ping timeout in milliseconds (default: 100). .PARAMETER Continuous Enables continuous ping mode. .PARAMETER ResolveDNS Resolves IP addresses to hostnames. .PARAMETER ShowHistory Displays ping history using symbols (! for success, . for failure). This is a monitor-mode view and always shows every target, including hosts that have never responded - nothing is hidden, regardless of -ShowAll. .PARAMETER ShowAll Displays every target in the console output, including hosts that have never responded. Without this switch, the table view only shows hosts that have responded at least once during the run (sticky - once shown, a host stays visible even if it later times out). Has no effect with -ShowHistory, which always shows every target. The "Responding hosts: X/Y" header always reflects the full list either way. .PARAMETER HistoryResetCount Number of results to keep in history before reset (default: 100). .PARAMETER DontSortIpList Prevents automatic IP address sorting. .PARAMETER MaxThreads Maximum number of concurrent ping threads (default: 100). .PARAMETER OutToPipe Outputs results to the pipeline instead of the console, once per sequence (works with -Continuous). Each object includes a Sequence number and Timestamp in addition to the usual IPAddress/ResponsTime/Result/DownTime. .PARAMETER logEvents Logs events related to downtime and uptime. .EXAMPLE # Copy these IPs to your clipboard: # 192.168.1.10 # 8.8.8.8 # 1.1.1.1 Ping-IpList -FromClipBoard -ShowHistory Output: 2024-11-19 14:01:24, Ping sequence: 1 192.168.1.10 [t:1ms DownFor:0s]:! 8.8.8.8 [t:27ms DownFor:0s]:! 1.1.1.1 [t:18ms DownFor:0s]:! This example shows how to quickly ping multiple IPs by copying them from any source (text file, Excel, web page). Just ensure each IP is on a new line before copying. .EXAMPLE Ping-IpList -ipList "8.8.8.8","1.1.1.1" -Count 10 Output: IPAddress ResponsTime Result DownTime --------- ----------- ------ -------- 1.1.1.1 18 Success 8.8.8.8 27 Success .EXAMPLE Ping-IpList -range "192.168.1.1-192.168.1.10" -Continuous -ShowHistory Output: 2024-11-19 13:57:24, Ping sequence: 7 192.168.1.1 [t:1ms DownFor:0s]:!!!!!!! 192.168.1.2 [t:-ms DownFor:9s]:....... 192.168.1.3 [t:-ms DownFor:9s]:....... [...] .EXAMPLE Ping-IpList -cidr "10.0.0.0/29" -ResolveDNS Output (by default, only hosts that have responded at least once are listed; once a host answers it stays listed even if it later times out): 2024-11-19 13:58:09, Ping sequence: 4, Responding hosts: 3/6 IPAddress ResponsTime Result DownTime --------- ----------- ------ -------- host1.example.com 1 Success 10.0.0.2 1 Success host3.example.com 1 Success [...] .EXAMPLE Ping-IpList -cidr "10.200.12.0/24" -ShowAll Shows every address in the /24, including hosts that have never responded. .EXAMPLE Ping-IpList -range "192.168.1.1-192.168.1.10" -Continuous # Press Ctrl+C to stop Output (printed once the run stops, whether by Ctrl+C or reaching -Count): ==================== Ping Summary ==================== Started : 2026-08-10 14:20:00 Ended : 2026-08-10 14:23:45 Duration: 00:03:45 Sequences: 45 192.168.1.1 Sent:45 Success:45 Failed:0 Rate:100.0% DownTime:0s 192.168.1.2 Sent:45 Success:10 Failed:35 Rate: 22.2% DownTime:35s [...] ======================================================== .NOTES Author: Iman Edrisian Date: October 2025 Requires: PowerShell 5.1 or higher Tags: Network, Monitoring, Ping #> [CmdletBinding()] param ( [switch]$FromClipBoard, [Parameter(Mandatory = $false, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [Alias('list')] [String[]]$ipList, [string]$range, [string]$cidr, [Alias('n')] [int]$Count = 4, [Alias('l')] [int]$BufferSize = 32, [Alias('f')] [switch]$DontFragment, [Alias('i')] [int]$Ttl = 128, [int]$Timeout = 100, [Alias('c', 't')] [switch]$Continuous, [Alias('a')] [switch]$ResolveDNS, [switch]$ShowHistory, [switch]$ShowAll, [int]$HistoryResetCount = 100, [switch]$DontSortIpList, [int]$MaxThreads = 100, [switch]$OutToPipe, [switch]$logEvents ) begin { # ValueFromPipeline binds $ipList once per piped object, so accumulate across # process calls - without this, only the last piped IP would survive to end{}. $pipedIpList = New-Object System.Collections.Generic.List[string] } process { if ($ipList) { foreach ($item in $ipList) { $pipedIpList.Add($item) } } } end { if ($pipedIpList.Count -gt 0) { $ipList = $pipedIpList.ToArray() } if ($range) { $ipList = Get-IpAddressesInRange $range } elseif ($cidr) { $ipList = Get-IPAddressesInSubnet $cidr } else { if ($FromClipBoard) { $ipList = Get-Clipboard } # Trim leading or trailing white spaces from each entry in the list $ipList = $ipList | ForEach-Object { $_.Trim() } # Removing any non-IP addresses or blank entry from the list $ipList = $ipList -match "\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|^(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$" # Check if the list does not contain any hostnames if (-not $DontSortIpList) { if (($ipList -match "^(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$").Count -eq 0) { $ipList = Sort-IpAddress $ipList } } } if ($ResolveDNS) { for ($i = 0; $i -lt $ipList.Count; $i++) { if ($ipList[$i] -match "\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}") { $NameHost = (Resolve-DnsName $ipList[$i] -Type PTR -DnsOnly -ErrorAction SilentlyContinue).NameHost if ($NameHost) { if ($NameHost.Count -gt 1) { $ipList[$i] = $NameHost[0] } else { $ipList[$i] = $NameHost } } } } } try { if ($Continuous) { $Count = -1 } $iCount = 0 $pingHistory = [ordered]@{} $scriptStartTime = Get-Date while ($iCount -ne $Count) { # Create and open a runspace pool $pool = [runspacefactory]::CreateRunspacePool(1, $MaxThreads) $pool.Open() # Collect the runspaces $runspaces = New-Object System.Collections.ArrayList foreach ($ip in $ipList) { # Prepare a script block to run in parallel $scriptBlock = { param($ip, $BufferSize, $Ttl, $DontFragment, $Timeout, $HistoryResetCount) function Send-Ping { [CmdletBinding()] param ( [string]$target, [int]$BufferSize = 32, [switch]$DontFragment, [int]$Ttl = 128, [int]$Timeout = 100 ) $ping = New-Object System.Net.NetworkInformation.Ping $pingOptions = New-Object System.Net.NetworkInformation.PingOptions($Ttl, $DontFragment) $pingResult = $ping.Send($target, $Timeout, [System.Text.Encoding]::ASCII.GetBytes(("a" * $BufferSize)), $pingOptions) Return $pingResult } # Simulating a ping result $pingResult = send-ping -Target $ip -BufferSize $BufferSize -Ttl $Ttl -DontFragment:$DontFragment -Timeout $Timeout $pingStatistics = [pscustomobject]@{ IPAddress = $ip ResponsTime = [double]0.0 Result = "" ResultHistory = "" DownTimeStart = $null DownTime = $null } if ($pingResult.Status -eq "Success") { $pingStatistics.ResponsTime = $pingResult.RoundtripTime $pingStatistics.Result = $pingResult.Status $pingStatistics.ResultHistory = "!" } else { $pingStatistics.ResponsTime = "-" $pingStatistics.Result = "TimedOut" $pingStatistics.ResultHistory = "." } # Return the result return @($ip, $pingStatistics) } # Create a new PowerShell runspace and configure it $powershell = [powershell]::Create().AddScript($scriptBlock).AddArgument($ip).AddArgument($BufferSize).AddArgument($Ttl).AddArgument($DontFragment).AddArgument($Timeout).AddArgument($HistoryResetCount) $powershell.RunspacePool = $pool # Start the asynchronous execution of the PowerShell instance $runspaces.Add([PSCustomObject]@{ Pipe = $powershell AsyncResult = $powershell.BeginInvoke() }) | Out-Null } # Collect and process the results as they complete foreach ($runspace in $runspaces) { $result = $runspace.Pipe.EndInvoke($runspace.AsyncResult) $runspace.Pipe.Dispose() $ipAddress = $result[0] $pingStatistics = $result[1] if ($pingHistory.Contains($ipAddress)) { if ($pingHistory[$ipAddress].ResultHistory.Length -eq $HistoryResetCount) { $pingHistory[$ipAddress].ResultHistory = "" } $pingHistory[$ipAddress].ResponsTime = $pingStatistics.ResponsTime $pingHistory[$ipAddress].Result = $pingStatistics.Result $pingHistory[$ipAddress].ResultHistory += $pingStatistics.ResultHistory } else { $pingStatistics | Add-Member -NotePropertyName SuccessCount -NotePropertyValue 0 $pingStatistics | Add-Member -NotePropertyName FailCount -NotePropertyValue 0 # Unlike ResultHistory (which is truncated at -HistoryResetCount), this # never resets during the run, so -ShowHistory coloring stays stable # across a reset instead of flickering back to "ok" for one sequence. $pingStatistics | Add-Member -NotePropertyName ConsecutiveFails -NotePropertyValue 0 $pingHistory.Add($ipAddress, $pingStatistics) } # Track cumulative success/failure counts for the end-of-run summary if ($pingStatistics.Result -eq "Success") { $pingHistory[$ipAddress].SuccessCount++ $pingHistory[$ipAddress].ConsecutiveFails = 0 } else { $pingHistory[$ipAddress].FailCount++ $pingHistory[$ipAddress].ConsecutiveFails++ } } # Close and dispose of the runspace pool $pool.Close() $pool.Dispose() # Check for DownTime foreach ($item in $pingHistory.Values) { $timeStamp = Get-Date if ($item.Result -eq "TimedOut") { if ($item.DownTimeStart) { $item.DownTime += ($timeStamp - $item.DownTimeStart).TotalSeconds $item.DownTimeStart = $timeStamp } else { $item.DownTimeStart = $timeStamp if ($logEvents) { Write-Log -Message "$($item.IPAddress) went down" -Level Warning -fileNamePrefix "Ping-IpList" } } } else { if ($logEvents -and $item.DownTimeStart) { Write-Log -Message "$($item.IPAddress) is back online" -Level Info -fileNamePrefix "Ping-IpList" } $item.DownTimeStart = $null } } # -ShowHistory is a monitor-mode view - every target stays on screen so the # symbol history and current down/up state are visible for the whole run. # -ShowAll asks for the same "everything" behavior in table mode. # Otherwise (default table mode), a host is shown once it has responded at # least once during this run ("sticky") - it won't be hidden again just # because it's currently timing out, only genuinely dead hosts stay hidden. $displayItems = if ($ShowHistory -or $ShowAll) { $pingHistory.Values } else { @($pingHistory.Values | Where-Object { $_.SuccessCount -gt 0 }) } if (-not $OutToPipe) { Clear-Host $successCount = ($pingHistory.Values | Where-Object { $_.Result -eq "Success" }).Count $totalCount = $pingHistory.Values.Count $color = if ($successCount -eq $totalCount) { 'Green' } elseif ($successCount -eq 0) { 'Red' } else { 'Yellow' } Write-Host "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), Ping sequence: $($iCount + 1), Responding hosts: " -NoNewline Write-Host "$successCount/$totalCount" -ForegroundColor $color if (-not $ShowAll -and -not $ShowHistory) { if ($displayItems.Count -eq 0) { Write-Host "No hosts have responded yet (0/$totalCount silent so far) - use -ShowAll to see every target" -ForegroundColor DarkGray } else { Write-Host "(showing $($displayItems.Count)/$totalCount hosts that have responded at least once, use -ShowAll to include hosts that never respond)" -ForegroundColor DarkGray } } if ($ShowHistory) { foreach ($item in $displayItems) { # $paddingSize = 20 - $item.IPAddress.length # if ($paddingSize -lt 0) { $paddingSize = 0 } Write-Host -ForegroundColor Green -NoNewline "$($item.IPAddress) [" Write-Host -NoNewline "t:"#.PadLeft($paddingSize, " ") Write-Host -ForegroundColor Green -NoNewline "$($item.ResponsTime)ms " Write-Host -NoNewline "DownFor:" Write-Host -ForegroundColor Green -NoNewline "$([math]::Round($item.DownTime,2))s]:" # Colored on ConsecutiveFails (persists across HistoryResetCount # truncation) rather than parsing ResultHistory, so a long-down host # doesn't flash back to yellow for one sequence right after a reset. if ($item.ConsecutiveFails -ge 2) { Write-Host -ForegroundColor Red "$($item.ResultHistory)" } elseif ($item.ConsecutiveFails -eq 1) { Write-Host -ForegroundColor Yellow "$($item.ResultHistory)" } else { Write-Host "$($item.ResultHistory)" } } } else { Write-Output -InputObject $displayItems | Select-Object IPAddress, ResponsTime, Result, DownTime | Format-Table -RepeatHeader -AutoSize } } else { # Emit every sequence (not just the last one) - the previous "only on the # final iteration" check never fired under -Continuous, since $Count is -1 # there and $iCount never equals $Count - 1. Sequence/Timestamp are added # as data instead of a Write-Host banner, keeping pipeline output clean. Write-Output -InputObject $displayItems | Select-Object @{Name = 'Sequence'; Expression = { $iCount + 1 } }, @{Name = 'Timestamp'; Expression = { Get-Date } }, IPAddress, ResponsTime, Result, ResultHistory, DownTime } $iCount++ Start-Sleep -Seconds 1 } } finally { # Runs both when the loop finishes normally (reached -Count) and when interrupted # with Ctrl+C, so cleanup and the summary always happen. if ($pool) { try { if ($pool.RunspacePoolStateInfo.State -ne 'Closed') { $pool.Close() } } catch {} try { $pool.Dispose() } catch {} } if ($pingHistory -and $pingHistory.Count -gt 0) { $endTime = Get-Date $duration = $endTime - $scriptStartTime Write-Host "" Write-Host "==================== Ping Summary ====================" -ForegroundColor Cyan Write-Host "Started : $($scriptStartTime.ToString('yyyy-MM-dd HH:mm:ss'))" Write-Host "Ended : $($endTime.ToString('yyyy-MM-dd HH:mm:ss'))" Write-Host "Duration: $($duration.ToString('hh\:mm\:ss')) Sequences: $iCount" Write-Host "" foreach ($item in $pingHistory.Values) { $sent = $item.SuccessCount + $item.FailCount $successRate = if ($sent -gt 0) { [math]::Round(($item.SuccessCount / $sent) * 100, 1) } else { 0 } $rateColor = if ($successRate -eq 100) { 'Green' } elseif ($successRate -eq 0) { 'Red' } else { 'Yellow' } Write-Host ("{0,-16}" -f $item.IPAddress) -NoNewline Write-Host ("Sent:{0,-5} " -f $sent) -NoNewline Write-Host "Success:" -NoNewline Write-Host ("{0,-5} " -f $item.SuccessCount) -ForegroundColor Green -NoNewline Write-Host "Failed:" -NoNewline Write-Host ("{0,-5} " -f $item.FailCount) -ForegroundColor Red -NoNewline Write-Host ("Rate:{0,6}% " -f $successRate) -ForegroundColor $rateColor -NoNewline Write-Host ("DownTime:{0}s" -f [math]::Round($item.DownTime, 2)) } Write-Host "========================================================" -ForegroundColor Cyan } } } } |