1#!/usr/bin/env bash
2
3GETOPT_BIN=$IN_GETOPT_BIN
4GETOPT_BIN=${GETOPT_BIN:-getopt}
5
6__sleep_amount() {
7  if [ -n "$constant_sleep" ]; then
8    sleep_time=$constant_sleep
9  else
10    #TODO: check for awk
11    #TODO: check if user would rather use one of the other possible dependencies: python, ruby, bc, dc
12    sleep_time=`awk "BEGIN {t = $min_sleep * $(( (1<<($attempts -1)) )); print (t > $max_sleep ? $max_sleep : t)}"`
13  fi
14}
15
16__log_out() {
17  echo "$1" 1>&2
18}
19
20# Parameters: max_tries min_sleep max_sleep constant_sleep fail_script EXECUTION_COMMAND
21retry()
22{
23  local max_tries="$1"; shift
24  local min_sleep="$1"; shift
25  local max_sleep="$1"; shift
26  local constant_sleep="$1"; shift
27  local fail_script="$1"; shift
28  if [ -n "$VERBOSE" ]; then
29    __log_out "Retry Parameters: max_tries=$max_tries min_sleep=$min_sleep max_sleep=$max_sleep constant_sleep=$constant_sleep"
30    if [ -n "$fail_script" ]; then __log_out "Fail script: $fail_script"; fi
31    __log_out ""
32    __log_out "Execution Command: $*"
33    __log_out ""
34  fi
35
36  local attempts=0
37  local return_code=1
38
39
40  while [[ $return_code -ne 0 && $attempts -le $max_tries ]]; do
41    if [ $attempts -gt 0 ]; then
42      __sleep_amount
43      __log_out "Before retry #$attempts: sleeping $sleep_time seconds"
44      sleep $sleep_time
45    fi
46
47    P="$1"
48    for param in "${@:2}"; do P="$P '$param'"; done
49    #TODO: replace single quotes in each arg with '"'"' ?
50    export RETRY_ATTEMPT=$attempts
51    bash -c "$P"
52    return_code=$?
53    #__log_out "Process returned $return_code on attempt $attempts"
54    if [ $return_code -eq 127 ]; then
55      # command not found
56      exit $return_code
57    elif [ $return_code -ne 0 ]; then
58      attempts=$[$attempts +1]
59    fi
60  done
61
62  if [ $attempts -gt $max_tries ]; then
63    if [ -n "$fail_script" ]; then
64      __log_out "Retries exhausted, running fail script"
65      eval $fail_script
66    else
67      __log_out "Retries exhausted"
68    fi
69  fi
70
71  exit $return_code
72}
73
74# If we're being sourced, don't worry about such things
75if [ "$BASH_SOURCE" == "$0" ]; then
76  # Prints the help text
77  help()
78  {
79    local retry=$(basename $0)
80    cat <<EOF
81Usage: $retry [options] -- execute command
82    -h, -?, --help
83    -v, --verbose                    Verbose output
84    -t, --tries=#                    Set max retries: Default 10
85    -s, --sleep=secs                 Constant sleep amount (seconds)
86    -m, --min=secs                   Exponential Backoff: minimum sleep amount (seconds): Default 0.3
87    -x, --max=secs                   Exponential Backoff: maximum sleep amount (seconds): Default 60
88    -f, --fail="script +cmds"        Fail Script: run in case of final failure
89EOF
90  }
91
92  # show help for no arguments if stdin is a terminal
93  if { [ -z "$1" ] && [ -t 0 ] ; } || [ "$1" == '-h' ] || [ "$1" == '-?' ] || [ "$1" == '--help' ]
94  then
95    help
96    exit 0
97  fi
98
99  $GETOPT_BIN --test > /dev/null
100  if [[ $? -ne 4 ]]; then
101      echo "I’m sorry, 'getopt --test' failed in this environment. Please load GNU getopt."
102      exit 1
103  fi
104
105  OPTIONS=vt:s:m:x:f:
106  LONGOPTIONS=verbose,tries:,sleep:,min:,max:,fail:
107
108  PARSED=$($GETOPT_BIN --options="$OPTIONS" --longoptions="$LONGOPTIONS" --name "$0" -- "$@")
109  if [[ $? -ne 0 ]]; then
110    # e.g. $? == 1
111    #  then getopt has complained about wrong arguments to stdout
112    exit 2
113  fi
114  # read getopt’s output this way to handle the quoting right:
115  eval set -- "$PARSED"
116
117  max_tries=10
118  min_sleep=0.3
119  max_sleep=60.0
120  constant_sleep=
121  fail_script=
122
123  # now enjoy the options in order and nicely split until we see --
124  while true; do
125      case "$1" in
126          -v|--verbose)
127              VERBOSE=true
128              shift
129              ;;
130          -t|--tries)
131              max_tries="$2"
132              shift 2
133              ;;
134          -s|--sleep)
135              constant_sleep="$2"
136              shift 2
137              ;;
138          -m|--min)
139              min_sleep="$2"
140              shift 2
141              ;;
142          -x|--max)
143              max_sleep="$2"
144              shift 2
145              ;;
146          -f|--fail)
147              fail_script="$2"
148              shift 2
149              ;;
150          --)
151              shift
152              break
153              ;;
154          *)
155              echo "Programming error"
156              exit 3
157              ;;
158      esac
159  done
160
161  retry "$max_tries" "$min_sleep" "$max_sleep" "$constant_sleep" "$fail_script" "$@"
162
163fi
164