¿Cómo leer stderr del proceso que se ejecuta en segundo plano?

¿Cómo leer stderr del proceso que se ejecuta en segundo plano?

Quiero enviar el demonio a un segundo plano y solo continuar con la ejecución de mi script cuando el demonio envíe la línea particular al stderr, algo entre las líneas:

# 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

Respuesta1

Con un mydaemonque se comporta como:

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

Es decir, que tarda 4 segundos en inicializarse, sale readycuando está listo y continúa trabajando desde entonces, todo en el mismo proceso, podría escribir un start-mydaemonscript como:

#! /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-mydaemonno regresa hasta que mydaemonhaya indicado que estaba listo. Aquí, mydaemonstdout y stderr van a un archivo de registro, mientras que su stdin se redirige desde /dev/null. setsid -f(no es un comando estándar, pero se encuentra en la mayoría de las distribuciones de Linux) debería garantizar que el demonio esté desconectado de la terminal (si se inicia desde una).

Sin embargo, tenga en cuenta que si mydaemonno se inicializa y muere sin siquiera escribir ready, ese script esperará para siempre hasta que readynunca llegará (o llegará la próxima vez que mydaemonse inicie correctamente).

También tenga en cuenta que sh -c ...taily el demonio se inician simultáneamente. Si mydaemonya se ha inicializado e impreso readycuando tailha comenzado el tiempo y ha buscado hasta el final del archivo de registro, tailse perderá el readymensaje.

Podrías abordar esos problemas con algo como:

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

Aunque eso está empezando a ser bastante complicado. También tenga en cuenta que, dado que tail -fahora funciona en stdin, en Linux, no utilizará inotify para detectar cuándo hay nuevos datos disponibles en el archivo y recurrirá a lo habitual.comprobar cada segundolo que significa que puede tardar hasta un segundo más en detectarse readyen el archivo de registro.

Respuesta2

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

Eso no funciona como crees.

El estándar de $PIDes

  • el tty controlador, en cuyo caso intentará leer una cadena ingresada por el usuario en lo que probablemente también sea elentrada estándarde$PID
  • una pipa: competirías con quien ya esté leyendo en ella, lo que resultaría en un completo desastre
  • /dev/null--¡EOF!
  • algo más ;-) ?

Solo hay formas ingeniosas de redirigir a otro lugar un descriptor de archivo desde un proceso en ejecución, por lo que lo mejor que puede hacer es hacer que su código de espera de entrada se degrade y se ejecute cat >/dev/nullen segundo plano.

Por ejemplo, esto "esperará" hasta que el demonio genere 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
%

Después de lo cual /tmp/daemoncontinuará escribiendo en cat >/dev/null &, fuera del control de Shell.

Otra solución sería redirigir el stderr del demonio a algún archivo normal y tail -fen él, pero luego el demonio seguirá llenando su disco con basura (incluso si guarda rmel archivo, el espacio que ocupa no se liberará hasta que el demonio lo cierra), lo cual es incluso peor que tener a una persona de bajos recursos catholgazaneando.

Por supuesto, lo mejor será escribir /tmp/daemoncomo un demonio real, que se pone en segundo plano después de la inicialización, cierra sus descriptores de archivos estándar, los utiliza syslog(3)para errores de impresión, etc.

Respuesta3

Otras respuestas implican el uso de archivos o canalizaciones con nombre. No necesitas tampoco. Una simple pipa es suficiente.

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

Las secciones marcadas ## or ##son alternativas si no desea mostrar la salida del demonio mezclada. Tenga en cuenta que no puede omitir el gato porque es lo único que impide que el demonio obtenga un SIGPIPE cuando sale.

Intenté no redirigir la entrada a un descriptor de archivo diferente (el exec), pero sin eso, algo cierra la entrada y el demonio finaliza.

También puedes notar que hice que el demonio no estuviera explícitamente en segundo plano. Con la tubería no es necesario. Espera waital gato, no al demonio. Todavía funciona si el demonio está en segundo plano.

waites análogo a fg, pero no hace que la consola controle. (También es mucho más antiguo).

Respuesta4

mkfifopuede hacer lo que necesita. Vea esta respuesta: https://stackoverflow.com/questions/43949166/reading-value-of-stdout-and-stderr

Utilice un FIFO en su lugar

Técnicamente, /dev/stdout y /dev/stderr son en realidad descriptores de archivos, no FIFO ni canalizaciones con nombre. En mi sistema, en realidad son solo enlaces simbólicos a /dev/fd/1 y /dev/fd/2. Esos descriptores suelen estar vinculados a su TTY o PTY. Por lo tanto, realmente no puedes leerlos de la manera que estás intentando hacerlo.

Este código hace lo que estás buscando (eliminé los pasos de procesamiento). No es necesario dormir en el bucle que lee. Tenga en cuenta que cuando la lectura de código del quince se detiene, el demonio continuará intentando escribir en él hasta que se llene y el demonio se bloqueará hasta que algo se lea del quince.

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

información relacionada