raldi
raldi

Reputation: 22160

Quick-and-dirty way to ensure only one instance of a shell script is running at a time

What's a quick-and-dirty way to make sure that only one instance of a shell script is running at a given time?

Upvotes: 213

Views: 147715

Answers (30)

Majal
Majal

Reputation: 1741

Quick and dirty?

This one-liner on the top of your script may work most of the time. If you want reliability don't do this, use the clean solution below. (Keeping this first answer for history's sake. 😅)

[[ $(pgrep -c "`basename \"$0\"`") -gt 1 ]] && exit

Quick and clean (and official)?

This one-liner to the top of your script came straight from the flock manpage:

[ "${FLOCKER}" != "$0" ] && exec env FLOCKER="$0" flock -en "$0" "$0" "$@" || :

This is useful boilerplate code for shell scripts. Put it at the top of the shell script you want to lock and it’ll automatically lock itself on the first run. If the environment variable $FLOCKER is not set to the shell script that is being run, then execute flock and grab an exclusive non-blocking lock (using the script itself as the lock file) before re-execing itself with the right arguments. It also sets the FLOCKER environment variable to the right value so it doesn’t run again.

Of course, just make sure that your script name is unique. :)

Upvotes: 14

AnyDev
AnyDev

Reputation: 513

I have following problems with the existing answers:

  • Some answers use script file itself $0 or $BASH_SOURCE for locking often referring to examples from man flock. This fails when the script file is replaced due to update or edit causing next run to open and obtain a lock on the new script file even though another instance holding a lock on the removed file is still running.
  • Few answers use a fixed file descriptor. This is not ideal. I do not want to rely on how this will behave e.g. opening lock file fails but gets mishandled and attempts to lock on unrelated file descriptor inherited from parent process. Another fail case is injecting locking wrapper for a 3rd party binary that does not handle locking itself but fixed file descriptors can interfere with file descriptor passing to child processes.
  • I reject answers using process lookup for already running script name. There are several reasons for it, such as but not limited to reliability/atomicity, parsing output, and having script that does several related functions some of which do not require locking.
  • Some answers try to clean up lock files and then having to deal with stale lock files caused by e.g. sudden crash/reboot. IMO that is unnecessarily complicated. Let lock files stay.

This answer does:

  • rely on flock because it gets kernel to provide locking and therefore benefits from flock features such as timeout for attempting to acquire a lock (without polling) and shared vs exclusive lock type, something that would be messy to do by most other means.
  • assume and rely on lock file being stored on the local filesystem as opposed to NFS. I assume the goal is to allow programs running on same host to lock each other out. Distributed applications are "out of scope" here.
  • change lock file presence to NOT mean anything about a running instance. Its role is purely to prevent two concurrent instances creating file with same name and replacing another's copy. Lock file does not get deleted, it gets left behind and allowed to remain across reboots. The locking is indicated via flock and NOT via lock file existence.
  • assume bash shell, as tagged by the question.

It's not a oneliner, but without comments nor error messages it's small enough:

#!/bin/bash

LOCKFILE=/var/lock/TODO

set -o noclobber
exec {lockfd}<> "${LOCKFILE}" || exit 1
set +o noclobber # depends on what you need
flock --exclusive --nonblock ${lockfd} || exit 1

But I prefer comments and error messages:

#!/bin/bash

# TODO Set a lock file name
LOCKFILE=/var/lock/myprogram.lock

# Set noclobber option to ensure lock file is not REPLACED.
set -o noclobber

# Open lock file for R+W on a new file descriptor
# and assign the new file descriptor to "lockfd" variable.
# If lock file does not exist it gets created non-destructively
# without loops and without race condition due to 'set -o noclobber'
# This does NOT obtain a lock.
exec {lockfd}<> "${LOCKFILE}" || {
  echo "pid=$$ failed to open LOCKFILE='${LOCKFILE}'" 1>&2
  exit 1
}

# TODO!!!! undo/set the desired noclobber value for the remainder of the script
set +o noclobber

# Lock on the allocated file descriptor or fail
# TODO: Adjust flock options e.g. --noblock or --timeout as needed
flock --exclusive --nonblock ${lockfd} || {
  echo "pid=$$ failed to obtain lock fd='${lockfd}' LOCKFILE='${LOCKFILE}'" 1>&2
  exit 1
}

