#!/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 |
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
@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.
@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"
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.