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++) {
            try { $ping.Send($Target, $TimeoutMs) } catch { $null }
            Start-Sleep -Milliseconds 100
        }
    }
    finally { $ping.Dispose() }

    $ok = @($replies | Where-Object { $_ -and $_.Status -eq 'Success' })

    [pscustomobject]@{
        PSTypeName  = 'Gutcheck.Data.Latency'
        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 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
        }
    }
}