Как прочитать stderr из процесса, работающего в фоновом режиме?

Как прочитать stderr из процесса, работающего в фоновом режиме?

Я хочу отправить демон в фоновый режим и продолжить выполнение скрипта только тогда, когда демон выведет определенную строку в stderr, что-то из строк:

# Fictional daemon
{
    for x in {1..9}; do
        sleep 1
        if [ "$x" != "5" ]; then
            echo $x 1>&2
        else
            echo now 1>&2
        fi
    done
} &

# Daemon PID
PID="$!"

# Wait until the particular output... 
until { read -r line && grep -q "now" <<<"$line"; } do 
    sleep 1
done </proc/$PID/fd/2

#
# Do more stuff...
#

fg

решение1

С , mydaemonкоторый ведет себя как:

#! /bin/sh -
i=1 stage=init
while true; do
  if [ "$i" -eq 5 ]; then
    echo >&2 ready
    stage=working
  fi
  echo >&2 "$stage $i"
  sleep 1
  i=$((i + 1))
done

То есть, инициализация занимает 4 секунды, вывод readyпо готовности и продолжение работы в том же процессе, вы можете написать start-mydaemonтакой скрипт:

#! /bin/sh -
DAEMON=./mydaemon
LOGFILE=mydaemon.log
umask 027

: >> "$LOGFILE" # ensures it exists
pid=$(
  sh -c 'echo "$$"; exec "$0" "$@"' tail -fn0 -- "$LOGFILE" | {
    IFS= read -r tail_pid
    export LOGFILE DAEMON
    setsid -f sh -c '
      echo "$$"
      exec "$DAEMON" < /dev/null >> "$LOGFILE" 2>&1'
    grep -q ready
    kill -s PIPE "$tail_pid"
  }
)
printf '%s\n' "$DAEMON started in process $pid and now ready"
$ time ./start-mydaemon
./mydaemon started in process 230254 and now ready
./start-mydaemon  0.01s user 0.01s system 0% cpu 4.029 total
$ ps -fjC mydaemon
UID          PID    PPID    PGID     SID  C STIME TTY          TIME CMD
chazelas  230254    6175  230254  230254  0 10:28 ?        00:00:00 /bin/sh - ./mydaemon

start-mydaemonне возвращается, пока mydaemonне укажет, что он готов. Здесь mydaemonstdout и stderr направляются в файл журнала, а его stdin перенаправляется из /dev/null. setsid -f(нестандартная команда, но есть в большинстве дистрибутивов Linux) должна гарантировать, что демон отсоединен от терминала (если запущен с одного из них).

Однако следует отметить, что если mydaemonинициализация не удалась и скрипт завершается, так и не записав ready, то он будет вечно ждать , readyкоторый никогда не появится (или появится при следующем mydaemonуспешном запуске).

Также обратите внимание, что sh -c ...tailи демон запускаются одновременно. Если к моменту запуска mydaemonуже инициализировался и напечатал и дошел до конца файла журнала, сообщение будет пропущено .readytailtailready

Вы можете решить эти проблемы с помощью чего-то вроде:

#! /bin/sh -
DAEMON=./mydaemon
LOGFILE=mydaemon.log
export DAEMON LOGFILE
umask 027

died=false ready=false canary_pid= tail_pid= daemon_pid=
: >> "$LOGFILE" # ensures it exists
{
  exec 3>&1
  {
    tail -c1 <&5 5<&- > /dev/null # skip to the end synchronously
    (
      sh -c 'echo "tail_pid=$$" >&3; exec tail -fn+1' |
        { grep -q ready && echo ready=true; }
    ) <&5 5<&- &
  } 5< "$LOGFILE"
  setsid -f sh -c '
    echo "daemon_pid=$$" >&3
    exec "$DAEMON" < /dev/null 3>&- 4>&1 >> "$LOGFILE" 2>&1' 4>&1 |
      (read anything; echo died=true) &
  echo "canary_pid=$!"
} | {
  while
    IFS= read -r line &&
      eval "$line" &&
      ! "$died" &&
      ! { [ -n "$daemon_pid" ] && "$ready" ; }
  do
    continue
  done
  if "$ready"; then
    printf '%s\n' "$DAEMON started in process $daemon_pid and now ready"
  else
    printf >&2 '%s\n' "$DAEMON failed to start"
  fi
  kill -s PIPE "$tail_pid" "$canary_pid" 2> /dev/null
  "$ready"
}

