Checking for duplicate cron jobs - bash

We are deploying code to our application server environment, and part of that process is creating a number of cron jobs on the server. When code gets pushed, our deployment script creates the required cron jobs without a problem using the following:
CRON_FILE=$SCRIPT_DIR/cron.txt
if [[ ! -f "$CRON_FILE" ]]; then
printf "Cron template file missing!\n\n"
exit 1
fi
while read LINE || [[ -n "$LINE" ]]; do
printf "\n> Adding cron job \"$LINE\"\n"
crontab -l | { cat; echo "$LINE"; } | crontab -
done < $CRON_FILE
The issue is that after the initial deployment, additional deployments are creating duplicate cron jobs.
Any pointers on how to detect if a cron job already exists?

When you add your cron job, include a comment with a unique label. Later you can use that unique label to determine if the cron job exists or not, and also to "uninstall" the cron job.
I do this all the time. I have a reusable script for this:
#!/bin/sh
#
# Usage:
# 1. Put this script somewhere in your project
# 2. Edit "$0".crontab file, it should look like this,
# but without the # in front of the lines
#0 * * * * stuff_you_want_to_do
#15 */5 * * * stuff_you_want_to_do
#* * 1,2 * * and_so_on
# 3. To install the crontab, simply run the script
# 4. To remove the crontab, run ./crontab.sh --remove
#
cd $(dirname "$0")
test "$1" = --remove && mode=remove || mode=add
cron_unique_label="# $PWD"
crontab="$0".crontab
crontab_bak=$crontab.bak
test -f $crontab || cp $crontab.sample $crontab
crontab_exists() {
crontab -l 2>/dev/null | grep -x "$cron_unique_label" >/dev/null 2>/dev/null
}
# if crontab is executable
if type crontab >/dev/null 2>/dev/null; then
if test $mode = add; then
if ! crontab_exists; then
crontab -l > $crontab_bak
echo 'Appending to crontab:'
cat $crontab
crontab -l 2>/dev/null | { cat; echo; echo $cron_unique_label; cat $crontab; echo; } | crontab -
else
echo 'Crontab entry already exists, skipping ...'
echo
fi
echo "To remove previously added crontab entry, run: $0 --remove"
echo
elif test $mode = remove; then
if crontab_exists; then
echo Removing crontab entry ...
crontab -l 2>/dev/null | sed -e "\?^$cron_unique_label\$?,/^\$/ d" | crontab -
else
echo Crontab entry does not exist, nothing to do.
fi
fi
fi
Save the script as crontab.sh in your project directory, and create a crontab.sh.crontab with your cron job definitions, for example:
0 0 * * * echo hello world
0 0 * * * date
To install your cron jobs, simply run ./crontab.sh
The script is safe to run multiple times: it will detect if the unique label already exists and skip adding your cron jobs again
To uninstall the cron jobs, run ./crontab.sh --remove
I put this on GitHub too: https://github.com/janosgyerik/crontab-script
Explanation of sed -e "\?^$cron_unique_label\$?,/^\$/ d":
In its simplest form the expression is basically: sed -e '/start/,/end/ d'
It means: delete the content between the lines matching the start pattern and the end pattern, including the lines containing the patterns
The script quotes the sed command with double-quotes instead of single quotes, because it needs to expand the value of the $cron_unique_label shell variable
The start pattern \?^$cron_unique_label\$? uses a pair of ? instead of / to enclose the pattern, because $cron_unique_label contains /, which would cause problems
The starting ? must be escaped with a backslash, but to be honest I don't know why.
The ^ matches start of the line and $ end of the line, and the $ must be escaped, otherwise the shell would expand the value of the $? shell variable
The end pattern /^\$/ is relatively simple, it matches a start of line followed by end of line, in other words an empty line, and again the $ must be escaped
The d at the end is the sed command, to delete the matched lines, effectively removing it from the content of crontab -l, which we can pipe to crontab -

Weird, but very thorough answers. IMO overly complex.
Here's a decent one liner for anyone coming to this for a 2017 answer:
crontab -l | grep 'match-your-cronjob-search' || (crontab -l 2>/dev/null; echo "* * * * * /bin/cronjobCommandYouWant >> /dev/null 2>&1") | crontab -
Works great for us to not have dupe crons.
And here's the edited script from the original poster:
CRON_FILE=$SCRIPT_DIR/cron.txt
if [[ ! -f "$CRON_FILE" ]]; then
printf "Cron template file missing!\n\n"
exit 1
fi
while read LINE || [[ -n "$LINE" ]]; do
printf "\n> Adding cron job \"$LINE\"\n"
crontab -l | grep "$LINE" || (crontab -l 2>/dev/null; echo "$LINE") | crontab -
done < $CRON_FILE

Your script janos is awesome, works perfectly and was exactly what i was looking for with one little glitch.
I couldnt manage multiple xxx.crontab templates.
Your script worked fine and i added it to my bootstrapping routines with a little modification so i can pass a first parameter $1 of the xx.crontab filename and second parameter $2 can be the removal.
I have parent shell scripts which then conditional decide, which, all or combination of crontab files i want to add/remove.
Here the script with my modifications included:
#!/bin/sh
#
# Usage:
# 1. Put this script somewhere in your project
# 2. Edit "$1".crontab file, it should look like this,
# but without the # in front of the lines
#0 * * * * stuff_you_want_to_do
#15 */5 * * * stuff_you_want_to_do
#* * 1,2 * * and_so_on
# 3. To install the crontab, run ./crontab.sh <nameOfCronTabfile>
# 4. To remove the crontab, run ./crontab.sh <nameOfCronTabfile> --remove
cd $(dirname "$0")
test "$2" = --remove && mode=remove || mode=add
cron_unique_label="# cmID:$PWD|$1#"
crontab="$1".crontab
crontab_bak=$crontab.bak
test -f $crontab || cp $crontab.sample $crontab
crontab_exists() {
crontab -l 2>/dev/null | grep -x "$cron_unique_label" >/dev/null 2>/dev/null
}
# if crontab is executable
if type crontab >/dev/null 2>/dev/null; then
if test $mode = add; then
if ! crontab_exists; then
crontab -l > $crontab_bak
echo 'Appending to crontab:'
echo '-----------------------------------------------'
cat $crontab
crontab -l 2>/dev/null | { cat; echo; echo $cron_unique_label; cat $crontab; echo "# cm #"; } | crontab -
else
echo 'Crontab entry already exists, skipping ...'
echo
fi
echo '-----------------------------------------------'
echo "To remove previously added crontab entry, run: $0 $1 --remove"
echo
elif test $mode = remove; then
if crontab_exists; then
echo 'Removing crontab entry ...'
crontab -l 2>/dev/null | sed -e "\?^$cron_unique_label\$?,/^# cm #\$/ d" | crontab -
else
echo 'Crontab entry does not exist, nothing to do.'
fi
fi
fi
UPDATE:
Sry, didnt noticed the weak empty line pattern for removing a crontab. Simply would delete everything after the found crontab id, including manually added crontabs.
Changed the empty line end pattern to a little end tag.
So it will add crontabs with:
crontab -l 2>/dev/null | { cat; echo; echo $cron_unique_label; cat $crontab; echo "# cm #"; } | crontab -
.. and removing exactly only this cron with:
crontab -l 2>/dev/null | sed -e "\?^$cron_unique_label\$?,/^# cm #\$/ d" | crontab -

We can also do it in code:
first in bash, check the manual: man 2 open:
int fd = open('/tmp/1.txt', O_CREAT|O_EXCL)
if (fd === -1) {
print "job exist"
exit(1)
}
//unlink()
// your code
Js version:
const fs = require('fs')
try {
const fd = fs.openSync('/tmp/1.txt', 'wx')
} catch(err) {
console.log('job exist')
process.exit(0)
}
fs.unlinkSync('/tmp/1.txt')
fs.appendFileSync(fd, 'pid', 'utf8');

Related

Crontab will not execute .sh but crontab will execute a command

This issue is currently driving me nuts.
I setup a crontab with sudo crontab -e
The contents are 1 * * * * /home/bolte/bin/touchtest.sh
The contents of that file are:
#!/bin/bash
touch /home/bolte/bin/test.log
It creates the file. But the below script will not run.
#!/bin/bash
# CHANGE THESE
auth_email="11111111#live.co.uk"
auth_key="11111111111111111" # found in cloudflare
account settings
zone_name="11111.io"
record_name="11111.bolte.io"
# MAYBE CHANGE THESE
ip=$(curl -s http://ipv4.icanhazip.com)
ip_file="/home/bolte/ip.txt"
id_file="/home/bolte/cloudflare.ids"
log_file="/home/bolte/cloudflare.log"
# LOGGER
log() {
if [ "$1" ]; then
echo -e "[$(date)] - $1" >> $log_file
fi
}
# SCRIPT START
log "Check Initiated"
if [ -f $ip_file ]; then
old_ip=$(cat $ip_file)
if [ $ip == $old_ip ]; then
echo "IP has not changed."
exit 0
fi
fi
if [ -f $id_file ] && [ $(wc -l $id_file | cut -d " " -f 1) == 2 ]; then
zone_identifier=$(head -1 $id_file)
record_identifier=$(tail -1 $id_file)
else
zone_identifier=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones?name=$zone_name" -H "X-Auth-E$
record_identifier=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones/$zone_identifier/dns_record$
echo "$zone_identifier" > $id_file
echo "$record_identifier" >> $id_file
fi
update=$(curl -s -X PUT "https://api.cloudflare.com/client/v4/zones/$zone_identifier/dns_records/$record_ident$
[ Read 55 lines (Warning: No write permission) ]
^G Get Help ^O Write Out ^W Where Is ^K Cut Text ^J Justify ^C Cur Pos ^Y Prev Page
^X Exit ^R Read File ^\ Replace ^U Uncut Text ^T To Linter ^_ Go To Line ^V Next Page
I've been trying to troubleshoot why this code will not run every minute, there doesn't seem to be any output in the same folder as the script, which is located at /home/bolte/cloudflare-update-record.sh
Ok so the answer to this was, I was editing crontab with sudo, and the files were located in my users home folder. This is why they weren't working. Resolved my own issue.
If you have this issue just use $ crontab -e rather than sudo crontab -e, and specify full paths for your file outputs, unless you are putting the proper path variables in your script.

My bash script to track disk space usage over time: any advice on how to make it less hacky (e.g. avoiding 'eval')?

The following script sits at the end of my ~/.bash_profile. I want it to run once a day when I open an interactive login shell (unless I chose to skip), to keep track of larger changes in disk space usage, with the output of (g)du stored inside a file named after the current date.
Leaving aside, if you allow, the wisdom of what the script itself does, do you have any advice how to rewrite it to make it less hacky?
For example, I'm using 'eval', which I understand is frowned upon. Then again, simply replacing
eval $gdu_log_command
by
($gdu_log_command)
doesn't work.
Or: putting the entire thing into an alias is pretty ugly I guess, but I don't know how to avoid it.
Any suggestions what I could change, while preserving what it does?
gdu_log_today=$(date +"%y-%m-%d")$(echo ".txt")
gdu_log_command="sudo gdu / -hPx --si --threshold=1G > ~/Downloads/gdu-logs/$gdu_log_today"
alias gdu_log='if [[ ! -e ~/Downloads/gdu-logs/$gdu_log_today ]]
then read -p "Run disk space log now? (y)es, (s)kip, no: " gdu_log_user_input
if [[ "$gdu_log_user_input" = "y" ]]
then
echo "$gdu_log_command"
read -p "Execute? (y)es, cancel: " gdu_log_user_input_2
if [[ "$gdu_log_user_input_2" = "y" ]]
then eval $gdu_log_command
fi
elif [[ "$gdu_log_user_input" = "s" ]]
then
echo "Skipping today."
touch ~/Downloads/gdu-logs/$gdu_log_today
else
echo "Will ask again later."
fi
fi'
gdu_log
Why not use functions:
gdu_log_today=$(date +"%y-%m-%d")$(echo ".txt")
gdu_log_command() {
sudo gdu / -hPx --si --threshold=1G > ~/Downloads/gdu-logs/$gdu_log_today
}
gdu_log() {
if [[ -e ~/Downloads/gdu-logs/$gdu_log_today ]] && return
read -p "Run disk space log now? (y)es, (s)kip, no: " gdu_log_user_input
if [[ "$gdu_log_user_input" = "y" ]]; then
declare -f gdu_log_command # Edited command
# Also consider this alternative, which uses `sed` to remove the
# first line (i.e., the function header)
# declare -f gdu_log_command | sed '1,2d;$d'
read -p "Execute? (y)es, cancel: " gdu_log_user_input_2
if [[ "$gdu_log_user_input_2" = "y" ]]; then
gdu_log_command
fi
elif [[ "$gdu_log_user_input" = "s" ]]; then
echo "Skipping today."
touch ~/Downloads/gdu-logs/$gdu_log_today
else
echo "Will ask again later."
fi
}
gdu_log
EDIT Just to neaten the code slightly.
Also, instead of adding it to your ~/.bash_profile, why not use a crontab? If you saved your above script to a file located at ~/gdu_log.sh, you could do:
crontab -e
Then add this line:
# The 5 characters represent:
# minute hour day-of-month week-of-year day-of-week
# So, <MM> <HH> * * * represents every day at HH:MM
# e.g., everyday at 3:30am => 30 3 * * *
# everyday at 9:15pm => 15 21 * * *
# everyday at midnight => 0 0 * * *
0 3 * * * root /bin/sh /home/root/script.sh
That'll run the script daily at 03:00am.
EDIT per comments, see the script above (edited line has a comment next to it). declare -f prints the entire function and code to the console. E.g.,
# This first line is me pasting the function declaration into
# my console, so I can run `declare -f` against it
nick#nick-lt:~$ gdu_log_command() { sudo gdu / -hPx --si --threshold=1G > ~/Downloads/gdu-logs/$gdu_log_today; }
# The `declare -f` command and its output to the console:
nick#nick-lt:~$ declare -f gdu_log_command
gdu_log_command ()
{
sudo gdu / -hPx --si --threshold=1G > ~/Downloads/gdu-logs/$gdu_log_today
}

Bash Script - Will not completely execute

I am writing a script that will take in 3 outputs and then search all files within a predefined path. However, my grep command seems to be breaking the script with error code 123. I have been staring at it for a while and cannot really seem the error so I was hoping someone could point out my error. Here is the code:
#! /bin/bash -e
#Check if path exists
if [ -z $ARCHIVE ]; then
echo "ARCHIVE NOT SET, PLEASE SET TO PROCEED."
echo "EXITING...."
exit 1
elif [ $# -ne 3 ]; then
echo "Illegal number of arguments"
echo "Please enter the date in yyyy mm dd"
echo "EXITING..."
exit 1
fi
filename=output.txt
#Simple signal handler
signal_handler()
{
echo ""
echo "Process killed or interrupted"
echo "Cleaning up files..."
rm -f out
echo "Finsihed"
exit 1
}
trap 'signal_handler' KILL
trap 'signal_handler' TERM
trap 'signal_handler' INT
echo "line 32"
echo $1 $2 $3
#Search for the TimeStamp field and replace the / and : characters
find $ARCHIVE | xargs grep -l "TimeStamp: $2/$3/$1"
echo "line 35"
fileSize=`wc -c out.txt | cut -f 1 -d ' '`
echo $fileSize
if [ $fileSize -ge 1 ]; then
echo "no"
xargs -n1 basename < $filename
else
echo "NO FILES EXIST"
fi
I added the echo's to know where it was breaking. My program prints out line 32 and the args but never line 35. When I check the exit code I get 123.
Thanks!
Notes:
ARCHIVE is set to a test directory, i.e. /home/'uname'/testDir
$1 $2 $3 == yyyy mm dd (ie a date)
In testDir there are N number of directories. Inside these directories there are data files that have contain data as well as a time tag. The time tag is of the following format: TimeStamp: 02/02/2004 at 20:38:01
The scripts goal is to find all files that have the date tag you are searching for.
Here's a simpler test case that demonstrates your problem:
#!/bin/bash -e
echo "This prints"
true | xargs false
echo "This does not"
The snippet exits with code 123.
The problem is that xargs exits with code 123 if any command fails. When xargs exits with non-zero status, -e causes the script to exit.
The quickest fix is to use || true to effectively ignore xargs' status:
#!/bin/bash -e
echo "This prints"
true | xargs false || true
echo "This now prints too"
The better fix is to not rely on -e, since this option is misleading and unpredictable.
xargs makes the error code 123 when grep returns a nonzero code even just once. Since you're using -e (#!/bin/bash -e), bash would exit the script when one of its commands return a nonzero exit code. Not using -e would allow your code to continue. Just disabling it on that part can be a solution too:
set +e ## Disable
find "$ARCHIVE" | xargs grep -l "TimeStamp: $2/$1/$3" ## If one of the files doesn't match the pattern, `grep` would return a nonzero code.
set -e ## Enable again.
Consider placing your variables around quotes to prevent word splitting as well like "$ARCHIVE".
-d '\n' may also be required if one of your files' filename contain spaces.
find "$ARCHIVE" | xargs -d '\n' grep -l "TimeStamp: $2/$1/$3"

BASH script not work properly in crontab

Below line of my bash script not write output of /tmp/DPE_SC/LoadUnits/ttx/bin/deasn9 -b -a cdrr6 $fnames to file $dst_dir"/"$fstat"-"$fnames".txt when I execute from crontab.
It only creates empty file named $dst_dir"/"$fstat"-"$fnames".txt
Sure it works properly from command line manually.
/tmp/DPE_SC/LoadUnits/ttx/bin/deasn9 -b -a cdrr6 $fnames > $dst_dir/$fstat-$fnames.txt
What is my mistake?
This is my whole script
#!/bin/bash
export PATH=/tmp/DPE_SC/LoadUnits/ttx/bin:/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin:/tmp/DPE_SC/Tools:/usr/X11R6/bin
src_dir=/charging/chsLog/ready
dst_dir=/Core/cdr
cd $src_dir
lastfile=cat $dst_dir/last_cdr.txt
filenames=ls -t | grep ^chsLog
fcounter=1
for fnames in $filenames
do
fstat=`stat -c %y ${fnames} | cut -d '.' -f1`
fstat=`echo ${fstat//[^0-9]/}`
if [[ $fstat -gt $lastfile ]]
then
if [[ $fcounter -eq 1 ]]
then
echo $fstat > $dst_dir/last_cdr.txt
let "fcounter = $fcounter + 1"
fi
deasn9 -b -a cdrr6 ${fnames} > $dst_dir/$fstat-${fnames}.txt
fi
done
Remember that your .profile, .bashrc, et. al. are not available from inside cron.
Environment variables have to be defined directly in the crontab.
e.g.
fstat=myValue
fname=aName
#hourly myJob ${fstat} ${fname}
I found what I mistaken. cdrr6 was not only option. It is cdr formatting library. Then I exported LIB path from scipt.
Now it worked perfectly.

Simple bash script count running processes by name

i'm working on a small bash script which counts how often a script with a certain name is running.
ps -ef | grep -v grep | grep scrape_data.php | wc -l
is the code i use, via ssh it outputs the number of times scrape_data.php is running. Currently the output is 3 for example. So this works fine.
Now I'm trying to make a little script which does something when the count is smaller than 1.
#!/bin/sh
if [ ps -ef | grep -v grep | grep scrape_data.php | wc -l ] -lt 1; then
exit 0
#HERE PUT CODE TO START NEW PROCESS
else
exit 0
fi
The script above is what I have so far, but it does not work. I'm getting this error:
[root#s1 crons]# ./check_data.sh
./check_data.sh: line 4: [: missing `]'
wc: invalid option -- e
What am I doing wrong in the if statement?
Your test syntax is not correct, the lt should be within the test bracket:
if [ $(ps -ef | grep -v grep | grep scrape_data.php | wc -l) -lt 1 ]; then
echo launch
else
echo no launch
exit 0
fi
or you can test the return value of pgrep:
pgrep scrape_data.php &> /dev/null
if [ $? ]; then
echo no launch
fi
if you're using Bash then drop [ and -lt and use (( for arithmetic comparisons.
ps provides the -C switch, which accepts the process name to look for.
grep -v trickery are just hacks.
#!/usr/bin/env bash
proc="scrape_data.php"
limit=1
numproc="$(ps hf -opid,cmd -C "$proc" | awk '$2 !~ /^[|\\]/ { ++n } END { print n }')"
if (( numproc < limit ))
then
# code when less than 'limit' processes run
printf "running processes: '%d' less than limit: '%d'.\n" "$numproc" "$limit"
else
# code when more than 'limit' processes run
printf "running processes: '%d' more than limit: '%d'.\n" "$numproc" "$limit"
fi
Counting the lines is not needed. Just check the return value of grep:
if ! ps -ef | grep -q '[s]crape_data.php' ; then
...
fi
The [s] trick avoids the grep -v grep.
While the top-voted answer does in fact work, I have a solution that I used for my scraper that worked for me.
<?php
/**
* Go_Get.php
* -----------------------------------------
* #author Thomas Kroll
* #copyright Creative Commons share alike.
*
* #synopsis:
* This is the main script that calls the grabber.php
* script that actually handles the scraping of
* the RSI website for potential members
*
* #usage: php go_get.php
**/
ini_set('max_execution_time', 300); //300 seconds = 5 minutes
// script execution timing
$start = microtime(true);
// how many scrapers to run
$iter = 100;
/**
* workload.txt -- next record to start with
* workload-end.txt -- where to stop at/after
**/
$s=(float)file_get_contents('./workload.txt');
$e=(float)file_get_contents('./workload-end.txt');
// if $s >= $e exit script otherwise continue
echo ($s>=$e)?exit("Work is done...exiting".PHP_EOL):("Work is not yet done...continuing".PHP_EOL);
echo ("Starting Grabbers: ".PHP_EOL);
$j=0; //gotta start somewhere LOL
while($j<$iter)
{
$j++;
echo ($j %20!= 0?$j." ":$j.PHP_EOL);
// start actual scraping script--output to null
// each 'grabber' goes and gets 36 iterations (0-9/a-z)
exec('bash -c "exec nohup setsid php grabber.php '.$s.' > /dev/null 2>&1 &"');
// increment the workload counter by 36 characters
$s+=36;
}
echo PHP_EOL;
$end = microtime(true);
$total = $end - $start;
print "Script Execution Time: ".$total.PHP_EOL;
file_put_contents('./workload.txt',$s);
// don't exit script just yet...
echo "Waiting for processes to stop...";
// get number of php scrapers running
exec ("pgrep 'php'",$pids);
echo "Current number of processes:".PHP_EOL;
// loop while num of pids is greater than 10
// if less than 10, go ahead and respawn self
// and then exit.
while(count($pids)>10)
{
sleep(2);
unset($pids);
$pids=array();
exec("pgrep 'php'",$pids);
echo (count($pids) %15 !=0 ?count($pids)." ":count($pids).PHP_EOL);
}
//execute self before exiting
exec('bash -c "exec nohup setsid php go_get.php >/dev/null 2>&1 &"');
exit();
?>
Now while this seems like a bit of overkill, I was already using PHP to scrape the data (like your php script in the OP), so why not use PHP as the control script?
Basically, you would call the script like this:
php go_get.php
and then just wait for the first iteration of the script to finish. After that, it runs in the background, which you can see if you use your pid counting from the command line, or a similar tool like htop.
It's not glamorous, but it works. :)

Resources