I know what you are thinking, another ping script? Yes, there are thousands of ping tools and scripts out there and every IT administrator has a couple handy that they use on a regular basis. I ended up writing this one because I could not find PowerShell code that met the specific criteria that I needed. I was working with one of my peers on SCCM client health. In large enterprise environments it is an ongoing challenge to ensure that all devices on the network are accounted for and managed. We wrote some PowerShell to query some databases and come up with a list of machines we knew about that were not managed by SCCM. The next question I had was how many were actually on the network.

I wanted my ping code to meet the following criteria.

  • Fast, so pings asynchronously
  • Reliable, can recover and continue if errors are encountered
  • Uses .NET or PS Cmdlets, I don’t want to have to externally launch anything
  • Returns nicely formatted data
  • Is modular, I want to be able to re-use this code in other scripts easily

The examples I found online had most of these features, but not all. Once I started writing it I thought this might be useful to the community so I added the txt file as an input and csv file as an output as my original code was just a few functions I called. Note, this does require PowerShell 3.0 or higher.

Syntax

.\Ping.ps1 –InputFilePath c:\temp\names.txt –MaxConcurrent 100 –TimesToPing 4 –TimeoutInSeconds 90 –ResolveNames true

Parameters

Name Description
InputFilePath Text file with a list of names or IP addresses you want to ping. Each name/IP should be on its own line in the file.
MaxConcurrent How many jobs / threads you want to use for pinging.
TimesToPing How many times you want to ping each name/IP.
TimeoutInSeconds Optional: If some jobs / threads get stuck running, this will ensure that we recover and continue if the timeout is reached. This timeout only applies to Test-Connection (Ping) and the GetHostEntry (DNS) parts of the script. If this is not specified it defaults to 120 seconds.
ResolveNames Optional: Set to true if you want to resolve the names using DNS.

Output

Part 1: This should happen very quickly
Sending Ping Command to 500 Machines… (Uses the Test-Connection Cmdlet with –AsJob)
Getting Ping Results……… (Gets the results from the Cmdlet. The timeout passed in applies to this part of the script. If you see a + here than one of the ping jobs failed and was resubmitted.)
Received Ping Results From 500 Machines

Part 2: This should happen quickly. Note, you can grab more properties from the ping objects in this function if you want. I did notice that getting certain properties cause the function to slow down significantly such as IPv4Address and IPv6Address which are of type System.Net.IPAddress.
Formatting Ping Results…… (Creates an array of objects for the ping results)
Formatted Ping Results for 500 Machines

Part 3: This can take a while
Resolving DNS Names for 500 Machines...*..*…… (GetHostEntry .NET call. The * means MaxConcurrent was hit, which in the case of DNS resolution is hardcoded to 5 since that seemed to work best in my testing.)
Getting DNS Results… (Gets the results of GetHostEntry. The timeout passed is applies to this part of the script. If you see a + here than one of the DNS resolution jobs failed and was resubmitted.)
Received DNS Results From 500 Machines
Formatting DNS Results.. (Adds the DNS information to the array of objects returned from Part 2)
Formatted DNS Results for 500 Machines

Part 4: This should happen quickly, just outputting the results
---Statistics---
Total Time Elapsed: 02 Minutes and 36 Seconds
Total Names Pinged: 500
Hosts that Responded: 250
DNS Names Resolved: 350
Percentage of Hosts that Responded at Least Once: 50%
CSV Output Location: C:\temp\PingResults.csv

Testing

I tested this on two machines using my home internet connection (Verizon FIOS) against 500 internet addresses. If you don’t resolve DNS names then this script finishes very quickly on a fast machine and even faster if you choose to only do 1 ping instead of the normal 4. You can increase the MaxConcurrent setting to get faster results from the ping but if you start seeing a lot of “+” signs in the output then then the jobs are failing (although we should recover from this). Unfortunately because of limitations in the Test-Connection Cmdlet I have to do the DNS resolution and pings separately and the DNS resolution is the longest part of the script. You can use the Win32_PingStatus class directly to do both at the same time, but I’m not sure which method is faster.

ResolveNames = true, MaxConcurrent=100, TimesToPing=4
HP 8570W (i7/32GB), WS2012, Gigabit Ethernet: Completed in 1 minute, 15 seconds.
Microsoft Surface Pro (i5/4GB), Windows8, Gigabit Ethernet: Completed in 2 minutes, 36 seconds.

ResolveNames = false, MaxConcurrent=100, TimesToPing=4
HP 8570W (i7/32GB), WS2012, Gigabit Ethernet: Completed in 19 seconds.
Microsoft Surface Pro (i5/4GB), Windows8, Gigabit Ethernet: Completed in 33 seconds.

