LingSys
↓ 0 downloadsYour 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 LingSysCode / 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)