26

A solution that does not require additional tools would be prefered.

  • What about a lock file? – Marco Sep 18 '12 at 11:07
  • @Marco I found this SO answer using that, but as stated in a comment, this can create a race condition – Tobias Kienzler Sep 18 '12 at 11:18
  • 3
    This is BashFAQ 45. – jw013 Sep 18 '12 at 13:49
  • @jw013 thanks! So maybe something like ln -s my.pid .lock will claim the lock (followed by echo $$ > my.pid) and on failure can check whether the PID stored in .lock is really an active instance of the script – Tobias Kienzler Sep 18 '12 at 15:22

11 Answers 11

17

Almost like nsg's answer: use a lock directory. Directory creation is atomic under linux and unix and *BSD and a lot of other OSes.

if mkdir $LOCKDIR
then
    # Do important, exclusive stuff
    if rmdir $LOCKDIR
    then
        echo "Victory is mine"
    else
        echo "Could not remove lock dir" >&2
    fi
else
    # Handle error condition
    ...
fi

You can put the PID of the locking sh into a file in the lock directory for debugging purposes, but don't fall into the trap of thinking you can check that PID to see if the locking process still executes. Lots of race conditions lie down that path.

  • 1
    I'd consider using the stored PID to check whether the locking instance is still alive. However, here's a claim that mkdir is not atomic on NFS (which is not the case for me, but I guess one should mention that, if true) – Tobias Kienzler Sep 18 '12 at 13:08
  • Yes, by all means use the stored PID to see if the locking process still executes, but don't attempt to do anything other than log a message. The work of checking the stored pid, creating a new PID file, etc, leaves a big window for races. – Bruce Ediger Sep 18 '12 at 13:37
  • Ok, as Ihunath stated, the lockdir would most likely be in /tmp which is usually not NFS shared, so that should be fine. – Tobias Kienzler Sep 19 '12 at 8:33
  • I would use rm -rf to remove the lock directory. rmdir will fail if someone (not necessarily you) managed to add a file to the directory. – chepner Sep 22 '12 at 4:32
18

To add to Bruce Ediger's answer, and inspired by this answer, you should also add more smarts to the cleanup to guard against script termination:

#Remove the lock directory
function cleanup {
    if rmdir $LOCKDIR; then
        echo "Finished"
    else
        echo "Failed to remove lock directory '$LOCKDIR'"
        exit 1
    fi
}

if mkdir $LOCKDIR; then
    #Ensure that if we "grabbed a lock", we release it
    #Works for SIGTERM and SIGINT(Ctrl-C)
    trap "cleanup" EXIT

    echo "Acquired lock, running"

    # Processing starts here
else
    echo "Could not create lock directory '$LOCKDIR'"
    exit 1
fi
  • Alternatively, if ! mkdir "$LOCKDIR"; then handle failure to lock and exit; fi trap and do processing after if-statement. – Kusalananda Feb 22 '18 at 13:19
6

This may be too simplistic, please correct me if I'm wrong. Isn't a simple ps enough?

#!/bin/bash 

me="$(basename "$0")";
running=$(ps h -C "$me" | grep -wv $$ | wc -l);
[[ $running > 1 ]] && exit;

# do stuff below this comment
  • 1
    Nice and/or brilliant. :) – Spooky Mar 3 '17 at 16:49
  • 1
    I've used this condition for a week, and in 2 occasions it didn't prevent new process from starting. I figured what the problem is - new pid is a substring of the old one and gets hidden by grep -v $$. real examples: old - 14532, new - 1453, old - 28858, new - 858. – Naktibalda Feb 22 '18 at 11:30
  • I fixed it by changing grep -v $$ to grep -v "^${$} " – Naktibalda Feb 22 '18 at 11:52
  • @Naktibalda good catch, thanks! You could also fix it with grep -wv "^$$" (see edit). – terdon Feb 22 '18 at 12:38
  • Thanks for that update. My pattern occasionally failed because shorter pids were left padded with spaces. – Naktibalda Mar 8 '18 at 16:50
