FastPing.psm1
class FastPingResponse { [String] $HostName [Boolean] $Online [System.Net.NetworkInformation.IPStatus] $Status [int] $Sent [int] $Received [int] $Lost [int] $PercentLost [Nullable[Double]] $Min [Nullable[Double]] $p50 [Nullable[Double]] $p90 [Nullable[Double]] $Max [int[]] $RawValues hidden [Nullable[Double]] $RoundtripAverage hidden [System.Version] $HostNameAsVersion hidden [string]$IPRegex = '^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$' FastPingResponse( [String] $HostName, [Boolean] $Online, [System.Net.NetworkInformation.IPStatus] $Status, [int] $Sent, [int] $Received, [Nullable[Double]] $RoundtripAverage, [int[]]$RawValues ) { $this.HostName = $HostName $this.Online = $Online $this.Status = $Status $this.RoundtripAverage = $RoundtripAverage $this.Sent = $Sent $this.Received = $Received $this.Lost = $this.Sent - $this.Received $this.PercentLost = $this.Lost / $this.Sent * 100 $this.RawValues = $RawValues if ($HostName -match $this.IPRegex) { $this.HostNameAsVersion = $HostName } $sortedLatency = $this.RawValues | Sort-Object $this.Min = $sortedLatency | Select-Object -First 1 $this.p50 = $sortedLatency | Select-Object -First 1 -Skip ([Math]::Floor($this.RawValues.Count * .5)) $this.p90 = $sortedLatency | Select-Object -First 1 -Skip ([Math]::Floor($this.RawValues.Count * .9)) $this.Max = $sortedLatency | Select-Object -Last 1 } } <# .SYNOPSIS Converts a Decimal IP address into a 32-bit unsigned integer. .DESCRIPTION ConvertToDecimalIP takes a decimal IP, uses a shift operation on each octet and returns a single UInt32 value. .INPUTS System.Net.IPAddress .EXAMPLE ConvertToDecimalIP 1.2.3.4 Converts an IP address to an unsigned 32-bit integer value. .NOTES This code is copied from the Indented.Net.IP module (https://github.com/indented-automation/Indented.Net.IP). The copy is due to not wanting to take a dependency, and that module licensed with a permissive license. Thanks Chris Dent! #> function ConvertToDecimalIP { [CmdletBinding()] [OutputType([UInt32])] param ( # An IP Address to convert. [Parameter(Mandatory, Position = 1, ValueFromPipeline )] [IPAddress] $IPAddress ) process { [UInt32]([IPAddress]::HostToNetworkOrder($IPAddress.Address) -shr 32 -band [UInt32]::MaxValue) } } <# .SYNOPSIS Converts IP address formats to a set a known styles. .DESCRIPTION ConvertToNetwork ensures consistent values are recorded from parameters which must handle differing addressing formats. This Cmdlet allows all other the other functions in this module to offload parameter handling. .NOTES Change log: 05/03/2016 - Chris Dent - Refactored and simplified. 14/01/2014 - Chris Dent - Created. This code is copied from the Indented.Net.IP module (https://github.com/indented-automation/Indented.Net.IP). The copy is due to not wanting to take a dependency, and that module licensed with a permissive license. Thanks Chris Dent! #> function ConvertToNetwork { [CmdletBinding()] [OutputType('Indented.Net.IP.Network')] param ( # Either a literal IP address, a network range expressed as CIDR notation, or an IP address and subnet mask in a string. [Parameter(Mandatory = $true, Position = 1)] [String] $IPAddress, # A subnet mask as an IP address. [Parameter(Position = 2)] [AllowNull()] [String] $SubnetMask ) $validSubnetMaskValues = @( '0.0.0.0', '128.0.0.0', '192.0.0.0', '224.0.0.0', '240.0.0.0', '248.0.0.0', '252.0.0.0', '254.0.0.0', '255.0.0.0', '255.128.0.0', '255.192.0.0', '255.224.0.0', '255.240.0.0', '255.248.0.0', '255.252.0.0', '255.254.0.0', '255.255.0.0', '255.255.128.0', '255.255.192.0', '255.255.224.0', '255.255.240.0', '255.255.248.0', '255.255.252.0', '255.255.254.0', '255.255.255.0', '255.255.255.128', '255.255.255.192', '255.255.255.224', '255.255.255.240', '255.255.255.248', '255.255.255.252', '255.255.255.254', '255.255.255.255' ) $network = [PSCustomObject]@{ IPAddress = $null SubnetMask = $null MaskLength = 0 PSTypeName = 'Indented.Net.IP.Network' } # Override ToString $network | Add-Member ToString -MemberType ScriptMethod -Force -Value { '{0}/{1}' -f $this.IPAddress, $this.MaskLength } if (-not $psboundparameters.ContainsKey('SubnetMask') -or $SubnetMask -eq '') { $IPAddress, $SubnetMask = $IPAddress.Split([Char[]]'\/ ', [StringSplitOptions]::RemoveEmptyEntries) } # IPAddress while ($IPAddress.Split('.').Count -lt 4) { $IPAddress += '.0' } if ([IPAddress]::TryParse($IPAddress, [Ref]$null)) { $network.IPAddress = [IPAddress]$IPAddress } else { $errorRecord = [System.Management.Automation.ErrorRecord]::new( [ArgumentException]'Invalid IP address.', 'InvalidIPAddress', 'InvalidArgument', $IPAddress ) $pscmdlet.ThrowTerminatingError($errorRecord) } # SubnetMask if ($null -eq $SubnetMask -or $SubnetMask -eq '') { $network.SubnetMask = [IPAddress]$validSubnetMaskValues[32] $network.MaskLength = 32 } else { $maskLength = 0 if ([Int32]::TryParse($SubnetMask, [Ref]$maskLength)) { if ($MaskLength -ge 0 -and $maskLength -le 32) { $network.SubnetMask = [IPAddress]$validSubnetMaskValues[$maskLength] $network.MaskLength = $maskLength } else { $errorRecord = [System.Management.Automation.ErrorRecord]::new( [ArgumentException]'Mask length out of range (expecting 0 to 32).', 'InvalidMaskLength', 'InvalidArgument', $SubnetMask ) $pscmdlet.ThrowTerminatingError($errorRecord) } } else { while ($SubnetMask.Split('.').Count -lt 4) { $SubnetMask += '.0' } $maskLength = $validSubnetMaskValues.IndexOf($SubnetMask) if ($maskLength -ge 0) { $Network.SubnetMask = [IPAddress]$SubnetMask $Network.MaskLength = $maskLength } else { $errorRecord = [System.Management.Automation.ErrorRecord]::new( [ArgumentException]'Invalid subnet mask.', 'InvalidSubnetMask', 'InvalidArgument', $SubnetMask ) $pscmdlet.ThrowTerminatingError($errorRecord) } } } $network } <# .SYNOPSIS Get a list of IP addresses within the specified network. .DESCRIPTION GetNetworkRange finds the network and broadcast address as decimal values then starts a counter between the two, returning IPAddress for each. .INPUTS System.String .EXAMPLE GetNetworkRange 192.168.0.0 255.255.255.0 Returns all IP addresses in the range 192.168.0.0/24. .EXAMPLE GetNetworkRange 10.0.8.0/22 Returns all IP addresses in the range 192.168.0.0 255.255.252.0. .NOTES This code is copied from the Indented.Net.IP module (https://github.com/indented-automation/Indented.Net.IP). The copy is due to not wanting to take a dependency, and that module licensed with a permissive license. Thanks Chris Dent! #> function GetNetworkRange { [CmdletBinding(DefaultParameterSetName = 'FromIPAndMask')] [OutputType([IPAddress])] param ( # Either a literal IP address, a network range expressed as CIDR notation, or an IP address and subnet mask in a string. [Parameter(Mandatory, Position = 1, ValueFromPipeline, ParameterSetName = 'FromIPAndMask')] [String] $IPAddress, # A subnet mask as an IP address. [Parameter(Position = 2, ParameterSetName = 'FromIPAndMask')] [String] $SubnetMask, # Include the network and broadcast addresses when generating a network address range. [Parameter(ParameterSetName = 'FromIPAndMask')] [Switch] $IncludeNetworkAndBroadcast, # The start address of a range. [Parameter(Mandatory, ParameterSetName = 'FromStartAndEnd')] [IPAddress] $StartIPAddress, # The end address of a range. [Parameter(Mandatory, ParameterSetName = 'FromStartAndEnd')] [IPAddress] $EndIPAddress ) process { if ($pscmdlet.ParameterSetName -eq 'FromIPAndMask') { try { $null = $psboundparameters.Remove('IncludeNetworkAndBroadcast') $network = ConvertToNetwork @psboundparameters } catch { $pscmdlet.ThrowTerminatingError($_) } $decimalIP = ConvertToDecimalIP -IPAddress $network.IPAddress $decimalMask = ConvertToDecimalIP -IPAddress $network.SubnetMask $startDecimal = $decimalIP -band $decimalMask $endDecimal = $decimalIP -bor (-bnot $decimalMask -band [UInt32]::MaxValue) if (-not $IncludeNetworkAndBroadcast) { $startDecimal++ $endDecimal-- } } else { $startDecimal = ConvertToDecimalIP -IPAddress $StartIPAddress $endDecimal = ConvertToDecimalIP -IPAddress $EndIPAddress } for ($i = $startDecimal; $i -le $endDecimal; $i++) { [IPAddress]([IPAddress]::NetworkToHostOrder([Int64]$i) -shr 32 -band [UInt32]::MaxValue) } } } <# .SYNOPSIS Performs a series of asynchronous pings against a set of target hosts. .DESCRIPTION This function uses the System.Net.Networkinformation.Ping object to perform a series of asynchronous pings against a set of target hosts. Each ping result is calculated the specified number of echo requests. .PARAMETER HostName String array of target hosts. .PARAMETER Count Number of ping requests to send. Aliased with 'n', like ping.exe. .PARAMETER Continuous Enables continuous pings against the target hosts. Stop with CTRL+C. Aliases with 't', like ping.exe. .PARAMETER Timeout Timeout in milliseconds to wait for each reply. Defaults to 2 seconds (5000 ms). Aliased with 'w', like ping.exe. Per MSDN Documentation, "When specifying very small numbers for timeout, the Ping reply can be received even if timeout milliseconds have elapsed." (https://msdn.microsoft.com/en-us/library/ms144955.aspx). .PARAMETER Interval Number of milliseconds between ping requests. .PARAMETER EchoRequests Number of echo requests to use for each ping result. Used to generate the calculated output fields. Defaults to 4. .EXAMPLE Invoke-FastPing -HostName 'andrewpearce.io' HostName Online Status p90 PercentLost -------- ------ ------ --- ----------- andrewpearce.io True Success 4 0 .EXAMPLE Invoke-FastPing -HostName 'andrewpearce.io','doesnotexist.andrewpearce.io' HostName Online Status p90 PercentLost -------- ------ ------ --- ----------- andrewpearce.io True Success 5 0 doesnotexist.an… False Unknown 100 .EXAMPLE Invoke-FastPing -HostName 'andrewpearce.io' -Count 5 This example generates five ping results against the host 'andrewpearce.io'. .EXAMPLE fp andrewpearce.io -n 5 This example pings the host 'andrewpearce.io' five times using syntax similar to ping.exe. .EXAMPLE Invoke-FastPing -HostName 'microsoft.com' -Timeout 500 This example pings the host 'microsoft.com' with a 500 millisecond timeout. .EXAMPLE fp microsoft.com -w 500 This example pings the host 'microsoft.com' with a 500 millisecond timeout using syntax similar to ping.exe. .EXAMPLE fp andrewpearce.io -Continuous This example pings the host 'andrewpearce.io' continuously until CTRL+C is used. .EXAMPLE fp andrewpearce.io -t This example pings the host 'andrewpearce.io' continuously until CTRL+C is used. #> function Invoke-FastPing { [CmdletBinding(DefaultParameterSetName = 'Count')] [Alias('FastPing', 'fping', 'fp')] param ( [Parameter(Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [ValidateNotNullOrEmpty()] [Alias('Computer', 'ComputerName', 'Host')] [String[]] $HostName, [Parameter(ParameterSetName = 'Count')] [ValidateRange(1, [Int]::MaxValue)] [Alias('N')] [Int] $Count = 1, [Parameter(ParameterSetName = 'Continuous')] [Alias('T')] [Switch] $Continuous, [ValidateRange(1, [Int]::MaxValue)] [Alias('W')] [Int] $Timeout = 5000, [ValidateRange(1, [Int]::MaxValue)] [Int] $Interval = 1000, [ValidateRange(1, [Int]::MaxValue)] [Alias('RoundtripAveragePingCount')] [Int] $EchoRequests = 4 ) begin { # The time used for the ping async wait() method $asyncWaitMilliseconds = 500 # Used to control the Count of echo requests $loopCounter = 0 # Used to control the Interval between echo requests $loopTimer = [System.Diagnostics.Stopwatch]::new() # Regex for identifying an IPv4 address $ipRegex = '^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$' # Used to shorten line length when filtering results $sortCount = @{ Property = 'Count' Descending = $true } } process { try { while ($true) { $loopTimer.Restart() # Objects to hold items as we process pings $queue = [System.Collections.Queue]::new() $pingHash = @{} # Start an asynchronous ping against each computer foreach ($hn in $HostName) { if ($pingHash.Keys -notcontains $hn) { $pingHash.Add($hn, [System.Collections.ArrayList]::new()) # Attempt to resolve the hostname to prevent issues where the first host fails to resolve if ($hn -notmatch $ipRegex) { try { $null = [System.Net.Dns]::Resolve($hn) } catch { if ($_.Exception.Message -like '*No such host is known*') { Write-Warning "The HostName $hn cannot be resolved." } } } } for ($i = 0; $i -lt $EchoRequests; $i++) { $ping = [System.Net.Networkinformation.Ping]::new() $object = @{ Host = $hn Ping = $ping Async = $ping.SendPingAsync($hn, $Timeout) } $queue.Enqueue($object) } } # Process the asynchronous pings while ($queue.Count -gt 0) { $object = $queue.Dequeue() try { # Wait for completion if ($object.Async.Wait($asyncWaitMilliseconds) -eq $true) { [Void]$pingHash[$object.Host].Add(@{ Host = $object.Host RoundtripTime = $object.Async.Result.RoundtripTime Status = $object.Async.Result.Status }) continue } } catch { # The Wait() method can throw an exception if the host does not exist. if ($object.Async.IsCompleted -eq $true) { [Void]$pingHash[$object.Host].Add(@{ Host = $object.Host RoundtripTime = $object.Async.Result.RoundtripTime Status = $object.Async.Result.Status }) continue } else { Write-Warning -Message ('Unhandled exception: {0}' -f $_.Exception.Message) } } $queue.Enqueue($object) } # Using the ping results in pingHash, calculate the average RoundtripTime foreach ($key in $pingHash.Keys) { $pingStatus = $pingHash.$key.Status | Select-Object -Unique $unsuccessfulPingStatus = $pingStatus | Where-Object {$_ -ne [System.Net.NetworkInformation.IPStatus]::Success} $sent = 0 $received = 0 $latency = [System.Collections.ArrayList]::new() foreach ($value in $pingHash.$key) { $sent++ if (-not([String]::IsNullOrWhiteSpace($value.RoundtripTime)) -and $value.Status -eq [System.Net.NetworkInformation.IPStatus]::Success) { $received++ [Void]$latency.Add($value.RoundtripTime) } } $sortedLatency = $latency | Sort-Object if ($received -ge 1) { $online = $true $status = [System.Net.NetworkInformation.IPStatus]::Success $roundtripAverage = [Math]::Round(($sortedLatency | Measure-Object -Average).Average, 2) } else { $online = $false if ($unsuccessfulPingStatus.Count -eq 1) { $status = $pingStatus } else { $groupedPingStatus = $pingHash.$key.Status | Group-Object $status = ($groupedPingStatus | Sort-Object @sortCount | Select-Object -First 1).Name } if ([String]::IsNullOrWhiteSpace($status)) { $status = [System.Net.NetworkInformation.IPStatus]::Unknown } $roundtripAverage = $null } [FastPingResponse]::new( $key, $online, $status, $sent, $received, $roundtripAverage, $latency ) } # End result processing # Increment the loop counter $loopCounter++ if ($loopCounter -lt $Count -or $Continuous -eq $true) { $timeToSleep = $Interval - $loopTimer.Elapsed.TotalMilliseconds if ($timeToSleep -gt 0) { Start-Sleep -Milliseconds $timeToSleep } } else { break } } } catch { throw } finally { $loopTimer.Stop() } } # End Process } <# .SYNOPSIS Performs a ping sweep against a series of target IP Addresses. .DESCRIPTION This function calculates the list of IP Addresses to target, and wraps a call to Invoke-FastPingto perform the ping sweep. .PARAMETER StartIP The IP Address to start from. .PARAMETER EndIp The IP Address to finish with. .PARAMETER IPAddress An IP Address, to be matched with an appropriate Subnet Mask. .PARAMETER SubnetMask A Subnet Mask for network range calculations. .EXAMPLE Invoke-PingSweep -StartIP '1.1.1.1' -EndIP '1.1.1.5' HostName RoundtripAverage Online Status -------- ---------------- ------ ------ 1.1.1.3 19 True Success 1.1.1.4 22 True Success 1.1.1.1 21 True Success 1.1.1.2 19 True Success 1.1.1.5 24 True Success .EXAMPLE Invoke-PingSweep -IPAddress '1.1.1.1' -SubnetMask '255.255.255.252' HostName RoundtripAverage Online Status -------- ---------------- ------ ------ 1.1.1.2 21 True Success 1.1.1.1 16 True Success #> function Invoke-PingSweep { [CmdletBinding(DefaultParameterSetName = 'FromStartAndEnd')] [Alias('PingSweep', 'psweep')] param ( [Parameter( Mandatory = $true, Position = 0, ParameterSetName = 'FromStartAndEnd')] [ValidateScript( { [System.Net.IPAddress]$_ } )] [String] $StartIP, [Parameter( Mandatory = $true, Position = 1, ParameterSetName = 'FromStartAndEnd')] [ValidateScript( { [System.Net.IPAddress]$_ } )] [String] $EndIP, [Parameter( Mandatory = $true, Position = 0, ParameterSetName = 'FromIPAndMask')] [ValidateScript( { [System.Net.IPAddress]$_ } )] [String] $IPAddress, [Parameter( Mandatory = $true, Position = 1, ParameterSetName = 'FromIPAndMask')] [ValidateScript( { [System.Net.IPAddress]$_ } )] [String] $SubnetMask, [Switch] $ReturnOnlineOnly ) switch ($PSCmdlet.ParameterSetName) { 'FromIPAndMask' { $getNetworkRange = @{ IPAddress = $IPAddress SubnetMask = $SubnetMask } } 'FromStartAndEnd' { $getNetworkRange = @{ StartIPAddress = $StartIP EndIPAddress = $EndIP } } } $networkRange = (GetNetworkRange @getNetworkRange).IPAddressToString if ($ReturnOnlineOnly) { $whereObject = { $_.Online -eq $true } Invoke-FastPing -HostName $networkRange | Where-Object $whereObject | Sort-Object -Property HostNameAsVersion } else { Invoke-FastPing -HostName $networkRange | Sort-Object -Property HostNameAsVersion } } |