#!/bin/bash # multiping.sh - V 0.5.02: Simple bash based inter process communication demo # (C) 2020-2024 - F-Hauri.ch # Licensed by GNU GENERAL PUBLIC LICENSE Version 3 # Syntax: $0 [host or ip] [host or ip...] # Require: ping # Recommand: tput gnuplot xterm declare DOPLOT=false DOCOLORS=false prog=${0##*/} INTERVAL=1.0 declare MPDEBUG=${MPDEBUG:-false} declare -iA pids fds seqs lastread declare -A stats target tims close declare -i tglen=0 lastPlotTime GPFD GPPid GPWidth GPHeigth=400 LINES usage() { cat <<-EOUsage Usage: $prog [-[cdhp]] [-i N] [host or ip] [host or ip]... Options: -c colors (red for no answer, green for higher "seq" number) -d Debug this (by openning 2 XTerm windows... look at script) -h help (print this) -iN Interval for ping command (min 0.2 in user mode) -p Plot dynamically (require gnuplot) -w Force gnuplot window width All following argument will be "target" for "ping" command. When run, this will show all target's answers in a line, sorted by IP address (command line order is used, until all IP are knowns) A newline is put each time any target upstate change. User interactions: h Print Headline p Start plotting dynamically (using gnuplot) q Quit. r Clear terminal s Stop plotting and storring points. EOUsage } debug() { local args=() opt OPTARG OPTIND while getopts "dcpi:" opt; do case $opt in i ) args+=("-$opt" "$OPTARG") ;; c|p ) args+=("-$opt") ;; d ) ;; * ) ;; esac done exec {log}<> >(:) xterm -geom 80x24+840+20 -T MPLog -S00/$log & XTPid=$! xterm -geom 80x24+0+20 -e "watch -n.3 ps --tty $(tty) fw" & WPid=$! echo >&$log $'\e[20h' echo >&$log "$0 ${args[*]} ${*:OPTIND}" MPDEBUG=true $0 2>&$log "${args[@]}" "${@:OPTIND}" read -rsn 1 -t 30 -p Hit\ a\ key... _ exec {log}>&- kill $XTPid $WPid echo -en '\r\e[K\r' exit 0 } doping() { # run ping once, storing target, pid and file descriptor local fd LANG=C exec {fd}< <(exec stdbuf -o0 ping -nOi "$INTERVAL" "$1" 2>&1) pids["$1"]=$! fds["$1"]="$fd" target["$1"]="$1" seqs["$1"]=0 lastread["$1"]=$(( ${EPOCHREALTIME/.} + tzoff )) tims["$1"]='---' close["$1"]=false } pingInput() { # Read one line from ping output local line seq tim fd int=0 tgt arr read -u "${fds[$1]}" -r line lastread["$1"]=$(( ${EPOCHREALTIME/.} + tzoff )) case $line in '' ) if ${close[$1]}; then fd=${fds[$1]} (( fd )) && exec {fd}<&- unset "fds[$1]" fi ;; PING* ) # First line from ping output IFS=$' \t()\r\n' read -r _ host ip _ <<<"$line" printf -v stats["$1"] '%-*s%17s ' $((1+tglen)) \ "${target[$1]}:" "$ip" printf '%sStarted: %s\n' "$ret" "$line" (( LINECNT++ )) ip2int "$ip" int if [[ ${byIp[int]} ]]; then $MPDEBUG && >&2 printf '\nTarget %s: %s == %s is used twice.\n'\ "$ip" "${byIp[int]}" "$1" fd=${fds[$1]} (( fd )) && exec {fd}<&- for arr in pids fds target seqs lastread tims close; do local -n crtarr=$arr unset "crtarr[$1]" done else byIp[int]="$1" ret=$'\r' $MPDEBUG && >&2 declare -p fds target byIp fi ;; 64* ) # regular ping output seq=${line#*[rs]eq=} tim=${line#*time=} seqs[$1]=${seq%% *} tims[$1]=${tim%% *} ;; ---\ * ) # first of statistic lines close[$1]=true ;; *packets\ transmitted* ) # 2nd stat line packets=${line%% *} recv=${line#*transmitted, } err=${line#*eived, } stats[$1]+=" ${recv%% *} / $packets -> ${err%\% *}% err." ;; rtt\ min*|pipe\ *) # last output line: close FD stats[$1]+=" ${line##*= }" fd=${fds[$1]} (( fd )) && exec {fd}<&- unset "fds[$1]" ;; ping:\ *not\ known ) tgt=${line#ping: } tgt=${tgt%: *} fd=${fds[$tgt]} (( fd )) && exec {fd}<&- for arr in pids fds target seqs lastread tims close; do local -n crtarr=$arr unset "crtarr[$tgt]" $MPDEBUG && >&2 declare -p crtarr done ;; * ) # regular unanswered ping output seq=${line#*[rs]eq=} seq=${seq%% *} ((seq)) && seqs[$1]=$seq tims[$1]=--- esac } breakLoop() { kill -INT "${pids[@]}" if ((PNLT==3)) ;then echo ;else sleep .5 ;fi printf ' -- %s --\e[K\n' "$1" ((PNLT--))||fds=() } ip2int() { local _a _b _c _d; IFS=. read -r _a _b _c _d <<< "$1" printf -v "$2" %u $(( _a << 24 | _b << 16 | _c << 8 | _d )) ;} int2ip() { printf -v "$2" "%d.%d.%d.%d" \ $(($1>>24)) $(($1>>16&255)) $(($1>>8&255)) $(($1&255)) ;} initGnuPlot() { local -a plotlines=() local -i fld=2 local TMPDIR=/tmp host ip [[ -d /dev/shm ]] && [[ -w /dev/shm ]] && TMPDIR=/dev/shm ! { [[ $TMPDATA ]] && [[ -f $TMPDATA ]] ;} && read -r TMPDATA < <(mktemp -p $TMPDIR --suffix .dat plot-XXXXXXXXXX) [[ -L /dev/fd/$GPFD ]] && ((GPFD)) && exec {GPFD}>&- ((GPPid > 1 )) && [[ -d /proc/$GPPid ]] && kill $GPPid exec {GPFD}> >(exec gnuplot &>/dev/null) GPPid=$! cat <<-EOGPInit >&$GPFD set term wxt size $GPWidth,$GPHeigth noraise persist title 'Ping $*'; set xdata time; set timefmt '%s.%N'; EOGPInit if $sortedByIp; then $MPDEBUG && >&2 declare -p byIp $MPDEBUG && >&2 echo -- "${*@Q}" for i in "${!byIp[@]}"; do int2ip "$i" ip printf -v i '"%s" using 1:%d with step title "%s [%s]"' \ "$TMPDATA" $((fld++)) "${byIp[i]}" "$ip" plotlines+=("$i") done else for host; do plotlines+=( "'$TMPDATA' using 1:$((fld++)) with step title '$host'" ) done fi printf -v plotcmd "%s, " "${plotlines[@]}" plotcmd="plot ${plotcmd%, };" doplot=false } doPlot() { ! $doplot && doplot=true && return (( nowms - lastPlotTime < 500000 )) && return [[ $GPFD ]] || return [[ -e /dev/fd/$GPFD ]] || return # Check if gnuplot's cpu usage under 90% _gplut=$(( ${_gput/.} )) _gplgu=$(( _gpstat[13] + _gpstat[14] )) read -ra _gpstat < /proc/$GPPid/stat read -r _gput _ 900 )) && return echo >&$GPFD "$plotcmd" && lastPlotTime=nowms } GPWidth=$(xrandr | sed -e '/[*]/{s/^ *\([0-9]\+\)x.*/\1/;q;};d') ((GPWidth)) || GPWidth=1600 while getopts "cdhpw:i:" opt;do case $opt in h ) usage; exit 0 ;; c ) DOCOLORS=true ;; d ) debug "$@" ;; w ) GPWidth=$OPTARG ;; p ) DOPLOT=true ;; i ) INTERVAL=$OPTARG ;; ? ) b=$((OPTIND-1)); echo >&2 "$prog ERROR: '${!b}' not valid option."; exit 1 ;; * ) echo >&2 "$prog ERROR: opt: '$opt' ??"; usage; exit 2 ;; esac done shift $((OPTIND-1)) # Init some values printf -v fmtline '%*s' ${#} '' printf -v headline '\r x/%d '"${fmtline// / %16s}"'\e[K\n' $# "$@" fmtline=${fmtline// / %8s %-7s} [[ $LINES ]] || LINES=$(tput lines 2>/dev/null) [[ $LINES ]] || LINES=24 printf '\e[8;%d;%dt' $LINES $((120>${#headline}?120:${#headline})) # resize term LINECNT=0 ret=$'\r' sortedByIp=false $DOPLOT && initGnuPlot "$@" for ip ;do tglen=" ${#ip} > tglen ? ${#ip} : tglen " doping "$ip" done PNLT=3 trap '$MPDEBUG && >&2 echo CHLD $$' CHLD trap '$MPDEBUG && >&2 echo PIPE $$;$doplot&&DOPLOT=false' PIPE trap 'breakLoop stop' INT printf -v tzoff '%(%z)T' -1 tzoff=$(( ${tzoff::1}1 * ( 3600 * 10#${tzoff:1:2} + 60 * 10#${tzoff:3:2} ) )) tzoff+=000000 while ((${#fds[@]})); do if read -rsn 1 -t .2 key; then case $key in q ) breakLoop quit ;; r ) clear;LINECNT=0 ;; p ) DOPLOT=true; initGnuPlot "$@" ;; s ) DOPLOT=false;[[ -L /dev/fd/$GPFD ]]&&((GPFD))&&exec {GPFD}>&- ;; h ) printf "%s" "$headline";LINECNT=0 ;; esac fi out="" cnt=0 strline='' # strline store 1 flag for each ping printf -v mseqa [%d]+=1\ "${seqs[@]}" declare -ia "mseq=($mseqa)" maxseq=("${!mseq[@]}") maxseq=("${maxseq[-1]}") nowms=$(( ${EPOCHREALTIME/.} + tzoff )) now=${nowms::-6}.${nowms: -6} pltline='' if $sortedByIp || ((${#byIp[@]}==${#fds[@]})); then if ! $sortedByIp; then headline=$'\r x/'"${#byIp[@]} " printf -v i '%*s' ${#byIp[@]} '' printf -v headline "\\r%10sx/%d ${i// /%18s}\\n%15s" \ '' ${#byIp[@]} "${byIp[@]}" '' for i in "${!byIp[@]}"; do int2ip "$i" ip printf -v i " %16s" "$ip" headline+="$i" done if $DOPLOT; then [[ $GPFD ]] && [[ -e /dev/fd/$GPFD ]] && exec {GPFD}>&- [[ $TMPDATA ]] && [[ -f $TMPDATA ]] && : >"$TMPDATA" initGnuPlot "$@" fi headline+=$'\e[K\n' printf "%s" "$headline" sortedByIp=true fi ar=("${byIp[@]}") else ar=("${!fds[@]}") fi for i in "${ar[@]}"; do # for each opened FD [[ ${fds[$i]} ]] && read -rt 0 -u "${fds[$i]}" && pingInput "$i" #read input if data available if ((nowms-${lastread[$i]:-0}>2000000)) || # if last read older than 2s [[ -z ${tims[$i]#---} ]]; then # or time string == '---' if $DOCOLORS; then # show datas with red foreground printf -v strng ' \e[31;1m%8s %-7s\e[0m' \ "${seqs[$i]}" "${tims[$i]}" else printf -v strng ' %8s %-7s' "${seqs[$i]}" "${tims[$i]}" fi [[ -z ${tims[$i]#---} ]] && strline+="-1 " || strline+="0 " $DOPLOT && pltline+=" -3" else if $DOCOLORS && (( seqs[$i] == maxseq )); then printf -v strng ' \e[32;1m%8s %-7s\e[0m' \ "${seqs[$i]}" "${tims[$i]}" else printf -v strng ' %8s %-7s' "${seqs[$i]}" "${tims[$i]}" fi ((cnt++)) strline+="1 " $DOPLOT && pltline+=" "${tims[$i]} fi out+="$strng" done if $DOPLOT && [[ $pltline != "$lastpltline" ]]; then [[ $TMPDATA ]] && echo >>"$TMPDATA" "$now $pltline" doPlot lastpltline="$pltline" fi (( LINECNT % LINES )) || { printf "%s" "$headline" ;LINECNT=0 ;} [[ $strline != "$lastline" ]] && echo && (( LINECNT++ )) (( LINECNT % LINES )) || { printf "%s" "$headline" ; LINECNT=0 ;} (( ( cnt==0 ) & ( LINECNT-1 <= $# ) )) || { printf '\r%(%T)T %2d/%d %s\e[K' -1 "$cnt" "${#pids[@]}" "$out" ret=$'\n' if [[ $strline != "$lastline" ]]; then #next when strline differs echo (( LINECNT++ )) fi lastline="$strline" } done $DOPLOT && ((GPFD)) && exec {GPFD}>&- [[ -f $TMPDATA ]] && rm "$TMPDATA" for i in "${ar[@]}"; do printf '\r%s\e[K\n' "${stats[$i]}" done