diff --git a/composer b/composer index a420351..573217b 100755 Binary files a/composer and b/composer differ diff --git a/prettyping b/prettyping new file mode 100755 index 0000000..aa4e845 --- /dev/null +++ b/prettyping @@ -0,0 +1,655 @@ +#!/bin/bash +# +# Written by Denilson Figueiredo de Sá +# MIT license +# +# Requirements: +# * bash (tested on 4.20, should work on older versions too) +# * gawk (GNU awk, tested on 4.0.1, should work on older versions too) +# * ping (from iputils) + +# TODO: Detect the following kind of message and avoid printing it repeatedly. +# From 192.168.1.11: icmp_seq=4 Destination Host Unreachable +# +# TODO: print the destination (also) at the bottom bar. Useful after leaving +# the script running for quite some time. +# +# TODO: Implement audible ping. +# +# TODO: Autodetect the width of printf numbers, so they will always line up correctly. +# +# TODO: Test the behavior of this script upon receiving out-of-order packets, like these: +# http://www.blug.linux.no/rfc1149/pinglogg.txt +# +# TODO? How will prettyping behave if it receives a duplicate response? + +print_help() { + cat << EOF +Usage: $MYNAME [prettyping parameters] + +This script is a wrapper around the system's "ping" tool. It will substitute +each ping response line by a colored character, giving a very compact overview +of the ping responses. + +prettyping parameters: + --[no]color Enable/disable color output. (default: enabled) + --[no]multicolor Enable/disable multi-color unicode output. Has no effect if + either color or unicode is disabled. (default: enabled) + --[no]unicode Enable/disable unicode characters. (default: enabled) + --[no]terminal Force the output designed to a terminal. (default: auto) + --last Use the last "n" pings at the statistics line. (default: 60) + --columns Override auto-detection of terminal dimensions. + --lines Override auto-detection of terminal dimensions. + --rttmin Minimum RTT represented in the unicode graph. (default: auto) + --rttmax Maximum RTT represented in the unicode graph. (default: auto) + +ping parameters handled by prettyping: + -a Audible ping is not implemented yet. + -f Flood mode is not allowed in prettyping. + -q Quiet output is not allowed in prettyping. + -R Record route mode is not allowed in prettyping. + -v Verbose output seems to be the default mode in ping. + +Tested with Linux ping tool from "iputils" package: +http://www.linuxfoundation.org/collaborate/workgroups/networking/iputils +EOF +} + +# Thanks to people at #bash who pointed me at +# http://bash-hackers.org/wiki/doku.php/scripting/posparams +parse_arguments() { + USE_COLOR=1 + USE_MULTICOLOR=1 + USE_UNICODE=1 + + if [ -t 1 ]; then + IS_TERMINAL=1 + else + IS_TERMINAL=0 + fi + + LAST_N=60 + OVERRIDE_COLUMNS=0 + OVERRIDE_LINES=0 + RTT_MIN=auto + RTT_MAX=auto + + PING_PARAMS=( ) + + while [[ $# != 0 ]] ; do + case "$1" in + -h | -help | --help ) + print_help + exit + ;; + + # Forbidden ping parameters within prettyping: + -f ) + echo "${MYNAME}: You can't use the -f (flood) option." + exit 1 + ;; + -R ) + # -R prints extra information at each ping response. + echo "${MYNAME}: You can't use the -R (record route) option." + exit 1 + ;; + -q ) + echo "${MYNAME}: You can't use the -q (quiet) option." + exit 1 + ;; + -v ) + # -v enables verbose output. However, it seems the output with + # or without this option is the same. Anyway, prettyping will + # strip this parameter. + ;; + # Note: + # Small values for -s parameter prevents ping from being able to + # calculate RTT. + + # New parameters: + -a ) + # TODO: Implement audible ping for responses or for missing packets + ;; + + -color | --color ) USE_COLOR=1 ;; + -nocolor | --nocolor ) USE_COLOR=0 ;; + -multicolor | --multicolor ) USE_MULTICOLOR=1 ;; + -nomulticolor | --nomulticolor ) USE_MULTICOLOR=0 ;; + -unicode | --unicode ) USE_UNICODE=1 ;; + -nounicode | --nounicode ) USE_UNICODE=0 ;; + -terminal | --terminal ) IS_TERMINAL=1 ;; + -noterminal | --noterminal ) IS_TERMINAL=0 ;; + + #TODO: Check if these parameters are numbers. + -last | --last ) LAST_N="$2" ; shift ;; + -columns | --columns ) OVERRIDE_COLUMNS="$2" ; shift ;; + -lines | --lines ) OVERRIDE_LINES="$2" ; shift ;; + -rttmin | --rttmin ) RTT_MIN="$2" ; shift ;; + -rttmax | --rttmax ) RTT_MAX="$2" ; shift ;; + + * ) + PING_PARAMS+=("$1") + ;; + esac + shift + done + + if [[ "${RTT_MIN}" -gt 0 && "${RTT_MAX}" -gt 0 && "${RTT_MIN}" -ge "${RTT_MAX}" ]] ; then + echo "${MYNAME}: Invalid --rttmin and -rttmax values." + exit 1 + fi + + if [[ "${#PING_PARAMS[@]}" = 0 ]] ; then + echo "${MYNAME}: Missing parameters, use --help for instructions." + exit 1 + fi +} + +MYNAME=`basename "$0"` +parse_arguments "$@" + + +export LC_ALL=C + +# Warning! Ugly code ahead! +# The code is so ugly that the comments explaining it are +# bigger than the code itself! +# +# Suppose this: +# +# cmd_a | cmd_b & +# +# I need the PID of cmd_a. How can I get it? +# In bash, $! will give me the PID of cmd_b. +# +# So, I came up with this ugly solution: open a subshell, like this: +# +# ( +# cmd_a & +# echo "This is the PID I want $!" +# wait +# ) | cmd_b + + +# Ignore Ctrl+C here. +# If I don't do this, this shell script is killed before +# ping and gawk can finish their work. +trap '' 2 + +# Now the ugly code. +( + ping "${PING_PARAMS[@]}" & + # Commented out, because it looks like this line is not needed + #trap "kill -2 $! ; exit 1" 2 # Catch Ctrl+C here + wait +) 2>&1 | gawk ' +# Weird that awk does not come with abs(), so I need to implement it. +function abs(x) { + return ( (x < 0) ? -x : x ) +} + +# Ditto for ceiling function. +function ceil(x) { + return (x == int(x)) ? x : int(x) + 1 +} + +# Currently, this function is called once, at the beginning of this +# script, but it is also possible to call this more than once, to +# handle window size changes while this program is running. +# +# Local variables MUST be declared in argument list, else they are +# seen as global. Ugly, but that is how awk works. +function get_terminal_size(SIZE,SIZEA) { + if( HAS_STTY ) { + if( (STTY_CMD | getline SIZE) == 1 ) { + split(SIZE, SIZEA, " ") + LINES = SIZEA[1] + COLUMNS = SIZEA[2] + } else { + HAS_STTY = 0 + } + close(STTY_CMD) + } + if ( int('"${OVERRIDE_COLUMNS}"') ) { COLUMNS = int('"${OVERRIDE_COLUMNS}"') } + if ( int('"${OVERRIDE_LINES}"') ) { LINES = int('"${OVERRIDE_LINES}"') } +} + +############################################################ +# Functions related to cursor handling + +# Function called whenever a non-dotted line is printed. +# +# It will move the cursor to the line next to the statistics and +# restore the default color. +function other_line_is_printed() { + if( IS_PRINTING_DOTS ) { + if( '"${IS_TERMINAL}"' ) { + printf( ESC_DEFAULT ESC_NEXTLINE ESC_NEXTLINE "\n" ) + } else { + printf( ESC_DEFAULT "\n" ) + print_statistics_bar() + } + } + IS_PRINTING_DOTS = 0 + CURR_COL = 0 +} + +# Function called whenever a non-dotted line is repeated. +function other_line_is_repeated() { + if (other_line_times < 2) { + return + } + if( '"${IS_TERMINAL}"' ) { + printf( ESC_DEFAULT ESC_ERASELINE "\r" ) + } + printf( "Last message repeated %d times.", other_line_times ) + if( ! '"${IS_TERMINAL}"' ) { + printf( "\n" ) + } +} + +# Prints the newlines required for the live statistics. +# +# I need to print some newlines and then return the cursor back to its position +# to make sure the terminal will scroll. +# +# If the output is not a terminal, break lines on every LAST_N dots. +function print_newlines_if_needed() { + if( '"${IS_TERMINAL}"' ) { + # COLUMNS-1 because I want to avoid bugs with the cursor at the last column + if( CURR_COL >= COLUMNS-1 ) { + CURR_COL = 0 + } + if( CURR_COL == 0 ) { + if( IS_PRINTING_DOTS ) { + printf( "\n" ) + } + #printf( "\n" "\n" ESC_PREVLINE ESC_PREVLINE ESC_ERASELINE ) + printf( ESC_DEFAULT "\n" "\n" ESC_CURSORUP ESC_CURSORUP ESC_ERASELINE ) + } + } else { + if( CURR_COL >= LAST_N ) { + CURR_COL = 0 + printf( ESC_DEFAULT "\n" ) + print_statistics_bar() + } + } + CURR_COL++ + IS_PRINTING_DOTS = 1 +} + +############################################################ +# Functions related to the data structure of "Last N" statistics. + +# Clears the data structure. +function clear(d) { + d["index"] = 0 # The next position to store a value + d["size"] = 0 # The array size, goes up to LAST_N +} + +# This function stores the value to the passed data structure. +# The data structure holds at most LAST_N values. When it is full, +# a new value overwrite the oldest one. +function store(d, value) { + d[d["index"]] = value + d["index"]++ + if( d["index"] >= d["size"] ) { + if( d["size"] < LAST_N ) { + d["size"]++ + } else { + d["index"] = 0 + } + } +} + +############################################################ +# Functions related to processing the received response + +function process_rtt(rtt) { + # Overall statistics + last_rtt = rtt + total_rtt += rtt + if( last_seq == 0 ) { + min_rtt = max_rtt = rtt + } else { + if( rtt < min_rtt ) min_rtt = rtt + if( rtt > max_rtt ) max_rtt = rtt + } + + # "Last N" statistics + store(lastn_rtt,rtt) +} + +############################################################ +# Functions related to printing the fancy ping response + +# block_index is just a local variable. +function print_response_legend(i) { + if( '"${USE_UNICODE}"' ) { + printf( BLOCK[0] ESC_DEFAULT "%4d ", 0) + for( i=1 ; i= BLOCK_RTT_MAX ) { + block_index = BLOCK_LEN - 1 + } else { + block_index = 1 + int((rtt - BLOCK_RTT_MIN) * (BLOCK_LEN - 2) / BLOCK_RTT_RANGE) + } + printf( BLOCK[block_index] ) + } else { + printf( ESC_GREEN "." ) + } +} + +function print_missing_response(rtt) { + printf( ESC_RED "!" ) +} + +############################################################ +# Functions related to printing statistics + +function print_overall() { + if( '"${IS_TERMINAL}"' ) { + printf( "%2d/%3d (%2d%%) lost; %4.0f/" ESC_BOLD "%4.0f" ESC_DEFAULT "/%4.0fms; last: " ESC_BOLD "%4.0f" ESC_DEFAULT "ms", + lost, + lost+received, + (lost*100/(lost+received)), + min_rtt, + (total_rtt/received), + max_rtt, + last_rtt ) + } else { + printf( "%2d/%3d (%2d%%) lost; %4.0f/" ESC_BOLD "%4.0f" ESC_DEFAULT "/%4.0fms", + lost, + lost+received, + (lost*100/(lost+received)), + min_rtt, + (total_rtt/received), + max_rtt ) + } +} + +function print_last_n(i, sum, min, avg, max, diffs) { + # Calculate and print the lost packets statistics + sum = 0 + for( i=0 ; i max ) max = lastn_rtt[i] + } + avg = sum/lastn_rtt["size"] + + # Calculate mdev (mean absolute deviation) + for( i=0 ; i 0 && int('"${RTT_MAX}"') > 0 ) { + BLOCK_RTT_MIN = int('"${RTT_MIN}"') + BLOCK_RTT_MAX = int('"${RTT_MAX}"') + } else if( int('"${RTT_MIN}"') > 0 ) { + BLOCK_RTT_MIN = int('"${RTT_MIN}"') + BLOCK_RTT_MAX = BLOCK_RTT_MIN * (BLOCK_LEN - 1) + } else if( int('"${RTT_MAX}"') > 0 ) { + BLOCK_RTT_MAX = int('"${RTT_MAX}"') + BLOCK_RTT_MIN = int(BLOCK_RTT_MAX / (BLOCK_LEN - 1)) + } + + BLOCK_RTT_RANGE = BLOCK_RTT_MAX - BLOCK_RTT_MIN + print_response_legend() + } +} + +############################################################ +# Main loop +{ + # Sample line: + # 64 bytes from 8.8.8.8: icmp_seq=1 ttl=49 time=184 ms + if( $0 ~ /^[0-9]+ bytes from .*: icmp_[rs]eq=[0-9]+ ttl=[0-9]+ time=[0-9.]+ *ms/ ) { + if( other_line_times >= 2 ) { + if( '"${IS_TERMINAL}"' ) { + printf( "\n" ) + } else { + other_line_is_repeated() + } + } + other_line = "" + other_line_times = 0 + + # $1 = useless prefix string + # $2 = icmp_seq + # $3 = ttl + # $4 = time + + # This must be called before incrementing the last_seq variable! + rtt = int($4) + process_rtt(rtt) + + seq = int($2) + + while( last_seq < seq - 1 ) { + # Lost a packet + print_newlines_if_needed() + print_missing_response() + + last_seq++ + lost++ + store(lastn_lost, 1) + } + + # Received a packet + print_newlines_if_needed() + print_received_response(rtt) + + last_seq++ + received++ + store(lastn_lost, 0) + + if( '"${IS_TERMINAL}"' ) { + print_statistics_bar() + } + } else if ( $0 == "" ) { + # Do nothing on blank lines. + } else { + other_line_is_printed() + if ( $0 == other_line ) { + other_line_times++ + if( '"${IS_TERMINAL}"' ) { + other_line_is_repeated() + } + } else { + other_line = $0 + other_line_times = 1 + printf( "%s\n", $0 ) + } + } + + # Not needed when the output is a terminal, but does not hurt either. + fflush() +}'