Wie lese ich stderr aus dem im Hintergrund laufenden Prozess?

Wie lese ich stderr aus dem im Hintergrund laufenden Prozess?

Ich möchte den Daemon in den Hintergrund schicken und die Ausführung meines Skripts erst fortsetzen, wenn der Daemon die bestimmte Zeile an stderr ausgibt, etwas zwischen den Zeilen:

# 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

Antwort1

Mit einem mydaemon, das sich wie folgt verhält:

#! /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

Das heißt, die Initialisierung dauert 4 Sekunden, es gibt eine Ausgabe aus, readywenn es bereit ist, und von da an arbeitet alles im selben Prozess weiter. Sie könnten ein start-mydaemonSkript wie das folgende schreiben:

#! /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-mydaemonkehrt erst zurück, wenn mydaemonangezeigt wurde, dass es bereit ist. Dabei mydaemongehen stdout und stderr in eine Protokolldatei, während stdin von umgeleitet wird /dev/null. setsid -f(kein Standardbefehl, aber in den meisten Linux-Distributionen vorhanden) sollte garantieren, dass der Daemon vom Terminal getrennt ist (falls er von einem gestartet wurde).

Beachten Sie jedoch, dass, wenn mydaemondie Initialisierung fehlschlägt und das Skript beendet wird, ohne jemals geschrieben zu haben ready, das Skript ewig auf ein wartet, readydas nie kommt (oder beim nächsten mydaemonerfolgreichen Start kommt).

Beachten Sie auch, dass sh -c ...tailund der Daemon gleichzeitig gestartet werden. Wenn zum Zeitpunkt des Starts mydaemonbereits initialisiert und gedruckt wurde und bis zum Ende der Protokolldatei gesucht wurde, wird die Meldung übersehen .readytailtailready

Sie könnten diese Probleme etwa folgendermaßen angehen:

#! /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"
}

Allerdings wird das langsam ziemlich kompliziert. Beachten Sie auch, dass tail -fes jetzt auf stdin läuft, unter Linux wird es inotify nicht verwenden, um zu erkennen, wann neue Daten in der Datei verfügbar sind, und auf das übliche zurückgreifenjede Sekunde prüfenDies bedeutet, dass es bis zu einer zusätzlichen Sekunde dauern kann, bis es readyin der Protokolldatei erkannt wird.

Antwort2

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

Das funktioniert nicht so, wie Sie denken.

Der Standardfehler von $PIDist entweder

  • das steuernde TTY, in diesem Fall werden Sie versuchen, eine vom Benutzer eingegebene Zeichenfolge zu lesen, was wahrscheinlich auch derstdinvon$PID
  • eine Pipe - Sie würden mit demjenigen konkurrieren, der bereits daraus liest, was zu einem kompletten Durcheinander führen würde
  • /dev/null-- Schluss mit dem Teufel!
  • etwas anderes ;-) ?

Es gibt nur Hacks, um einen Dateideskriptor aus einem laufenden Prozess an einen anderen Ort umzuleiten. Daher ist es am besten, wenn Sie Ihren auf die Eingabe wartenden Code so herunterstufen, dass er cat >/dev/nullim Hintergrund ausgeführt wird.

Dies „wartet“ beispielsweise, bis der Daemon Folgendes ausgibt 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
%

Danach /tmp/daemonwird mit dem Schreiben fortgefahren cat >/dev/null &, außerhalb der Kontrolle der Shell.

Eine andere Lösung besteht darin, den Standardfehler des Daemons auf eine normale Datei umzuleiten und tail -fdarauf zu klicken. Dadurch füllt der Daemon Ihre Festplatte jedoch weiterhin mit Datenmüll (selbst wenn Sie rmdie Datei haben, wird der von ihr belegte Speicherplatz erst freigegeben, wenn der Daemon sie schließt). Dies ist noch schlimmer, als wenn eine Datei mit geringen Ressourcen catherumliegt.

Am besten ist es natürlich, es /tmp/daemonals echten Daemon zu schreiben, der sich nach der Initialisierung selbst in den Hintergrund stellt, seine Standarddateideskriptoren schließt, syslog(3)zum Drucken von Fehlern verwendet usw.

Antwort3

Andere Antworten beinhalten die Verwendung von Dateien oder benannten Pipes. Sie brauchen beides nicht. Eine einfache Pipe reicht aus.

#!/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
)

Die markierten Abschnitte ## or ##sind Alternativen, wenn Sie die Ausgabe des Daemons nicht eingemischt anzeigen möchten. Beachten Sie, dass Sie die Katze nicht weglassen können, da sie das einzige ist, was verhindert, dass der Daemon bei der Ausgabe ein SIGPIPE erhält.

Ich habe versucht, die Eingabe nicht auf einen anderen Dateideskriptor (den exec) umzuleiten, aber ohne das schließt etwas die Eingabe und der Daemon wird beendet.

Möglicherweise fällt Ihnen auch auf, dass ich den Daemon nicht explizit in den Hintergrund gestellt habe. Mit der Pipe ist das nicht nötig. Er waitwartet auf die Katze, nicht auf den Daemon. Er funktioniert trotzdem, wenn der Daemon im Hintergrund ist.

waitist analog zu fg, steuert die Konsole aber nicht. (Es ist auch viel älter.)

Antwort4

mkfifokann tun, was Sie brauchen. Siehe diese Antwort: https://stackoverflow.com/questions/43949166/reading-value-of-stdout-and-stderr

Verwenden Sie stattdessen ein FIFO

Technisch gesehen sind /dev/stdout und /dev/stderr eigentlich Dateideskriptoren, keine FIFOs oder benannten Pipes. Auf meinem System sind sie eigentlich nur symbolische Links zu /dev/fd/1 und /dev/fd/2. Diese Deskriptoren sind normalerweise mit Ihrem TTY oder PTY verknüpft. Sie können sie also nicht so lesen, wie Sie es versuchen.

Dieser Code macht das, was Sie suchen (ich habe die Verarbeitungsschritte entfernt). In der Schleife, die liest, ist kein Ruhezustand erforderlich. Beachten Sie, dass der Daemon, wenn der Code zum Lesen aus dem FIFO gestoppt wurde, weiterhin versucht, in den FIFO zu schreiben, bis er voll ist, und der Daemon blockiert, bis etwas aus dem FIFO gelesen wird.

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

verwandte Informationen