1

I wrote my first bash script, wich is checking folders for changes with the function "inotify" and starts some actions. The whole process is runned by nohup as a backgroundprocess.

The folder is the destination of several Dataloggers, which are pushing files in zip-Format via ftp into different subfolders. The bash script unzips the files and starts a php-script afterwards, which is processing the content of the zip files.

My Problem: Sometimes the bash script gives me errors like the following:

- No zipfiles found.
- unzip:  cannot find zipfile...

This shouldn't happen, because the files exist and I can run the same command in terminal without errors. I had the same problem before, when I accendently ran the script multiple times, so I guess this is somehow causing the problem.

I tried to manage the problem with a PID File, which is located in my home dir. For some reason, it still runs two instances of the bash script. If I try to run another instance, it shows the warning "Process already running" as its supposed to (see program code). When I kill the process of the second instance manually (kill $$), it restarts after a while and again there are two instances of the process running.

#!/bin/bash

PIDFILE=/home/PIDs/myscript.pid

if [ -f $PIDFILE ]
then
  PID=$(cat $PIDFILE)
  ps -p $PID > /dev/null 2>&1
  if [ $? -eq 0 ]
  then
    echo "Process already running"
    exit 1
  else
    ## Process not found assume not running
    echo $$ > $PIDFILE
    if [ $? -ne 0 ]
    then
      echo "Could not create PID file"
      exit 1
    fi
  fi
else
  echo $$ > $PIDFILE
  if [ $? -ne 0 ]
  then
    echo "Could not create PID file"
    exit 1
  fi
fi

while true;
do inotifywait -q -r -e move -e create --format %w%f /var/somefolder | while read FILE

