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) $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 } } catch { $watch.Stop() [pscustomobject]@{ PSTypeName = 'Gutcheck.Data.Resolution' HostName = $HostName Address = $null Resolved = $false Milliseconds = $null } } } 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 } |