Хотя это начинает быть довольно запутанным. Также обратите внимание, что поскольку tail -fтеперь он работает на stdin, в Linux он не будет использовать inotify для обнаружения новых данных в файле и прибегнет к обычномупроверяйте каждую секундуreadyЭто означает, что для обнаружения в файле журнала может потребоваться до одной дополнительной секунды .

решение2

...
done </proc/$PID/fd/2

Это не работает так, как вы думаете.

Stderr — это $PIDлибо

  • управляющий tty, в этом случае вы попытаетесь прочитать строку, введенную пользователем, в то время как это, вероятно, такжестандартный вводиз$PID
  • трубка — вы будете конкурировать с тем, кто уже читает из нее, что приведет к полной неразберихе
  • /dev/null-- Конец света!
  • что-то другое ;-) ?

Существуют только хакерские способы перенаправить куда-либо дескриптор файла из запущенного процесса, поэтому лучшим вариантом будет перевести код ожидания ввода в режим cat >/dev/nullработы в фоновом режиме.

Например, это будет «ждать», пока демон не выведет 4:

% cat /tmp/daemon
#! /bin/sh
while sleep 1; do echo $((i=i+1)) >&2; done

% (/tmp/daemon 2>&1 &) | (sed /4/q; cat >/dev/null &)
1
2
3
4
%

После чего /tmp/daemonпродолжит запись в cat >/dev/null &, вне контроля оболочки.

Другим решением было бы перенаправить stderr демона в какой-нибудь обычный файл и tail -fна него, но тогда демон продолжит заполнять ваш диск мусором (даже если вы удалите rmфайл, занимаемое им место не освободится, пока демон его не закроет), что даже хуже, чем иметь файл с низким потреблением ресурсов, который будет слоняться catбез дела.

Конечно, лучше всего будет написать /tmp/daemonнастоящий демон, который уходит в фоновый режим после инициализации, закрывает свои дескрипторы файлов std, использует syslog(3)для печати ошибки и т. д.

решение3

Другие ответы предполагают использование файлов или именованных каналов. Вам не нужно ни то, ни другое. Достаточно простого канала.

#!/bin/bash

run_daemon() {
    # Fictional daemon
    {
        for x in {1..9}; do
            sleep 1
            if [ "$x" != "5" ]; then
                echo $x 1>&2
            else
                echo now 1>&2
            fi
        done
    }
}

run_daemon 2>&1 | (

    exec 9<&0 0</dev/null

    while read -u 9 line && test "$line" != now
    do
        echo "$line"    ## or ## :
    done
    echo "$line"        ## or ##
    cat /dev/fd/9 &     ## or ## cat /dev/fd/9 >/dev/null &

    #fictional stuff
    for a in 1 2 3
    do
        echo do_stuff $a
        sleep 1
    done
    echo done

    wait
)

Отмеченные разделы ## or ##являются альтернативными вариантами, если вы не хотите показывать смешанный вывод демона. Обратите внимание, что вы не можете пропустить cat, поскольку это единственное, что мешает демону получать SIGPIPE при выводе.

Я пробовал не перенаправлять ввод в другой файловый дескриптор ( exec), но без этого что-то закрывает ввод и демон завершает работу.

Вы также можете заметить, что я сделал демона явно не фоновым. С конвейером он не нужен. Ожидает waitкота, а не демона. Он все равно работает, если демон фоновым.

waitаналогичен fg, но не делает консоль управляющей. (Она также намного старше.)

решение4

mkfifoможет сделать то, что вам нужно. Смотрите этот ответ: https://stackoverflow.com/questions/43949166/чтение-значения-stdout-и-stderr

Вместо этого используйте FIFO

Технически, /dev/stdout и /dev/stderr на самом деле являются файловыми дескрипторами, а не FIFO или именованными каналами. В моей системе они на самом деле просто символические ссылки на /dev/fd/1 и /dev/fd/2. Эти дескрипторы обычно связаны с вашим TTY или PTY. Поэтому вы не можете читать из них так, как пытаетесь сделать.

Этот код делает то, что вы ищете (я удалил этапы обработки). В цикле чтения сон не нужен. Обратите внимание, что когда чтение кода из fifo остановлено, демон продолжит попытки записи в него, пока он не заполнится, и демон заблокируется, пока что-нибудь не прочитает из fifo.

fifo="daemon_errors"
{
    mkfifo $fifo
    for x in {1..9}; do
        sleep 1
        if [ "$x" != "5" ]; then
            echo $x 1>&2
        else
            echo now 1>&2
        fi
    done 2>$fifo
} &

sleep 1 # need to wait until the file is created

# Read all output 
while read -r line; do
    echo "just read: $line"
done < $fifo

Связанный контент