如何從後台運行的進程中讀取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/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現在在 Linux 上的 stdin 上運行,因此它不會使用 inotify 來檢測文件中何時有新資料可用,並訴諸通常的方法每秒檢查一次這意味著可能需要額外一秒鐘的時間才能ready在日誌檔案中進行檢測。

答案2

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

這並不像你想像的那樣有效。

的 stderr$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 &,不受 shell 控制。

另一個解決方案是將守護程序的 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 ##如果您不想顯示混合的守護程序的輸出,則標記的部分是替代方案。

我嘗試不將輸入重定向到不同的檔案描述符(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

相關內容