Recopilar códigos de salida de procesos en segundo plano paralelos (subcapas)

18

Digamos que tenemos un script bash así:

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

¿hay una manera de recoger los códigos de salida de los subprocesos conchas / sub? Buscando manera de hacer esto y no puede encontrar nada. Necesito funcionar estos subniveles en paralelo, de lo contrario sí, esto sería más fácil.

Busco a una genérica solución (no tengo un número desconocido / dinámica de subprocesos para ejecutar en paralelo).

Alexander Mills
fuente
1
Voy a sugerir a averiguar qué es lo que desea y luego hacer una nueva pregunta, tratando de ser claro exactamente el comportamiento que está buscando (quizás con pseudocódigo o un ejemplo más extenso).
Michael Homer
3
De hecho, creo que la pregunta es bien ahora - Tengo una serie dinámica de subprocesos. Necesito recopilar todos los códigos de salida. Eso es todo.
Alexander Mills

Respuestas:

6

La respuesta de Alexander Mills que usa handleJobs me dio un excelente punto de partida, pero también me dio este error

advertencia: run_pending_traps: valor incorrecto en trap_list [17]: 0x461010

Que puede ser un problema de condición de carrera bash

En su lugar lo hice tienda pid de cada niño y espera y recibe el código de salida para cada niño en particular. Encuentro este limpiador en términos de subprocesos que generan subprocesos en funciones y evitan el riesgo de esperar un proceso principal donde pretendía esperar al hijo. Su más claro lo que sucede porque no es el uso de la trampa.

#!/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
fuente
genial, creo que for job in "${childen[@]}"; dodebería ser for job in "${1}"; do, para mayor claridad
Alexander Mills
La única preocupación que tengo con este script es si children_pids+=("$!")realmente está capturando el pid deseado para el sub shell.
Alexander Mills
1
Lo probé con "$ {1}" y no funciona. Estoy pasando una matriz a la función, y aparentemente eso necesita atención especial en bash. PS es el pid del último trabajo generado, consulte tldp.org/LDP/abs/html/internalvariables.html Parece que funciona correctamente en mis pruebas, y ahora estoy usando el script in_RARA cache_dirs, y parece funcionar su trabajo Estoy usando bash 4.4.12.
arberg
sí, parece que tienes razón
Alexander Mills
20

Úselo waitcon un PID, que :

Espere hasta que el proceso secundario especificado por cada ID de proceso pid o especificación de trabajo se cierre y regrese el estado de salida del último comando esperado.

Deberá guardar el PID de cada proceso a medida que avanza:

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

También puede habilitar el control del trabajo en la secuencia de comandos con set -muna %nespecificación de trabajo y usarla , pero es casi seguro que no quiera hacerlo: el control del trabajo tiene muchos otros efectos secundarios .

waitdevolverá el mismo código que el proceso finalizado. Puede usar wait $Xen cualquier punto posterior (razonable) para acceder al código final $?o simplemente usarlo como verdadero / falso:

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

wait se detendrá hasta que el comando se complete si aún no lo ha hecho.

Si se quiere evitar estancamiento de esa manera, se puede establecer un trapsobreSIGCHLD , contar el número de terminaciones, y manejar todos los waits de una sola vez, cuando todos ellos han terminado. Probablemente pueda salirse con el uso waitsolo casi todo el tiempo.

Michael Homer
fuente
1
ughh, lo siento, necesito ejecutar estas subcapas en paralelo, lo especificaré en la pregunta ...
Alexander Mills
no importa, tal vez esto funcione con mi configuración ... ¿dónde entra en juego el comando de espera en su código? No te sigo
Alexander Mills
1
@AlexanderMills Se están ejecutando en paralelo. Si tiene un número variable de ellos, use una matriz. (como, por ejemplo, aquí, que puede ser un duplicado).
Michael Homer
sí, gracias, lo comprobaré, si el comando de espera corresponde a su respuesta, agréguelo
Alexander Mills
Corres wait $Xen cualquier punto posterior (razonable).
Michael Homer
5

Si tuviera una buena manera de identificar los comandos, podría imprimir su código de salida en un archivo tmp y luego acceder al archivo específico que le interesa:

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

No olvides eliminar los archivos tmp.

Rolf
fuente
4

Use a compound command- ponga la declaración entre paréntesis:

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

dará la salida

x
X: 0
TRUE: 0
FALSE: 1

Una forma realmente diferente de ejecutar varios comandos en paralelo es usando GNU Parallel . Haga una lista de comandos para ejecutar y póngalos en el archivo list:

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

Ejecute todos los comandos en paralelo y recopile los códigos de salida en el archivo job.log:

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

y la salida es:

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
fuente
ok gracias, ¿hay alguna forma de generar esto? No solo tengo 3 subprocesos, tengo Z subprocesos.
Alexander Mills
Actualicé la pregunta original para reflejar que estoy buscando una solución genérica, gracias
Alexander Mills
¿Una forma de generarlo podría ser usar una construcción en bucle?
Alexander Mills
Bucle? ¿Tiene una lista fija de comandos o eso es controlado por el usuario? No estoy seguro de entender lo que está tratando de hacer, pero tal vez PIPESTATUSes algo que debe salir. Esto seq 10 | gzip -c > seq.gz ; echo ${PIPESTATUS[@]}regresa 0 0(código de salida del primer y último comando).
hschou
Sí esencialmente controlado por el usuario
Alexander Mills
2

Este es el script genérico que estás buscando. El único inconveniente es que sus comandos están entre comillas, lo que significa que el resaltado de sintaxis a través de su IDE realmente no funcionará. De lo contrario, he probado algunas de las otras respuestas y esta es la mejor. Esta respuesta incorpora la idea de usar wait <pid>dada por @Michael pero va un paso más allá al usar el trapcomando que parece funcionar mejor.

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

gracias a @michael homer por llevarme por el buen camino, pero usar el trapcomando es el mejor enfoque AFAICT.

Alexander Mills
fuente
1
También puede usar la trampa SIGCHLD para procesar a los niños cuando salen, como imprimir el estado en ese momento. O actualizar un contador de progreso: declare una función y luego use "trap function_name CHLD" aunque eso también puede requerir que se active una opción en un shell no interactivo, como posiblemente "set -m"
Chunko
1
Además, "wait -n" esperará a cualquier hijo y luego devolverá el estado de salida de ese hijo en $? variable. Para que pueda imprimir el progreso a medida que sale cada uno. Sin embargo, tenga en cuenta que a menos que use la trampa CHLD, puede perder algunas salidas de niños de esa manera.
Chunko
@ Chunko gracias! esa es buena información, ¿podrías actualizar la respuesta con algo que creas que es mejor?
Alexander Mills
gracias @Chunko, la trampa funciona mejor, tienes razón. Con esperar <pid>, me falló.
Alexander Mills
¿Puedes explicar cómo y por qué crees que la versión con la trampa es mejor que la que no tiene? (Creo que no es mejor y, por lo tanto, que es peor, porque es más complejo sin ningún beneficio.)
Scott
1

Otra variación de la respuesta de @rolf:

Otra forma de guardar el estado de salida sería algo así como

mkdir /tmp/status_dir

y luego tener cada script

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

Esto le proporciona un nombre único para cada archivo de estado, incluido el nombre del script que lo creó y su ID de proceso (en caso de que se esté ejecutando más de una instancia del mismo script) que puede guardar como referencia más adelante y ponerlos todos en el mismo lugar para que pueda eliminar todo el subdirectorio cuando haya terminado.

Incluso puede guardar más de un estado de cada script haciendo algo como

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

que crea el archivo como antes, pero le agrega una cadena aleatoria única.

O simplemente puede agregar más información de estado al mismo archivo.

Joe
fuente
1

script3se ejecutará sólo si script1y script2tienen éxito y script1y script2serán ejecutadas en paralelo:

./script1 &
process1=$!

./script2 &
process2=$!

wait $process1
rc1=$?

wait $process2
rc2=$?

if [[ $rc1 -eq 0 ]] && [[ $rc2 -eq 0  ]];then
./script3
fi
venkata
fuente
AFAICT, esto no es más que una repetición de la respuesta de Michael Homer .
Scott