# DO work here
echo "pid=$$ obtained exclusive lock fd='${lockfd}' LOCKFILE='${LOCKFILE}'"

# TODO: After critical code can unlock and do more work after unlocking
#flock -u ${lockfd};
# if unlocking then might as well close lockfd too
#exec {lockfd}<&-

Upvotes: 2

Hardguy
Hardguy

Reputation: 43

There are many good answers above. You also can use dotlockfile.

This is some example code you can use in your script:

$LOCKFILENAME=/var/run/test.lock

if ! dotlockfile -l -p -r 2 $LOCKFILENAME
then
    echo "This test process already running!"
    exit 1
fi

Upvotes: 0

Keyless
Keyless

Reputation: 106

If you do not want to or cannot use flock (e.g. you are not using a shared file system), consider using an external service like lockable.

It exposes advisory lock primitives, much like flock would. In particular, you can acquire a lock via:

https://lockable.dev/api/acquire/my-lock-name

and release it via

https://lockable.dev/api/release/my-lock-name

By wrapping script execution with lock acquisition and release, you can make sure only a single instance of the process is running at any given time.

Upvotes: 1

lhunath
lhunath

Reputation: 125456

Naive approaches that test the existence of "lock files" are flawed.

Why? Because they don't check whether the file exists and create it in a single atomic action. Because of this; there is a race condition that WILL make your attempts at mutual exclusion break.

Instead, you can use mkdir. mkdir creates a directory if it doesn't exist yet, and if it does, it sets an exit code. More importantly, it does all this in a single atomic action making it perfect for this scenario.

if ! mkdir /tmp/myscript.lock 2>/dev/null; then
    echo "Myscript is already running." >&2
    exit 1
fi

For all details, see the excellent BashFAQ: http://mywiki.wooledge.org/BashFAQ/045

If you want to take care of stale locks, fuser(1) comes in handy. The only downside here is that the operation takes about a second, so it isn't instant.

Here's a function I wrote once that solves the problem using fuser:

#       mutex file
#
# Open a mutual exclusion lock on the file, unless another process already owns one.
#
# If the file is already locked by another process, the operation fails.
# This function defines a lock on a file as having a file descriptor open to the file.
# This function uses FD 9 to open a lock on the file.  To release the lock, close FD 9:
# exec 9>&-
#
mutex() {
    local file=$1 pid pids 

    exec 9>>"$file"
    { pids=$(fuser -f "$file"); } 2>&- 9>&- 
    for pid in $pids; do
        [[ $pid = $$ ]] && continue

        exec 9>&- 
        return 1 # Locked by a pid.
    done 
}

You can use it in a script like so:

mutex /var/run/myscript.lock || { echo "Already running." >&2; exit 1; }

If you don't care about portability (these solutions should work on pretty much any UNIX box), Linux' fuser(1) offers some additional options and there is also flock(1).

Upvotes: 177

Z KC
Z KC

Reputation: 36

I use oneliner @ the very beginning of script:

#!/bin/bash

if [[ $(pgrep -afc "$(basename "$0")") -gt "1" ]]; then echo "Another instance of "$0" has already been started!" && exit; fi
.
the_beginning_of_actual_script

It is good to see the presence of process in the memory (no matter what the status of process is); but it does the job for me.

Upvotes: 1

user3132194
user3132194

Reputation: 2557

Add this line at the beginning of your script

[ "${FLOCKER}" != "$0" ] && exec env FLOCKER="$0" flock -en "$0" "$0" "$@" || :

It's a boilerplate code from man flock.

If you want more logging, use this one

[ "${FLOCKER}" != "$0" ] && { echo "Trying to start build from queue... "; exec bash -c "FLOCKER='$0' flock -E $E_LOCKED -en '$0' '$0' '$@' || if [ \"\$?\" -eq $E_LOCKED ]; then echo 'Locked.'; fi"; } || echo "Lock is free. Completing."

This sets and checks locks using flock utility. This code detects if it was run first time by checking FLOCKER variable, if it is not set to script name, then it tries to start script again recursively using flock and with FLOCKER variable initialized, if FLOCKER is set correctly, then flock on previous iteration succeeded and it is OK to proceed. If lock is busy, it fails with configurable exit code.

