Files
quickshell_bar/services/topnet.sh
Asmir A dfb16cce4e 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>
2026-06-02 18:40:26 +02:00

98 lines
4.0 KiB
Bash
Executable File

#!/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