4

I would use a lock file, as mentioned by Marco

#!/bin/bash

# Exit if /tmp/lock.file exists
[ -f /tmp/lock.file ] && exit

# Create lock file, sleep 1 sec and verify lock
echo $$ > /tmp/lock.file
sleep 1
[ "x$(cat /tmp/lock.file)" == "x"$$ ] || exit

# Do stuff
sleep 60

# Remove lock file
rm /tmp/lock.file
  • 1
    (I think you forgot to create the lock file) What about race conditions? – Tobias Kienzler Sep 18 '12 at 11:28
  • ops :) Yes, race conditions is a problem in my example, I usually write hourly or daily cron jobs and race conditions are rare. – nsg Sep 18 '12 at 11:32
  • They shouldn't be relevant in my case either, but it's something one should keep in mind. Maybe using lsof $0 isn't bad, either? – Tobias Kienzler Sep 18 '12 at 11:34
  • You can diminish the race condition by writing your $$ in the lock file. Then sleep for a short interval and read it back. If the PID is still yours, you successfully acquired the lock. Needs absolutely no additional tools. – manatwork Sep 18 '12 at 11:41
  • 1
    I have never used lsof for this purpose, I this it should work. Note that lsof is really slow in my system (1-2 sec) and most likely there is a lot of time for race conditions. – nsg Sep 18 '12 at 11:45
3

If you want to make sure that only one instance of your script is running take a look at:

Lock your script (against parallel run)

Otherwise you can check ps or invoke lsof <full-path-of-your-script>, since i wouldn't call them additional tools.


Supplement:

actually i thought of doing it like this:

