LinuxQuestions.org

LinuxQuestions.org (/questions/)
-   Programming (http://www.linuxquestions.org/questions/programming-9/)
-   -   Single Instance of Bash Script (http://www.linuxquestions.org/questions/programming-9/single-instance-of-bash-script-831683/)

binbash 09-12-2010 06:09 AM

Single Instance of Bash Script
 
Hi guys,

I have a bash script which is called automatically

I want it so that if when the script is called if a previous instance of it is already running then to delay the running of it until the previous instance has stopped (effectively queue up ./script.sh var1 var2)

I have seen some posts about a 'lockfile' but this just seems to stop the second instance running rather than queueing it up to run next (it also needs to be able to queue up a 3rd/4th calling of the script and run them one at a time)

any suggestions?

p.s. the script is run using cygwin on windows

edit: I am very new to bash scripting so simple explanations please

konsolebox 09-12-2010 06:59 AM

You can try this code. I think the documentation's enough but you can ask me if there's something you don't understand. I hope you'll also refer to the bash manual first (man bash).
Code:

#!/bin/bash

INSTANCEDIR=/path/somewhere

function error {
        # send message to stderr
        echo "$1" >&- >&2
}

function main {
        local MYPID=$$ FILE PID
        local -a INSTANCEFILES

        echo "$MYPID" >> "$INSTANCEDIR/$MYPID" || {
                error "failed to record instance to $INSTANCEDIR/$MYPID"
                return 1
        }

        for (( ;; )); do
                INSTANCEFILES=("$INSTANCEDIR"/*)

                case "${#INSTANCEFILES[@]}" in
                1)
                        break
                        ;;
                0)
                        error "our instance file has been deleted."
                        return 1
                        ;;
                esac

                for FILE in "${INSTANCEFILES[@]}"; do
                        # read PID from instance file
                        PID=$(<"$FILE")

                        # delete the instance file if read data in it is not a digitt
                        if [[ $PID != +([[:digit:]]) ]]; then
                                rm "$FILE" || {
                                        error "failed to delete unowned instance file $FILE."
                                        return 1
                                }
                        fi

                        # skip the file if it's from us
                        [[ PID -eq MYPID ]] && continue

                        # Delete the instance file if the process presented by the PID no longer exists
                        # There might also be a better way to check the process but I'm not sure.
                        if ! kill -s 0 "$PID"; then
                                rm "$FILE" || {
                                        error "failed to delete unowned instance file $FILE."
                                        return 1
                                }
                        fi
                done
        done

        # continue the operations from here
}

---- EDIT ----

Other than this there should also be other methods like locking files with exec and using fuser to check if a process is still locked with the file; and also repeatedly using rm -f to locked files until the files count turn 1..... Although I still can't see cleaner implementation in those ways and I also find some of it as quirky.

You may also just use a single instance file to reference but I didn't choose it since I see a very tiny possibility that a race condition could occur. I get a feeling that other optimistic (ones who'll tend to say "that's ok") programmers may argue with this.

You might also want to add a code that automatically checks and/or creates INSTANCEDIR.

crts 09-12-2010 09:54 AM

Hi,

@konsolebox
not sure if I executed your script as it was intended to. This is what I did:
Code:

#!/bin/bash
# script.sh
error {
...
}
main {
...
}

main

# just do something unspectacular
for i in $(seq 1 10);do
        echo loop $i of script $1
        sleep 1
done

exit

Then I created another script to call it several times:
Code:

./script.sh 1
sleep 1
./script.sh 2
sleep 1
./script.sh 3
sleep 1
./script.sh 4

Some results:
Code:

$ ./caller.sh
loop 1 of script 1
loop 2 of script 1
loop 3 of script 1
$ loop 4 of script 1
loop 5 of script 1
loop 6 of script 1
loop 7 of script 1
loop 8 of script 1
loop 9 of script 1
loop 10 of script 1
./script.sh: line 50: kill: (1041) - No such process
./script.sh: line 50: kill: (1041) - No such process
rm: cannot remove `./lock/1041': No such file or directory
failed to delete unowned instance file ./lock/1041.
loop 1 of script 3
loop 2 of script 3
loop 3 of script 3
loop 4 of script 3
loop 5 of script 3
loop 6 of script 3
loop 7 of script 3
loop 8 of script 3
loop 9 of script 3
loop 10 of script 3
./script.sh: line 50: kill: (2296) - No such process
./script.sh: line 35: ./lock/2296: No such file or directory
rm: cannot remove `./lock/2296': No such file or directory
failed to delete unowned instance file ./lock/2296.
loop 1 of script 2
loop 2 of script 2
loop 3 of script 2
loop 4 of script 2
loop 5 of script 2
loop 6 of script 2
loop 7 of script 2
loop 8 of script 2
loop 9 of script 2
loop 10 of script 2
./script.sh: line 50: kill: (1047) - No such process
loop 1 of script 4
loop 2 of script 4
loop 3 of script 4
loop 4 of script 4
loop 5 of script 4
loop 6 of script 4
loop 7 of script 4
loop 8 of script 4
loop 9 of script 4
loop 10 of script 4

$ ./caller.sh
loop 1 of script 1
loop 2 of script 1
loop 3 of script 1
$ loop 4 of script 1
loop 5 of script 1
loop 6 of script 1
loop 7 of script 1
loop 8 of script 1
loop 9 of script 1
loop 10 of script 1
./script.sh: line 50: kill: (3258) - No such process
./script.sh: line 50: kill: (3258) - No such process
./script.sh: line 50: kill: (3258) - No such process
rm: cannot remove `./lock/3258': No such file or directory
failed to delete unowned instance file ./lock/3258.
rm: cannot remove `./lock/3258': No such file or directory
failed to delete unowned instance file ./lock/3258.
loop 1 of script 3
loop 1 of script 2
loop 2 of script 3
loop 2 of script 2
loop 3 of script 3
loop 3 of script 2
loop 4 of script 3
loop 4 of script 2
loop 5 of script 3
loop 5 of script 2
loop 6 of script 3
loop 6 of script 2
loop 7 of script 3
loop 7 of script 2
loop 8 of script 3
loop 8 of script 2
loop 9 of script 3
loop 9 of script 2
loop 10 of script 3
loop 10 of script 2
./script.sh: line 50: kill: (4380) - No such process
./script.sh: line 50: kill: (3268) - No such process
loop 1 of script 4
loop 2 of script 4
loop 3 of script 4
loop 4 of script 4
loop 5 of script 4
loop 6 of script 4
loop 7 of script 4
loop 8 of script 4
loop 9 of script 4
loop 10 of script 4

The first time the scripts were executed in the wrong order. The second time apparently the lock did not work 100% and the ouputs of script 2 and 3 interleaved.
But as I said, I am not sure if I used it right.

<removed: fawlty script>

konsolebox 09-12-2010 11:07 AM

sorry my mistake. i forgot to add shopt -s extglob
Code:

#!/bin/bash

shopt -s extglob

...


konsolebox 09-12-2010 11:11 AM

edit: mistakenly pressed quote button for edit.. guess i'll better just post a new complete code. The error messages of kill -s 0 "$PID" is now sent to null.
Code:

#!/bin/bash

shopt -s extglob

INSTANCEDIR=/path/somewhere

function error {
        # send message to stderr
        echo "$1" >&- >&2
}

function main {
        local MYPID=$$ FILE PID
        local -a INSTANCEFILES

        echo "$MYPID" >> "$INSTANCEDIR/$MYPID" || {
                error "failed to record instance to $INSTANCEDIR/$MYPID"
                return 1
        }

        for (( ;; )); do
                INSTANCEFILES=("$INSTANCEDIR"/*)

                case "${#INSTANCEFILES[@]}" in
                1)
                        break
                        ;;
                0)
                        error "our instance file has been deleted."
                        return 1
                        ;;
                esac

                for FILE in "${INSTANCEFILES[@]}"; do
                        # read PID from instance file
                        PID=$(<"$FILE")

                        # delete the instance file if read data in it is not a digitt
                        if [[ $PID != +([[:digit:]]) ]]; then
                                rm "$FILE" || {
                                        error "failed to delete unowned instance file $FILE."
                                        return 1
                                }
                        fi

                        # skip the file if it's from us
                        [[ PID -eq MYPID ]] && continue

                        # Delete the instance file if the process presented by the PID no longer exists
                        # There might also be a better way to check the process but I'm not sure.
                        if ! kill -s 0 "$PID" 2>/dev/null; then
                                rm "$FILE" || {
                                        error "failed to delete unowned instance file $FILE."
                                        return 1
                                }
                        fi
                done

                read -t 1 -n 1
        done

        # continue the operations from here
}

main

update: added 'read -t 1 -n 1'

binbash 09-15-2010 04:04 PM

thank you konsolebox

where it says # continue the operations from here

I decided to put /path/to/script.sh $1 $2 and call singleinstance.sh $1 $2 in place of the original call (I find it easier to keep the 2 pieces of code separate) but script.sh is saying var 1 is null so it doesn't seem to be passing them

any ideas?

konsolebox 09-15-2010 08:12 PM

I didn't know that you'd also need to pass the arguments.
Did you already add "$@" to the call to main?
Code:

...

main "$@"

Also you should place the variables inside quotes:
Code:

/path/to/script.sh "$1" "$2"

binbash 09-16-2010 03:29 PM

seems to be working good now - thanks konsolebox

konsolebox 09-16-2010 06:57 PM

No prob. You may now mark the thread as solved then.

binbash 09-17-2010 12:43 PM

sorry but how to mark it?

MTK358 09-17-2010 01:25 PM

Thread Tools.

binbash 09-29-2010 05:36 AM

the script has been running good for a number of days

but lately not sure why sometimes the script hangs not sure if it is the single instance one or the script i am running through it (need to look into it more - have been away for a few days)

and then any other instances get queued up and don't run as the first one never exits

could there be an issue if it is called twice very quickly i.e. within 1 second or less?

also could it be made so that if the instance has been running for 5 minutes it terminates (therefore allowing any other queued instances to start)?

thanks in advance

edit: could it be to do with this? http://inamidst.com/eph/cygwin

Quote:

Process ID Shortage

Cygwin does some weird stuff sometimes. For example I keep running into this problem and not being able to find the solution easily:

676102803 [main] zsh 840377 fork_copy: user/cygwin data
pass 2 failed, 0x970000..0xAF3000, done 0, windows
pid 4294640815, Win32 error 8
withcd: fork failed: resource temporarily unavailable

This is due to it running out of PIDs. I noted the problem and solution on Swhack; basically you have to run "rebaseall", but that means closing down all of your current processes and actually running it in bash which is somewhat irritating. If anyone has a better solution, I'd be glad if they could let me know. Thanks.
also stackdump:
Quote:

Exception: STATUS_ACCESS_VIOLATION at eip=61020137
eax=00E0D008 ebx=6123A614 ecx=764C0D06 edx=003441B8 esi=00000000 edi=0028F9E8
ebp=61020890 esp=0028C7B8 program=C:\cygwin\bin\sh.exe, pid 6656, thread main
cs=0023 ds=002B es=002B fs=0053 gs=002B ss=002B
Stack trace:
Frame Function Args
End of stack trace

konsolebox 09-30-2010 04:55 AM

Quite very indirect. I wonder, could the script be sometimes called within an sh.exe environment instead of bash.exe?
Quote:

Originally Posted by binbash (Post 4112409)
ebp=61020890 esp=0028C7B8 program=C:\cygwin\bin\sh.exe, pid 6656, thread main


binbash 09-30-2010 11:34 AM

do you mean sometimes run it in bash and sometimes in sh?

i think it is a cygwin error though as it has been happening all the time today... didn't happen at all last week

rebaseall helped for a while doesn't anymore

konsolebox 10-04-2010 07:57 AM

The script's only meant to run with bash. If sh.exe is your default shell, that might give a difference with operations.


All times are GMT -5. The time now is 07:30 AM.