灵符lingfu

LingSys

↓ 0 downloads

Your machine's memory, GPU, motherboard and network, drawn and played in the Ling design system: LingSys, a small build for USB liquid-cooler displays, LingNet for IPv4/IPv6, DNS and packets, and a LingOS edition. 64-bit and 32-bit Windows builds included.

Keywords system-monitor, visualization, network, ipv6, gpu, lcd, lingos, ling
lingfu install LingSys
LicenseMIT
AuthorsLing Lin <taellinglin@gmail.com>
READMECode / FilesVersionsDependency GraphDependentsSecurityBuildsSettings

Code / Files (48)

sampler_hw.ps1
# HatchSYS hardware sampler: writes a small text snapshot of GPU, motherboard/CPU or network counters.
#
#   .\sampler_hw.ps1 -Kind gpu      ->  gpu.snap
#   .\sampler_hw.ps1 -Kind board    ->  board.snap
#   .\sampler_hw.ps1 -Kind net      ->  net.snap
#
# Ling has no builtin that reads operating-system or hardware state, so these scripts are the only bridge.
# They read the same performance counters Task Manager shows, plus nvidia-smi (if installed) and netstat.
# They never touch process memory and send nothing off the machine (the only packet is one ping to your
# own default gateway, for the latency readout).
# Every file is replaced atomically each tick, one record per line, fields split by '|'.
#
# gpu.snap     (one set of lines per GPU, idx = 0, 1, ...; NVIDIA cards first, then any other adapter in use)
#   t|<unix seconds>
#   n|idx|name|driver
#   v|idx|temp C|fan %|power W|power limit W|core MHz|core max|mem MHz|mem max|util %|mem util %|vram used MB|vram total MB|pcie gen|pcie width|power throttle|thermal throttle
#   e|idx|3D %|compute %|copy %|video decode %|video encode %|other %
#   a|idx|process|pid|dedicated MB|shared MB|engine %
#
# board.snap
#   t|..   b|maker|product|version   i|bios vendor|bios version|date   c|cpu|cores|threads|max MHz|sockets
#   y|dimm slots|max GB   m|locator|GB|MHz|maker   d|model|GB|bus
#   l|util %|MHz|% of max MHz|temp C|interrupts/s|dpcs/s|ctx switches/s|syscalls/s|processes|threads|uptime s|disk read B/s|disk write B/s|disk busy %|disk queue
#   k|cpu index|util %|MHz|interrupts/s|dpcs/s     s|disk|read B/s|write B/s|busy %
#   h|type|name|value           (only when LibreHardwareMonitor / OpenHardwareMonitor is running)
#
# netx.snap   (the detailed network picture for LingNet; one row per fact, ifIndex = Windows interface number)
#   t|..
#   a|ifIndex|description|alias|1 if up|link bits/s|rx B/s|tx B/s|rx pkts/s|tx pkts/s|rx non-unicast pkts/s|tx non-unicast pkts/s|errors+discards
#   m|ifIndex|4 or 6|dhcp 1/0|mtu|metric        p|ifIndex|4 or 6|address|prefix|scope|origin
#   g|ifIndex|4 or 6|gateway                    s|ifIndex|4 or 6|dns server
#   n|ifIndex|4 or 6|neighbour hash 0..1|0 reachable, 1 stale, 2 incomplete
#   r|ipv4 dgrams/s|ipv6|tcp4 segs/s|tcp6|udp4 dgrams/s|udp6|icmp4 msgs/s|icmp6|tcp4 established|tcp6|retrans4/s|retrans6/s
#   c|4 or 6|ifIndex of the local address (0 = unknown)|0 established, 1 syn sent|remote hash|remote port|process
#   k|4 or 6|established|listen|time_wait|close_wait|syn_sent|other|loopback
#   d|A|AAAA|CNAME|PTR|SRV|other|negative|total          (local DNS cache by record type; names are never written)
#   q|4 or 6|dns server|ms (-1 = no answer)|1 if it answered|query type 1 A / 28 AAAA
# net.snap
#   t|..   i|adapter|rx B/s|tx B/s|link bits/s|rx pkts/s|tx pkts/s|errors+discards
#   k|tcp established|retransmits/s|failed connects (total)|tcp segments/s|udp datagrams/s
#   r|ping ms to gateway (-1 = no answer)
#   c|established|listen|time_wait|close_wait|syn_sent|other|loopback
#   x|process|pid|connections      d|remote hash 0..1|remote port|state|process
param(
    [Parameter(Mandatory = $true)][ValidateSet('gpu', 'board', 'net', 'netx')][string]$Kind,
    [string]$Out,
    [double]$Interval = 1.0,
    [switch]$Once,
    [int]$ParentPid = 0     # exit when this process is gone (the exe starts the samplers and passes its own id)
)
$ErrorActionPreference = 'Stop'
if (-not $Out) { $Out = Join-Path $PSScriptRoot ($Kind + '.snap') }
$inv = [Globalization.CultureInfo]::InvariantCulture
function F($v) { try { ([double]$v).ToString('0.##', $inv) } catch { '0' } }
function Clean($s) { (([string]$s) -replace '[|\r\n#]', '_').Trim() }
function Num($s) {
    $d = 0.0
    if ([double]::TryParse(([string]$s).Trim(), [Globalization.NumberStyles]::Float, $inv, [ref]$d)) { $d } else { 0.0 }
}
function Unix { [DateTimeOffset]::UtcNow.ToUnixTimeSeconds() }
function PidNames($pids) {
    $map = @{}
    if ($pids.Count -gt 0) {
        foreach ($p in (Get-Process -Id ([int[]]$pids) -ErrorAction SilentlyContinue)) { $map[[int]$p.Id] = $p.ProcessName }
    }
    $map
}

