Private/Probe.ps1
|
# How Gatherers reach the network. # # Both of these are Gatherer machinery: they talk to something outside this machine and # decide nothing about what comes back. They are shared because the Network Kind and the # Server connectivity Kind measure the same two things - can I reach it, and how long does # it take - and a Technician comparing a gateway against a database server is comparing # numbers that must have been produced the same way. # # Deliberately untested, like every Gatherer: faking a ping tests the fake. function Measure-Latency { <# .SYNOPSIS Pings a target repeatedly and reports what came back. Judges none of it. .DESCRIPTION Loss matters more than latency for the problems Gutcheck exists to find - a client that loses one packet in fifty hangs rather than slows - so a window of pings is sent rather than one, and the loss count is reported alongside the timings. #> [CmdletBinding()] [OutputType([psobject])] param( [Parameter(Mandatory)][string]$Target, [int]$Count = 20, [int]$TimeoutMs = 1000 ) $ping = New-Object System.Net.NetworkInformation.Ping try { $replies = for ($i = 0; $i -lt $Count; $i++) { # Collected as the replies, so the progress call must emit nothing - it doesn't. Write-CheckProgress -Status ((Get-Text 'Console.Progress.Ping') -f $Target) -Step $i -Of $Count try { $ping.Send($Target, $TimeoutMs) } catch { $null } Start-Sleep -Milliseconds 100 } } finally { $ping.Dispose() } $ok = @($replies | Where-Object { $_ -and $_.Status -eq 'Success' }) # Where some pings went missing and not all, how busy the machine is: a machine at # full load loses pings on a line that is in order, and the Judge is to know. $load = $null if ($ok.Count -and $ok.Count -lt $Count) { $load = Get-MachineLoadPercent } [pscustomobject]@{ PSTypeName = 'Gutcheck.Data.Latency' CpuPercent = $load Target = $Target Sent = $Count Received = $ok.Count Lost = $Count - $ok.Count LossPercent = [math]::Round((1 - $ok.Count / [double]$Count) * 100) AverageMs = $(if ($ok.Count) { [math]::Round(($ok | Measure-Object RoundtripTime -Average).Average, 1) } else { $null }) MaximumMs = $(if ($ok.Count) { ($ok | Measure-Object RoundtripTime -Maximum).Maximum } else { $null }) } } function Get-MachineLoadPercent { <# .SYNOPSIS How busy the processors are right now, in percent, or $null where Windows does not say. Never throws. #> [CmdletBinding()] [OutputType([double])] param() try { $total = Get-CimInstance Win32_PerfFormattedData_PerfOS_Processor -Filter "Name='_Total'" -ErrorAction Stop if ($null -eq $total) { return $null } [double]$total.PercentProcessorTime } catch { $null } } function Measure-TcpConnect { <# .SYNOPSIS Opens a TCP connection and reports how long it took, or $null when it did not open. .DESCRIPTION A single connect is not evidence: a firewall that drops one connection in five is the thing that makes a business application feel unreliable, and one successful probe would report it as healthy. Callers connect repeatedly and judge the spread. #> [CmdletBinding()] [OutputType([double])] param( [Parameter(Mandatory)][string]$HostName, [Parameter(Mandatory)][int]$Port, [int]$TimeoutMs = 3000 ) $client = New-Object System.Net.Sockets.TcpClient try { $watch = [Diagnostics.Stopwatch]::StartNew() $task = $client.ConnectAsync($HostName, $Port) if ($task.Wait($TimeoutMs) -and $client.Connected) { return [math]::Round($watch.Elapsed.TotalMilliseconds, 1) } $null } catch { $null } finally { $client.Dispose() } } function Resolve-HostAddress { <# .SYNOPSIS Resolves a host name and reports how long it took. Reports failure; judges nothing. .DESCRIPTION Slow name resolution delays every connection an application makes, so the duration is as much of the answer as the address is. #> [CmdletBinding()] [OutputType([psobject])] param( [Parameter(Mandatory)][string]$HostName, # Ask the DNS client first whether it holds an answer for the name: then the time # below is the cache's, not a DNS server's, and Cached says so. [switch]$CheckCache ) $cached = $null if ($CheckCache) { $cached = Test-DnsClientCacheEntry -HostName $HostName } $watch = [Diagnostics.Stopwatch]::StartNew() try { $address = @([Net.Dns]::GetHostAddresses($HostName))[0] $watch.Stop() [pscustomobject]@{ PSTypeName = 'Gutcheck.Data.Resolution' HostName = $HostName Address = "$address" Resolved = $true Milliseconds = $watch.Elapsed.TotalMilliseconds Cached = $cached } } catch { $watch.Stop() [pscustomobject]@{ PSTypeName = 'Gutcheck.Data.Resolution' HostName = $HostName Address = $null Resolved = $false Milliseconds = $null Cached = $cached } } } function Test-DnsClientCacheEntry { <# .SYNOPSIS Whether the DNS client of Windows holds an answer for a name: $true, $false, or $null where Windows has no cmdlet to ask. Asks; changes nothing. Never throws. .DESCRIPTION A name the cache holds is answered from memory in well under a millisecond, and that time says nothing about a DNS server. A short name is cached under its full name (FS01 as FS01.contoso.example), so an entry that begins with the name and a dot counts too. Only answers count: a name that did not exist is cached as well, with a status other than 0. #> [CmdletBinding()] [OutputType([bool])] # -Name too: the HttpsEndpoint and Website Gatherers ask by that name. param([Alias('Name')][AllowNull()][AllowEmptyString()][string]$HostName) if (-not $HostName -or -not (Get-Command -Name Get-DnsClientCache -ErrorAction SilentlyContinue)) { return $null } $name = $HostName.Trim().TrimEnd('.') if (-not $name) { return $false } $entries = @() try { $entries = @(Get-DnsClientCache -Entry ('{0}*' -f $name) -ErrorAction SilentlyContinue) } catch { return $null } $pattern = '{0}.*' -f [System.Management.Automation.WildcardPattern]::Escape($name) [bool]@($entries | Where-Object { "$($_.Status)" -eq '0' -and ("$($_.Entry)" -eq $name -or "$($_.Entry)" -like $pattern) }).Count } function Get-AddressRoute { <# .SYNOPSIS Which adapter Windows would send to an address through, as plain data; nothing where it cannot say. Never throws. .DESCRIPTION Asked of Find-NetRoute, which chooses as Windows chooses for a connection. The adapter's description and interface type are read beside it, because whether that is a VPN is what the Zone of a Measurement turns on - and that is the Judge's to decide (Get-MeasurementRouteKind), not this. #> [CmdletBinding()] [OutputType([psobject])] param([AllowNull()][AllowEmptyString()][string]$Address) if (-not $Address -or -not (Get-Command -Name Find-NetRoute -ErrorAction SilentlyContinue)) { return } try { $route = @(Find-NetRoute -RemoteIPAddress $Address -ErrorAction Stop | Where-Object { $_.PSObject.Properties['NextHop'] }) | Select-Object -First 1 if (-not $route) { return } $adapter = Get-NetAdapter -InterfaceIndex $route.InterfaceIndex -IncludeHidden -ErrorAction SilentlyContinue | Select-Object -First 1 [pscustomobject]@{ PSTypeName = 'Gutcheck.Data.Route' Address = $Address InterfaceIndex = $route.InterfaceIndex InterfaceAlias = "$($route.InterfaceAlias)" InterfaceDescription = "$($adapter.InterfaceDescription)" InterfaceType = $(if ($null -ne $adapter.InterfaceType) { [int]$adapter.InterfaceType } else { $null }) NextHop = "$($route.NextHop)" } } catch { } } function Get-OwnPrefix { <# .SYNOPSIS The machine's own addresses, each with its prefix length: what its own subnets are. Nothing where Windows would not say. Never throws. #> [CmdletBinding()] [OutputType([psobject])] param() if (-not (Get-Command -Name Get-NetIPAddress -ErrorAction SilentlyContinue)) { return } try { Get-NetIPAddress -ErrorAction Stop | ForEach-Object { [pscustomobject]@{ InterfaceIndex = $_.InterfaceIndex InterfaceAlias = "$($_.InterfaceAlias)" Address = "$($_.IPAddress)" PrefixLength = $_.PrefixLength } } } catch { } } function Test-WindowsServer { <# .SYNOPSIS Whether this Windows is a server: $true, $false, or $null where Windows did not say. Asks Windows; changes nothing. .DESCRIPTION ProductType of Win32_OperatingSystem, as Microsoft documents it: 1 is a workstation, 2 a domain controller, 3 a server. Some things a Check reads exist on a workstation only - the Security Center, the recording of how long a start took - and their absence on a server is to be said as that. #> [CmdletBinding()] [OutputType([bool])] param() try { $type = [int](Get-CimInstance -ClassName Win32_OperatingSystem -ErrorAction Stop).ProductType if ($type -eq 1) { return $false } if ($type -eq 2 -or $type -eq 3) { return $true } } catch { } $null } |