Skip to content

Instantly share code, notes, and snippets.

@liquidgecka
Last active September 28, 2023 15:35
  • Star 87 You must be signed in to star a gist
  • Fork 21 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save liquidgecka/9788122 to your computer and use it in GitHub Desktop.
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
Copy link

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

@doublesharp
Copy link

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
Copy link

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