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. The address is the one every Check shows a Server at (Select-ServerAddress): IPv4 first, in the order DNS answered. #> [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 = Select-ServerAddress -Address @([Net.Dns]::GetHostAddresses($HostName)) $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 } |