do
    dir=$(dirname $FILE)
    filename=${FILE:$((${#dir}+1))}

    if [[ "$filename" == *.zip ]];
           then
                unzip $FILE
                php somephpscript $dir 
    fi
done
done

The Output of ps -ef looks Like this:

UID  PID  PPID C STIME TTY   TIME     CMD

root 1439 1433 0 11:19 pts/0 00:00:00 /bin/bash /.../my_script 
root 3488 1439 0 15:10 pts/0 00:00:00 /bin/bash /.../my_script 

As you can see, the second instances Parent-PID is the script itself

EDIT: I changed the bash script as recommended by Fred. The source code now looks like this:

#!/bin/bash

PIDFILE=/home/PIDs/myscript.pid

if [ -f $PIDFILE ]
then
  PID=$(cat $PIDFILE)
  ps -p $PID > /dev/null 2>&1
  if [ $? -eq 0 ]
  then
    echo "Process already running"
    exit 1
  else
    ## Process not found assume not running
    echo $$ > $PIDFILE
    if [ $? -ne 0 ]
    then
      echo "Could not create PID file"
      exit 1
    fi
  fi
else
  echo $$ > $PIDFILE
  if [ $? -ne 0 ]
  then
    echo "Could not create PID file"
    exit 1
  fi
fi

while read -r FILE
do
    dir=$(dirname $FILE)
    filename=${FILE:$((${#dir}+1))}

    if [[ "$filename" == *.zip ]];
           then
                unzip $FILE
                php somephpscript $dir 
    fi


done < <(inotifywait -q -m -r -e move -e create --format %w%f /var/somefolder)

Output of ps -ef still shows two instances:

UID       PID   PPID  C STIME TTY      TIME     CMD
root      7550  7416  0 15:59 pts/0    00:00:00 /bin/bash /.../my_script
root      7553  7550  0 15:59 pts/0    00:00:00 /bin/bash /.../my_script
root      7554  7553  0 15:59 pts/0    00:00:00 inotifywait -q -m -r -e move -e create --format %w%f /var/somefolder
franktank
  • 11
  • 1
  • 6
  • Are you saying the script, when launched a second time, displays the warning message and then does NOT stop after the "exit 1" statement? If you are really attempting "kill $$" in your interactive shell, that does nothing, as $$ is the PID of the shell and the shell sending itself that signal does nothing. – Fred Jan 18 '17 at 12:16
  • Quite ambitious for a first script I must say! Inside single-bracketed conditionals [ ] you should always quote your variables, like [ -f "$PIDFILE" ]. Well, you should quote them pretty much everywhere. It is generally recommended that you use lowercase variable names to avoid conflict with environment variables (though I have been known to ignoring this myself...). – Fred Jan 18 '17 at 12:19
  • @Fred When I start the script a second time it works the way its supposed to work. It gives the error message and doesn't start the process. I dont understand your second part, but what I meant by kill $$ is, that I manually search for the PID with the command "ps -ef" and then kill it. Thanks for your advice, I will change it in my source code. I must say that I copied the first part with the PID File from another forum post, but still the rest was quite hard because there are a lot more actions I didn't put in this example! – franktank Jan 18 '17 at 12:52
  • When you write "The bash script is running a second instance of the bash script", do you mean the bash script for which you provide the code is calling itself? Or do you mean this first script calls a second script (maybe the PHP script) twice? – Fred Jan 18 '17 at 13:44
  • I mean somehow it calls itself. Here both entries of ps -ef: 1. root 1439 1433 0 11:19 pts/0 00:00:00 /bin/bash /.../my_script 2. root 3488 1439 0 15:10 pts/0 00:00:00 /bin/bash /.../my_script – franktank Jan 18 '17 at 14:14
  • This cannot happen by magic. Are you sure there is not a second instance of your script running in the background that was simply never stopped? – Fred Jan 18 '17 at 14:46
  • I also thought about that, so I put the script into bash.bashhrc and restarted my server. Still the same problem...As you can see in my post before, the parent-process ID of the second instance is the script itself. Also it is weird that the second instance comes back after manually killing it. Maybe it has something to do with the inotify loop but I really don't understand why?! – franktank Jan 18 '17 at 14:57
  • Ok, now I know what it is. Do not have time right now to post an answer, will come back later. – Fred Jan 18 '17 at 14:58
  • Awesome, thanks for your help! – franktank Jan 18 '17 at 15:00
  • The comment #5 should be added to the question text because text in a comment are displayed as just one line. – Jdamian Jan 18 '17 at 15:29
  • Instead of `filename=${FILE:$((${#dir}+1))}` you may use `filename=${FILE##*/}` – Jdamian Jan 18 '17 at 15:32
  • Do you realize there is a repeated part of code? – Jdamian Jan 18 '17 at 15:36
  • @Jdamian which part do you mean? – franktank Jan 18 '17 at 15:55
  • The code following the `else` keywords. By the way, you don't need to separate the `echo` and the `if [ $? -ne 0 ]`, you can use `if ! echo ···` – Jdamian Jan 18 '17 at 16:04
  • I think there are two different cases. First case is, the PID File exists but the PID# doesn't exist, second case happens if the file doesn't exist. – franktank Jan 18 '17 at 16:14
  • Using a PID file isn't the best way to ensure process uniqueness. See this post: http://stackoverflow.com/questions/455911/whats-the-best-way-to-make-sure-only-one-instance-of-a-perl-program-is-running – codeforester Jan 18 '17 at 19:47
  • And this one for Bash: http://stackoverflow.com/questions/185451/quick-and-dirty-way-to-ensure-only-one-instance-of-a-shell-script-is-running-at – codeforester Jan 18 '17 at 19:54

1 Answers1

0

You are seeing two lines in the ps output and assumes this means your script was launched twice, but it is not the case.

You pipe inotifywait into a while loop (which is OK). What you may not realize is that, by doing so, you cause Bash to create a subshell to execute the while loop. That subshell is not a full copy of the whole script.

If you kill that subshell, because of the while true loop, it gets recreated instantly. Please note that inotifywait has a --monitor option ; I have not studied your script in enough detail, but maybe you could do away with the external while loop by using it.

There is another way to write the loop that will not eliminate the subshell but has other advantages. Try something like :

while IFS= read -r FILE
do
  BODY OF THE LOOP
done < <(inotifywait --monitor OTHER ARGUMENTS)

The first < indicates input redirection, and the <( ) syntax indicates "execute this command, pipe its output to a FIFO, and give me the FIFO path so that I can redirect from this special file to feed its output to the loop".

You can get a feel for what I mean by doing :

echo <(cat </dev/null)

You will see that the argument that echo sees when using that syntax is a file name, probably something like /dev/fd/XX.

There is one MAJOR advantage to getting rid of the subshell : the loop executes in the main shell scope, so any change in variables you perform in the loop can be seen outside the loop once it terminates. It may not matter much here but, mark my words, you will come to appreciate the enormous difference it makes in many, many situations.

To illustrate what happens with the subshell, here is a small code snippet :

while IFS= read -r line
do
  echo Main $$ $BASHPID
  echo $line
done < <(echo Subshell $$ $BASHPID)

Special variable $$ contains the main shell PID, and special variable BASHPID contains the current subshell (or the main shell PID if no subshell was launched). You will see that the main shell PID is the same in the loop and in the process substitution, but BASHPID changes, illustrating that a subshell is launched. I do not think there is a way to get rid of that subshell.

Fred
  • 6,590
  • 9
  • 20
  • Thanks for your reply, I dont understand it completly though. The output of <(inotifywait --monitor OTHER ARGUMENTS) is passed to the variable FILE then? I tried, I am getting an error: Syntax error: redirection unexpected. – franktank Jan 19 '17 at 14:23
  • Using < <(COMMAND) pipes the output of COMMAND to the loop. The only difference compared to a pipe, is that the body of the loop is executed in the scope of the current shell. – Fred Jan 19 '17 at 14:32
  • Can you please post your non-working code so that I can check where the syntax error is? I do not see a problem with my code above, and I use this syntax on a daily basis, so maybe there is something else causing a problem. Please note that the <( need to be together, no spacing in between, but the initial < must have spacing before <(, – Fred Jan 19 '17 at 14:34
  • Maybe it is your inotifywait command that fails for some reason? Ths first hist I find searching the web for this error relate to problems mounting a CIFS share. – Fred Jan 19 '17 at 14:36
  • I changed the source code again and now it worked. Propably some copy fail. Still have two instances as shown above...Just saw that the second instance is parent of the inotify process. You know why? – franktank Jan 19 '17 at 15:28
  • I made some tests and realized that my previous recommendation did not remove the subshell. I now think there is no way to get rid of it, and have corrected my answer and updated it with code that shows a subshell is launched even for a trivial example not involving inotifywait. – Fred Jan 23 '17 at 11:36
  • ok but maybe this is not causing the problem..?I am watching several folders for changes and if it does, it starts the unzip command and afterwards the php script. That means, the procedure can run parallel, if there are changes in two or more folders at the same time. Could that be a problem? Maybe I should try some kind of process chronology, so that only one job is running at a time? – franktank Jan 23 '17 at 12:41
  • Your current script should not be able to run things in parallel, as the commands are in the loop, they are not run in the background (with &), and the loop itself does not work in parallel. So unless you manually launch the script twice, nothing should work in parallel. It is not clear to me if working in parallel is something you want (it seems like it should be easily doable, if you take a few simple precautions and you are not talking hundreds of changes occurring simulteanously), or something you want to avoid. – Fred Jan 23 '17 at 12:49
  • I dont think that I need to run the processes in parallel. Im running the script with nohup..could that be a problem? – franktank Jan 23 '17 at 13:49