It seems to not work on Debian 7, but seems to work back again with experimental util-linux 2.25 package. It writes "flock: ... Text file busy". It could be overridden by disabling write permission on your script.

Upvotes: 6

Sudhir Kumar
Sudhir Kumar

Reputation: 11

Using the process's lock is much stronger and takes care of the ungraceful exits also. lock_file is kept open as long as the process is running. It will be closed (by shell) once the process exists (even if it gets killed). I found this to be very efficient:

lock_file=/tmp/`basename $0`.lock

if fuser $lock_file > /dev/null 2>&1; then
    echo "WARNING: Other instance of $(basename $0) running."
    exit 1
fi
exec 3> $lock_file 

Upvotes: 1

untore
untore

Reputation: 633

This I have not found mentioned anywhere, it uses read, I don't exactly know if read is actually atomic but it has served me well so far..., it is juicy because it is only bash builtins, this is an in process implementation, you start the locker coprocess and use its i/o to manage locks, the same can be done interprocess by just swapping the target i/o from the locker file descriptors to a on filesystem file descriptor (exec 3<>/file && exec 4</file)

## gives locks
locker() {
    locked=false
    while read l; do
        case "$l" in
            lock)
                if $locked; then
                    echo false
                else
                    locked=true
                    echo true
                fi
                ;;
            unlock)
                if $locked; then
                    locked=false
                    echo true
                else
                    echo false
                fi
                ;;
            *)
                echo false
                ;;
        esac
    done
}
## locks
lock() {
    local response
    echo lock >&${locker[1]}
    read -ru ${locker[0]} response
    $response && return 0 || return 1
}

## unlocks
unlock() {
    local response
    echo unlock >&${locker[1]}
    read -ru ${locker[0]} response
    $response && return 0 || return 1
}

Upvotes: 0

Filidor Wiese
Filidor Wiese

Reputation: 682

Answered a million times already, but another way, without the need for external dependencies:

LOCK_FILE="/var/lock/$(basename "$0").pid"
trap "rm -f ${LOCK_FILE}; exit" INT TERM EXIT
if [[ -f $LOCK_FILE && -d /proc/`cat $LOCK_FILE` ]]; then
   // Process already exists
   exit 1
fi
echo $$ > $LOCK_FILE

Each time it writes the current PID ($$) into the lockfile and on script startup checks if a process is running with the latest PID.

Upvotes: 1

WinEunuuchs2Unix
WinEunuuchs2Unix

Reputation: 1949

This one line answer comes from someone related Ask Ubuntu Q&A:

[ "${FLOCKER}" != "$0" ] && exec env FLOCKER="$0" flock -en "$0" "$0" "$@" || :
#     This is useful boilerplate code for shell scripts.  Put it at the top  of
#     the  shell script you want to lock and it'll automatically lock itself on
#     the first run.  If the env var $FLOCKER is not set to  the  shell  script
#     that  is being run, then execute flock and grab an exclusive non-blocking
#     lock (using the script itself as the lock file) before re-execing  itself
#     with  the right arguments.  It also sets the FLOCKER env var to the right
#     value so it doesn't run again.

Upvotes: 2

Mark Setchell
Mark Setchell

Reputation: 207650

You can use GNU Parallel for this as it works as a mutex when called as sem. So, in concrete terms, you can use:

sem --id SCRIPTSINGLETON yourScript

If you want a timeout too, use:

sem --id SCRIPTSINGLETON --semaphoretimeout -10 yourScript

Timeout of <0 means exit without running script if semaphore is not released within the timeout, timeout of >0 mean run the script anyway.

Note that you should give it a name (with --id) else it defaults to the controlling terminal.

GNU Parallel is a very simple install on most Linux/OSX/Unix platforms - it is just a Perl script.

Upvotes: 26

sivann
sivann

Reputation: 2131

An example with flock(1) but without subshell. flock()ed file /tmp/foo is never removed, but that doesn't matter as it gets flock() and un-flock()ed.

#!/bin/bash

exec 9<> /tmp/foo
flock -n 9
RET=$?
if [[ $RET -ne 0 ]] ; then
    echo "lock failed, exiting"
    exit
fi

