Con dos comandos en segundo plano, finalice el restante cuando salga

14

Tengo un script bash simple que inicia dos servidores:

#!/bin/bash
(cd ./frontend && gulp serve) & (cd ./backend && gulp serve --verbose)

Si sale el segundo comando, parece que el primer comando continúa ejecutándose.

¿Cómo puedo cambiar esto para que si alguno de los comandos sale, el otro se termina?

Tenga en cuenta que no necesitamos verificar los niveles de error de los procesos en segundo plano, solo si han salido.

blah238
fuente
¿Por qué no gulp ./fronend/serve && gulp ./backend/serve --verbose?
heemayl
servees un argumento, no un archivo, por lo que debe establecerse el directorio actual.
blah238
1
Además, estos son procesos de larga duración que deben ejecutarse simultáneamente, lo siento si eso no estaba claro.
blah238

Respuestas:

22

Esto inicia ambos procesos, espera el primero que finaliza y luego mata al otro:

#!/bin/bash
{ cd ./frontend && gulp serve; } &
{ cd ./backend && gulp serve --verbose; } &
wait -n
pkill -P $$

Cómo funciona

  1. Comienzo:

    { cd ./frontend && gulp serve; } &
    { cd ./backend && gulp serve --verbose; } &

    Los dos comandos anteriores inician ambos procesos en segundo plano.

  2. Espere

    wait -n

    Esto espera a que finalice cualquiera de los trabajos en segundo plano.

    Debido a la -nopción, esto requiere bash 4.3 o superior.

  3. Matar

    pkill -P $$

    Esto mata cualquier trabajo para el cual el proceso actual es el padre. En otras palabras, esto mata cualquier proceso en segundo plano que todavía se está ejecutando.

    Si su sistema no tiene pkill, intente reemplazar esta línea con:

    kill 0

    que también mata al grupo de proceso actual .

Ejemplo fácilmente comprobable

Al cambiar el script, podemos probarlo incluso sin tener gulpinstalado:

$ cat script.sh 
#!/bin/bash
{ sleep $1; echo one;  } &
{ sleep $2; echo two;  } &
wait -n
pkill -P $$
echo done

El script anterior se puede ejecutar como bash script.sh 1 3y el primer proceso termina primero. Alternativamente, uno puede ejecutarlo como bash script.sh 3 1y el segundo proceso terminará primero. En cualquier caso, se puede ver que esto funciona como se desea.

John1024
fuente
Esto se ve genial. Lamentablemente, esos comandos no funcionan en mi entorno bash (msysgit). Ese es mi mal por no especificar eso. Sin embargo, lo probaré en una caja real de Linux.
blah238
(1) No todas las versiones bashadmiten la -nopción del waitcomando. (2) Estoy de acuerdo al 100% con la primera oración: su solución inicia dos procesos, espera a que termine la primera y luego mata a la otra. Pero la pregunta dice: "... si alguno de los comandos falla , ¿el otro termina?" Creo que su solución no es lo que quiere el OP. (3) ¿Por qué cambió (…) &a { …; } &? De &todos modos, obliga a la lista (comando de grupo) a ejecutarse en una subshell. En mi humilde opinión, ha agregado caracteres y posiblemente introdujo confusión (tuve que mirarlo dos veces para comprenderlo) sin ningún beneficio.
G-Man dice 'reinstalar a Monica' el
1
John tiene razón, son servidores web que normalmente deberían seguir ejecutándose a menos que ocurra un error o se les indique que terminen. Por lo tanto, no creo que debamos verificar el nivel de error de cada proceso, solo si aún se está ejecutando.
blah238
2
pkillno está disponible para mí, pero kill 0parece tener el mismo efecto. También actualicé mi entorno Git para Windows y parece que wait -nfunciona ahora, así que acepto esta respuesta.
blah238
1
Interesante. Aunque veo que ese comportamiento está documentado para algunas versiones de kill. la documentación en mi sistema no lo menciona. Sin embargo, kill 0funciona de todos modos. ¡Buen descubrimiento!
John1024
1

Esto es complicado Esto es lo que ideé; puede ser posible simplificarlo / simplificarlo:

#!/bin/sh

pid1file=$(mktemp)
pid2file=$(mktemp)
stat1file=$(mktemp)
stat2file=$(mktemp)