Code

1 Param( 2 [parameter(Mandatory=$true)] 3 $InputFilePath, 4 [parameter(Mandatory=$true)] 5 $MaxConcurrent, 6 [parameter(Mandatory=$true)] 7 $TimesToPing, 8 $TimeoutInSeconds, 9 $ResolveNames 10 ) 11 12 $Start = [System.DateTime]::Now 13 Write-Host "Version 1.0" 14 Write-Host "InputFilePath:"$InputFilePath 15 Write-Host "MaxConcurrent:"$MaxConcurrent 16 Write-Host "TimesToPing:"$TimesToPing 17 Write-Host "TimeoutInSeconds:"$TimeoutInSeconds 18 Write-Host "ResolveNames:"$ResolveNames 19 20 function GetNamesFromTextFile 21 { 22 param($file) 23 24 $ht = @{} 25 26 try 27 { 28 foreach ($line in [System.IO.File]::ReadLines($file)) 29 { 30 try { $ht.Add($line.ToString().Trim(), $line.ToString().Trim()) } catch {} 31 } 32 } 33 catch 34 { 35 Write-Host "Failed to Read File, Exiting:"$ms -ForegroundColor Red 36 Write-Host $_.Exception.Message -ForegroundColor Yellow 37 exit 38 } 39 40 return $ht 41 } 42 43 function GetStatusCodeString 44 { 45 param ($code) 46 47 switch ($code) 48 { 49 $null {$ret = "Ping Command Failed"} 50 0 {$ret = "Success"} 51 11001 {$ret = "Buffer Too Small"} 52 11002 {$ret = "Destination Net Unreachable"} 53 11003 {$ret = "Destination Host Unreachable"} 54 11004 {$ret = "Destination Protocol Unreachable"} 55 11005 {$ret = "Destination Port Unreachable"} 56 11006 {$ret = "No Resources"} 57 11007 {$ret = "Bad Option"} 58 11008 {$ret = "Hardware Error"} 59 11009 {$ret = "Packet Too Big"} 60 11010 {$ret = "Request Timed Out"} 61 11011 {$ret = "Bad Request"} 62 11012 {$ret = "Bad Route"} 63 11013 {$ret = "TimeToLive Expired Transit"} 64 11014 {$ret = "TimeToLive Expired Reassembly"} 65 11015 {$ret = "Parameter Problem"} 66 11016 {$ret = "Source Quench"} 67 11017 {$ret = "Option Too Big"} 68 11018 {$ret = "Bad Destination"} 69 11032 {$ret = "Negotiating IPSEC"} 70 11050 {$ret = "General Error"} 71 default {$ret = "Ping Failed"} 72 } 73 74 return $ret 75 } 76 77 function GetPingResultsFromHashTable 78 { 79 param($ht, $maxConcurrent, $count, $timeout) 80 81 $bDone = $false 82 $i = 0 83 $totalMachines = 0 84 $htResults = @{} 85 $dotTime = [System.DateTime]::Now 86 if ($timeout -eq $null) {$timeout = 120} 87 88 Write-Host ("Sending Ping Command to {0} Machines" -f $ht.Count) -NoNewline 89 90 foreach ($name in $ht.GetEnumerator()) 91 { 92 while ((Get-Job -State Running).Count -ge $maxConcurrent) 93 { 94 Start-Sleep -Seconds 1 95 if ($i -ge 50) { Write-Host "*"; $i = 0 } 96 else { Write-Host "*" -NoNewline; $i++ } 97 } 98 99 $job = Test-Connection -ComputerName $name.Key.ToString() -Count $count -AsJob 100 $job.name = "ping:{0}" -f $name.Key.ToString() 101 102 if ([System.DateTime]::Now -gt $dotTime) 103 { 104 $dotTime = ([System.DateTime]::Now).AddSeconds(1) 105 if ($i -ge 50) { Write-Host "."; $i = 0 } 106 else { Write-Host "." -NoNewline; $i++ } 107 } 108 } 109 110 #Start time now, exit in case of timeout 111 $timeout = ([System.DateTime]::Now).AddSeconds($timeout) 112 $dotTime = [System.DateTime]::Now 113 $i = 0 114 Write-Host 115 Write-Host "Getting Ping Results" -NoNewline 116 117 while(!($bDone)) 118 { 119 $results = Get-Job -Name 'ping:*' 120 $bRunning = $false 121 122 foreach ($result in $results) 123 { 124 if ($result.State -ne 'Running') 125 { 126 if ($result.State -eq 'Failed') 127 { 128 #resubmit job 129 if ($i -ge 50) { Write-Host "+"; $i = 0 } 130 else { Write-Host "+" -NoNewline; $i++ } 131 $job = Test-Connection -ComputerName $result.Name.ToString().Split(":")[1] -Count $count -AsJob 132 $job.name = "ping:{0}" -f $result.Name.ToString().Split(":")[1] 133 } 134 else 135 { 136 try { $htResults.Add($result.Name.ToString().Split(":")[1], (Receive-Job $result)) } catch {} 137 $totalMachines++ 138 } 139 140 if ([System.DateTime]::Now -gt $dotTime) 141 { 142 $dotTime = ([System.DateTime]::Now).AddSeconds(1) 143 if ($i -ge 50) { Write-Host "."; $i = 0 } 144 else { Write-Host "." -NoNewline; $i++ } 145 } 146 147 try { Remove-Job $result } catch {} 148 } 149 else 150 { 151 $bRunning = $true 152 } 153 } 154 155 #Check for timeout condition, clean up all jobs if true 156 if ([System.DateTime]::Now -gt $timeout) 157 { 158 $bDone = $true 159 Write-Host "Timeout reached, removing jobs" 160 $results = Get-Job -Name 'ping:*' 161 foreach ($result in $results) 162 { 163 Write-Host "RemoveJob:"$result.Name 164 try 165 { 166 Stop-Job $result 167 try { Remove-Job $result -Force } catch {} 168 } 169 catch {} 170 } 171 } 172 173 #If the timeout hasn't been reached and jobs are still running, loop again 174 if (!($bRunning)) { $bDone = $true } 175 } 176 177 Write-Host 178 Write-Host ("Received Ping Results From {0} Machines" -f $totalMachines) 179 180 return $htResults 181 } 182 183 function ResolveNamesFromPingResults 184 { 185 param($array, $maxConcurrent, $resolveNames, $timeout) 186 187 try { if ($resolveNames -ne $null) { [bool]$resolveNames = [System.Convert]::ToBoolean($resolveNames) } } catch {} 188 189 $htResults = @{} 190 191 if ($resolveNames) 192 { 193 $dotTime = ([System.DateTime]::Now) 194 if ($timeout -eq $null) {$timeout = 120} 195 $i = 0 196 $scriptBlock = 197 { 198 param($s) 199 try { $ret = [System.Net.DNS]::GetHostEntry($s) } catch {} 200 return $ret 201 } 202 Write-Host ("Resolving DNS Names for {0} Machines" -f $array.Count) -NoNewline 203 foreach ($name in $array) 204 { 205 while ((Get-Job -State Running).Count -ge $maxConcurrent) 206 { 207 Start-Sleep -Seconds 1 208 if ($i -ge 50) { Write-Host "*"; $i = 0 } 209 else { Write-Host "*" -NoNewline; $i++ } 210 } 211 $job = Start-Job -ScriptBlock $scriptBlock -ArgumentList $name.NameInList 212 $job.name = "resolve:{0}" -f $name.NameInList 213 if ([System.DateTime]::Now -gt $dotTime) 214 { 215 $dotTime = ([System.DateTime]::Now).AddSeconds(1) 216 if ($i -ge 50) { Write-Host "."; $i = 0 } 217 else { Write-Host "." -NoNewline; $i++ } 218 } 219 } 220 221 #Start time now, exit in case of timeout 222 $timeout = ([System.DateTime]::Now).AddSeconds($timeout) 223 $dotTime = ([System.DateTime]::Now) 224 $i = 0 225 $bDone = $false 226 227 Write-Host 228 Write-Host "Getting DNS Results" -NoNewline 229 while(!($bDone)) 230 { 231 $results = Get-Job -Name 'resolve:*' 232 $bRunning = $false 233 234 foreach ($result in $results) 235 { 236 if ($result.State -ne 'Running') 237 { 238 if ($result.State -eq 'Failed') 239 { 240 #resubmit job 241 if ($i -ge 50) { Write-Host "+"; $i = 0 } 242 else { Write-Host "+" -NoNewline; $i++ } 243 $job = Start-Job -ScriptBlock $scriptBlock -ArgumentList $result.Name.ToString().Split(":")[1] 244 $job.name = "resolve:{0}" -f $result.Name.ToString().Split(":")[1] 245 } 246 else 247 { 248 try { $htResults.Add($result.Name.ToString().Split(":")[1], (Receive-Job $result)) } catch {continue} 249 } 250 251 if ([System.DateTime]::Now -gt $dotTime) 252 { 253 $dotTime = ([System.DateTime]::Now).AddSeconds(1) 254 if ($i -ge 50) { Write-Host "."; $i = 0 } 255 else { Write-Host "." -NoNewline; $i++ } 256 } 257 258 try { Remove-Job $result -Force} catch {} 259 } 260 else 261 { 262 $bRunning = $true 263 } 264 } 265 266 #Check for timeout condition, clean up all jobs if true 267 if ([System.DateTime]::Now -gt $timeout) 268 { 269 $bDone = $true 270 Write-Host "Timeout reached, removing jobs" 271 $results = Get-Job -Name 'resolve:*' 272 foreach ($result in $results) 273 { 274 Write-Host "RemoveJob:"$result.Name 275 try 276 { 277 Stop-Job $result 278 try { Remove-Job $result -Force } catch {} 279 } 280 catch {} 281 } 282 } 283 284 #If the timeout hasn't been reached and jobs are still running, loop again 285 if (!($bRunning)) { $bDone = $true } 286 } 287 Write-Host 288 Write-Host ("Received DNS Results From {0} Machines" -f $htResults.Count) 289 } 290 291 return $htResults 292 } 293 294 function GetFormattedPingResultsFromHashTable 295 { 296 param($ht) 297 298 $fResults = New-Object System.Collections.ArrayList 299 $dotTime = ([System.DateTime]::Now) 300 $i = 0 301 Write-Host "Formatting Ping Results" -NoNewLine 302 303 foreach ($result in $ht.GetEnumerator()) 304 { 305 #There are multiple pings here if we ping more than once per computer 306 $originalAddress = $result.Key.ToString() 307 $pingCount = 0 308 $successCount = 0 309 $status = 'Ping Job Failed' 310 $pingedFrom = 'Ping Job Failed' 311 $successPercentage = 0 312 313 try { $pings = $result.Value.Count } catch { $pings = 0 } 314 if ($pings -gt 0) 315 { 316 $status = GetStatusCodeString -code $result.Value[$pings-1].StatusCode 317 $pingedFrom = $result.Value[$pings-1].PSComputerName 318 } 319 320 foreach ($ping in $result.Value) 321 { 322 $pingCount++ 323 if ($ping.StatusCode -eq 0) { $successCount++ } 324 #If you try to get the IPv4Address or IPv6Address it slows down this loop significantly 325 } 326 327 #Calculate percentage 328 if ($pingCount -ne 0) { $successPercentage = ($successCount / $pingCount) * 100 } 329 else { $successPercentage = 0 } 330 331 #Add to array 332 $o = New-Object PSObject -Property @{ 333 NameInList = $originalAddress 334 PingedFrom = $pingedFrom 335 SuccessPercentage = $successPercentage 336 LastPingStatus = $status 337 } 338 339 [void]$fResults.Add($o) 340 341 if ([System.DateTime]::Now -gt $dotTime) 342 { 343 $dotTime = ([System.DateTime]::Now).AddSeconds(1) 344 if ($i -ge 50) { Write-Host "."; $i = 0 } 345 else { Write-Host "." -NoNewline; $i++ } 346 } 347 } 348 349 Write-Host 350 Write-Host ("Formatted Ping Results for {0} Machines" -f $fResults.Count) 351 352 return $fResults 353 } 354 355 function GetFormattedPingAndDNSResults 356 { 357 param($pingResults, $dnsResults) 358 359 if ($dnsResults.Count -ne 0) 360 { 361 Write-Host "Formatting DNS Results" -NoNewLine 362 $dotTime = ([System.DateTime]::Now) 363 $i = 0 364 foreach ($ping in $pingResults) 365 { 366 $dns = $dnsResults.Get_Item($ping.NameInList) 367 if ($dns -ne $null) 368 { 369 $bFirst = $true 370 foreach ($ip in $dns.AddressList) 371 { 372 if ($bFirst){ $ipList = $ip } 373 else { $ipList += "|" + $ip } 374 } 375 376 $fqdn = $dns.HostName 377 } 378 else 379 { 380 $ipList = $null 381 $fqdn = 'No DNS Entry Found' 382 } 383 384 $ping | Add-Member -MemberType NoteProperty -Name NameFromDNS -value $fqdn -Force 385 $ping | Add-Member -MemberType NoteProperty -Name IPAddressListFromDNS -value $ipList -Force 386 387 if ([System.DateTime]::Now -gt $dotTime) 388 { 389 $dotTime = ([System.DateTime]::Now).AddSeconds(1) 390 if ($i -ge 50) { Write-Host "."; $i = 0 } 391 else { Write-Host "." -NoNewline; $i++ } 392 } 393 } 394 Write-Host 395 Write-Host ("Formatted DNS Results for {0} Machines" -f $pingResults.Count) 396 } 397 398 return $pingResults 399 } 400 401 function GetOutputPath 402 { 403 param($fileName, $dir) 404 $outputPath = $dir + "\" + $fileName 405 return $outputPath 406 } 407 408 function GetTimeSpanStringInMinutesAndSeconds 409 { 410 param($startTime, $endTime) 411 412 $time = $startTime.Subtract($endTime) 413 $minutes = $time.ToString().Split(":")[1] 414 $seconds = $time.ToString().Split(":")[2].Split(".")[0] 415 $timeSpan = "{0} Minutes and {1} Seconds" -f $minutes, $seconds 416 return $timeSpan 417 } 418 419 function GetSuccessPingCount 420 { 421 param($results) 422 423 $successCount = 0 424 foreach ($result in $results) 425 { 426 if ($result.SuccessPercentage -gt 0) { $successCount++ } 427 } 428 429 return $successCount 430 } 431 432 function GetDNSNamesResolvedCount 433 { 434 param($results) 435 436 $namesResolved = 0 437 foreach ($result in $results) 438 { 439 if ($result.IPAddressListFromDNS -ne $null) { $namesResolved++ } 440 } 441 442 return $namesResolved 443 } 444 445 function GetPercentageAsString 446 { 447 param($n1, $n2) 448 449 if ($n1 -ne 0) { $percentage = ($n1 / $n2) * 100 } 450 else { $percentage = 0 } 451 452 $percentage = ("{0:N0}" -f $percentage) + "%" 453 454 return $percentage 455 } 456 457 #Read in Names from text file 458 $Names = GetNamesFromTextFile -file $InputFilePath 459 460 #Get ping results in a hash table. The key is the name and the value is the returned array of ping objects (one element per ping). 461 $Results = GetPingResultsFromHashTable -ht $Names -maxConcurrent $MaxConcurrent -count $TimesToPing -timeout $TimeoutInSeconds 462 463 #Format ping results into an array of objects 464 $FormattedPingResults = GetFormattedPingResultsFromHashTable -ht $Results 465 466 #Resolve DNS Names if specified 467 $DNSResults = ResolveNamesFromPingResults -array $FormattedPingResults -maxConcurrent 5 -resolveNames $ResolveNames -timeout $TimeoutInSeconds 468 469 #Format DNS results by adding them to the ping results 470 $FormattedPingResults = GetFormattedPingAndDNSResults -pingResults $FormattedPingResults -dnsResults $DNSResults 471 472 #Output to CSV 473 $OutputPath = GetOutputPath -fileName 'PingResults.csv' -dir ([Environment]::CurrentDirectory=(Get-Location -PSProvider FileSystem).ProviderPath) 474 try { if ($ResolveNames -ne $null) { [bool]$ResolveNames = [System.Convert]::ToBoolean($ResolveNames) } } catch {} 475 if ($ResolveNames) { $FormattedPingResults | Sort-Object SuccessPercentage | Select-Object NameInList, NameFromDNS, IPAddressListFromDNS, SuccessPercentage, LastPingStatus, PingedFrom | Export-Csv -Path $OutputPath -NoTypeInformation } 476 else { $FormattedPingResults | Sort-Object SuccessPercentage | Select-Object NameInList, SuccessPercentage, LastPingStatus, PingedFrom | Export-Csv -Path $OutputPath -NoTypeInformation } 477 478 #Output Statistics 479 $SuccessPingCount = GetSuccessPingCount -results $FormattedPingResults 480 Write-Host "---Statistics---" -ForegroundColor Green 481 Write-Host ("Total Time Elapsed: " + (GetTimeSpanStringInMinutesAndSeconds -startTime $Start -endTime ([System.DateTime]::Now))) -ForegroundColor Green 482 Write-Host "Total Names Pinged:"$FormattedPingResults.Count -ForegroundColor Green 483 Write-Host ("Hosts that Responded: " + ($SuccessPingCount)) -ForegroundColor Green 484 Write-Host ("DNS Names Resolved: " + (GetDNSNamesResolvedCount -results $FormattedPingResults)) -ForegroundColor Green 485 Write-Host ("Percentage of Hosts that Responded at Least Once: " + (GetPercentageAsString -n1 $SuccessPingCount -n2 $FormattedPingResults.Count)) -ForegroundColor Green 486 Write-Host "CSV Output Location:"$OutputPath -ForegroundColor Yellow