Окончание tail -f запущено в сценарии оболочки

У меня есть следующее.

  1. Процесс Java записывает журналы в стандартный вывод
  2. Сценарий оболочки, запускающий процесс Java
  3. Другой сценарий оболочки, который выполняет предыдущий и перенаправляет журнал
  4. Я проверяю файл журнала с помощью команды tail -f на наличие сообщения об успешном завершении.

Даже если у меня есть выход 0 в коде, я не могу завершить процесс tail -f.

Что не позволяет моему сценарию завершиться. Есть ли другой способ сделать это в Bash?

Код выглядит следующим образом.

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
}

person rangalo    schedule 11.01.2010    source источник
comment
не очень хороший ответ, но хвост умрет на сломанной трубе при первом выходе после того, как ваш скрипт закончился.   -  person falstro    schedule 11.01.2010
comment
Проблема в том, что происходит, если java-процесс после запуска ничего не пишет, кажется, что хвост ждет вечность.   -  person rangalo    schedule 11.01.2010
comment
Это когда-нибудь эхо "Сервер запущен"?   -  person Alex Brown    schedule 11.01.2010
comment
будьте осторожны, запустите свой хвост перед запуском сервера (с -n0, чтобы не читать старые записи). В противном случае он может быть уже вне хвоста «прокрутки», который дает вам (по умолчанию пять строк, я полагаю)   -  person falstro    schedule 11.01.2010
comment
@ Алекс Браун, да! Он повторяет «Сервер запущен». И петля тоже закончена. Я проверил это.   -  person rangalo    schedule 11.01.2010
comment
@rangalo: если сервер ничего не печатает, вы можете поставить тайм-аут на чтение, read -t 30 line будет ждать тридцать секунд после каждой прочитанной строки, а затем, если читать больше нечего, возвращает false (выход из петля)   -  person falstro    schedule 11.01.2010
comment
Это проблема, если я коснусь файла перед перенаправлением журналов на него. Я редактирую код с помощью сенсорной команды.   -  person rangalo    schedule 11.01.2010


Ответы (18)


Лучший ответ, который я могу придумать, это

  1. Поставьте тайм-аут на чтение, tail -f logfile | read -t 30 line
  2. Начните хвост с --pid=$$, таким образом, он завершится, когда процесс bash завершится.

Он охватывает все случаи, которые я могу придумать (сервер зависает без вывода, сервер завершает работу, сервер запускается правильно).

Не забудьте запустить свой хвост перед сервером.

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

-F "прочитает" файл, даже если он не существует (начните читать его, когда он появится). -n0 не будет ничего читать уже в файле, поэтому вы можете продолжать добавлять в файл журнала вместо того, чтобы каждый раз перезаписывать его, и использовать стандартную ротацию журнала.

EDIT:
Итак, довольно грубое «решение», если вы используете хвост. Вероятно, есть лучшие решения, использующие что-то другое, кроме хвоста, но я должен вам его дать, хвост довольно хорошо вытаскивает вас из сломанной трубы. Тройник, способный работать с SIGPIPE, вероятно, будет работать лучше. Java-процесс, активно выполняющий сброс файловой системы с каким-то сообщением «я жив», вероятно, еще легче дождаться.

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
}
person falstro    schedule 11.01.2010
comment
это не сработало, $$ был процессом bash -hB, который всегда запущен - person rangalo; 11.01.2010
comment
Понятно, я полагал, что вы запускаете скрипт для запуска своего сервера, а не то, что у вас есть один, который выполняет и запуск, и остановку. Но в любом случае, это должно быть решаемо... Я подумаю и вернусь к вам. - person falstro; 11.01.2010
comment
В настоящее время я не в системе, но скоро попробую и дам вам знать. - person rangalo; 12.01.2010
comment
Спасибо. Это работает, но только если я запускаю скрипт перед хвостом и циклом. В противном случае я получаю только тайм-аут без grep. Другим недостатком является то, что я получаю вывод от bash для kill и если жду, потому что сон уже умер. - person rangalo; 13.01.2010
comment
Пока это самый подходящий ответ для меня. - person rangalo; 13.01.2010

Основываясь на ответах, которые я нашел здесь, вот что я придумал.

Он напрямую имеет дело с хвостом и убивает его, как только мы видим необходимый вывод журнала. Использование «pkill -P $$ tail» должно гарантировать, что нужный процесс будет уничтожен.

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
}
person David Resnick    schedule 30.01.2011
comment
Это жемчужина! Действительно искал по всему интернету для этого. Я считаю, что это должен быть принятый ответ :) - person Ardee Aram; 04.12.2014
comment
Обновление: на самом деле, хотя это работает нормально в командной строке, почему-то это не так хорошо работает при запуске через SSH. Я заменил pkill -9 -P -$$ tail на killall tail. Немного грубый, но делает свою работу. - person Ardee Aram; 06.12.2014

Согласно справочной странице хвоста, вы можете заставить tail завершаться после процесс умирает

В BASH вы можете получить PID последнего запущенного фонового процесса, используя $! ТАК, если вы используете bash:

tail -f --pid=$! logfile
person Robert Christie    schedule 11.01.2010
comment
Проблема в том, что скрипт ждет. Я проделываю тот же трюк для сценария выключения, и он работает, потому что сценарий выключения завершается. Но сценарий запуска зависает. - person rangalo; 11.01.2010
comment
Это не остановит хвост, пока сервер не остановится, что может произойти через годы. Вместо этого используйте $$, он сработает при выходе из скрипта. - person falstro; 11.01.2010
comment
@roe, не могли бы вы немного уточнить? Моя проблема в том, что ни сервер, ни скрипт не завершаются - person rangalo; 11.01.2010

Захват pid фонового процесса

pid=$!

Используйте параметр хвоста --pid=PID, чтобы он завершался после завершения процесса с pid $PID.

person Alberto Zaccagni    schedule 11.01.2010

У меня была аналогичная ситуация, когда мне нужно было отслеживать журнал для «начатого» сообщения в течение разумного времени, и если оно не было найдено в течение этого времени, мне нужно выйти. Вот что я в итоге сделал.

wait_tomcat_start(){
WAIT=60
echo "Waiting for Tomcat to initialize for $WAIT seconds"

# Tail log file, do a while read loop with a timeout that checks for desired log status,
# if found kill the find and break the loop. If not found within timeout: the read -t will
# kill the while read loop and bounce to the OR statement that will in turn kill the tail 
# and echo some message to the console.
tail -n0 -f $SERVERLOG | while read -t $WAIT LINE || (pkill -f "tail -n0 -f" && echo "Tomcat did not start in a timely fashion! Please check status of tomcat!!!")
do
        echo "$LINE"
        [[ "${LINE}" == *"Server startup in"* ]] && pkill -f "tail -n0 -f" && break
done
}

Я не уверен, что это очень элегантный или даже лучший способ сделать это, но для меня он работает достаточно хорошо. Буду рад любым мнениям :)

person user985216    schedule 20.08.2012

Вместо выхода из процесса вы можете вместо этого найти идентификатор процесса tail -f и убить его (здесь kill -9 будет даже безопасно, если вы уверены, что файл журнала завершен).

Таким образом, while read line завершится естественным образом, и вам не нужно будет выходить.

Или, поскольку вы на самом деле не используете tail для вывода на экран, вы также можете попробовать более старую школу:

grep -q 'Started' logfile
while [[ $? -ne 0 ]] ; do
    sleep 1
    grep -q 'Started' logfile
done
person paxdiablo    schedule 11.01.2010
comment
Там может быть «Начато» из более ранних сессий, поэтому я бы не стал делать ставку на вашу старую версию. - person falstro; 11.01.2010
comment
Нет, не будет, потому что процесс java перезаписывает файл журнала, а не добавляет к нему. - person paxdiablo; 11.01.2010
comment
правильно, наверное, я не обращаю внимания.. чище было бы while ! grep -q 'Started' logfile; do sleep 1; done - person falstro; 11.01.2010

Как насчет использования бесконечного цикла вместо параметра командной строки -f для хвоста?

function startServer() {
  startJavaprocess > logfile &

  while [ 1 ]
  do
   if tail logfile | grep -q 'Started'; then
    echo 'Server started'
    exit 0
   fi
  done
}
person Flimm    schedule 11.01.2010
comment
Это неплохо, но есть риск, что вы можете пропустить строку «Начало», если, например, она написана между циклами, за которой следует достаточное количество строк, чтобы она не отображалась в следующем хвосте. - person paxdiablo; 11.01.2010
comment
Если вы хотите полностью устранить риск, вы можете выполнить поиск по всему файлу. - person Flimm; 11.01.2010

У меня была такая же проблема, не мог найти простого и хорошего решения. Я плохо разбираюсь в Python, но мне как-то удалось это решить:

wait_log.py:

#!/usr/bin/env python

from optparse import OptionParser
import os
import subprocess
import time

def follow(file):
    def file_size(file):
        return os.fstat(file.fileno())[6]
    def is_newLine(line):
        return line != None and line.find("\n") != -1;

    file.seek(0, os.SEEK_END)

    while True:
        if file.tell() > file_size(file):
            file.seek(0, os.SEEK_END)

        line_start = file.tell()
        line = file.readline()

        if is_newLine(line):
            yield line
        else:
            time.sleep(0.5)
            file.seek(line_start)

def wait(file_path, message):
    with open(file_path) as file:
        for line in follow(file):
            if line.find(message) != -1:
                break

def main():
    parser = OptionParser(description="Wait for a specific message in log file.", usage="%prog [options] message")
    parser.add_option("-f", "--file", help="log file")

    (options, args) = parser.parse_args()

    if len(args) != 1:
        parser.error("message not provided")

    if options.file == None:
        parser.error("file not provided")

    wait(options.file, args[0])

if __name__ == "__main__":
    main()
person user1008204    schedule 22.10.2011

Была аналогичная проблема, когда хвостовой процесс не убивался, когда

  1. Запустить через jsch
  2. tail не производил никакого вывода для jsch и, следовательно, для его выходного потока.

Использовал --pid=$!, чтобы убить его, и запустил бесконечный цикл while, чтобы отобразить что-то в фоновом режиме перед хвостом, который уничтожается, когда основной процесс уничтожается и, таким образом, убивает хвост.

( while true; do echo 'running';  sleep 5; done ) & ( tail -f --pid=$! log-file )
person CondorEye    schedule 31.07.2012

Можно запустить tail -f logfile в фоновом режиме, отправить tailpid в подоболочку цикла while read и реализовать trap при выходе, чтобы убить команду tail.

( (sleep 1; exec tail -f logfile) & echo $! ; wait) | (
  trap 'trap - EXIT; kill "$tailpid"; exit' EXIT
  tailpid="$(head -1)"
  while read line 
  do
    if echo $line | grep -q 'Started'; then
      echo 'Server Started'
      exit 0
    fi
  done
)
person nurro    schedule 21.03.2016

Это должно работать, и хвост должен умереть, как только умрет подоболочка.


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)
}
person rcarson    schedule 12.01.2010

Не используйте tail — вы можете получить то же самое «отслеживание новейших вещей в файле», используя read.

Здесь я использую FIFO вместо файла журнала:

function startServer() {
  mkfifo logfile
  startJavaprocess > logfile &

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

  echo "Server Started"
}

Обратите внимание, что это оставляет FIFO висящим без дела.

person Alex Brown    schedule 11.01.2010
comment
Это убьет процесс java (сломанный канал) при удалении fifo или во втором примере при выходе из цикла. Вероятно, не то, что вы хотели. - person falstro; 11.01.2010
comment
правда, я заметил терминацию во втором случае, но не в первом. простое исправление для первого, я удалю второе, пока не придумаю способ обойти это. - person Alex Brown; 11.01.2010

Использование tail -n0 -f конвейера для grep действительно хорошее решение, и действительно, первый процесс в конвейере умрет, когда он попытается вывести в мертвый процесс grep.

Но если вы ищете текст, который появляется рядом с последним текущим выводом хвоста, то grep уже прочитает весь ввод из хвоста (в одном блоке), и поэтому в выводе больше не будет текста. log, который нужно отправить по каналу, так как grep уже прочитал его перед выходом (или, может быть, он уже был в буфере канала) - по крайней мере, я так понимаю.