while true; do sleep 42; done &
main_sleeper=$!

(cd frontend && gulp serve           & echo "$!" > "$pid1file";
    wait "$!" 2> /dev/null; echo "$?" > "$stat1file"; kill "$main_sleeper" 2> /dev/null) &
(cd backend  && gulp serve --verbose & echo "$!" > "$pid2file";
    wait "$!" 2> /dev/null; echo "$?" > "$stat2file"; kill "$main_sleeper" 2> /dev/null) &
sleep 1
wait "$main_sleeper" 2> /dev/null

if stat1=$(<"$stat1file")  &&  [ "$stat1" != "" ]  &&  [ "$stat1" != 0 ]
then
        echo "First process failed ..."
        if pid2=$(<"$pid2file")  &&  [ "$pid2" != "" ]
        then
                echo "... killing second process."
                kill "$pid2" 2> /dev/null
        fi
fi
if [ "$stat1" = "" ]  &&  \
   stat2=$(<"$stat2file")  &&  [ "$stat2" != "" ]  &&  [ "$stat2" != 0 ]
then
        echo "Second process failed ..."
        if pid1=$(<"$pid1file")  &&  [ "$pid1" != "" ]
        then
                echo "... killing first process."
                kill "$pid1" 2> /dev/null
        fi
fi

wait
if stat1=$(<"$stat1file")
then
        echo "Process 1 terminated with status $stat1."
else
        echo "Problem getting status of process 1."
fi
if stat2=$(<"$stat2file")
then
        echo "Process 2 terminated with status $stat2."
else
        echo "Problem getting status of process 2."
fi
  • Primero, inicie un proceso ( while true; do sleep 42; done &) que duerme / pausa para siempre. Si está seguro de que sus dos comandos terminarán dentro de un cierto período de tiempo (p. Ej., Una hora), puede cambiar esto a una sola suspensión que excederá eso (p sleep 3600. Ej., ). Luego puede cambiar la siguiente lógica para usar esto como un tiempo de espera; es decir, elimine los procesos si todavía se están ejecutando después de tanto tiempo. (Tenga en cuenta que el script anterior actualmente no hace eso).
  • Inicie los dos procesos asincrónicos (fondo concurrente).
    • No es necesario ./para cd.
    • command & echo "$!" > somewhere; wait "$!" es una construcción complicada que inicia un proceso de forma asincrónica, captura su PID y luego lo espera; convirtiéndolo en una especie de proceso en primer plano (sincrónico). Pero esto sucede dentro de una (…)lista que está en segundo plano en su totalidad, por lo que los gulpprocesos se ejecutan de forma asincrónica.
    • Después de que cualquiera de los gulpprocesos salga, escriba su estado en un archivo temporal y elimine el proceso de "suspensión permanente".
  • sleep 1 para protegerse contra una condición de carrera donde el primer proceso en segundo plano muere antes de que el segundo tenga la oportunidad de escribir su PID en el archivo.
  • Espere a que termine el proceso de "sueño para siempre". Esto sucede después de que cualquiera de los gulpprocesos sale, como se indicó anteriormente.
  • Vea qué proceso en segundo plano finalizó. Si falla, mata al otro.
  • Si un proceso falló y matamos al otro, espere a que finalice el segundo y guarde su estado en un archivo. Si el primer proceso finalizó con éxito, espere a que termine el segundo.
  • Verifique los estados de los dos procesos.
G-Man dice 'restablecer a Mónica'
fuente
1

Para completar, esto es lo que terminé usando:

#!/bin/bash
(cd frontend && gulp serve) &
(cd backend && gulp serve --verbose) &
wait -n
kill 0

Esto funciona para mí en Git para Windows 2.5.3 de 64 bits. Las versiones anteriores pueden no aceptar la -nopción en wait.

blah238
fuente
1

En mi sistema (Centos), waitno tiene, -nasí que hice esto:

{ sleep 3; echo one;  } &
FOO=$!
{ sleep 6; echo two;  } &
wait $FOO
pkill -P $$

Esto no espera "tampoco", sino que espera el primero. Pero aún así puede ayudar si sabe qué servidor se detendrá primero.

Ondra Žižka
fuente
Si waittiene la -nopción o no, depende del shell que esté utilizando, no de la distribución de Linux.
Kusalananda