Skip to content

Instantly share code, notes, and snippets.

@xrat
Last active March 16, 2023 21:44
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save xrat/a3df5ff7c63f5c648708825f45372ea9 to your computer and use it in GitHub Desktop.
Save xrat/a3df5ff7c63f5c648708825f45372ea9 to your computer and use it in GitHub Desktop.
Calculate time needed to send 1 byte back and forth within an SSH connection
#!/bin/bash
#
me=sshpingpong
#
# Measure close to minimal packet latency (RTT) of an SSH connection
#
prgversion="$me * 2022-07-27 (c) Andreas Schamanek"
#
# @author Andreas Schamanek <https://andreas.schamanek.net>
# @license GPL <https://www.gnu.org/licenses/gpl.html>
# @copyright (c) 2022 Andreas Schamanek
#
usage="$prgversion
$me sshpingpong [-L] [-t] [-c N] [-i S] {sshlogin@host}
-c N ... quit after N measurements
-i S ... wait S seconds between pings; can be fractional, default 5s
-t ... terse/compact mode using _, 1, 2, ... for hundreds of ms
where 1 means 100<=RTT<200, 2 means 200<=RTT<300, ...
-L ... do not write to logfile
Cf. https://serverfault.com/q/807910/23900
"
while [[ $1 == -* ]] ; do
case $1 in
-t) compactmode=yes; shift;;
-c) [[ $2 != *[!0-9]* ]] || { echo "invalid count" >&2; exit 1; }
count="$2"; shift 2;;
-i) [[ $2 != *[!0-9.-]* ]] || { echo "invalid interval" >&2; exit 1; }
sleep="$2"; shift 2;;
-L) logfile=/dev/null; shift;;
-\?|-h|--help) echo "$usage"; exit 0;;
esac
done
ssh="${1:?$me error: missing argument}"
if [[ -z $logfile ]] ; then
logfile="${ssh#*@}"; logfile="spp_${logfile//[: .]/_}".log
echo "Logfile is $logfile"
elif [[ $logfile == /dev/null ]] ; then
logfile=
fi
declare -i c=-1 now=0 sent=0 rcvd=0 rtt=0 count="${count:-0}"
declare rttms=0.0
: "${sleep:=5}"
: "${initialsleep:=2}"
# compact mode factor 1 or 10: cf=10 will make _, 1, 2, 3, ... indicate RTTs
# of <10, <20, <30, ...; w/ cf=1 it will be <100, <200, <300, ...
: "${cf:=1}"
: "${trapsigs:="INT TERM EXIT"}"
if [[ -d $XDG_RUNTIME_DIR ]] ; then
mkdir -p "$XDG_RUNTIME_DIR/$me"
fifo="$XDG_RUNTIME_DIR/$me/$$"
else
fifo="$HOME/fifo$$"
fi
set_now() { now=$(date +%s%N); now="${now%???}"; } # microseconds
compactmodeoutput() {
if ((cf*rtt<100000)) ; then echo -n _ >&2 ; return; fi
if ((cf*rtt>999999)) ; then echo -n "#" >&2 ; return; fi
echo -n "$((cf*rtt/100000))" >&2
}
# logline() also printing "seq=$c" at the time it was sent
#logline() { printf '%(%Y-%m-%d %T)T seq=%d %.3f\n' "${sent%??????}" "$c" "$rttms" ; }
# logline() just printing the RTT at the time the data was received
logline() { printf '%(%Y-%m-%d %T)T %.3f\n' "${rcvd%??????}" "$rttms" ; }
trap "trapexit" $trapsigs
trapexit() {
trap '' $trapsigs
[[ ! -e $fifo ]] || rm "$fifo"
if [[ -z "$logfile" ]] ; then
printf '\n' >&2
else
printf '\nLogfile is %s\n' "$logfile" >&2
fi
exit
}
[[ ! -e $fifo ]] || rm "$fifo"
mkfifo "$fifo"
# compact mode header and output redirection
if [[ -z $compactmode ]] ; then
[[ -z "$logfile" ]] || exec 1> >(tee -a "$logfile")
else
# in compact mode, if no logging is requested we need to redirect to null
# so that logline() is silenced
if [[ -z "$logfile" ]] ; then
exec 1>>/dev/null
else
exec 1>>"$logfile"
fi
if ((cf==10)) ; then
echo "0 _ 10 1 20 2 30 3 40 4 50 5 60 6 70 7 80 8 90 9 99 # ..." >&2
else
echo "0 _ 100 1 200 2 300 3 400 4 500 5 600 6 700 7 800 8 900 9 999 # ..." >&2
fi
fi
mepid="$$"
( sleep "$initialsleep"; echo "ignore initial ping" >"$fifo"; ) &
cat 0<> "$fifo" | ssh $sshargs $ssh cat \
| while read -r R ; do
set_now; rcvd="$now"
rtt="$((rcvd-sent))"; rttms="${rtt%???}.${rtt: -3}"
if [[ $R == o ]] ; then
logline
[[ -z $compactmode ]] || compactmodeoutput
sleep "$sleep"
fi
if ((count>0 && c>=count)) ; then
# found no nicer way to make sure $ssh ends
# and w/o kill the subshell would never exit
pkill -2 --parent "$mepid"
exit
fi
c=$((c+1))
echo 'o' >"$fifo"
set_now; sent="$now"
done
@xrat
Copy link
Author

xrat commented Jul 27, 2022

In Version 2022-07-27 I mostly changed option -c (compact mode) to -t (terse/compact mode) and introduced -c N to quit the script after N ping-pongs.

@gullevek
Copy link

On macOS date with %a%N will not work and %() printf either

date -> gdate replacement works
but the gprintf replacement sadly doesn't support the %() style print either

@xrat
Copy link
Author

xrat commented Mar 16, 2023

@gullevek I have no macOS available nor any experience with it. My script is written for Bash on Linux only, I am afraid. I know that %N is not available everywhere, though it's trending ;) However, the %()T format for printf is a Bashism. It was introduced with Bash 4.4 and should be available on platforms.

@gullevek
Copy link

@xrat The best thing is to use "#!/usr/bin/env bash" because I have a bash 5.5 installed from macports, and bash doesn't even exist in macOS anymore.

For the date, that is fine, for mac users with macPorts and the coreutils installed they just need to replace "date" with "gdate"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment