Colete códigos de saída de processos em segundo plano paralelos (sub shells)

16

Digamos que tenhamos um script básico assim:

echo "x" &
echo "y" &
echo "z" &
.....
echo "Z" &
wait

Existe uma maneira de coletar os códigos de saída dos subprocessos / subprocessos? Procurando uma maneira de fazer isso e não consigo encontrar nada. Eu preciso executar esses subshells em paralelo, caso contrário, sim, isso seria mais fácil.

Estou à procura de uma solução genérica (tenho um número desconhecido / dinâmico de subprocessos para executar em paralelo).

    
por Alexander Mills 12.02.2017 / 09:31

7 respostas

3

A resposta de Alexander Mills, que usa o handleJobs, me deu um ótimo ponto de partida, mas também me deu esse erro

warning: run_pending_traps: bad value in trap_list[17]: 0x461010

O que pode ser um problema de condição de corrida bash

Em vez disso, eu apenas armazenei pid de cada criança e esperei e obtive o código de saída para cada criança especificamente. Eu acho esse limpador em termos de subprocessos gerando subprocessos em funções e evitando o risco de esperar por um processo pai onde eu pretendia esperar por um filho. É mais claro o que acontece porque não está usando a armadilha.

#!/usr/bin/env bash

# it seems it does not work well if using echo for function return value, and calling inside $() (is a subprocess spawned?) 
function wait_and_get_exit_codes() {
    children=("$@")
    EXIT_CODE=0
    for job in "${children[@]}"; do
       echo "PID => ${job}"
       CODE=0;
       wait ${job} || CODE=$?
       if [[ "${CODE}" != "0" ]]; then
           echo "At least one test failed with exit code => ${CODE}" ;
           EXIT_CODE=1;
       fi
   done
}

DIRN=$(dirname "$0");

commands=(
    "{ echo 'a'; exit 1; }"
    "{ echo 'b'; exit 0; }"
    "{ echo 'c'; exit 2; }"
    )

clen='expr "${#commands[@]}" - 1' # get length of commands - 1

children_pids=()
for i in 'seq 0 "$clen"'; do
    (echo "${commands[$i]}" | bash) &   # run the command via bash in subshell
    children_pids+=("$!")
    echo "$i ith command has been issued as a background job"
done
# wait; # wait for all subshells to finish - its still valid to wait for all jobs to finish, before processing any exit-codes if we wanted to
#EXIT_CODE=0;  # exit code of overall script
wait_and_get_exit_codes "${children_pids[@]}"

echo "EXIT_CODE => $EXIT_CODE"
exit "$EXIT_CODE"
# end
    
por 11.04.2018 / 09:06
18

Use wait com um PID, que irá:

Wait until the child process specified by each process ID pid or job specification jobspec exits and return the exit status of the last command waited for.

Você precisará salvar o PID de cada processo à medida que avança:

echo "x" & X=$!
echo "y" & Y=$!
echo "z" & Z=$!

Você também pode habilitar o controle de tarefas no script com set -m e usar %n jobspec, mas quase certamente não deseja - controle de trabalho tem muitos outros efeitos colaterais .

wait retornará o mesmo código que o processo terminou. Você pode usar wait $X em qualquer ponto (razoável) depois para acessar o código final como $? ou simplesmente usá-lo como verdadeiro / falso:

echo "x" & X=$!
echo "y" & Y=$!
...
wait $X
echo "job X returned $?"

wait fará uma pausa até que o comando seja concluído, se ainda não o tiver feito.

Se você quiser evitar esse tipo de paralisação, poderá definir um trap no SIGCHLD , conte o número de terminações e processe todos os wait s de uma só vez quando todos tiverem terminado. Você provavelmente pode usar wait sozinho quase o tempo todo.

    
por 12.02.2017 / 10:25
6

Se você tiver uma boa maneira de identificar os comandos, poderá imprimir o código de saída em um arquivo tmp e acessar o arquivo específico em que está interessado:

#!/bin/bash

for i in 'seq 1 5'; do
    ( sleep $i ; echo $? > /tmp/cmd__${i} ) &
done

wait

for i in 'seq 1 5'; do # or even /tmp/cmd__*
    echo "process $i:"
    cat /tmp/cmd__${i}
done

Não se esqueça de remover os arquivos tmp.

    
por 14.02.2017 / 09:05
4

Use um compound command - coloque a afirmação entre parênteses:

( echo "x" ; echo X: $? ) &
( true ; echo TRUE: $? ) &
( false ; echo FALSE: $? ) &

fornecerá a saída

x
X: 0
TRUE: 0
FALSE: 1

Uma maneira realmente diferente de executar vários comandos em paralelo é usando GNU Parallel . Faça uma lista de comandos para executar e coloque-os no arquivo list :

cat > list
sleep 2 ; exit 7
sleep 3 ; exit 55
^D

Execute todos os comandos em paralelo e colete os códigos de saída no arquivo job.log :

cat list | parallel -j0 --joblog job.log
cat job.log

e a saída é:

Seq     Host    Starttime       JobRuntime      Send    Receive Exitval Signal  Command
1       :       1486892487.325       1.976      0       0       7       0       sleep 2 ; exit 7
2       :       1486892487.326       3.003      0       0       55      0       sleep 3 ; exit 55
    
por 12.02.2017 / 10:11
1

Outra variação da resposta do @rolf:

Outra maneira de salvar o status de saída seria algo como

mkdir /tmp/status_dir

e, em seguida, cada script

script_name="${0##*/}"  ## strip path from script name
tmpfile="/tmp/status_dir/${script_name}.$$"
do something
rc=$?
echo "$rc" > "$tmpfile"

Isso fornece um nome exclusivo para cada arquivo de status, incluindo o nome do script que o criou e seu ID de processo (caso mais de uma instância do mesmo script esteja em execução), que você pode salvar para referência posterior e colocá-los tudo no mesmo lugar para que você possa excluir todo o subdiretório quando terminar.

Você pode até salvar mais de um status de cada script fazendo algo como

tmpfile="$(/bin/mktemp -q "/tmp/status_dir/${script_name}.$$.XXXXXX")"

que cria o arquivo como antes, mas adiciona uma string aleatória exclusiva a ele.

Ou você pode adicionar mais informações de status ao mesmo arquivo.

    
por 18.02.2017 / 20:38
1

script3 será executado somente se script1 e script2 forem bem-sucedidos e script1 e script2 forem executados em paralelo:

./script1 &
process1=$!

./script2 &
process2=$!

wait $process1
rc1=$?

wait $process2
rc2=$?

if [[ $rc1 -eq 0 ]] && [[ $rc2 -eq 0  ]];then
./script3
fi
    
por 28.03.2018 / 22:15
0

este é o script genérico que você está procurando. A única desvantagem é que seus comandos estão entre aspas, o que significa que o realce de sintaxe através do seu IDE não funcionará realmente. Caso contrário, tentei algumas das outras respostas e esta é a melhor. Esta resposta incorpora a idéia de usar wait <pid> dada por @Michael, mas vai um passo além usando o comando trap que parece funcionar melhor.

#!/usr/bin/env bash

set -m # allow for job control
EXIT_CODE=0;  # exit code of overall script

function handleJobs() {
     for job in 'jobs -p'; do
         echo "PID => ${job}"
         CODE=0;
         wait ${job} || CODE=$?
         if [[ "${CODE}" != "0" ]]; then
         echo "At least one test failed with exit code => ${CODE}" ;
         EXIT_CODE=1;
         fi
     done
}

trap 'handleJobs' CHLD  # trap command is the key part
DIRN=$(dirname "$0");

commands=(
    "{ echo 'a'; exit 1; }"
    "{ echo 'b'; exit 0; }"
    "{ echo 'c'; exit 2; }"
)

clen='expr "${#commands[@]}" - 1' # get length of commands - 1

for i in 'seq 0 "$clen"'; do
    (echo "${commands[$i]}" | bash) &   # run the command via bash in subshell
    echo "$i ith command has been issued as a background job"
done

wait; # wait for all subshells to finish

echo "EXIT_CODE => $EXIT_CODE"
exit "$EXIT_CODE"
# end

obrigado a @michael homer por me colocar no caminho certo, mas usar o comando trap é a melhor abordagem AFAICT.

    
por 12.02.2017 / 12:23