File: //bin/uicron
#!/bin/sh
set -e
UICRON=${0##*/}
DEBUG=${UICRON_DEBUG:-0}
unset UICRON_DEBUG
usage() {
cat <<EOF
Usage: ${0##*/} -c COMMAND_STRING
COMMAND_STRING commands to be passed to the executing shell
This is a cronjob wrapper to provide a combination of useful features,
such as:
* locking (see runlock(1))
* timeouts (see timeout(1))
* start time randomization
* improved exit/output behaviour (see chronic(1))
The wrapper behaves like a shell and may be set in the crontab SHELL
variable for a cronjob.
Behaviour is controlled via environment variables:
* UICRON_LOCKING=on|off (default: on)
* UICRON_TIMEOUT=<integer> (default: 600)
* UICRON_DELAY=<integer 0..60>|auto (default: auto)
* UICRON_CHRONIC=on|off (default: off)
For start time randomization, i.e., UICRON_DELAY=auto, an integer hash
between 0 and 50 is computed from the environment variable UICRON_ID
if present, or from a hash over the command string. This means that
the same cronjob always starts with the same delay, but the delay is
different for each individual cronjob. Set UICRON_DELAY=0 to disable
this behaviour.
EOF
}
error() {
echo "$UICRON: $1" >&2
exit "${2:-42}"
}
debug() {
if [ $DEBUG -gt 0 ]; then echo [DEBUG] "$@"; fi
}
#
# Parse UICRON command-line parameters
#
while true; do
case "$1" in
-h|--help)
usage
exit 0
;;
-c) # shell behaviour
shift
break
;;
-*)
error "invalid option '$1'"
;;
*)
usage >&2
exit 42
;;
esac
done
if [ -n "${UICRON_ID}" ]; then
CRONJOB_ID="${UICRON_ID}"
else
CRONJOB_ID=$( echo "$*" | sha1sum | awk '{print $1}' )
fi
unset UICRON_ID
#
# Parse UICRON environment variables
#
CHRONIC="${UICRON_CHRONIC:-off}"
CHRONIC_CMD=""
case "$CHRONIC" in
yes|on|1)
CHRONIC=on
CHRONIC_CMD="/usr/bin/chronic"
;;
no|off|0)
CHRONIC=off
;;
*)
error "invalid chronic value '${UICRON_CHRONIC}' (expect bool)"
;;
esac
unset UICRON_CHRONIC
LOCKING="${UICRON_LOCKING:-on}"
LOCKING_CMD=""
case "$LOCKING" in
yes|on|1)
LOCKING=on
FILETAG="${LOGNAME}-${CRONJOB_ID}"
LOCKING_CMD="/usr/bin/runlock -f/tmp/runlock-$FILETAG"
;;
no|off|0)
LOCKING=off
;;
*)
error "invalid locking value '${UICRON_LOCKING}' (expect bool)"
;;
esac
unset UICRON_LOCKING
TIMEOUT="${UICRON_TIMEOUT:-600}"
TIMEOUT_CMD=""
case "$TIMEOUT" in
''|*[!0-9]*) # not a positive number
error "invalid timeout '$TIMEOUT' (expect integer)"
;;
*)
TIMEOUT_CMD="/usr/bin/timeout -k5 -sTERM $TIMEOUT"
;;
esac
unset UICRON_TIMEOUT
SHELL="${UICRON_SHELL:-/bin/sh}"
SHELL_CMD=""
case "$SHELL" in
/bin/sh|/bin/bash)
SHELL_CMD="$SHELL -c"
;;
*)
error "invalid SHELL '$SHELL' (expect /bin/sh or /bin/bash)"
;;
esac
unset UICRON_SHELL
DELAY="${UICRON_DELAY:-auto}"
case "$DELAY" in
auto)
#
# By default, vary start delay between 0 and 50 according to
# the modulus of a reduced md5 hash sum:
#
# This behaviour
# - distributes ccron loads within the first 50 seconds of
# each minute
# - gives 10 seconds runtime without overlap in worst case
# until the next run is scheduled
#
DELAY=$( perl -MDigest::MD5=md5 -wle 'print unpack("L", md5(shift)) % 50' "${CRONJOB_ID}" )
;;
''|*[!0-9]*) # not a positive number
error "invalid delay '$DELAY' (expect integer)"
;;
*)
if [ "$DELAY" -ge 60 ]; then
error "delay is greater than maximum ($DELAY >= 60)"
fi
;;
esac
unset UICRON_DELAY
debug "Cronjob Summary:"
debug " Context: $CHRONIC_CMD $LOCKING_CMD $TIMEOUT_CMD $SHELL_CMD '...'"
debug " Command: ""$*"
debug " Delay: $DELAY seconds"
debug ""
debug "--> Delay $DELAY seconds ..."
sleep $DELAY
debug "--> Execute command within the wrapper context ..."
exec $CHRONIC_CMD $LOCKING_CMD $TIMEOUT_CMD $SHELL_CMD "$*"