バックグラウンドで実行されているプロセスから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

つまり、初期化してready準備ができたら出力し、作業を続けるのに4秒かかります。すべて同じプロセスで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準備ができたことを示すまで返されません。mydaemonstdoutと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 -f、Linuxのstdinで実行されるので、ファイルに新しいデータがあるかどうかを検出するためにinotifyを使用せずに一般的な方法を使用します。毎秒確認してくださいreadyこれは、ログファイルで検出するのにさらに時間がかかることを意味します。

答え2

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

これが思ったよりうまくいかなかったんです。

標準エラー$PID

  • ttyを制御します。この場合、ユーザーが入力した文字列を読み取ろうとします。標準入力~の$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

もちろん、最良の方法は、/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 ##混合デーモンの出力を表示したくない場合は、表示されているセクションが選択肢です。 catはデーモンが出力時にSIGPIPEを取得するのを防ぐ唯一の方法なので、省略することはできません。

入力を別のファイル記述子(exec)にリダイレクトしようとしませんでしたが、これがない場合は入力が閉じてデーモンが終了します。

また、私が明示的にデーモンを背景に設定していないことを知っているかもしれません。パイプを使用する必要はありません。waitデーモンではなく猫が待っています。デーモンがバックグラウンドにある場合は動作し続けます。

waitに似ていますfgが、コンソール制御を有効にしません。 (また、はるかに古いです。)

答え4

mkfifo必要なことができます。この回答をご覧ください。 https://stackoverflow.com/questions/43949166/reading-value-of-stdout-and-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

関連情報