Использование параметра «-m1» в grep выглядит так, как будто он сделает именно то, что вы хотите, и оставит ввод сразу после строки, с которой он совпал, но, похоже, это не имело значения и не помогло мне в поиске аналогичной функциональности. Я подозреваю, что буфер канала по-прежнему содержит весь вывод текста из хвоста или по какой-то другой причине, по которой у хвоста не осталось ничего для вывода. Вы хотели, чтобы этот текст post-grep-match все еще оставался для вывода следующим, потому что это убьет ваш хвост, когда он попытается (все еще рискованно - что произойдет, если по какой-то причине это последняя строка?), и вернет управление вызывающему сценарию. .

Я обнаружил, что один из способов — вывести что-либо в конец файла журнала после завершения работы grep; т.е.

хвост -f файл журнала | ( grep -q ; эхо >> файл журнала)

У меня есть теория о том, что (если мое предположение верно) вы можете заставить канал быть менее буферизованным, чтобы он работал без этого, или, может быть, добавление команды настройки huponexit к соответствующему компоненту канала, т.е. в (вероятно, фигурных) скобках помощь; но я не заботился о добавлении пустой строки в файл журнала, и он работал нормально, и это всего лишь меньший тестовый сценарий (так что это не долгоживущий файл журнала, который должен придерживаться формата для другой обработки).

shopt -s huponexit был бы полезен, если бы не подоболочка.

PS мой первый пост здесь, я хотел бы сделать это как комментарий к существующему ответу, а не повторять материал, но я не думаю, что могу сейчас.

person Breezer    schedule 27.06.2011
comment
PS это не зависит от слишком большой специфики bash - ‹ (файл) недоступен в более старых версиях оболочки ksh / других. - person Breezer; 28.06.2011

На первоначальный вопрос, почему команда выхода не завершилась, я получил ту же проблему и, наконец, нашел причину.

Используя режим отладки bash, я вижу, что команда выхода была вызвана, но процесс все еще завис до тех пор, пока еще одна строка не сбрасывается в файл журнала сразу после «Начало». Странно то, что когда вышло «Начало», даже был вызван выход, процесс все еще чем-то зацеплен. Я предполагаю, что это было tail -f, пока не выйдет еще одна строка, это действительно освободит крючок.

Поэтому, если вы напечатаете еще одну строку после ее запуска, ваш монитор сразу же выключится.

person Kevin    schedule 12.07.2012

Мое предпочтительное решение этой проблемы — поместить команду «tail» и ее потребителя в подоболочку и позволить логике фильтра убить родителя и его дочерние элементы (включая хвостовой процесс). Если посмотреть на дерево процессов, то оно будет таким:

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

В этом подходе логика обнаружения конечного маркера (pid 104) ищет свой родительский PID (102) и все его дочерние элементы и уничтожает весь пакет, включая себя. Затем прародитель (pid 101 выше) может продолжить работу.

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())
person Stabledog    schedule 03.12.2013

Используя комбинацию ответов, я придумал это простое решение. В этом примере вызывается сценарий запуска Tomcat.

#!/bin/bash

function logUntilStarted() {
    tail -n0 -F /home/tomcat/logs/catalina.out | while read line; do
        if echo $line && echo $line | grep -q 'Server startup' ; then
            pkill -9 -P $$ tail > /dev/null 2>&1
        fi
    done
}

/home/tomcat/bin/startup.sh
logUntilStarted
person Matt    schedule 22.03.2016

Выполните предыдущую команду с nohup.

В моем случае запустите java -jar с nohup, например

nohup java -jar trade.jar xx.jar &

лог не будет выводиться, но будет создан новый "nohup.out". Оригинальный файл журнала trade.log также работает.

Затем, tail -f trade.log, оболочка покажет информацию журнала, Ctrl-c может прервать ее, вернуться в оболочку.

person hatanooh    schedule 28.08.2018
comment
вопрос (и мой поиск в Google) ищут способы завершить этот хвост -f, когда файл журнала выполняется без доступа к оболочке. - person episodeyang; 27.02.2021

tail -n0 --pid=$(($BASHPID+1)) -F logfile | sed -n '/Started/{s/.*/Server Started/p; q}'

При передаче PID являются последовательными, поэтому pid хвоста будет $BASHPID, а pid sed будет $BASHPID+1. Переключатель --pid заставит tail выйти (правильно!), когда команда sed завершится. Эта команда sed будет искать /Started/, а затем заменит всю строку (.*) на «Server Started», а затем завершит работу.

person Christian Herr    schedule 21.04.2017