Pinging a List of Machines in PowerShell

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

Ping.renametops1