#!/bin/sh # Copyright (C) 2022 sean levy # # Permission to use, copy, modify, and distribute this software for any # purpose with or without fee is hereby granted, provided that the above # copyright notice and this permission notice appear in all copies. # # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. # indic - status indicator display # # display a tri-state color based the exit code of the command we're # given: # green: success (exit code 0) # yellow: failed when green or succeeded when red # red: failed more than once # meant to be run in a smallish tmux pane, so it looks like an # indicator light. ymmv. guaranteed to work under openbsd and void # linux (via loksh) # # examples: # $ indic -n DNS host google.com # $ indic -n WEB HEAD https://my.fav.url # $ indic -n TOR -- torsocks lwp-request -m HEAD my.fav.url # # BSD version: # $ indic -n LNK 'ping -qnc 1 $(netstat -nr -f inet | grep ^default | head -1 | awk "{print \$2}")' # # state machine: # from the start state -> green on win, -> red on fail # from the green state -> green on win, -> yellow on fail # from the yellow state -> stay in yellow until two consecutive # wins (-> green) or losses (-> red) # from the red state -> yellow on win, -> red on fail # ORIG_CMD="$0 $*" MONIKER=$(basename $0) # we have to assume *something*. i think in 2021 this is ok: export TERM=xterm die () { echo "[$MONIKER] FATAL: $*" 1>&2 exit 1 } usage () { if [ -n "$*" ]; then echo "error: $*" else echo "$MONIKER - green/yellow/red indicator light for cmd's exit code" fi echo "usage: $MONIKER [-hTv] [-d file] [-s secs] [-t secs] [-n name]" echo " [-S sig] [-l file] [--] cmd..." echo " -h show this help | -T force tmux mode (BSD SIGINFO)" echo " -v enable debug output | -d dump dbgout to file" echo " -s check sleep (10) | -n display name, def=command..." echo " -t cmd timeout (0 to disable, default=sleep)" echo " -S sig signal to use instead of SIGINFO (linux), def=none" echo " -l log state change data to file, def=none" echo " -- ... stop parsing options past that point (or just quote cmd)" echo ' e.g.: $ indic -n DNS host google.com' echo ' $ indic -n WEB -- HEAD -t 10 https://my.fav.url' echo ' $ indic -t 90 -s 60 -n TOR torsocks HEAD http://my.fav.url' exit 1 } state=start sleep=10 interval=0.1 err_count=0 win_count=0 FG=cyan tmpfile=${TMPDIR-/tmp}/indic.winlose.$$ last_check= last_dt= last_exit_code= entered_state= CUR_MSG="" CUR_MSG_Y=0 DEBUG=0 dbgout=${TMPDIR-/tmp}/indic.debug.$$ log="" DO_BLANK=0 display_name="" force_tmux=0 UP="↑" DN="↓" timeout="" siginfo="" debug () { if [ $DEBUG -ne 0 ]; then echo "[$(date +%Y-%m-%dT%H:%M:%S)] |$MONIKER $display_name| $$: $*" >> $dbgout fi return 0 } log () { if [ -n "$log" ]; then echo "$(date +%s) $display_name $*" >> $log fi } ## parse args args=$(getopt hTvd:l:n:s:S:t: $*) [ $? -ne 0 ] && usage set -- $args while [ $# -ne 0 ]; do case $1 in -v) DEBUG=1; shift ;; -h) usage ;; -d) dbgout=$2; shift; shift ;; -s) sleep=$2; shift; shift ;; -S) siginfo=$2; shift; shift ;; -n) display_name=$2; shift; shift ;; -t) timeout=$2; shift; shift ;; -T) force_tmux=1; shift ;; -l) log=$2; shift; shift ;; --) shift; break ;; esac done [ $# -eq 0 ] && usage cmd="$*" [ -z "$display_name" ] && display_name=$cmd [ -z "$timeout" ] && timeout=$sleep if [ $DEBUG -eq 0 ]; then exec 2>/dev/null fi debug "COMMAND: $cmd" debug "SLEEP: $sleep" # bsd polyfill case $(uname) in *BSD) netstat="netstat -na -f inet" [ -z "$siginfo" ] && siginfo="INFO" stty_size_field () { stty -a | perl -lne '/(\d+)\s+'$1'/ && print $1' } colorer=color_a setaf=setaf setab=setab ;; *) netstat="netstat -na4" stty_size_field () { stty -a | perl -lne '/'$1'\s+(\d+);/ && print $1' } colorer=color_ setaf=setf setab=setb ;; esac cursor_invis=true cursor_vis=true if tput civis >/dev/null; then cursor_invis="tput civis" cursor_vis="tput cnorm" fi tty_rows () { echo $(stty_size_field rows) } tty_cols () { echo $(stty_size_field columns) } center_msg () { local str="$*" w=$(tty_cols) h=$(tty_rows) x y mid_len if [ -n "$CUR_MSG" ]; then # erase previous message local bg=$($colorer $state) fg=$($colorer $FG) tput cup $CUR_MSG_Y 0 tput $setaf $fg tput $setab $bg echo -n "$(blanks $w)" fi mid_len=${#str} mid_len=$(expr $mid_len / 2) y=$(expr $h / 2) if [ $h -gt 3 ]; then y=$(expr $y - 1) fi x=$(expr $w / 2) x=$(expr $x - $mid_len) tput cup $y $x tput bold echo -n "$str" tput sgr0 CUR_MSG="$str" CUR_MSG_Y=$y } color_a () { # N.B. this is for seta[fb] c.f. terminfo(5) local c n=$(echo $1 | tr A-Z a-z) case $n in black) c=0 ;; red) c=1 ;; green) c=2 ;; yellow) c=3 ;; blue) c=4 ;; magenta)c=5 ;; cyan) c=6 ;; white) c=7 ;; *) die "wtf color is $1 ?" ;; esac echo $c } color_ () { # N.B. this is for set[fb] c.f. terminfo(5) local c n=$(echo $1 | tr A-Z a-z) case $n in black) c=0 ;; blue) c=1 ;; green) c=2 ;; cyan) c=3 ;; red) c=4 ;; magenta)c=5 ;; yellow) c=6 ;; white) c=7 ;; *) die "wtf color is $1 ?" ;; esac echo $c } MINS=60 HOURS=3600 DAYS=86400 WEEKS=604800 elapsed () { local dt=$1 n x elapsed [ -z "$dt" ] && dt=0 if [ $dt -ge $WEEKS ]; then n=$(expr $dt / $WEEKS) x=$(expr $n \* $WEEKS) dt=$(expr $dt - $x) elapsed="${elapsed}$(printf %02dw $n)" fi if [ $dt -ge $DAYS ]; then n=$(expr $dt / $DAYS) x=$(expr $n \* $DAYS) dt=$(expr $dt - $x) elapsed="${elapsed}$(printf %02dd $n)" fi if [ $dt -ge $HOURS ]; then n=$(expr $dt / $HOURS) x=$(expr $n \* $HOURS) dt=$(expr $dt - $x) elapsed="${elapsed}$(printf %02dh $n)" fi if [ $dt -ge $MINS ]; then n=$(expr $dt / $MINS) x=$(expr $n \* $MINS) dt=$(expr $dt - $x) elapsed="${elapsed}$(printf %02dm $n)" fi if [ $dt -gt 0 ]; then elapsed="${elapsed}$(printf %02ds $dt)" fi [ -z "${elapsed}" ] && elapsed="00s" echo ${elapsed} } flappy () { local flap=$1 dt=$2 msg if [ $state = yellow ]; then msg="${display_name} $flap $dt" else msg="${display_name} $dt" fi echo $msg } status_msg () { local msg now=$(date +%s) local dt="" if [ -n "$entered_state" ]; then dt=$(expr $now - $entered_state) dt=" $(elapsed $dt)" fi if [ $err_count -ne 0 ]; then msg=$(flappy $DN $dt) else msg=$(flappy $UP $dt) fi echo $msg } killer () { local secs=$1 other=$2 sleep $secs (kill ${other} && kill -9 ${other}) >/dev/null 2>&1 } check_command () { display $state "~$(status_msg)~" local ck=$(date +%s) last_check=$ck local cmd_pid tout_pid x if [ $timeout -eq 0 ]; then eval "$cmd" >$tmpfile 2>&1 $tmpfile 2>&1 /dev/null 2>&1 & tout_pid=$! wait $cmd_pid >/dev/null 2>&1 x=$? (kill $tout_pid && kill -9 $tout_pid && wait $tout_pid) \ >/dev/null 2>&1 fi local now=$(date +%s) last_dt=$(expr $now - $ck) last_exit_code=$x return $x } blanks () { perl -e "print ' ' x $1" } blank_screen () { local rows=$(tty_rows) cols=$(tty_cols) local bg=$($colorer $1) fg=$($colorer $FG) local one_row=$(blanks $cols) row=0 debug "blank_screen colorer $colorer $1: $setaf $fg $setab $bg" tput clear tput $setaf $fg tput $setab $bg while [ $row -lt $rows ]; do echo -n "$one_row" row=$(expr 1 + $row) done } display () { local color=$1 shift local msg="$*" debug "display color=$color state=$state" if [ "$state" != "$color" ]; then entered_state=$(date +%s) debug "-> ENTERED $color $entered_state" log $color blank_screen $color elif [ $DO_BLANK -ne 0 ]; then blank_screen $color DO_BLANK=0 fi state=$color if [ -z "$msg" ]; then msg=$(status_msg) fi center_msg "$msg" } green () { debug "-> GREEN $*" if [ $1 = win ]; then err_count=0 win_count=$(expr 1 + $win_count) display green else err_count=1 win_count=0 display yellow fi } yellow () { debug "-> YELLOW $*" if [ $1 = win ]; then err_count=0 if [ $win_count -gt 0 ]; then green win else win_count=$(expr $win_count + 1) display yellow fi else win_count=0 if [ $err_count -gt 0 ]; then red lose else err_count=$(expr 1 + $err_count) display yellow fi fi } red () { debug "-> RED $*" if [ $1 = win ]; then err_count=0 win_count=1 display yellow else err_count=$(expr $err_count + 1) win_count=0 display red fi } win () { debug "$cmd: WIN" if [ $state = start ]; then green win else $state win fi } lose () { debug "$cmd: LOSE" if [ $state = start ]; then red lose else $state lose fi } status () { local msg="${display_name}: ${state}, win: ${win_count}, lose: ${err_count}" echo ${msg} 1>&2 if [ -f $tmpfile ]; then if [ -n "$TMUX" -o $force_tmux -ne 0 ]; then tmux new-window -n "[$MONIKER $display_name]" \ "echo $msg; echo command: $cmd; echo last exit code: $last_exit_code; echo last dt: $(elapsed $last_dt); [ -f $dbgout ] && (tail $dbgout /dev/null | head -11); echo ''; more $tmpfile" else echo "latest output: $cmd" cat $tmpfile sleep 0.7 fi fi DO_BLANK=1 } redisplay () { case $state in start) ;; green|yellow|red) blank_screen $state display $state ;; *) die "redisplay: wtf state is $state ?" ;; esac } quit () { rm -f $tmpfile tput reset $cursor_vis exit 0 } restart () { tput reset $cursor_vis exec $ORIG_CMD } trap 'quit' INT trap 'restart' QUIT trap 'redisplay' WINCH [ -n "$siginfo" ] && trap 'status' $siginfo debug "STARTING UP: $cmd" log start clear $cursor_invis while true; do if check_command; then win else lose fi sleep $sleep >/dev/null 2>&1 & sleeper_pid=$! wait $sleeper_pid 2>/dev/null # SIGINFO done