Add per-process network drawer to the Network widget
Clicking the Network pill now toggles a drawer listing processes ranked by network throughput, refreshed live only while open — mirroring the CPU widget's top-processes drawer. services/topnet.sh (single-shot, like topproc.sh) takes two `ss` snapshots procInterval apart, diffs each TCP socket's cumulative rx/tx bytes by inode, sums positive deltas per owning PID, and prints ranked "N <rx/s> <tx/s> <name>" frames. SysStats re-runs it while netProcPollEnabled (drawer open) using the same SplitParser-accumulate / parse-on-exit / re-arm plumbing as procScan. Network.qml wraps its Pill in an Item and hangs a fixed-size PopupWindow drawer off it. ss (iproute2) is added to runtimeInputs since per-process byte accounting has no virtual-file equivalent. Caveat: ss only exposes byte counters for TCP sockets, so UDP/QUIC traffic is not attributed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
97
services/topnet.sh
Executable file
97
services/topnet.sh
Executable file
@@ -0,0 +1,97 @@
|
||||
#!/usr/bin/env bash
|
||||
# Print one frame of the top network-consuming processes, then exit.
|
||||
#
|
||||
# Single-shot by design, exactly like topproc.sh: SysStats re-runs it every
|
||||
# netProcInterval while the Network drawer is open, and exiting per frame
|
||||
# guarantees stdout is flushed (a long-running loop would block-buffer its
|
||||
# frame and the QML side would never see it). topproc.sh is the reference for
|
||||
# the bash-script + single-shot + SplitParser-parse pattern.
|
||||
#
|
||||
# How per-process bandwidth is derived: `ss` reports, per TCP socket, the
|
||||
# cumulative bytes_sent / bytes_received plus the owning pid (users:) and a
|
||||
# stable socket inode (ino:). We take two snapshots netProcInterval apart, diff
|
||||
# each socket's byte counters by inode, sum the positive deltas per process,
|
||||
# and divide by the interval to get bytes/sec. Only TCP carries these counters,
|
||||
# so UDP/QUIC traffic is not attributed — that is a kernel/ss limitation, not a
|
||||
# bug here.
|
||||
#
|
||||
# IMPORTANT: needs `ss` (iproute2) on PATH — added to runtimeInputs in flake.nix
|
||||
# alongside bash + coreutils. No awk/getconf (absent at runtime); bash builtins
|
||||
# and coreutils (sort, head, sleep) only.
|
||||
#
|
||||
# Output: one "N <rx_bytes_per_sec> <tx_bytes_per_sec> <name>" line per process,
|
||||
# highest combined throughput first.
|
||||
|
||||
intv="${1:-2}"
|
||||
n="${2:-6}"
|
||||
|
||||
# Snapshot every TCP socket's cumulative rx/tx bytes into the rx/tx assoc
|
||||
# arrays keyed by socket inode, and remember each inode's owning process in
|
||||
# pidOf/nameOf. `ss -O` prints one socket per line so a single read loop works.
|
||||
# -t TCP -i info(byte counters) -n numeric -H no header
|
||||
# -O oneline -p process -e extended(ino:)
|
||||
snapshot() {
|
||||
local -n _rx="$1" _tx="$2" _pid="$3" _name="$4"
|
||||
local line w ino bs br pid name tmp
|
||||
set -f # no globbing when word-splitting $line below
|
||||
while IFS= read -r line; do
|
||||
ino=""; bs=0; br=0
|
||||
# ino: / bytes_*: tokens never contain spaces, so word-splitting is safe
|
||||
# even when a process name does (e.g. "Web Content").
|
||||
for w in $line; do
|
||||
case "$w" in
|
||||
ino:*) ino="${w#ino:}" ;;
|
||||
bytes_sent:*) bs="${w#bytes_sent:}" ;;
|
||||
bytes_received:*) br="${w#bytes_received:}" ;;
|
||||
esac
|
||||
done
|
||||
[ -n "$ino" ] || continue
|
||||
_rx["$ino"]="$br"
|
||||
_tx["$ino"]="$bs"
|
||||
# name + pid from the users:(("name",pid=N,fd=M)) token; parse off the
|
||||
# whole line so a space inside the quoted name can't break it.
|
||||
tmp="${line#*users:((\"}"; name="${tmp%%\"*}"
|
||||
tmp="${line#*pid=}"; pid="${tmp%%,*}"
|
||||
_pid["$ino"]="$pid"
|
||||
_name["$ino"]="$name"
|
||||
done < <(ss -tinHOpe 2>/dev/null)
|
||||
set +f
|
||||
}
|
||||
|
||||
declare -A rx1 tx1 rx2 tx2 pidOf nameOf
|
||||
snapshot rx1 tx1 pidOf nameOf
|
||||
sleep "$intv"
|
||||
snapshot rx2 tx2 pidOf nameOf
|
||||
|
||||
# Per-process byte deltas over the interval. A socket present in both snapshots
|
||||
# contributes its counter delta; a socket new in the second snapshot contributes
|
||||
# its full byte count (all of it landed within the window). Aggregate by pid.
|
||||
declare -A prx ptx pname
|
||||
for ino in "${!rx2[@]}"; do
|
||||
pid="${pidOf[$ino]}"
|
||||
[ -n "$pid" ] || continue
|
||||
if [ -n "${rx1[$ino]:-}" ]; then
|
||||
drx=$(( ${rx2[$ino]} - ${rx1[$ino]} ))
|
||||
dtx=$(( ${tx2[$ino]} - ${tx1[$ino]} ))
|
||||
else
|
||||
drx="${rx2[$ino]}"
|
||||
dtx="${tx2[$ino]}"
|
||||
fi
|
||||
[ "$drx" -gt 0 ] || drx=0
|
||||
[ "$dtx" -gt 0 ] || dtx=0
|
||||
prx["$pid"]=$(( ${prx[$pid]:-0} + drx ))
|
||||
ptx["$pid"]=$(( ${ptx[$pid]:-0} + dtx ))
|
||||
pname["$pid"]="${nameOf[$ino]}"
|
||||
done
|
||||
|
||||
# Emit "<total/s> <rx/s> <tx/s> <name>" per process, rank by total throughput,
|
||||
# take the top n, then reformat to the N-line protocol the QML side parses.
|
||||
for pid in "${!prx[@]}"; do
|
||||
r=$(( ${prx[$pid]} / intv ))
|
||||
t=$(( ${ptx[$pid]} / intv ))
|
||||
tot=$(( r + t ))
|
||||
[ "$tot" -gt 0 ] || continue
|
||||
printf '%d %d %d %s\n' "$tot" "$r" "$t" "${pname[$pid]}"
|
||||
done | sort -rn | head -n "$n" | while read -r tot r t name; do
|
||||
printf 'N %s %s %s\n' "$r" "$t" "$name"
|
||||
done
|
||||
Reference in New Issue
Block a user