Como ler o stderr do processo em execução em segundo plano?

Como ler o stderr do processo em execução em segundo plano?

Quero enviar o daemon para segundo plano e apenas continuar a execução do meu script quando o daemon enviar a linha específica para o stderr, algo entre as linhas:

# 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

Responder1

Com um 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

Ou seja, que leva 4 segundos para inicializar, sai readyquando está pronto e continua funcionando a partir de então, tudo no mesmo processo, você poderia escrever um 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-mydaemonnão retorna até mydaemonindicar que estava pronto. Aqui, mydaemonstdout e stderr vão para um arquivo de log, enquanto seu stdin é redirecionado de /dev/null. setsid -f(não é um comando padrão, mas encontrado na maioria das distribuições Linux) deve garantir que o daemon seja desconectado do terminal (se iniciado a partir de um).

Observe, entretanto, que se mydaemonfalhar ao inicializar e morrer sem nunca escrever ready, esse script esperará para sempre por um readyque nunca virá (ou virá na próxima vez que mydaemonfor iniciado com sucesso).

Observe também que sh -c ...taile o daemon são iniciados simultaneamente. Se mydaemonjá tiver inicializado e impresso readyaté o momento tailde início e buscado até o final do arquivo de log, tailperderá a readymensagem.

Você poderia resolver esses problemas com 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"
}

Embora isso esteja começando a ser bastante complicado. Observe também que como tail -fagora está operando em stdin, no Linux, ele não usará o inotify para detectar quando novos dados estão disponíveis no arquivo e recorrerá ao habitualverifique a cada segundoo que significa que pode levar até um segundo a mais para ser detectado readyno arquivo de log.

Responder2

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

Isso não funciona como você pensa.

O stderr de $PIDé

  • o controle tty, caso em que você tentará ler uma string inserida pelo usuário no que provavelmente também é ostdinde$PID
  • um cachimbo - você competiria com quem já está lendo ele, resultando em uma bagunça completa
  • /dev/null-- EOF!
  • algo mais ;-) ?

Existem apenas maneiras hackeadas de redirecionar para outro lugar um descritor de arquivo de um processo em execução, portanto, sua melhor aposta é fazer com que seu código de espera de entrada seja rebaixado para uma cat >/dev/nullexecução em segundo plano.

Por exemplo, isso irá "esperar" até que o daemon produza 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
%

Depois disso, /tmp/daemoncontinuaremos escrevendo para cat >/dev/null &, fora do controle do shell.

Outra solução seria redirecionar o stderr do daemon para algum arquivo normal e tail -fnele, mas então o daemon continuará enchendo seu disco com lixo (mesmo que você tenha rmo arquivo, o espaço que ele ocupa não será liberado até que o daemon fecha), o que é ainda pior do que ter uma população com poucos recursos catvagando por aí.

Claro, a melhor coisa será escrever /tmp/daemoncomo um daemon real, que se coloca em segundo plano após a inicialização, fecha seus descritores de arquivo std, usa syslog(3)para erros de impressão, etc.

Responder3

Outras respostas envolvem o uso de arquivos ou pipes nomeados. Você também não precisa. Um simples cachimbo é 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
)

As seções marcadas ## or ##são alternativas se você não quiser mostrar a saída do daemon misturada. Observe que você não pode omitir o gato porque é a única coisa que impede o daemon de obter um SIGPIPE quando ele é gerado.

Tentei não redirecionar a entrada para um descritor de arquivo diferente (o exec), mas sem isso algo fecha a entrada e o daemon termina.

Você também pode notar que fiz com que o daemon não estivesse explicitamente em segundo plano. Com o tubo, não é necessário. Ele waitespera pelo gato, não pelo daemon. Ainda funciona se o daemon estiver em segundo plano.

waité análogo a fg, mas não faz com que o console controle. (Também é muito mais antigo.)

Responder4

mkfifopode fazer o que você precisa. Veja esta resposta: https://stackoverflow.com/questions/43949166/reading-value-of-stdout-and-stderr

Use um FIFO em vez disso

Tecnicamente, /dev/stdout e /dev/stderr são na verdade descritores de arquivos, não FIFOs ou pipes nomeados. No meu sistema, eles são apenas links simbólicos para /dev/fd/1 e /dev/fd/2. Esses descritores normalmente estão vinculados ao seu TTY ou PTY. Então, você realmente não pode ler eles da maneira que está tentando fazer.

Este código faz o que você procura (removi as etapas de processamento). Não é necessário dormir no loop que lê. Observe que quando a leitura do código do fifo for interrompida, o daemon continuará tentando escrever nele até que fique cheio e o daemon será bloqueado até que algo seja lido no 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

informação relacionada