#Now we are inside the "critical section"
echo "inside lock"
sleep 5
exec 9>&- #close fd 9, and release lock

#The part below is outside the critical section (the lock)
echo "lock released"
sleep 5

Upvotes: 2

presto8
presto8

Reputation: 517

If flock's limitations, which have already been described elsewhere on this thread, aren't an issue for you, then this should work:

#!/bin/bash

{
    # exit if we are unable to obtain a lock; this would happen if 
    # the script is already running elsewhere
    # note: -x (exclusive) is the default
    flock -n 100 || exit

    # put commands to run here
    sleep 100
} 100>/tmp/myjob.lock 

Upvotes: 5

David M. Syzdek
David M. Syzdek

Reputation: 15788

The existing answers posted either rely on the CLI utility flock or do not properly secure the lock file. The flock utility is not available on all non-Linux systems (i.e. FreeBSD), and does not work properly on NFS.

In my early days of system administration and system development, I was told that a safe and relatively portable method of creating a lock file was to create a temp file using mkemp(3) or mkemp(1), write identifying information to the temp file (i.e. PID), then hard link the temp file to the lock file. If the link was successful, then you have successfully obtained the lock.

When using locks in shell scripts, I typically place an obtain_lock() function in a shared profile and then source it from the scripts. Below is an example of my lock function:

obtain_lock()
{
  LOCK="${1}"
  LOCKDIR="$(dirname "${LOCK}")"
  LOCKFILE="$(basename "${LOCK}")"

  # create temp lock file
  TMPLOCK=$(mktemp -p "${LOCKDIR}" "${LOCKFILE}XXXXXX" 2> /dev/null)
  if test "x${TMPLOCK}" == "x";then
     echo "unable to create temporary file with mktemp" 1>&2
     return 1
  fi
  echo "$$" > "${TMPLOCK}"

  # attempt to obtain lock file
  ln "${TMPLOCK}" "${LOCK}" 2> /dev/null
  if test $? -ne 0;then
     rm -f "${TMPLOCK}"
     echo "unable to obtain lockfile" 1>&2
     if test -f "${LOCK}";then
        echo "current lock information held by: $(cat "${LOCK}")" 1>&2
     fi
     return 2
  fi
  rm -f "${TMPLOCK}"

  return 0;
};

The following is an example of how to use the lock function:

#!/bin/sh

. /path/to/locking/profile.sh
PROG_LOCKFILE="/tmp/myprog.lock"

clean_up()
{
  rm -f "${PROG_LOCKFILE}"
}

obtain_lock "${PROG_LOCKFILE}"
if test $? -ne 0;then
   exit 1
fi
trap clean_up SIGHUP SIGINT SIGTERM

# bulk of script

clean_up
exit 0
# end of script

Remember to call clean_up at any exit points in your script.

I've used the above in both Linux and FreeBSD environments.

Upvotes: 5

one-liner
one-liner

Reputation: 799

I wanted to do away with lockfiles, lockdirs, special locking programs and even pidof since it isn't found on all Linux installations. Also wanted to have the simplest code possible (or at least as few lines as possible). Simplest if statement, in one line:

if [[ $(ps axf | awk -v pid=$$ '$1!=pid && $6~/'$(basename $0)'/{print $1}') ]]; then echo "Already running"; exit; fi

Upvotes: 3

rubo77
rubo77

Reputation: 20855

This will work, if your script name is unique:

#!/bin/bash
if [ $(pgrep -c $(basename $0)) -gt 1 ]; then 
  echo $(basename $0) is already running
  exit 0
fi

If the scriptname is not unique, this works on most linux distributions:

#!/bin/bash
exec 9>/tmp/my_lock_file
if ! flock -n 9  ; then
   echo "another instance of this script is already running";
   exit 1
fi

source: http://mywiki.wooledge.org/BashFAQ/045

Upvotes: -1

biocyberman
biocyberman

Reputation: 5915

Late to the party, using the idea from @Majal, this is my script to start only one instance of emacsclient GUI. With it, I can set shortcutkey to open or jump back to the same emacsclient. I have another script to call emacsclient in terminals when I need it. The use of emacsclient here is just to show a working example, one can choose something else. This approach is quick and good enough for my tiny scripts. Tell me where it is dirty :)

#!/bin/bash

