35

I am looking for a simple and generic solution that would allow you to execute any script or application in crontab and prevent it from running twice.

The solution should be independent on the executed command.

I assume it should look like lock && (command ; unlock) where lock will return false if there was another lock.

The second part would be like if it acquired the lock, run command and unlock after command is executed, even if it returns error.

7 Answers 7

44

Take a look at the run-one package. From the manpage for the run-one command Manpage icon:

run-one is a wrapper script that runs no more than one unique instance of some command with a unique set of arguments.

This is often useful with cronjobs, when you want no more than one copy running at a time.

Like time or sudo, you just prepend it to the command. So a cronjob could look like:

  */60 * * * *   run-one rsync -azP $HOME example.com:/srv/backup

For more information and background, check out the blog post introducing it by Dustin Kirkland.

1
  • May I ask when I may use the short binary names run-one and when the absolute path name /usr/bin/run-one etc.? Thanks a lot.
    – uav
    Mar 17, 2020 at 18:19
5

A very simple way of settup a lock:

if mkdir /var/lock/mylock; then
  echo "Locking succeeded" >&2
else
  echo "Lock failed - exit" >&2
  exit 1
fi

A scripts which want to run needs te create the lock. If the lock exists, another script is busy, so the first script can't run. If the file don't exists, no script has acquired the lock. So the current script acquires the lock. When the script has finished the lock needs to be realeased by removeing the lock.

For more information about bash locks, check this page

2
  • 1
    You'll also want an EXIT trap that removes the lock on exit. echo "Locking succeeded" >&2; trap 'rm -rf /var/lock/mylock' EXIT
    – geirha
    May 25, 2012 at 10:23
  • 1
    Ideally you'd want to use advisory flock from a process that runs the command you want as a subtask. That way if they all die the flock is released automatically, which using the presence of a lock file doesn't do. Using a network port would work in a similar way - though that's a way smaller namespace, which is a problem. Mar 19, 2015 at 16:45
3

No need to install some fancy package:

#!/bin/bash
pgrep -xf "$*" > /dev/null || "$@"

It's faster to write that script yourself than to run "apt-get install", isn't it? You might want to add "-u $(id -u)" to the pgrep to check for instances run by the current user only.

3
  • 2
    this does not guarantee a single instance. two scripts can pass to the other side of || operator at the same time, before either has a chance to start the script yet. Aug 9, 2016 at 20:45
  • @SedatKapanoglu Granted, that script is not racecondition-proof, but the original question was about long running cron-jobs (which are run at most once a minute). If your system needs more than a minute for the creation of the process you have some other issues. However, if needed for some other reasons you could use flock(1) to protect above script against race conditions. Aug 10, 2016 at 22:59
  • I used this but for a bash script that should check itself. The code is this: v=$(pgrep -xf "/bin/bash $0 $@") [ "${v/$BASHPID/}" != "" ] && exit 2
    – ahofmann
    Jan 19, 2017 at 13:14
3

See also Tim Kay's solo, which performs locking by binding a port on a loopback address unique to the user:

http://timkay.com/solo/

In case his site goes down:

Usage:

solo -port=PORT COMMAND

where
    PORT        some arbitrary port number to be used for locking
    COMMAND     shell command to run

options
    -verbose    be verbose
    -silent     be silent

Use it like this:

* * * * * solo -port=3801 ./job.pl blah blah

Script:

#!/usr/bin/perl -s
#
# solo v1.7
# Prevents multiple cron instances from running simultaneously.
#
# Copyright 2007-2016 Timothy Kay
# http://timkay.com/solo/
#
# It is free software; you can redistribute it and/or modify it under the terms of either:
#
# a) the GNU General Public License as published by the Free Software Foundation;
#    either version 1 (http://dev.perl.org/licenses/gpl1.html), or (at your option)
#    any later version (http://www.fsf.org/licenses/licenses.html#GNUGPL), or
#
# b) the "Artistic License" (http://dev.perl.org/licenses/artistic.html), or
#
# c) the MIT License (http://opensource.org/licenses/MIT)
#

use Socket;

alarm $timeout                              if $timeout;

$port =~ /^\d+$/ or $noport                     or die "Usage: $0 -port=PORT COMMAND\n";

if ($port)
{
    # To work with OpenBSD: change to
    # $addr = pack(CnC, 127, 0, 1);
    # but make sure to use different ports across different users.
    # (Thanks to  www.gotati.com .)
    $addr = pack(CnC, 127, $<, 1);
    print "solo: bind ", join(".", unpack(C4, $addr)), ":$port\n"   if $verbose;

    $^F = 10;           # unset close-on-exec

    socket(SOLO, PF_INET, SOCK_STREAM, getprotobyname('tcp'))       or die "socket: $!";
    bind(SOLO, sockaddr_in($port, $addr))               or $silent? exit: die "solo($port): $!\n";
}

sleep $sleep if $sleep;

exec @ARGV;
1
  • For Mac OSX, this will fail with Error solo(3801): Can't assign requested address unless you force a 0 for the 3rd parameter of method pack. What's good for the BSD is also good for the Mac. Mar 5, 2019 at 17:51
3

You need a lock. run-one does the job, but you may also want to look into flock from util-linux package.

It is a standard package provided by the kernel developers, allows for more customization than run-one and is still very simple.

0

A simple solution from bash-hackers.org that worked for me was using mkdir. This is an easy way how to make sure that only one instance of your program is running. Create a directory with mkdir .lock which returns

  • true if the creation was successful and
  • false if the lock file exists, indicating that there is currently one instance running.

So this simple function did all the file locking logic:

if mkdir .lock; then
    echo "Locking succeeded"
    eval startYourProgram.sh ;
else
    echo "Lock file exists. Program already running? Exit. "
    exit 1
fi


echo "Program finished, Removing lock."
rm -r .lock
0
0

This solution is for a bash script that needs to check itself

v=$(pgrep -xf "/bin/bash $0 $@")
[ "${v/$BASHPID/}" != "" ] && exit 0

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .