web-dev-qa-db-fra.com

Collecter les codes de sortie des processus d'arrière-plan parallèles (sous-shells)

Disons que nous avons un script bash comme ceci:

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

existe-t-il un moyen de collecter les codes de sortie des sous-shells/sous-processus? Vous cherchez un moyen de le faire et vous ne trouvez rien. J'ai besoin d'exécuter ces sous-coquilles en parallèle, sinon oui ce serait plus facile.

Je recherche une solution générique (j'ai un nombre inconnu/dynamique de sous-processus à exécuter en parallèle).

19
Alexander Mills

La réponse d'Alexander Mills qui utilise handleJobs m'a donné un excellent point de départ, mais m'a aussi donné cette erreur

avertissement: run_pending_traps: mauvaise valeur dans trap_list [17]: 0x461010

Ce qui peut être un problème de condition de course bash

Au lieu de cela, je viens de stocker le pid de chaque enfant et d'attendre et d'obtenir le code de sortie pour chaque enfant spécifiquement. Je trouve cela plus propre en termes de sous-processus engendrant des sous-processus dans les fonctions et évitant le risque d'attendre un processus parent où je voulais attendre l'enfant. C'est plus clair ce qui se passe parce qu'il n'utilise pas le piège.

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

Utilisez wait avec un PID, qui :

Attendez que le processus enfant spécifié par chaque ID de processus pid ou spécification de travail jobspec quitte et renvoie l'état de sortie de la dernière commande attendue.

Vous devrez enregistrer le PID de chaque processus au fur et à mesure:

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

Vous pouvez également activer le contrôle des travaux dans le script avec set -m et utilisez un %n jobspec, mais vous ne voulez certainement pas - le contrôle des tâches a beaucoup d'autres effets secondaires .

wait renverra le même code que le processus terminé avec. Vous pouvez utiliser wait $X à tout moment (raisonnable) ultérieur pour accéder au code final en tant que $? ou utilisez-le simplement comme vrai/faux:

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

wait s'arrêtera jusqu'à ce que la commande se termine si ce n'est pas déjà fait.

Si vous voulez éviter le blocage comme ça, vous pouvez définir un trap sur SIGCHLD , compter le nombre de terminaisons et gérer tous les waits à la fois quand ils ont tous fini. Vous pouvez probablement vous en sortir en utilisant wait seul presque tout le temps.

21
Michael Homer

Si vous aviez un bon moyen d'identifier les commandes, vous pouvez imprimer leur code de sortie dans un fichier tmp puis accéder au fichier spécifique qui vous intéresse:

#!/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'oubliez pas de supprimer les fichiers tmp.

5
Rolf

Utiliser un compound command - mettez l'instruction entre parenthèses:

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

donnera la sortie

x
X: 0
TRUE: 0
FALSE: 1

Une manière vraiment différente d'exécuter plusieurs commandes en parallèle est d'utiliser GNU Parallel . Faites une liste des commandes à exécuter et placez-les dans le fichier list:

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

Exécutez toutes les commandes en parallèle et collectez les codes de sortie dans le fichier job.log:

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

et la sortie est:

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
4
hschou

c'est le script générique que vous recherchez. Le seul inconvénient est que vos commandes sont entre guillemets, ce qui signifie que la coloration syntaxique via votre IDE ne fonctionnera pas vraiment. Sinon, j'ai essayé quelques autres réponses et c'est la meilleure. Cette réponse intègre l'idée d'utiliser wait <pid> donné par @Michael mais va plus loin en utilisant la commande trap qui semble fonctionner le mieux.

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

merci à @michael homer de m'avoir mis sur la bonne voie, mais utiliser la commande trap est la meilleure approche AFAICT.

2
Alexander Mills

Une autre variante de la réponse de @rolf:

Une autre façon de sauvegarder le statut de sortie serait quelque chose comme

mkdir /tmp/status_dir

puis avoir chaque script

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

Cela vous donne un nom unique pour chaque fichier d'état, y compris le nom du script qui l'a créé et son identifiant de processus (dans le cas où plusieurs instances du même script sont en cours d'exécution) que vous pouvez enregistrer pour référence plus tard et les mettre toutes dans le même endroit afin que vous puissiez simplement supprimer le sous-répertoire entier lorsque vous avez terminé.

Vous pouvez même enregistrer plusieurs statuts de chaque script en faisant quelque chose comme

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

qui crée le fichier comme précédemment, mais y ajoute une chaîne aléatoire unique.

Ou, vous pouvez simplement ajouter plus d'informations sur l'état au même fichier.

1
Joe

script3 ne sera exécuté que si script1 et script2 réussissent et script1 et script2 sera exécuté en parallèle:

./script1 &
process1=$!

./script2 &
process2=$!

wait $process1
rc1=$?

wait $process2
rc2=$?

if [[ $rc1 -eq 0 ]] && [[ $rc2 -eq 0  ]];then
./script3
fi
1
venkata