# if [ $(pgrep -c $(basename $0)) -lt 2 ]; then # this works but requires script name to be unique
if [ $(pidof -x "$0"|wc -w ) -lt 3 ]; then
    echo -e "Starting $(basename $0)"
    emacsclient --alternate-editor="" -c "$@"
else
    echo -e "$0 is running already"
fi

Upvotes: 0

Gianluca Casati
Gianluca Casati

Reputation: 3751

I have a simple solution based on the file name

#!/bin/bash

MY_FILENAME=`basename "$BASH_SOURCE"`

MY_PROCESS_COUNT=$(ps a -o pid,cmd | grep $MY_FILENAME | grep -v grep | grep -v $$ | wc -
l)

if [ $MY_PROCESS_COUNT -ne 0  ]; then
  echo found another process
  exit 0
if

# Follows the code to get the job done.

Upvotes: 0

Rudolf L&#246;rcks
Rudolf L&#246;rcks

Reputation: 11

if [ 1 -ne $(/bin/fuser "$0" 2>/dev/null | wc -w) ]; then
    exit 1
fi

Upvotes: 0

Jabir Ahmed
Jabir Ahmed

Reputation: 82

why dont we use something like

pgrep -f $cmd || $cmd

Upvotes: 0

Alex B
Alex B

Reputation: 84882

Use flock(1) to make an exclusive scoped lock a on file descriptor. This way you can even synchronize different parts of the script.

#!/bin/bash

(
  # Wait for lock on /var/lock/.myscript.exclusivelock (fd 200) for 10 seconds
  flock -x -w 10 200 || exit 1

  # Do stuff

) 200>/var/lock/.myscript.exclusivelock

This ensures that code between ( and ) is run only by one process at a time and that the process doesn’t wait too long for a lock.

Caveat: this particular command is a part of util-linux. If you run an operating system other than Linux, it may or may not be available.

Upvotes: 251

Tim Bunce
Tim Bunce

Reputation: 1120

The semaphoric utility uses flock (as discussed above, e.g. by presto8) to implement a counting semaphore. It enables any specific number of concurrent processes you want. We use it to limit the level of concurrency of various queue worker processes.

It's like sem but much lighter-weight. (Full disclosure: I wrote it after finding the sem was way too heavy for our needs and there wasn't a simple counting semaphore utility available.)

Upvotes: 1

Znik
Znik

Reputation: 1146

This example is explained in the man flock, but it needs some impovements, because we should manage bugs and exit codes:

   #!/bin/bash
   #set -e this is useful only for very stupid scripts because script fails when anything command exits with status more than 0 !! without possibility for capture exit codes. not all commands exits >0 are failed.

( #start subprocess
  # Wait for lock on /var/lock/.myscript.exclusivelock (fd 200) for 10 seconds
  flock -x -w 10 200
  if [ "$?" != "0" ]; then echo Cannot lock!; exit 1; fi
  echo $$>>/var/lock/.myscript.exclusivelock #for backward lockdir compatibility, notice this command is executed AFTER command bottom  ) 200>/var/lock/.myscript.exclusivelock.
  # Do stuff
  # you can properly manage exit codes with multiple command and process algorithm.
  # I suggest throw this all to external procedure than can properly handle exit X commands

) 200>/var/lock/.myscript.exclusivelock   #exit subprocess

FLOCKEXIT=$?  #save exitcode status
    #do some finish commands

exit $FLOCKEXIT   #return properly exitcode, may be usefull inside external scripts

You can use another method, list processes that I used in the past. But this is more complicated that method above. You should list processes by ps, filter by its name, additional filter grep -v grep for remove parasite nad finally count it by grep -c . and compare with number. Its complicated and uncertain

Upvotes: 5

Stefan Rogin
Stefan Rogin

Reputation: 1527

Here is a more elegant, fail-safe, quick & dirty method, combining the answers provided above.

Usage

  1. include sh_lock_functions.sh
  2. init using sh_lock_init
  3. lock using sh_acquire_lock
  4. check lock using sh_check_lock
  5. unlock using sh_remove_lock

Script File

sh_lock_functions.sh

#!/bin/bash

