123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350 |
- #!/bin/bash
- # No way I try to deal with a crippled sh just for POSIX foo.
- # Copyright (C) 2009-2016, 2018 Joerg Jaspert <joerg@debian.org>
- #
- # This program is free software; you can redistribute it and/or
- # modify it under the terms of the GNU General Public License as
- # published by the Free Software Foundation; version 2.
- #
- # This program is distributed in the hope that it will be useful, but
- # WITHOUT ANY WARRANTY; without even the implied warranty of
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- # General Public License for more details.
- #
- # You should have received a copy of the GNU General Public License
- # along with this program; if not, write to the Free Software
- # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
- # Homer: Are you saying you're never going to eat any animal again? What
- # about bacon?
- # Lisa: No.
- # Homer: Ham?
- # Lisa: No.
- # Homer: Pork chops?
- # Lisa: Dad, those all come from the same animal.
- # Homer: Heh heh heh. Ooh, yeah, right, Lisa. A wonderful, magical animal.
- # exit on errors
- set -e
- # A pipeline's return status is the value of the last (rightmost)
- # command to exit with a non-zero status, or zero if all commands exit
- # successfully.
- set -o pipefail
- # make sure to only use defined variables
- set -u
- # ERR traps should be inherited from functions too. (And command
- # substitutions and subshells and whatnot, but for us the functions is
- # the important part here)
- set -E
- # If the extglob shell option is enabled using the shopt builtin,
- # several extended pattern matching operators are recognized. We use
- # it for the POSSIBLEARGS and the first case ${ARGS} matching.
- shopt -s extglob
- # And use one locale, no matter what the caller has set
- export LANG=C.UTF-8
- export LC_ALL=C.UTF-8
- # One arg please
- declare -lr ARG=${1:-"meh"}
- # program name is the (lower cased) first argument.
- PROGRAM="${ARG}"
- # import the general variable set. (This will overwrite configdir, but
- # it is expected to have the same value)
- export SCRIPTVARS=${configdir:?Please define configdir to run this script}/vars
- . ${SCRIPTVARS}
- # set DEBUG if you want to see a little more logs (needs to be used more)
- DEBUG=${DEBUG:-0}
- # Check if the argument is a known one. If so, lock us so that only
- # one copy of the type of cronscript runs. The $type.tasks file is
- # mandantory, so use that for locking.
- case ${ARG} in
- ${POSSIBLEARGS})
- # Only one of me should ever run.
- FLOCKER=${FLOCKER:-""}
- [[ ${FLOCKER} != ${configdir}/${PROGRAM}.tasks ]] && exec env FLOCKER="${configdir}/${PROGRAM}.tasks" flock -E 0 -en "${configdir}/${PROGRAM}.tasks" "$0" "$@" || :
- ;;
- *)
- cat - <<EOF
- This is the cronscript. It needs an argument or it won't do anything
- for you.
- Currently accepted Arguments: ${POSSIBLEARGS}
- To see what they do, you want to look at the files
- \$ARGUMENT.{tasks,functions,variables} in ${configdir}.
- EOF
- exit 0
- ;;
- esac
- function includetasks() {
- local NAME=${1:?}
- _preparetasks ${NAME}
- _runtasks ${NAME}
- }
- function _preparetasks() {
- local NAME=${1:?}
- # Each "cronscript" may have a variables and a functions file
- # that we source
- for what in variables functions; do
- if [[ -f ${configdir}/${NAME}.${what} ]]; then
- . ${configdir}/${NAME}.${what}
- fi
- done
- }
- function _runtasks() {
- local NAME=${1:?}
- # Which list of tasks should we run?
- local TASKLIST="${configdir}/${NAME}.tasks"
- # This loop simply wants to be fed by a list of values (see below)
- # made out of 5 columns.
- # The first four are the array values for the stage function, the
- # fifth tells us if we should background the stage call.
- #
- # - FUNC - the function name to call
- # - ARGS - Possible arguments to hand to the function. Can be the empty string
- # - TIME - The timestamp name. Can be the empty string
- # - ERR - if this is the string false, then the call will be surrounded by
- # set +e ... set -e calls, so errors in the function do not exit
- # the script. Can be the empty string, meaning true.
- # - BG - Background the function stage?
- #
- # ATTENTION: Spaces in arguments or timestamp names need to be escaped by \
- #
- # NOTE 1: There are special values for the first column (FUNC).
- # NOSTAGE - do not call stage function, call the command directly.
- # RMSTAGE - clean out the stages directory, and as such
- # the recording what already ran in an earlier cronscript.
- # Note: Only really makes sense at beginning of a tasks file,
- # the stages directory gets cleared at successful exit anyways.
- # RMSTAGE simply ensures that ALL of the crons tasks ALWAYS run.
- # INCLUDE - Runs another task list after including corresponding functions
- # Note 2: If you want to hand an empty value to the stage function,
- # use the word "none" in the list below.
- while read FUNC ARGS TIME ERR BACKGROUND; do
- debug "FUNC: $FUNC ARGS: $ARGS TIME: $TIME ERR: $ERR BG: $BACKGROUND"
- # Empty values in the value list are the string "none" (or the
- # while read loop won't work). Here we ensure that variables that
- # can be empty, are empty if the string none is set for them.
- for var in ARGS TIME; do
- if [[ ${!var} == none ]]; then
- typeset ${var}=''
- fi
- done
- # ERR/BACKGROUND are boolean for all but LOCK/UNLOCK, check that they are.
- for var in ERR BACKGROUND; do
- if [[ ${!var} != false ]] && [[ ${!var} != true ]]; then
- if [[ ${FUNC} != LOCK ]] && [[ ${FUNC} != UNLOCK ]]; then
- error "Illegal value ${!var} for ${var} (should be true or false), line for function ${FUNC}"
- fi
- fi
- done
- case ${FUNC} in
- NOSTAGE)
- ${ARGS}
- ;;
- RMSTAGE)
- # Make sure we remove our stage files, so all the
- # actions will be done again.
- rm -f ${stagedir}/*
- ;;
- LOCK)
- # We are asked to set a lock, so try to get it.
- # For this we redefine what the columns mean.
- # ARGS: Name of the lockfile
- # TIME: How long to wait for getting the (exclusive) lock
- # ERR: shared == shared lock, may be hold more than once, exclusive == exclusive, only one.
- lock ${ARGS} ${TIME} ${ERR}
- ;;
- UNLOCK)
- unlock ${ARGS}
- ;;
- INCLUDE)
- includetasks ${ARGS}
- ;;
- *)
- GO=(
- FUNC="${FUNC}"
- TIME="${TIME}"
- ARGS="${ARGS}"
- ERR="${ERR}"
- )
- if [[ ${BACKGROUND} == true ]]; then
- stage $GO &
- else
- stage $GO
- fi
- ;;
- esac < /dev/null
- done < <(grep -v '^#' ${TASKLIST} )
- }
- function lock() {
- local LOCK=${1:-}
- local TIME=${2:-600}
- local TYPE=${3:-exclusive}
- if [[ -z ${LOCK} ]]; then
- log_error "No lockfile name given"
- exit 21
- fi
- local LOCKFILE=
- if [[ $LOCK == /* ]]; then
- LOCKFILE=${LOCK}
- else
- # Prepend LOCK_ to lock name to get to variable name,
- # kind of namespace
- local lvar="LOCK_${LOCK}"
- LOCKFILE=${!lvar}
- fi
- # bash can't open a file read-only, while creating it,
- # so we need to create it ourselves.
- if ! [[ -e $LOCKFILE ]]; then
- install -m 444 /dev/null $LOCKFILE || {
- log_error "Could not create lock ${LOCKFILE}"
- laststeps 2
- }
- fi
- # Get filehandle
- local randomstring
- exec {randomstring}<${LOCKFILE}
- # Store filehandle for later
- LOCKFD[${LOCK}]=${randomstring}
- # "Abusing" the err column, expecting the shared/exclusive value there.
- # Any wrong value means exclusive.
- case ${ERR} in
- shared|exclusive)
- flockparm="--${ERR}"
- ;;
- *)
- flockparm="--exclusive"
- ;;
- esac
- # Deal with time being special, usually it means false or true,
- # but for locks we want a timeout. So if its set to one of the usuals,
- # assume 300
- if [[ ${TIME} == none ]]; then
- TIME=300
- fi
- # Now try to get the lock
- set +e
- flock ${flockparm} --timeout ${TIME} --conflict-exit-code 3 ${LOCKFD[${LOCK}]}
- ret=$?
- set -e
- case ${ret} in
- 0)
- return
- ;;
- 3)
- log_error "Could not get lock ${LOCKFILE}, timeout"
- laststeps 2
- ;;
- *)
- log_error "Could not get lock ${LOCKFILE}"
- laststeps 2
- esac
- }
- function unlock() {
- local LOCK=${1:-}
- if [[ -z ${LOCK} ]]; then
- # Warn, but continue, unlock will happen at script end time
- log "No lockfile name given"
- fi
- local randomstring=${LOCKFD[${LOCK}]}
- exec {randomstring}>&-
- }
- function laststeps() {
- local successval=${1:-0}
- # Redirect output to another file, as we want to compress our logfile
- # and ensure its no longer used
- exec > "$logdir/after${PROGRAM}.log" 2>&1
- # Rotate out old logfiles
- find ${logdir} -name "${PROGRAM}_*.log.bz2" -mtime +${logkeep} -delete
- # Now, at the very (successful) end of this run, make sure we remove
- # our stage files, so the next script run will do it all again.
- if [[ ${successval} -eq 0 ]]; then
- rm -f ${stagedir}/*
- fi
- bzip2 -9 ${LOGFILE}
- # Logfile should be gone, remove the symlink
- [[ -L ${logdir}/${PROGRAM} ]] && [[ ! -f ${logdir}/${PROGRAM} ]] && rm -f ${logdir}/${PROGRAM} || log "Logfile still exists or symlink gone already? Something fishy going on"
- # FIXME: Mail the log when its non-empty
- [[ -s "${logdir}/after${PROGRAM}.log" ]] || rm "${logdir}/after${PROGRAM}.log"
- }
- (
- # Where we store lockfile filehandles
- declare -A LOCKFD
- # common functions are "outsourced"
- . "${configdir}/common"
- # Timestamp when we started
- NOW=$(date "+%Y.%m.%d-%H:%M:%S")
- # A logfile for every cron script
- LOGFILE="${logdir}/${PROGRAM}_${NOW}.log"
- # Each "cronscript" may have a variables and a functions file
- # that we source
- _preparetasks ${PROGRAM}
- # Get rid of tempfiles at the end
- trap cleanup EXIT TERM HUP INT QUIT
- # An easy access by name for the current log
- ln -sf ${LOGFILE} ${logdir}/${PROGRAM}
- # And from here, all output to the log please
- exec >> "$LOGFILE" 2>&1
- # The stage function uses this directory
- # This amends the stagedir variable from "vars"
- stagedir="${stagedir}/${PROGRAM}"
- # Ensure the dir exists
- mkdir -p ${stagedir}
- # Run all tasks
- _runtasks ${PROGRAM}
- # we need to wait for the background processes before the end of the cron script
- wait
- # Common to all cron scripts
- log "Cron script successful, all done"
- laststeps 0
- )
|