バックグラウンドで実行中のプロセスから 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-mydaemonmydaemonは、準備完了を示すまで戻りません。ここで、mydaemonの stdout と stderr はログファイルに送られ、 の stdin は からリダイレクトされます/dev/nullsetsid -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 -fLinuxではstdinで動作しているため、ファイル内に新しいデータが利用可能になったときにinotifyを使用せず、通常の方法に頼ることに注意してください。毎秒チェックするつまり、readyログ ファイルで検出するのに最大 1 秒余分にかかる可能性があります。

答え2

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

それはあなたが思っているようには機能しません。

のstderr$PID

  • 制御端末の場合、ユーザーが入力した文字列を、おそらく制御端末の標準入力$PID
  • パイプの場合、すでにパイプから読み込んでいる人と競合することになり、大混乱に陥ることになります。
  • /dev/null-- EOF!
  • 何か他のもの ;-) ?

実行中のプロセスからファイル記述子を他の場所にリダイレクトする方法はハッキーな方法しかないため、入力待機コードをダウングレードしてバック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うろつかせるよりもさらに悪い状況です。

もちろん、最善の方法は、初期化後にバックグラウンドで実行し、std ファイル記述子を閉じ、エラーの印刷などに/tmp/daemon使用する実際のデーモンとして記述することです。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 ##デーモンの出力を混在させて表示したくない場合の代替手段です。デーモンが出力時に SIGPIPE を取得するのを防ぐ唯一のものは cat であるため、cat を省略できないことに注意してください。

入力を別のファイル記述子 ( exec) にリダイレクトしないようにしてみましたが、そうしないと何かが入力を閉じ、デーモンが終了します。

また、デーモンを明示的にバックグラウンドにしないようにしたことにも注意してください。パイプがあれば、これは必要ありません。waitデーモンではなく、cat を待機します。デーモンがバックグラウンドになっている場合でも、動作します。

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 から何かを読み取るまでデーモンはブロックされることに注意してください。

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

関連情報