function sh_lock_init {
    sh_lock_scriptName=$(basename $0)
    sh_lock_dir="/tmp/${sh_lock_scriptName}.lock" #lock directory
    sh_lock_file="${sh_lock_dir}/lockPid.txt" #lock file
}

function sh_acquire_lock {
    if mkdir $sh_lock_dir 2>/dev/null; then #check for lock
        echo "$sh_lock_scriptName lock acquired successfully.">&2
        touch $sh_lock_file
        echo $$ > $sh_lock_file # set current pid in lockFile
        return 0
    else
        touch $sh_lock_file
        read sh_lock_lastPID < $sh_lock_file
        if [ ! -z "$sh_lock_lastPID" -a -d /proc/$sh_lock_lastPID ]; then # if lastPID is not null and a process with that pid exists
            echo "$sh_lock_scriptName is already running.">&2
            return 1
        else
            echo "$sh_lock_scriptName stopped during execution, reacquiring lock.">&2
            echo $$ > $sh_lock_file # set current pid in lockFile
            return 2
        fi
    fi
    return 0
}

function sh_check_lock {
    [[ ! -f $sh_lock_file ]] && echo "$sh_lock_scriptName lock file removed.">&2 && return 1
    read sh_lock_lastPID < $sh_lock_file
    [[ $sh_lock_lastPID -ne $$ ]] && echo "$sh_lock_scriptName lock file pid has changed.">&2  && return 2
    echo "$sh_lock_scriptName lock still in place.">&2
    return 0
}

function sh_remove_lock {
    rm -r $sh_lock_dir
}

Usage example

sh_lock_usage_example.sh

#!/bin/bash
. /path/to/sh_lock_functions.sh # load sh lock functions

sh_lock_init || exit $?

sh_acquire_lock
lockStatus=$?
[[ $lockStatus -eq 1 ]] && exit $lockStatus
[[ $lockStatus -eq 2 ]] && echo "lock is set, do some resume from crash procedures";

#monitoring example
cnt=0
while sh_check_lock # loop while lock is in place
do
    echo "$sh_scriptName running (pid $$)"
    sleep 1
    let cnt++
    [[ $cnt -gt 5 ]] && break
done

#remove lock when process finished
sh_remove_lock || exit $?

exit 0

Features

  • Uses a combination of file, directory and process id to lock to make sure that the process is not already running
  • You can detect if the script stopped before lock removal (eg. process kill, shutdown, error etc.)
  • You can check the lock file, and use it to trigger a process shutdown when the lock is missing
  • Verbose, outputs error messages for easier debug

Upvotes: 0

Sadhun
Sadhun

Reputation: 264

Try something like the below,

ab=`ps -ef | grep -v grep | grep -wc processname`

Then match the variable with 1 using an if loop.

Upvotes: -2

bk138
bk138

Reputation: 3088

Here's an approach that combines atomic directory locking with a check for stale lock via PID and restart if stale. Also, this does not rely on any bashisms.

#!/bin/dash

SCRIPTNAME=$(basename $0)
LOCKDIR="/var/lock/${SCRIPTNAME}"
PIDFILE="${LOCKDIR}/pid"

if ! mkdir $LOCKDIR 2>/dev/null
then
    # lock failed, but check for stale one by checking if the PID is really existing
    PID=$(cat $PIDFILE)
    if ! kill -0 $PID 2>/dev/null
    then
       echo "Removing stale lock of nonexistent PID ${PID}" >&2
       rm -rf $LOCKDIR
       echo "Restarting myself (${SCRIPTNAME})" >&2
       exec "$0" "$@"
    fi
    echo "$SCRIPTNAME is already running, bailing out" >&2
    exit 1
else
    # lock successfully acquired, save PID
    echo $$ > $PIDFILE
fi

trap "rm -rf ${LOCKDIR}" QUIT INT TERM EXIT


echo hello

sleep 30s

echo bye

Upvotes: 7

tiian
tiian

Reputation: 29

Take a look to FLOM (Free LOck Manager) http://sourceforge.net/projects/flom/: you can synchronize commands and/or scripts using abstract resources that does not need lock files in a filesystem. You can synchronize commands running in different systems without a NAS (Network Attached Storage) like an NFS (Network File System) server.

Using the simplest use case, serializing "command1" and "command2" may be as easy as executing:

flom -- command1

and