# ───────────────────────────── GPU ───────────────────────────────────────────
$smi = (Get-Command nvidia-smi -ErrorAction SilentlyContinue).Source
$otherGpuName = $null
function Get-GpuLines {
    $lines = New-Object System.Collections.Generic.List[string]
    $lines.Add('t|' + (Unix))
    $gpus = New-Object System.Collections.Generic.List[object]
    if ($smi) {
        $q = 'index,name,driver_version,temperature.gpu,fan.speed,power.draw,power.limit,clocks.gr,clocks.max.gr,clocks.mem,clocks.max.mem,utilization.gpu,utilization.memory,memory.used,memory.total,pcie.link.gen.current,pcie.link.width.current,clocks_throttle_reasons.active'
        foreach ($ln in (& $smi "--query-gpu=$q" --format=csv,noheader,nounits 2>$null)) {
            $f = @(([string]$ln) -split ',' | ForEach-Object { $_.Trim() })
            if ($f.Count -lt 18) { continue }
            $thr = 0L
            try { $thr = [Convert]::ToInt64(($f[17] -replace '^0x', ''), 16) } catch { }
            $gpus.Add([pscustomobject]@{
                    Idx = [int](Num $f[0]); Name = $f[1]; Drv = $f[2]
                    V = @((Num $f[3]), (Num $f[4]), (Num $f[5]), (Num $f[6]), (Num $f[7]), (Num $f[8]), (Num $f[9]), (Num $f[10]),
                        (Num $f[11]), (Num $f[12]), (Num $f[13]), (Num $f[14]), (Num $f[15]), (Num $f[16]),
                        $(if ($thr -band 0x84) { 1 } else { 0 }), $(if ($thr -band 0x68) { 1 } else { 0 }))
                    Luid = $null
                })
        }
    }
    # Windows' GPU counters name adapters by LUID, nvidia-smi by index. Match them by VRAM in use.
    $am = @{}
    foreach ($a in (Get-CimInstance Win32_PerfFormattedData_GPUPerformanceCounters_GPUAdapterMemory)) {
        if ($a.Name -match 'luid_(0x[0-9A-Fa-f]+_0x[0-9A-Fa-f]+)') { $am[$Matches[1]] = [double]$a.DedicatedUsage / 1MB }
    }
    if ($gpus.Count -eq 0) {
        $i = 0
        foreach ($k in ($am.Keys | Sort-Object { - $am[$_] })) {
            if ($am[$k] -le 0) { continue }
            $gpus.Add([pscustomobject]@{ Idx = $i; Name = "GPU $i"; Drv = ''; V = @(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, $am[$k], 0, 0, 0, 0, 0); Luid = $k })
            $i++
        }
    }
    else {
        $free = New-Object System.Collections.Generic.List[string]
        $am.Keys | ForEach-Object { $free.Add($_) }
        foreach ($g in $gpus) {
            $best = $null; $bd = [double]::MaxValue
            foreach ($k in $free) { $d = [math]::Abs($am[$k] - $g.V[10]); if ($d -lt $bd) { $bd = $d; $best = $k } }
            if ($best) { $g.Luid = $best; [void]$free.Remove($best) }
        }
        # adapters nvidia-smi does not know (Intel, AMD, ...) that are really in use get a row of their own
        foreach ($k in $free) {
            if ($am[$k] -lt 64) { continue }
            if (-not $script:otherGpuName) {
                $on = (Get-CimInstance Win32_VideoController | Where-Object { $_.Name -notmatch 'NVIDIA|Virtual|Remote|Basic|Mirror' } | Select-Object -First 1).Name
                $script:otherGpuName = if ($on) { $on } else { 'OTHER ADAPTER' }
            }
            $gpus.Add([pscustomobject]@{ Idx = $gpus.Count; Name = $script:otherGpuName; Drv = ''; V = @(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, $am[$k], 0, 0, 0, 0, 0); Luid = $k; Extra = $true })
        }
    }
    # engines: per adapter, per engine instance summed over processes; a type's value is its busiest engine
    $engInst = @{}; $pidUtil = @{}
    foreach ($e in (Get-CimInstance Win32_PerfFormattedData_GPUPerformanceCounters_GPUEngine)) {
        if ($e.Name -notmatch 'pid_(\d+)_luid_(0x[0-9A-Fa-f]+_0x[0-9A-Fa-f]+)_phys_\d+_eng_(\d+)_engtype_(.+)$') { continue }
        $pid_ = [int]$Matches[1]; $luid = $Matches[2]; $eng = $Matches[3]; $ty = $Matches[4]
        $u = [double]$e.UtilizationPercentage
        $cls = 5
        if ($ty -like '3D*') { $cls = 0 } elseif ($ty -like 'Compute*' -or $ty -like 'Cuda*') { $cls = 1 } elseif ($ty -like 'Copy*') { $cls = 2 }
        elseif ($ty -like 'VideoDecode*') { $cls = 3 } elseif ($ty -like 'VideoEncode*') { $cls = 4 }
        $key = "$luid|$cls|$eng"
        $engInst[$key] = [double]$engInst[$key] + $u
        $pk = "$luid|$pid_"
        $pidUtil[$pk] = [double]$pidUtil[$pk] + $u
    }
    $procMem = @{}
    foreach ($m in (Get-CimInstance Win32_PerfFormattedData_GPUPerformanceCounters_GPUProcessMemory)) {
        if ($m.Name -notmatch 'pid_(\d+)_luid_(0x[0-9A-Fa-f]+_0x[0-9A-Fa-f]+)') { continue }
        $luid = $Matches[2]
        if (-not $procMem.ContainsKey($luid)) { $procMem[$luid] = New-Object System.Collections.Generic.List[object] }
        $procMem[$luid].Add([pscustomobject]@{ Pid = [int]$Matches[1]; Ded = [double]$m.DedicatedUsage / 1MB; Sh = [double]$m.SharedUsage / 1MB })
    }
    $allPids = @($procMem.Values | ForEach-Object { $_ } | ForEach-Object { $_.Pid } | Select-Object -Unique)
    $names = PidNames $allPids
    foreach ($g in $gpus) {
        $i = $g.Idx
        $eng = @(0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
        if ($g.Luid) {
            foreach ($k in $engInst.Keys) {
                $p = $k -split '\|'
                if ($p[0] -eq $g.Luid) { $c = [int]$p[1]; $eng[$c] = [math]::Max($eng[$c], [math]::Min($engInst[$k], 100.0)) }
            }
        }
        # a card nvidia-smi cannot see has no utilisation figure: use its busiest 3D engine
        if ($g.Extra) { $g.V[8] = $eng[0] }
        $lines.Add("n|$i|" + (Clean $g.Name) + '|' + (Clean $g.Drv))
        $lines.Add("v|$i|" + (($g.V | ForEach-Object { F $_ }) -join '|'))
        if ($g.Luid) {
            $lines.Add("e|$i|" + (($eng | ForEach-Object { F $_ }) -join '|'))
            if ($procMem.ContainsKey($g.Luid)) {
                foreach ($m in ($procMem[$g.Luid] | Sort-Object Ded -Descending | Select-Object -First 14)) {
                    $nm = if ($names.ContainsKey($m.Pid)) { $names[$m.Pid] } else { "pid$($m.Pid)" }
                    $lines.Add("a|$i|" + (Clean $nm) + '|' + $m.Pid + '|' + (F $m.Ded) + '|' + (F $m.Sh) + '|' + (F ([double]$pidUtil["$($g.Luid)|$($m.Pid)"])))
                }
            }
        }
        else { $lines.Add("e|$i|0|0|0|0|0|0") }
    }
    $lines
}

# ───────────────────────────── motherboard / CPU ─────────────────────────────
$static = $null
function Get-Static {
    $bb = Get-CimInstance Win32_BaseBoard | Select-Object -First 1
    $bi = Get-CimInstance Win32_BIOS | Select-Object -First 1
    $cp = @(Get-CimInstance Win32_Processor)
    $arr = Get-CimInstance Win32_PhysicalMemoryArray | Select-Object -First 1
    $L = New-Object System.Collections.Generic.List[string]
    $L.Add('b|' + (Clean $bb.Manufacturer) + '|' + (Clean $bb.Product) + '|' + (Clean $bb.Version))
    $bd = ''
    try { $bd = ([datetime]$bi.ReleaseDate).ToString('yyyy-MM-dd') } catch { }
    $L.Add('i|' + (Clean $bi.Manufacturer) + '|' + (Clean $bi.SMBIOSBIOSVersion) + '|' + $bd)
    $cores = ($cp | Measure-Object NumberOfCores -Sum).Sum
    $thr = ($cp | Measure-Object NumberOfLogicalProcessors -Sum).Sum
    $L.Add('c|' + (Clean $cp[0].Name) + '|' + $cores + '|' + $thr + '|' + $cp[0].MaxClockSpeed + '|' + $cp.Count)
    $L.Add('y|' + $arr.MemoryDevices + '|' + (F ([double]$arr.MaxCapacity / 1MB)))
    foreach ($m in (Get-CimInstance Win32_PhysicalMemory)) {
        $sp = if ($m.ConfiguredClockSpeed) { $m.ConfiguredClockSpeed } else { $m.Speed }
        $L.Add('m|' + (Clean $m.DeviceLocator) + '|' + (F ([double]$m.Capacity / 1GB)) + '|' + $sp + '|' + (Clean $m.Manufacturer))
    }
    foreach ($d in (Get-CimInstance Win32_DiskDrive | Where-Object { $_.MediaType -match 'Fixed' } | Select-Object -First 8)) {
        $L.Add('d|' + (Clean $d.Model) + '|' + (F ([double]$d.Size / 1GB)) + '|' + (Clean $d.InterfaceType))
    }
    , $L
}
function Get-BoardLines {
    if (-not $script:static) { $script:static = Get-Static }
    $lines = New-Object System.Collections.Generic.List[string]
    $lines.Add('t|' + (Unix))
    $lines.AddRange($script:static)
    $pi = @(Get-CimInstance Win32_PerfFormattedData_Counters_ProcessorInformation)
    $tot = $pi | Where-Object { $_.Name -eq '_Total' } | Select-Object -First 1
    $sys = Get-CimInstance Win32_PerfFormattedData_PerfOS_System | Select-Object -First 1
    $dk = @(Get-CimInstance Win32_PerfFormattedData_PerfDisk_PhysicalDisk)
    $dt = $dk | Where-Object { $_.Name -eq '_Total' } | Select-Object -First 1
    $tz = @(Get-CimInstance Win32_PerfFormattedData_Counters_ThermalZoneInformation)
    $temp = 0.0
    foreach ($z in $tz) { $c = [double]$z.HighPrecisionTemperature / 10.0 - 273.15; if ($c -gt $temp) { $temp = $c } }
    $util = [math]::Min([double]$tot.PercentProcessorUtility, 100.0)
    $mhz = [double]$tot.ActualFrequency
    $lines.Add('l|' + ((@($util, $mhz, $tot.PercentofMaximumFrequency, $temp, $tot.InterruptsPersec, $tot.DPCsQueuedPersec,
                    $sys.ContextSwitchesPersec, $sys.SystemCallsPersec, $sys.Processes, $sys.Threads, $sys.SystemUpTime,
                    $dt.DiskReadBytesPersec, $dt.DiskWriteBytesPersec, $dt.PercentDiskTime, $dt.CurrentDiskQueueLength) |
                ForEach-Object { F $_ }) -join '|'))
    foreach ($c in $pi) {
        if ($c.Name -notmatch '^\d+,(\d+)$') { continue }
        $lines.Add('k|' + $Matches[1] + '|' + (F ([math]::Min([double]$c.PercentProcessorUtility, 100.0))) + '|' + (F $c.ActualFrequency) + '|' + (F $c.InterruptsPersec) + '|' + (F $c.DPCsQueuedPersec))
    }
    foreach ($d in $dk) {
        if ($d.Name -eq '_Total') { continue }
        $lines.Add('s|' + (Clean $d.Name) + '|' + (F $d.DiskReadBytesPersec) + '|' + (F $d.DiskWriteBytesPersec) + '|' + (F ([math]::Min([double]$d.PercentDiskTime, 100.0))))
    }
    # optional: real sensors (fans, voltages, CPU package temperature) when a monitor that exposes WMI is running
    foreach ($ns in 'root/LibreHardwareMonitor', 'root/OpenHardwareMonitor') {
        try {
            $sn = @(Get-CimInstance -Namespace $ns -ClassName Sensor -ErrorAction Stop | Where-Object { $_.SensorType -in 'Temperature', 'Fan', 'Voltage', 'Power' })
            foreach ($s in ($sn | Select-Object -First 40)) { $lines.Add('h|' + $s.SensorType + '|' + (Clean $s.Name) + '|' + (F $s.Value)) }
            if ($sn.Count -gt 0) { break }
        }
        catch { }
    }
    $lines
}

# ───────────────────────────── network ───────────────────────────────────────
$gateway = $null; $gwAge = 0
$ping = New-Object System.Net.NetworkInformation.Ping
$tick = 0
$lastNet = $null
function Get-NetLines {
    $script:tick++
    $lines = New-Object System.Collections.Generic.List[string]
    $lines.Add('t|' + (Unix))
    foreach ($n in (Get-CimInstance Win32_PerfFormattedData_Tcpip_NetworkInterface | Where-Object { [double]$_.CurrentBandwidth -gt 0 } | Sort-Object BytesTotalPersec -Descending | Select-Object -First 6)) {
        $lines.Add('i|' + (Clean $n.Name) + '|' + (F $n.BytesReceivedPersec) + '|' + (F $n.BytesSentPersec) + '|' + (F $n.CurrentBandwidth) + '|' +
            (F $n.PacketsReceivedPersec) + '|' + (F $n.PacketsSentPersec) + '|' + (F ([double]$n.PacketsReceivedErrors + $n.PacketsOutboundErrors + $n.PacketsReceivedDiscarded + $n.PacketsOutboundDiscarded)))
    }
    $t4 = Get-CimInstance Win32_PerfFormattedData_Tcpip_TCPv4 | Select-Object -First 1
    $t6 = Get-CimInstance Win32_PerfFormattedData_Tcpip_TCPv6 -ErrorAction SilentlyContinue | Select-Object -First 1
    $u4 = Get-CimInstance Win32_PerfFormattedData_Tcpip_UDPv4 | Select-Object -First 1
    $est = [double]$t4.ConnectionsEstablished + [double]$t6.ConnectionsEstablished
    $lines.Add('k|' + (F $est) + '|' + (F ([double]$t4.SegmentsRetransmittedPersec + [double]$t6.SegmentsRetransmittedPersec)) + '|' +
        (F ([double]$t4.ConnectionFailures + [double]$t6.ConnectionFailures)) + '|' + (F ([double]$t4.SegmentsPersec + [double]$t6.SegmentsPersec)) + '|' + (F $u4.DatagramsPersec))
    # latency to the default gateway (a local hop; nothing leaves your network)
    if ($script:tick % 30 -eq 1 -or -not $script:gateway) {
        $gw = Get-CimInstance Win32_NetworkAdapterConfiguration -Filter 'IPEnabled=true' | Where-Object { $_.DefaultIPGateway } | Select-Object -First 1
        if ($gw) { $script:gateway = [string]$gw.DefaultIPGateway[0] }
    }
    $rtt = -1
    if ($script:gateway) {
        try { $r = $ping.Send($script:gateway, 1000); if ($r.Status -eq 'Success') { $rtt = $r.RoundtripTime } } catch { }
        if ($rtt -lt 0) {
            # many routers drop ICMP: a TCP connect (accepted or reset) to the gateway still times one round trip
            foreach ($port in 80, 53, 443) {
                $sw = [Diagnostics.Stopwatch]::StartNew()
                $c = New-Object Net.Sockets.TcpClient
                try {
                    $iar = $c.BeginConnect($script:gateway, $port, $null, $null)
                    if ($iar.AsyncWaitHandle.WaitOne(1000)) { try { $c.EndConnect($iar) } catch { }; $rtt = [math]::Round($sw.Elapsed.TotalMilliseconds, 1) }
                }
                catch { }
                finally { $c.Close() }
                if ($rtt -ge 0) { break }
            }
        }
    }
    $lines.Add('r|' + $rtt)
    # connections: netstat is far faster than Get-NetTCPConnection
    if ($script:tick % 2 -eq 1 -or -not $script:lastNet) {
        $cnt = @{ ESTABLISHED = 0; LISTENING = 0; TIME_WAIT = 0; CLOSE_WAIT = 0; SYN_SENT = 0; OTHER = 0; LOOP = 0 }
        $rows = New-Object System.Collections.Generic.List[object]
        $ns4 = @(netstat -ano -p tcp)
        $ns6 = @(netstat -ano -p tcpv6)
        foreach ($ln in ($ns4 + $ns6)) {
            if ($ln -notmatch '^\s*TCP\s+(\S+)\s+(\S+)\s+(\S+)\s+(\d+)') { continue }
            $loc = $Matches[1]; $rem = $Matches[2]; $st = $Matches[3]; $pid_ = [int]$Matches[4]
            $ri = $rem.LastIndexOf(':'); $rip = $rem.Substring(0, $ri); $rport = [int]($rem.Substring($ri + 1))
            $loop = $rip -match '^(127\.|\[?::1\]?$)' -or $rip -eq $loc.Substring(0, $loc.LastIndexOf(':'))
            if ($st -eq 'LISTENING') { $cnt.LISTENING++; continue }
            if ($loop) { $cnt.LOOP++; continue }
            if ($cnt.ContainsKey($st)) { $cnt[$st]++ } else { $cnt.OTHER++ }
            $rows.Add([pscustomobject]@{ Ip = $rip; Port = $rport; St = $st; Pid = $pid_ })
        }
        $names = PidNames @($rows | ForEach-Object { $_.Pid } | Select-Object -Unique)
        $out = New-Object System.Collections.Generic.List[string]
        $out.Add('c|' + $cnt.ESTABLISHED + '|' + $cnt.LISTENING + '|' + $cnt.TIME_WAIT + '|' + $cnt.CLOSE_WAIT + '|' + $cnt.SYN_SENT + '|' + $cnt.OTHER + '|' + $cnt.LOOP)
        foreach ($g in ($rows | Group-Object Pid | Sort-Object Count -Descending | Select-Object -First 12)) {
            $nm = if ($names.ContainsKey([int]$g.Name)) { $names[[int]$g.Name] } else { "pid$($g.Name)" }
            $out.Add('x|' + (Clean $nm) + '|' + $g.Name + '|' + $g.Count)
        }
        foreach ($r in ($rows | Where-Object { $_.St -in 'ESTABLISHED', 'SYN_SENT' } | Select-Object -First 56)) {
            $h = 0; foreach ($ch in $r.Ip.ToCharArray()) { $h = ($h * 31 + [int]$ch) % 99991 }
            $nm = if ($names.ContainsKey($r.Pid)) { $names[$r.Pid] } else { "pid$($r.Pid)" }
            $out.Add('d|' + (F ($h / 99991.0)) + '|' + $r.Port + '|' + $r.St + '|' + (Clean $nm))
        }
        $script:lastNet = $out
    }
    $lines.AddRange($script:lastNet)
    $lines
}

# ───────────────────────────── network, in detail (netx) ─────────────────────
# Fast facts every tick; the slow ones (addresses, neighbours, DNS) every eighth tick and cached.
$nxTick = 0
$nxSlow = $null
function Hash01($s) { $h = 0; foreach ($ch in ([string]$s).ToCharArray()) { $h = ($h * 31 + [int]$ch) % 99991 }; $h / 99991.0 }
function Ascii($s) { (([string]$s) -replace '[^\x20-\x7E]', '' -replace '[|#]', '_').Trim() }
function ScopeOf($ip, $ver) {
    if ($ver -eq 6) {
        if ($ip -match '^fe80') { return 'LINKLOCAL' }
        if ($ip -match '^f[cd]') { return 'ULA' }
        if ($ip -eq '::1') { return 'LOOPBACK' }
        if ($ip -match '^[23]') { return 'GLOBAL' }
        return 'OTHER'
    }
    if ($ip -match '^169\.254\.') { return 'LINKLOCAL' }
    if ($ip -match '^127\.') { return 'LOOPBACK' }
    if ($ip -match '^(10\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.)') { return 'PRIVATE' }
    if ($ip -match '^100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\.') { return 'CGNAT' }
    'PUBLIC'
}
# one DNS query straight to a server over UDP (so the local cache cannot answer): @(ms, answered)
function DnsProbe($server, $ver, $qtype) {
    $u = $null
    try {
        $fam = if ($ver -eq 6) { [Net.Sockets.AddressFamily]::InterNetworkV6 } else { [Net.Sockets.AddressFamily]::InterNetwork }
        $u = New-Object Net.Sockets.UdpClient($fam)
        $u.Client.ReceiveTimeout = 1000
        $id = Get-Random -Minimum 1 -Maximum 65535
        $q = New-Object System.Collections.Generic.List[byte]
        $q.AddRange([byte[]]@([byte]($id -shr 8), [byte]($id -band 255), 1, 0, 0, 1, 0, 0, 0, 0, 0, 0))
        foreach ($lab in 'dns.msftncsi.com'.Split('.')) { $q.Add([byte]$lab.Length); $q.AddRange([Text.Encoding]::ASCII.GetBytes($lab)) }
        $q.AddRange([byte[]]@(0, 0, [byte]$qtype, 0, 1))
        $sw = [Diagnostics.Stopwatch]::StartNew()
        $u.Connect([Net.IPAddress]::Parse($server), 53)
        [void]$u.Send($q.ToArray(), $q.Count)
        $ep = New-Object Net.IPEndPoint([Net.IPAddress]::Any, 0)
        $resp = $u.Receive([ref]$ep)
        $ms = [math]::Round($sw.Elapsed.TotalMilliseconds, 1)
        $ok = if ($resp.Length -ge 12 -and (($resp[3] -band 15) -eq 0) -and ($resp[0] -eq [byte]($id -shr 8))) { 1 } else { 0 }
        return @($ms, $ok)
    }
    catch { return @(-1, 0) }
    finally { if ($u) { $u.Close() } }
}
function Get-NetxSlow {
    $L = New-Object System.Collections.Generic.List[string]
    $ads = @(Get-NetAdapter | Where-Object { $_.Status -eq 'Up' -and $_.InterfaceDescription -notmatch 'Loopback' } | Sort-Object ifIndex)
    $idxs = @{}; $seenDesc = @{}
    foreach ($a in $ads) {
        $desc = Ascii $a.InterfaceDescription; if (-not $desc) { $desc = "ADAPTER $($a.ifIndex)" }
        $alias = Ascii $a.Name; if ($alias -notmatch '[A-Za-z0-9]{2}') { $alias = $desc }
        # the counter instance is the description with ( ) # / \ replaced; repeats get _2, _3, ...
        $mang = ([string]$a.InterfaceDescription) -replace '\(', '[' -replace '\)', ']' -replace '#', '_' -replace '/', '_' -replace '\\', '_'
        $n = [int]$seenDesc[$mang]; $seenDesc[$mang] = $n + 1
        $inst = if ($n -eq 0) { $mang } else { "${mang}_$($n + 1)" }
        $idxs[[int]$a.ifIndex] = @{ Desc = $desc; Alias = $alias; Inst = $inst; Speed = [double]$a.Speed }
    }
    $ipif = @(Get-NetIPInterface -ErrorAction SilentlyContinue)
    foreach ($k in $idxs.Keys) {
        foreach ($v in 4, 6) {
            $fam = if ($v -eq 4) { 'IPv4' } else { 'IPv6' }
            $f = $ipif | Where-Object { $_.InterfaceIndex -eq $k -and $_.AddressFamily -eq $fam } | Select-Object -First 1
            if ($f) { $L.Add("m|$k|$v|" + $(if ($f.Dhcp -eq 'Enabled') { 1 } else { 0 }) + '|' + $f.NlMtu + '|' + $f.InterfaceMetric) }
        }
    }
    $addrIdx = @{}
    foreach ($ip in (Get-NetIPAddress -ErrorAction SilentlyContinue | Where-Object { $_.AddressState -eq 'Preferred' -and $idxs.ContainsKey([int]$_.InterfaceIndex) })) {
        $v = if ($ip.AddressFamily -eq 'IPv6') { 6 } else { 4 }
        $addr = ([string]$ip.IPAddress) -replace '%.*$', ''
        $addrIdx[$addr] = [int]$ip.InterfaceIndex
        $L.Add("p|$($ip.InterfaceIndex)|$v|$addr|$($ip.PrefixLength)|$(ScopeOf $addr $v)|$($ip.PrefixOrigin)")
    }
    foreach ($r in (Get-NetRoute -DestinationPrefix '0.0.0.0/0', '::/0' -ErrorAction SilentlyContinue | Where-Object { $idxs.ContainsKey([int]$_.InterfaceIndex) })) {
        $v = if ($r.DestinationPrefix -eq '::/0') { 6 } else { 4 }
        $L.Add("g|$($r.InterfaceIndex)|$v|" + (([string]$r.NextHop) -replace '%.*$', ''))
    }
    foreach ($d in (Get-DnsClientServerAddress -ErrorAction SilentlyContinue | Where-Object { $idxs.ContainsKey([int]$_.InterfaceIndex) -and $_.ServerAddresses })) {
        $v = if ($d.AddressFamily -eq 23) { 6 } else { 4 }
        foreach ($sv in $d.ServerAddresses) { if ($sv -notmatch '^fec0:') { $L.Add("s|$($d.InterfaceIndex)|$v|$sv") } }
    }
    foreach ($nb in (Get-NetNeighbor -ErrorAction SilentlyContinue | Where-Object { $idxs.ContainsKey([int]$_.InterfaceIndex) })) {
        $st = [string]$nb.State
        if ($st -eq 'Permanent' -or $st -eq 'Unreachable') { continue }
        $ipn = ([string]$nb.IPAddress) -replace '%.*$', ''
        if ($ipn -match '^(22[4-9]|23\d|255\.|ff)') { continue }
        $v = if ($nb.AddressFamily -eq 'IPv6') { 6 } else { 4 }
        $code = if ($st -eq 'Reachable') { 0 } elseif ($st -eq 'Incomplete') { 2 } else { 1 }
        $L.Add("n|$($nb.InterfaceIndex)|$v|$(F (Hash01 $ipn))|$code")
    }
    $cnt = @{ A = 0; AAAA = 0; CNAME = 0; PTR = 0; SRV = 0; OTHER = 0; NEG = 0 }; $tot = 0
    foreach ($e in (Get-DnsClientCache -ErrorAction SilentlyContinue)) {
        $tot++
        if ($e.Status -ne 0) { $cnt.NEG++; continue }
        switch ([int]$e.Type) { 1 { $cnt.A++ } 28 { $cnt.AAAA++ } 5 { $cnt.CNAME++ } 12 { $cnt.PTR++ } 33 { $cnt.SRV++ } default { $cnt.OTHER++ } }
    }
    $L.Add("d|$($cnt.A)|$($cnt.AAAA)|$($cnt.CNAME)|$($cnt.PTR)|$($cnt.SRV)|$($cnt.OTHER)|$($cnt.NEG)|$tot")
    # DNS latency: ask each configured server (up to 3) for a Microsoft connectivity-check name, bypassing the cache
    $servers = @($L | Where-Object { $_ -like 's|*' } | ForEach-Object { $f = $_ -split '\|'; [pscustomobject]@{ V = [int]$f[2]; S = $f[3] } } | Select-Object -Unique -Property V, S | Select-Object -First 3)
    $first = $true
    foreach ($sv in $servers) {
        $r = DnsProbe $sv.S $sv.V 1
        $L.Add("q|$($sv.V)|$($sv.S)|$(F $r[0])|$($r[1])|1")
        if ($first -and $r[1] -eq 1) { $first = $false; $r6 = DnsProbe $sv.S $sv.V 28; $L.Add("q|$($sv.V)|$($sv.S)|$(F $r6[0])|$($r6[1])|28") }
    }
    [pscustomobject]@{ Lines = $L; Idxs = $idxs; AddrIdx = $addrIdx }
}
function Get-NetxLines {
    $script:nxTick++
    if ($script:nxTick % 8 -eq 1 -or -not $script:nxSlow) { $script:nxSlow = Get-NetxSlow }
    $idxs = $script:nxSlow.Idxs
    $lines = New-Object System.Collections.Generic.List[string]
    $lines.Add('t|' + (Unix))
    # per-adapter counters
    $perf = @{}
    foreach ($n in (Get-CimInstance Win32_PerfFormattedData_Tcpip_NetworkInterface)) { $perf[[string]$n.Name] = $n }
    foreach ($k in ($idxs.Keys | Sort-Object)) {
        $x = $idxs[$k]; $n = $perf[$x.Inst]
        if ($n) {
            $speed = if ([double]$n.CurrentBandwidth -gt 0) { [double]$n.CurrentBandwidth } else { $x.Speed }
            $err = [double]$n.PacketsReceivedErrors + [double]$n.PacketsOutboundErrors + [double]$n.PacketsReceivedDiscarded + [double]$n.PacketsOutboundDiscarded
            $lines.Add("a|$k|$($x.Desc)|$($x.Alias)|1|$(F $speed)|$(F $n.BytesReceivedPersec)|$(F $n.BytesSentPersec)|$(F $n.PacketsReceivedPersec)|$(F $n.PacketsSentPersec)|$(F $n.PacketsReceivedNonUnicastPersec)|$(F $n.PacketsSentNonUnicastPersec)|$(F $err)")
        }
        else { $lines.Add("a|$k|$($x.Desc)|$($x.Alias)|1|$(F $x.Speed)|0|0|0|0|0|0|0") }
    }
    $lines.AddRange($script:nxSlow.Lines)
    # protocol counters, v4 against v6
    $i4 = Get-CimInstance Win32_PerfFormattedData_Tcpip_IPv4 | Select-Object -First 1
    $i6 = Get-CimInstance Win32_PerfFormattedData_Tcpip_IPv6 | Select-Object -First 1
    $t4 = Get-CimInstance Win32_PerfFormattedData_Tcpip_TCPv4 | Select-Object -First 1
    $t6 = Get-CimInstance Win32_PerfFormattedData_Tcpip_TCPv6 | Select-Object -First 1
    $u4 = Get-CimInstance Win32_PerfFormattedData_Tcpip_UDPv4 | Select-Object -First 1
    $u6 = Get-CimInstance Win32_PerfFormattedData_Tcpip_UDPv6 | Select-Object -First 1
    $c4 = Get-CimInstance Win32_PerfFormattedData_Tcpip_ICMP | Select-Object -First 1
    $c6 = Get-CimInstance Win32_PerfFormattedData_Tcpip_ICMPv6 | Select-Object -First 1
    $lines.Add('r|' + ((@($i4.DatagramsPersec, $i6.DatagramsPersec, $t4.SegmentsPersec, $t6.SegmentsPersec, $u4.DatagramsPersec, $u6.DatagramsPersec,
                    $c4.MessagesPersec, $c6.MessagesPersec, $t4.ConnectionsEstablished, $t6.ConnectionsEstablished,
                    $t4.SegmentsRetransmittedPersec, $t6.SegmentsRetransmittedPersec) | ForEach-Object { F $_ }) -join '|'))
    # connections, split by IP version and by the adapter that owns the local address
    $addrIdx = $script:nxSlow.AddrIdx
    $cnt = @{ 4 = @(0, 0, 0, 0, 0, 0, 0); 6 = @(0, 0, 0, 0, 0, 0, 0) }
    $rows = New-Object System.Collections.Generic.List[object]
    $stIx = @{ ESTABLISHED = 0; LISTENING = 1; TIME_WAIT = 2; CLOSE_WAIT = 3; SYN_SENT = 4 }
    foreach ($v in 4, 6) {
        $proto = if ($v -eq 4) { 'tcp' } else { 'tcpv6' }
        $ns = @(netstat -ano -p $proto)
        foreach ($ln in $ns) {
            if ($ln -notmatch '^\s*TCP\s+(\S+)\s+(\S+)\s+(\S+)\s+(\d+)') { continue }
            $loc = $Matches[1]; $rem = $Matches[2]; $st = $Matches[3]; $pid_ = [int]$Matches[4]
            $li = $loc.LastIndexOf(':'); $lip = ($loc.Substring(0, $li)).Trim('[', ']') -replace '%.*$', ''
            $ri = $rem.LastIndexOf(':'); $rip = ($rem.Substring(0, $ri)).Trim('[', ']') -replace '%.*$', ''; $rport = [int]($rem.Substring($ri + 1))
            $slot = if ($stIx.ContainsKey($st)) { $stIx[$st] } else { 5 }
            if ($st -ne 'LISTENING' -and ($rip -match '^(127\.|::1$)' -or $rip -eq $lip)) { $cnt[$v][6]++; continue }
            $cnt[$v][$slot]++
            if ($st -eq 'ESTABLISHED' -or $st -eq 'SYN_SENT') {
                $st01 = if ($st -eq 'SYN_SENT') { 1 } else { 0 }
                $rows.Add([pscustomobject]@{ V = $v; Ix = [int]$addrIdx[$lip]; St = $st01; Ip = $rip; Port = $rport; Pid = $pid_ })
            }
        }
    }
    $pn = PidNames @($rows | ForEach-Object { $_.Pid } | Select-Object -Unique)
    foreach ($v in 4, 6) { $c = $cnt[$v]; $lines.Add("k|$v|$($c[0])|$($c[1])|$($c[2])|$($c[3])|$($c[4])|$($c[5])|$($c[6])") }
    foreach ($r in ($rows | Select-Object -First 120)) {
        $nm = if ($pn.ContainsKey($r.Pid)) { $pn[$r.Pid] } else { "pid$($r.Pid)" }
        $lines.Add("c|$($r.V)|$($r.Ix)|$($r.St)|$(F (Hash01 $r.Ip))|$($r.Port)|$(Clean $nm)")
    }
    $lines
}


do {
    if ($ParentPid -gt 0 -and -not (Get-Process -Id $ParentPid -ErrorAction SilentlyContinue)) { break }
    $t0 = Get-Date
    try {
        $lines = switch ($Kind) { 'gpu' { Get-GpuLines } 'board' { Get-BoardLines } 'net' { Get-NetLines } 'netx' { Get-NetxLines } }
        $tmp = "$Out.tmp"
        [IO.File]::WriteAllText($tmp, ($lines -join "`n") + "`n")
        Move-Item -Force $tmp $Out
    }
    catch { [Console]::Error.WriteLine("sampler_hw $Kind : $($_.Exception.Message)") }
    if ($Once) { break }
    $left = $Interval - ((Get-Date) - $t0).TotalSeconds
    if ($left -gt 0) { Start-Sleep -Milliseconds ([int]($left * 1000)) }
} while ($true)

← Back to index