如何从后台运行的进程中读取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 ##如果您不想显示混合的守护进程的输出,则标记的部分是替代方案。请注意,您不能省略 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

相关内容