flom -- command2

from two different shell scripts.

Upvotes: 0

rouble
rouble

Reputation: 18211

I use a simple approach that handles stale lock files.

Note that some of the above solutions that store the pid, ignore the fact that the pid can wrap around. So - just checking if there is a valid process with the stored pid is not enough, especially for long running scripts.

I use noclobber to make sure only one script can open and write to the lock file at one time. Further, I store enough information to uniquely identify a process in the lockfile. I define the set of data to uniquely identify a process to be pid,ppid,lstart.

When a new script starts up, if it fails to create the lock file, it then verifies that the process that created the lock file is still around. If not, we assume the original process died an ungraceful death, and left a stale lock file. The new script then takes ownership of the lock file, and all is well the world, again.

Should work with multiple shells across multiple platforms. Fast, portable and simple.

#!/usr/bin/env sh
# Author: rouble

LOCKFILE=/var/tmp/lockfile #customize this line

trap release INT TERM EXIT

# Creates a lockfile. Sets global variable $ACQUIRED to true on success.
# 
# Returns 0 if it is successfully able to create lockfile.
acquire () {
    set -C #Shell noclobber option. If file exists, > will fail.
    UUID=`ps -eo pid,ppid,lstart $$ | tail -1`
    if (echo "$UUID" > "$LOCKFILE") 2>/dev/null; then
        ACQUIRED="TRUE"
        return 0
    else
        if [ -e $LOCKFILE ]; then 
            # We may be dealing with a stale lock file.
            # Bring out the magnifying glass. 
            CURRENT_UUID_FROM_LOCKFILE=`cat $LOCKFILE`
            CURRENT_PID_FROM_LOCKFILE=`cat $LOCKFILE | cut -f 1 -d " "`
            CURRENT_UUID_FROM_PS=`ps -eo pid,ppid,lstart $CURRENT_PID_FROM_LOCKFILE | tail -1`
            if [ "$CURRENT_UUID_FROM_LOCKFILE" == "$CURRENT_UUID_FROM_PS" ]; then 
                echo "Script already running with following identification: $CURRENT_UUID_FROM_LOCKFILE" >&2
                return 1
            else
                # The process that created this lock file died an ungraceful death. 
                # Take ownership of the lock file.
                echo "The process $CURRENT_UUID_FROM_LOCKFILE is no longer around. Taking ownership of $LOCKFILE"
                release "FORCE"
                if (echo "$UUID" > "$LOCKFILE") 2>/dev/null; then
                    ACQUIRED="TRUE"
                    return 0
                else
                    echo "Cannot write to $LOCKFILE. Error." >&2
                    return 1
                fi
            fi
        else
            echo "Do you have write permissons to $LOCKFILE ?" >&2
            return 1
        fi
    fi
}

# Removes the lock file only if this script created it ($ACQUIRED is set), 
# OR, if we are removing a stale lock file (first parameter is "FORCE") 
release () {
    #Destroy lock file. Take no prisoners.
    if [ "$ACQUIRED" ] || [ "$1" == "FORCE" ]; then
        rm -f $LOCKFILE
    fi
}

# Test code
# int main( int argc, const char* argv[] )
echo "Acquring lock."
acquire
if [ $? -eq 0 ]; then 
    echo "Acquired lock."
    read -p "Press [Enter] key to release lock..."
    release
    echo "Released lock."
else
    echo "Unable to acquire lock."
fi

Upvotes: 3

bmdhacks
bmdhacks

Reputation: 16421

Here's an implementation that uses a lockfile and echoes a PID into it. This serves as a protection if the process is killed before removing the pidfile:

LOCKFILE=/tmp/lock.txt
if [ -e ${LOCKFILE} ] && kill -0 `cat ${LOCKFILE}`; then
    echo "already running"
    exit
fi

# make sure the lockfile is removed when we exit and then claim it
trap "rm -f ${LOCKFILE}; exit" INT TERM EXIT
echo $$ > ${LOCKFILE}

# do stuff
sleep 1000

rm -f ${LOCKFILE}

The trick here is the kill -0 which doesn't deliver any signal but just checks if a process with the given PID exists. Also the call to trap will ensure that the lockfile is removed even when your process is killed (except kill -9).

Upvotes: 126

Related Questions