crontab: some tips

Some tricks in the use of the crontab in GNU/Linux

Introduction

crontab is a great feature of GNU/Linux systems. It can be used to schedule the execution of jobs that need to run with any frequency from every minute to yearly. I look here at the system-wide crontab, the one living at /etc/crontab, not the user-specific ones which are edited with commands such as crontab -e because the latter require the corresponding user to log in or the jobs will NOT run.

Each line in /etc/crontab is made of several fields. A comment line found in the default crontab in debian/ubuntu/mint/… is a useful reminder:

# m h dom mon dow user  command

with easy to remember meaning:

  • minutes
  • hours
  • day of month (1-31)
  • month (1-12)
  • day of week (0-7, with Sunday usually being both 0 and 7, for all tastes 😄 )
  • user, as whom to run the command
  • command to run

A typical line could be:

*/5 7-23   * * 2,4  mik /usr/bin/aplay /home/mik/ping.wav

This will play a sound every 5’ between 7.00 and 23.55 on any Tuesday and Thursday to come. Perfect if you want to stress out someone from far afield 😉.

It is recommended, for security reasons, to always specify the full path of the command, especially if it’s run as root. So use /usr/bin/aplay and not aplay. Note that for commands like rm, this will mean you will not be using the shell built-in, so watch out for quirks in less commonly used options.

Mind your shell 🐢

That’s all fine to here. One important caveat that caused me heavy headaches at the beginning (and sometimes still now) is that jobs are run by /bin/sh, not the more familiar /bin/bash.

One case where the different shell can catch you off-balance is when you want to launch a python script living in a virtualenv: sh doesn’t understand source. One solution is simply to invoke bash and ask it to do the work:

53 16   *  *  * mik     /bin/bash -c "source /home/mik/finance/bin/activate && python3 /home/mik/finance/UpdateFin.py"

Note that I haven’t specified the path for python3 because source will ensure the one in the virtualenv is used.

A new lease of life for defunct processes 💀

As part of my home automation system, I have several services that need to be running all the time on my hub (a RPi3). Despite earnest efforts in error trapping, things can still go badly and the service can crash. I therefore run scripts at boot (should probably use systemd for this…) and periodically re-launch them with crontab. The problem is that you want only one instance of the service to be executing. I’ve had mixed experience with locking files in python, so a more universal approach, identical for .py and .sh scripts, is to check if the process is already running using pgrep. The advantage is also that you don’t fire up python just to check that the service is already running. So we can do something like this:

@reboot          mik      cd /home/mik && python3 -u RepeatPiReceive.py 1>> /var/log/RepeatPiReceive/log 2>> /var/log/RepeatPiReceive/err
25 *    * * *    mik      [ `/usr/bin/pgrep RepeatPiReceive.py -fc` -lt 2 ] && /home/mik/RepeatPiReceive.py 1>> /var/log/RepeatPiReceive/log 2>> /var/log/RepeatPiReceive/err

The first line launches the script when the GNU-Linux machine boots up. The second is executed every hour and checks how many instances of RepeatPiReceive.py are running. Here’s the subtlety, though. If you use:

pgrep RepeatPiReceive.py -c

it won’t work, because RepeatPiReceive.py is actually an argument of the python3 executable. That’s why I’ve used:

pgrep RepeatPiReceive.py -fc

which means that the regex is applied to the full command line, including the arguments (check out man pgrep). However, once you do this, you will get at least one match in any case, because of the shell process executing your command, which has RepeatPiReceive.py in it. In fact, calling pgrep in the CLI gives:

24739 /bin/sh -c      [ `/usr/bin/pgrep RepeatPiReceive.py -fc` -lt 2 ] && [omissis]
24741 python3 /home/mik/RepeatPiReceive.py

note that two processes match, even if only one instance of the script is in execution (#24741). That’s why I’ve used the test to see if there are less than 2 matches (I could have used -eq 1).

My advice here is to run the pgrep command with and without your service running and see how many matches you get in each case and adjust accordingly. This should be done within crontab to be sure.

Check your 📬

One issue is that etc/crontab has no access to a terminal where to output errors. Knowing the errors is essential to resolving the issues (computer savvies are way too familiar with the layman complaints: “It won’t work. It just gives me an error”). So the advice is simple: check your mail, since crontab will send emails to the owner of the job with outputs and errors from the commands, provided you have a mailer system installed.

Avatar
Michele Pozzi
Engineer and scientist