Exit-Codes von parallelen Hintergrundprozessen (Subshells) sammeln

18

Angenommen, wir haben ein Bash-Skript wie folgt:

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

Gibt es eine Möglichkeit, die Exit-Codes der Sub-Shells / Sub-Prozesse zu sammeln? Suchen Sie nach einer Möglichkeit, dies zu tun, und finden Sie nichts. Ich muss diese Subshells parallel betreiben, sonst wäre das ja einfacher.

Ich suche nach einer generischen Lösung (ich habe eine unbekannte / dynamische Anzahl von Unterprozessen, die parallel ausgeführt werden sollen).

Alexander Mills
quelle
1
Ich werde vorschlagen, dass Sie herausfinden, was Sie wollen, und dann eine neue Frage stellen und versuchen, genau das Verhalten zu bestimmen, nach dem Sie suchen (möglicherweise mit Pseudocode oder einem größeren Beispiel).
Michael Homer
3
Ich denke tatsächlich, dass die Frage jetzt gut ist - ich habe eine dynamische Anzahl von Unterprozessen. Ich muss alle Exit-Codes sammeln. Das ist alles.
Alexander Mills

Antworten:

6

Die Antwort von Alexander Mills, der handleJobs verwendet, gab mir einen guten Ausgangspunkt, gab mir aber auch diesen Fehler

Warnung: run_pending_traps: falscher Wert in trap_list [17]: 0x461010

Welches ein heftiges Rennenbedingungsproblem sein kann

Stattdessen habe ich nur pid jedes Kindes gespeichert und gewartet und Exit-Code für jedes Kind speziell abgerufen. Ich finde diesen Cleaner in Bezug auf Unterprozesse, die Unterprozesse in Funktionen erzeugen und das Risiko vermeiden, auf einen übergeordneten Prozess zu warten, bei dem ich auf ein Kind warten wollte. Es ist klarer, was passiert, weil die Falle nicht benutzt wird.

#!/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
Arberg
quelle
Aus Gründen der Klarheit for job in "${childen[@]}"; dosollte es for job in "${1}"; doaber cool sein
Alexander Mills
Das einzige Problem, das ich mit diesem Skript habe, ist, ob children_pids+=("$!")tatsächlich die gewünschte PID für die Sub-Shell erfasst wird.
Alexander Mills
1
Ich habe mit "$ {1}" getestet und es funktioniert nicht. Ich übergebe ein Array an die Funktion, und anscheinend muss dies in bash besonders beachtet werden. $! ist die PID des zuletzt erstellten Jobs, siehe tldp.org/LDP/abs/html/internalvariables.html. Sie scheint in meinen Tests korrekt zu funktionieren, und ich verwende sie jetzt im Skript in unRAID cache_dirs seine Aufgabe. Ich benutze Bash 4.4.12.
Arberg
nice yep scheint, wie Sie richtig sind
Alexander Mills
20

Verwendung waitmit einer PID, die :

Warten Sie, bis der untergeordnete Prozess, der von der Prozess-ID pid oder der Jobspezifikation jobspec angegeben wurde, beendet ist, und geben Sie den Beendigungsstatus des letzten Befehls zurück, auf den gewartet wurde.

Sie müssen die PID jedes Prozesses währenddessen speichern:

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

Sie können die Jobsteuerung im Skript auch mit set -meiner %nJobspezifikation aktivieren und verwenden, möchten dies aber mit ziemlicher Sicherheit nicht. Die Jobsteuerung hat viele andere Nebenwirkungen .

waitGibt den gleichen Code zurück, mit dem der Vorgang abgeschlossen wurde. Sie können wait $Xzu einem beliebigen (angemessenen) späteren Zeitpunkt auf den endgültigen Code zugreifen $?oder ihn einfach als true / false verwenden:

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

wait wird angehalten, bis der Befehl abgeschlossen ist, sofern dies noch nicht geschehen ist.

Wenn Sie möchten , dass ein Abwürgen vermeiden möchten, können Sie einen Satz trapaufSIGCHLD , zählen die Anzahl der Abschlüsse und behandeln alle die waits auf einmal , wenn sie alle fertig sind. Sie können wahrscheinlich waitfast die ganze Zeit damit davonkommen, allein zu sein.

Michael Homer
quelle
1
ughh, sorry, ich muss diese Subshells parallel ausführen, ich werde das in der Frage angeben ...
Alexander Mills
egal, vielleicht funktioniert das mit meinem setup ... wo kommt der wait befehl in ihrem code ins spiel? Ich folge nicht
Alexander Mills
1
@AlexanderMills Sie sind parallel laufen. Wenn Sie eine variable Anzahl von ihnen haben, verwenden Sie ein Array. (wie zB hier was also ein Duplikat sein kann).
Michael Homer
ja dank ich werde prüfen, ob aus, wenn die Wartebefehl bezieht sich auf Ihre Antwort, dann fügen Sie ihn in
Alexander Mills
Sie rennen wait $Xzu einem (angemessenen) späteren Zeitpunkt.
Michael Homer
5

Wenn Sie die Befehle gut identifizieren können, können Sie ihren Beendigungscode in eine tmp-Datei drucken und dann auf die gewünschte Datei zugreifen:

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

Vergessen Sie nicht, die tmp-Dateien zu entfernen.

Rolf
quelle
4

Verwenden Sie a compound command- setzen Sie die Anweisung in Klammern:

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

wird die Ausgabe geben

x
X: 0
TRUE: 0
FALSE: 1

Eine andere Art, mehrere Befehle parallel auszuführen, ist die Verwendung von GNU Parallel . Erstellen Sie eine Liste der auszuführenden Befehle und fügen Sie sie in die Datei ein list:

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

Führen Sie alle Befehle parallel aus und sammeln Sie die Exit-Codes in der Datei job.log:

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

und die Ausgabe ist:

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
hschou
quelle
ok danke, gibt es eine Möglichkeit dies zu generieren? Ich habe nicht nur 3 Unterprozesse, ich habe Z Unterprozesse.
Alexander Mills
Ich habe die ursprüngliche Frage aktualisiert, um zu berücksichtigen, dass ich nach einer generischen Lösung suche, danke
Alexander Mills
Eine Möglichkeit, dies zu generieren, könnte die Verwendung eines Schleifenkonstrukts sein.
Alexander Mills
Looping? Haben Sie eine feste Befehlsliste oder wird diese vom Benutzer gesteuert? Ich bin nicht sicher, ob ich verstehe, was Sie versuchen, aber vielleicht PIPESTATUSsollten Sie es überprüfen. Dies gibt seq 10 | gzip -c > seq.gz ; echo ${PIPESTATUS[@]}zurück 0 0(Exit-Code vom ersten und letzten Befehl).
hschou
Ja im Wesentlichen vom Benutzer gesteuert
Alexander Mills
2

Dies ist das generische Skript, nach dem Sie suchen. Der einzige Nachteil ist, dass Ihre Befehle in Anführungszeichen stehen, was bedeutet, dass das Hervorheben der Syntax über Ihre IDE nicht wirklich funktioniert. Ansonsten habe ich ein paar andere Antworten ausprobiert und dies ist die beste. Diese Antwort enthält die Idee der Verwendung wait <pid>von @Michael, geht aber noch einen Schritt weiter und verwendet den trapBefehl, der am besten zu funktionieren scheint.

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

danke an @michael homer, der mich auf den richtigen Weg gebracht hat, aber die Verwendung von trapbefehlen ist der beste Ansatz von AFAICT.

Alexander Mills
quelle
1
Sie können SIGCHLD trap auch verwenden, um die untergeordneten Elemente beim Beenden zu verarbeiten, z. B. um den aktuellen Status auszudrucken. Oder Aktualisieren eines Fortschrittszählers: Deklarieren Sie eine Funktion und verwenden Sie dann "trap function_name CHLD". Möglicherweise muss jedoch auch eine Option in einer nicht interaktiven Shell
aktiviert werden
1
Auch "wait -n" wartet auf ein untergeordnetes Element und gibt dann den Beendigungsstatus dieses untergeordneten Elements in $? Variable. So können Sie den Fortschritt bei jedem Beenden ausdrucken. Beachten Sie jedoch, dass Sie möglicherweise einige untergeordnete Ausgänge auf diese Weise verpassen, es sei denn, Sie verwenden die CHLD-Falle.
Chunko
@Chunko danke! Das ist eine gute Info. Könnten Sie die Antwort vielleicht mit etwas aktualisieren, das Sie für das Beste halten?
Alexander Mills
danke @Chunko, trap funktioniert besser, du hast recht. Mit wait <pid> bin ich durchgefallen.
Alexander Mills
Können Sie erklären, wie und warum Sie glauben, dass die Version mit der Falle besser ist als die ohne? (Ich glaube, es ist nicht besser und deshalb auch nicht so gut, weil es komplexer und ohne Nutzen ist.)
Scott
1

Eine andere Variante der Antwort von @rolf:

Eine andere Möglichkeit, den Exit-Status zu speichern, wäre etwa

mkdir /tmp/status_dir

und dann habe jedes script

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

Auf diese Weise erhalten Sie einen eindeutigen Namen für jede Statusdatei, einschließlich des Namens des Skripts, das sie erstellt hat, und der Prozess-ID (falls mehr als eine Instanz desselben Skripts ausgeführt wird), die Sie zur späteren Bezugnahme speichern und in die Datei einfügen können Sie können das gesamte Unterverzeichnis löschen, wenn Sie fertig sind.

Sie können sogar mehr als einen Status von jedem Skript speichern, indem Sie so etwas wie tun

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

Dadurch wird die Datei wie zuvor erstellt, es wird jedoch eine eindeutige zufällige Zeichenfolge hinzugefügt.

Sie können auch einfach weitere Statusinformationen an dieselbe Datei anhängen.

Joe
quelle
1

script3wird nur ausgeführt, wenn script1und script2erfolgreich sind und script1und script2wird parallel ausgeführt:

./script1 &
process1=$!

./script2 &
process2=$!

wait $process1
rc1=$?

wait $process2
rc2=$?

if [[ $rc1 -eq 0 ]] && [[ $rc2 -eq 0  ]];then
./script3
fi
Venkata
quelle
AFAICT, das ist nichts weiter als eine Wiederholung von Michael Homers Antwort .
Scott