for LINE in `lsof -c <your_script> -F p`; do 
    if [ $$ -gt ${LINE#?} ] ; then
        echo "'$0' is already running" 1>&2
        exit 1;
    fi
done

this ensures that only the process with the lowest pid keeps on running even if you fork-and-exec several instances of <your_script> simultaneously.

  • 1
    Thanks for the link, but could you include the essential parts in your answer? It's common policy at SE to prevent link rot... But something like [[(lsof $0 | wc -l) > 2]] && exit might actually be enough, or is this also prone to race conditions? – Tobias Kienzler Sep 18 '12 at 11:30
  • You are right the essential part of my answer was missing and only posting links is pretty lame. I added my own suggestion to the answer. – user1146332 Sep 18 '12 at 12:52
3

One other way to make sure a single instance of bash script runs:

#!/bin/bash

# Check if another instance of script is running
pidof -o %PPID -x $0 >/dev/null && echo "ERROR: Script $0 already running" && exit 1

...

pidof -o %PPID -x $0 gets the PID of the existing script if its already running or exits with error code 1 if no other script is running

2

Although you've asked for a solution without additional tools, this is my favourite way using flock:

#!/bin/sh

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

echo "servus!"
sleep 10

This comes from the examples section of man flock, which further explains:

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.

Points to consider:

See also https://stackoverflow.com/questions/185451/quick-and-dirty-way-to-ensure-only-one-instance-of-a-shell-script-is-running-at.

1

This is a modified version of Anselmo's Answer. The idea is to create a read only file descriptor using the bash script itself and use flock to handle the lock.

SCRIPT=`realpath $0`     # get absolute path to the script itself
exec 6< "$SCRIPT"        # open bash script using file descriptor 6
flock -n 6 || { echo "ERROR: script is already running" && exit 1; }   # lock file descriptor 6 OR show error message if script is already running

echo "Run your single instance code here"

The main difference to all other answer's is that this code doesn't modify the filesystem, uses a very low footprint and doesn't need any cleanup since the file descriptor is closed as soon as the script finishes independent of the exit state. Thus it doesn't matter if the script fails or succeeds.

  • You should always quote all shell variable references unless you have a good reason not to, and you’re sure you know what you’re doing.  So you should be doing exec 6< "$SCRIPT". – Scott Nov 2 '18 at 6:01
  • @Scott I've changed the code according your suggestions. Many thanks. – John Doe Nov 2 '18 at 6:38
1

I am using cksum to check my script is truly running single instance, even I change filename & file path.

I am not using trap & lock file, because if my server suddenly down, I need to remove manually lock file after server goes up.

Note: #!/bin/bash in first line is required for grep ps

#!/bin/bash

checkinstance(){
   nprog=0
   mysum=$(cksum $0|awk '{print $1}')
   for i in `ps -ef |grep /bin/bash|awk '{print $2}'`;do 
        proc=$(ls -lha /proc/$i/exe 2> /dev/null|grep bash) 
        if [[ $? -eq 0 ]];then 
           cmd=$(strings /proc/$i/cmdline|grep -v bash)
                if [[ $? -eq 0 ]];then 
                   fsum=$(cksum /proc/$i/cwd/$cmd|awk '{print $1}')
                   if [[ $mysum -eq $fsum ]];then
                        nprog=$(($nprog+1))
                   fi
                fi
        fi
   done

   if [[ $nprog -gt 1 ]];then
        echo $0 is already running.
        exit
   fi
}

checkinstance 

#--- run your script bellow 

echo pass
while true;do sleep 1000;done

Or you can hardcoded cksum inside your script, so you no worry again if you want to change filename, path, or content of your script.

#!/bin/bash

mysum=1174212411

checkinstance(){
   nprog=0
   for i in `ps -ef |grep /bin/bash|awk '{print $2}'`;do 
        proc=$(ls -lha /proc/$i/exe 2> /dev/null|grep bash) 
        if [[ $? -eq 0 ]];then 
           cmd=$(strings /proc/$i/cmdline|grep -v bash)
                if [[ $? -eq 0 ]];then 
                   fsum=$(grep mysum /proc/$i/cwd/$cmd|head -1|awk -F= '{print $2}')
                   if [[ $mysum -eq $fsum ]];then
                        nprog=$(($nprog+1))
                   fi
                fi
        fi
   done

   if [[ $nprog -gt 1 ]];then
        echo $0 is already running.
        exit
   fi
}

checkinstance

#--- run your script bellow

echo pass
while true;do sleep 1000;done
  • 1
    Please explain exactly how hardcoding the checksum is a good idea. – Scott May 24 at 0:14
  • not hardcoding checksum, its only create identity key of your script, when another instance will running, it will check other shell script process and cat the file first, if your identity key is on that file, so its mean your instance already running. – arputra May 24 at 6:35
  • OK; please edit your answer to explain that.  And, in the future, please don’t post multiple 30-line long blocks of code that look like they’re (almost) identical without saying and explaining how they’re different.  And don’t say things like “you can hardcoded [sic] cksum inside your script”, and don’t continue to use variable names mysum and fsum, when you’re not talking about a checksum any more. – Scott May 24 at 7:08
  • Looks interesting, thanks! And welcome to unix.stackexchange :) – Tobias Kienzler May 24 at 8:08
0

You can use this: https://github.com/sayanarijit/pidlock

sudo pip install -U pidlock

pidlock -n sleepy_script -c 'sleep 10'
  • > A solution that does not require additional tools would be prefered. – dhag Jan 12 '18 at 19:13
0

My code to you

#!/bin/bash

script_file="$(/bin/readlink -f $0)"
lock_file=${script_file////_}

function executing {
  echo "'${script_file}' already executing"
  exit 1
}

(
  flock -n 9 || executing

  sleep 10

) 9> /var/lock/${lock_file}

Based on man flock, improving only:

  • the name of the lock file, to be based on the full name of the script
  • the message executing

Where I put here the sleep 10, you can put all the main script.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.