Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Cron helper
#!/bin/bash
usage() {
cat << EOF
Usage: $0 [OPTION]... COMMAND
Execute the given command in a way that works safely with cron. This should
typically be used inside of a cron job definition like so:
* * * * * $(which "$0") [OPTION]... COMMAND
Arguments:
-c Ensure that the job never exits non zero.
-e EXITFILE The exit file that will have a time stamp written to it if the
job succeeds.
-h This help message.
-i Nice the job so that it doesn't over consume resources.
-k LOCKFILE The lock file to hold while the job is running.
-l LOGFILE The log file to write stdout and stderr too.
-n NAME The name of this cron job. Giving the cron job a name causes
log lines to include it in the output, and automatically sets
EXITFILE, LOCKFILE, and LOGFILE to some simple default values.
EXITFILE will be set to /var/run/\$USER/NAME.exit if
/var/run/\$USER exists, otherwise it will write to
/var/tmp/\$USER/NAME.exit
LOCKFILE will be set to /var/run/\$USER/NAME.lock if
/var/run\$USER exists, otherwise it will write to
/var/tmp/\$USER/NAME.lock
LOGFILE will be set to /var/log/\$USER/NAME.log if
/var/log/\$USER exists, otherwise it will write to
/var/tmp/$USER/NAME.log
-s If set the script will sleep a random time between 0 and 60
seconds before starting the command.
-t If set then the output from the script will be automatically
timestamped when being written to the log file.
COMMAND is the command that should be executed.
EOF
}
SAFE_EXIT=0
EXIT_FILE=""
NICE=0
LOCK_FILE=""
LOG_FILE=""
NAME=""
SLEEP=0
TIMESTAMP=0
# This is a cach of all the groups the user is a member of. We use it with
# canwrite() later.
GROUPS=""
# Checks to see if the shell array ($2) contains $1.
contains() {
local i
for i in "${@:2}"; do
[[ "$i" == "$1" ]] && return 0
done
return 1
}
# Checks to see if the current user can write to the given file. This will check the
# file permissions first, and if the file does not exist then it will check the
# directory permissions.
canwrite() {
local perm
local owner
local group
if [ -f "$1" ] ; then
read perm owner group < <(stat -Lc "%a %G %U" "$1" 2> /dev/null)
else
read perm owner group < <(stat -Lc "%a %G %U" "$(dirname $1)" 2> /dev/null)
fi
if [ $? -ne 0 ] ; then
return 1
fi
if [ "$owner" == "$USER" ] ; then
if [ $((perm&0200)) -ne 0 ] ; then
return 1
fi
return 0
elif contains "$group" "${GROUPS[@]}" ; then
if [ $((perm&0020)) -ne 0 ] ; then
return 1
fi
return 0
else
if [ $((perm&0002)) -ne 0 ] ; then
return 1
fi
return 0
fi
}
name() {
NAME="$1"
# Exit file
if [ -z "$EXIT_FILE" ] ; then
if canwrite "/var/run/${USER}/${NAME}.exit" ; then
EXIT_FILE="/var/run/${USER}/${NAME}.exit"
else
mkdir -p "/var/tmp/${USER}"
EXIT_FILE="/var/tmp/${USER}/${NAME}.exit"
fi
fi
# Lock File
if [ -z "$LOCK_FILE" ] ; then
if canwrite "/var/run/${USER}/${NAME}.lock" ; then
LOCK_FILE="/var/run/${USER}/${NAME}.lock"
else
mkdir -p "/var/tmp/${USER}"
LOCK_FILE="/var/tmp/${USER}/${NAME}.lock"
fi
fi
# Log File
if [ -z "$LOG_FILE" ] ; then
if canwrite "/var/run/${USER}/${NAME}.lock" ; then
LOG_FILE="/var/log/${USER}/${NAME}.log"
else
mkdir -p "/var/tmp/${USER}"
LOG_FILE="/var/tmp/${USER}/${NAME}.log"
fi
fi
}
while getopts "ce:hik:l:n:st" arg; do
case $arg in
c) SAFE_EXIT=1 ;;
e) EXIT_FILE="$OPTARG" ;;
h) usage ; exit ;;
i) NICE=1 ;;
k) LOCK_FILE="$OPTARG" ;;
l) LOG_FILE="$OPTARG" ;;
n) name "$OPTARG" ;;
s) SLEEP=1 ;;
t) TIMESTAMP=1 ;;
esac
done
shift $((OPTIND-1))
# This function will write a log line to the output. This can either be called by the
# timestamper, or internally.
log() {
if [ $TIMESTAMP -eq 1 ] ; then
echo "$(date) $*"
else
echo "$*"
fi
}
# Setup logging first so we can report to the user what is happening.
if [ -n "$LOG_FILE" ] ; then
exec > "$LOG_FILE" 2>&1
fi
# Attempt to lock the lock file if it is set.
if [ -n "$LOCK_FILE" ] ; then
exec 200>> "$LOCK_FILE"
flock -w 0 -x 200
if [ $? -ne 0 ] ; then
log "Unable to obtain a lock, is a job already running?"
if [ $SAFE_EXIT -eq 1 ] ; then
exit 0
else
exit 1
fi
fi
fi
# Sleep a random amount between 0 and 60 seconds.
if [ $SLEEP -eq 1 ] ; then
sleep $((RANDOM%60))
fi
# Get the pre-command in case we need to nice the job.
PRECOMMAND=""
if [ $NICE -eq 1 ] ; then
PRECOMMAND="nice"
fi
# Run the command.
if [ $TIMESTAMP -eq 1 ] ; then
$PRECOMMAND "$@" 2>&1 | while read line ; do log "$line" ; done
else
$PRECOMMAND "$@" 2>&1
fi
EXIT_STATUS=$?
# Process the exit file.
if [ -n "$EXIT_FILE" -a $EXIT_STATUS -eq 0 ] ; then
date > "$EXIT_FILE"
fi
# Exit status
if [ $SAFE_EXIT -eq 1 ] ; then
exit 0
else
exit $EXIT_STATUS
fi
@joelparkerhenderson

This comment has been minimized.

Copy link

@joelparkerhenderson joelparkerhenderson commented Oct 20, 2015

Beautiful code - very well thought through. Great blog post too. Thanks for sharing this!

@doublesharp

This comment has been minimized.

Copy link

@doublesharp doublesharp commented Mar 26, 2016

Note that flock -w 0 -x 200 is no longer valid, so it will fail if you specify a name or lockfile. To correct it replace line 167 with flock -n -x 200

@sfrique

This comment has been minimized.

Copy link

@sfrique sfrique commented Apr 16, 2016

You could also implement a timeout for the cron script!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.