Ending tail -f started in a shell script

Viewed 42141

I have the following.

  1. A Java process writing logs to the stdout
  2. A shell script starting the Java process
  3. Another shell script which executes the previous one and redirects the log
  4. I check the log file with the tail -f command for the success message.

Even if I have exit 0 in the code I cannot end the tail -f process.

Which doesn't let my script to finish. Is there any other way of doing this in Bash?

The code looks like the following.

function startServer() {
  touch logfile
  startJavaprocess > logfile &

  tail -f logfile | while read line 
  do
    if echo $line | grep -q 'Started'; then
      echo 'Server Started'
      exit 0
    fi
  done
}
18 Answers

The best answer I can come up with is this

  1. Put a timeout on the read, tail -f logfile | read -t 30 line
  2. Start tail with --pid=$$, that way it'll exit when the bash-process has finished.

It'll cover all cases I can think of (server hangs with no output, server exits, server starts correctly).

Dont forget to start your tail before the server.

tail -n0 -F logfile 2>/dev/null | while read -t 30 line

the -F will 'read' the file even if it doesn't exist (start reading it when it appears). The -n0 won't read anything already in the file, so you can keep appending to the logfile instead of overwriting it each time, and to standard log rotation on it.

EDIT:
Ok, so a rather crude 'solution', if you're using tail. There are probably better solutions using something else but tail, but I got to give it to you, tail gets you out of the broken-pipe quite nicely. A 'tee' which is able to handle SIGPIPE would probably work better. The java process actively doing a file system drop with an 'im alive' message of some sort is probably even easier to wait for.

function startServer() {
  touch logfile

  # 30 second timeout.
  sleep 30 &
  timerPid=$!

  tail -n0 -F --pid=$timerPid logfile | while read line 
  do
    if echo $line | grep -q 'Started'; then
      echo 'Server Started'
      # stop the timer..
      kill $timerPid
    fi
  done &

  startJavaprocess > logfile &

  # wait for the timer to expire (or be killed)
  wait %sleep
}

Based on the answers I found here, this is what I've come up with.

It directly deals with tail and kills it once we've seen the needed log output. Using 'pkill -P $$ tail' should ensure that the right process is killed.

wait_until_started() {
    echo Waiting until server is started
    regex='Started'
    tail logfile -n0 -F | while read line; do
            if [[ $line =~ $regex ]]; then
                    pkill -9 -P $$ tail
            fi
    done
    echo Server is started
}

According to the tail man page, you can get tail to terminate after the a process dies

In BASH, you can get the PID of the last started background process using $! SO if you're using bash:

tail -f --pid=$! logfile

Capture the pid of the background process

pid=$!

Use tail's --pid=PID option, so that it terminates after the process having pid $PID terminates.

Rather than exiting the process, you can instead find the process ID of the tail -f process and kill it (a kill -9 would even be safe here if you're sure the log file has finished).

That way, the while read line will terminate naturally and you won't need to exit.

Or, since you're not really using the tail to output to the screen, you could also try the more old-school:

grep -q 'Started' logfile
while [[ $? -ne 0 ]] ; do
    sleep 1
    grep -q 'Started' logfile
done

How about using an infinite loop instead of the -f command-line option for tail?

function startServer() {
  startJavaprocess > logfile &

  while [ 1 ]
  do
   if tail logfile | grep -q 'Started'; then
    echo 'Server started'
    exit 0
   fi
  done
}

My preferred solution for this problem is to put the 'tail' command and its consumer into a subshell, and let the filter logic kill the parent and its children (which includes the tail process). If you look at the process tree, it will be:

startServer (pid=101)
   startServer (pid=102) << This is the subshell created by using parens "(...)"
      tail -f logfile (pid=103) << Here's the tail process
      startServer (pid=104)     << Here's the logic that detects the end-marker

In this approach, the end-marker detection logic (pid 104) looks for its parent PID (102), and all of its children, and kills the whole batch -- including itself. Then the grandparent (pid 101 above) is free to continue.

function startServer() {
  touch logfile
  startJavaprocess > logfile &

  tail -f logfile | while read line 
  do
    if echo $line | grep -q 'Started'; then
      echo 'Server Started'
      mypid=$BASHPID
      pipeParent=$(awk '/^PPid/ {print $2}' /proc/$mypid/status)
      kill -TERM $pipeParent $(pgrep -P $pipeParent)  # Kill the subshell and kids
    fi
  done
}

# To invoke startServer(), add a set of parens -- that puts it in a subshell:
(startServer())

Don't use tail - you can get the same 'monitor the newest thing in the file' using read.

Here I use a FIFO instead of the log file:

function startServer() {
  mkfifo logfile
  startJavaprocess > logfile &

  a=""; while [ "$a" != "Started" ]; do read <logfile a; done

  echo "Server Started"
}

Note that this leaves a FIFO hanging around.

This should work and tail should die once the sub shell dies


function startServer() {
  touch logfile
  startJavaprocess > logfile &

  while read line 
  do
    if echo $line | grep -q 'Started'; then
      echo 'Server Started'
      exit 0
    fi
  done < <(tail -f logfile)
}

Try this:

function startServer() {
  while read line 
  do
    if echo $line | grep -q 'Started'; then
      echo 'Server Started'
      return 0
    fi
  done < <(startJavaprocess | tee logfile)
}

Run the previous command with nohup.

In my case, Run java -jar with nohup,such as

nohup java -jar trade.jar xx.jar &

there will no log output,but a new "nohup.out" will created. The original log file trade.log works as well.

Then , tail -f trade.log, the shell will show log info , Ctrl-c can interrupt it ,return to shell.

Related