Étant donné deux commandes d'arrière-plan, mettez fin à la dernière lorsque l'une ou l'autre se ferme

14

J'ai un script bash simple qui démarre deux serveurs:

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

Si la deuxième commande se termine, il semble que la première commande continue de s'exécuter.

Comment puis-je changer cela de sorte que si l'une des commandes se termine, l'autre se termine?

Notez que nous n'avons pas besoin de vérifier les niveaux d'erreur des processus d'arrière-plan, juste s'ils se sont arrêtés.

blah238
la source
Pourquoi pas gulp ./fronend/serve && gulp ./backend/serve --verbose?
heemayl
serveest un argument, pas un fichier, donc le répertoire courant doit être défini.
blah238
1
Ce sont également des processus de longue durée qui doivent s'exécuter simultanément, désolé si cela n'était pas clair.
blah238

Réponses:

22

Cela démarre les deux processus, attend le premier qui se termine puis tue l'autre:

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

Comment ça fonctionne

  1. Début:

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

    Les deux commandes ci-dessus démarrent les deux processus en arrière-plan.

  2. Attendez

    wait -n

    Cela attend la fin de l'un ou l'autre des travaux en arrière-plan.

    En raison de l' -noption, cela nécessite bash 4.3 ou mieux.

  3. Tuer

    pkill -P $$

    Cela tue tout travail dont le processus actuel est le parent. En d'autres termes, cela tue tout processus d'arrière-plan toujours en cours d'exécution.

    Si votre système n'en a pas pkill, essayez de remplacer cette ligne par:

    kill 0

    ce qui tue également le groupe de processus actuel .

Exemple facilement testable

En changeant le script, nous pouvons le tester même sans gulpinstallé:

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

Le script ci-dessus peut être exécuté en tant que bash script.sh 1 3et le premier processus se termine en premier. Alternativement, on peut l'exécuter en tant que bash script.sh 3 1et le deuxième processus se terminera en premier. Dans les deux cas, on peut voir que cela fonctionne comme souhaité.

John1024
la source
Ça a l'air génial. Malheureusement, ces commandes ne fonctionnent pas sur mon environnement bash (msysgit). C'est mon mal de ne pas l'avoir précisé. Je vais cependant l'essayer sur une vraie boîte Linux.
blah238
(1) Toutes les versions de ne bashprennent pas en charge l' -noption de la waitcommande. (2) Je suis d'accord à 100% avec la première phrase - votre solution démarre deux processus, attend que le premier se termine, puis tue l'autre. Mais la question dit «… si l'une des erreurs de commande , l'autre est terminée?» Je pense que votre solution n'est pas celle que souhaite le PO. (3) Pourquoi avez-vous changé (…) &pour { …; } &? La &force la liste (commande de groupe) à s'exécuter dans un sous-shell de toute façon. À mon humble avis, vous avez ajouté des caractères et éventuellement introduit de la confusion (j'ai dû le regarder deux fois pour le comprendre) sans aucun avantage.
G-Man dit `` Réintègre Monica '' le
1
John a raison, ce sont des serveurs Web qui devraient normalement continuer à fonctionner, sauf si une erreur se produit ou s'ils sont signalés de se terminer. Je ne pense donc pas que nous devions vérifier le niveau d'erreur de chaque processus, juste s'il est toujours en cours d'exécution.
blah238
2
pkilln'est pas disponible pour moi mais kill 0semble avoir le même effet. J'ai également mis à jour mon environnement Git pour Windows et cela semble wait -nfonctionner maintenant, donc j'accepte cette réponse.
blah238
1
Intéressant. Bien que je constate que ce comportement est documenté pour certaines versions de kill. la documentation de mon système ne le mentionne pas. Cependant, kill 0ça marche quand même. Bonne trouvaille!
John1024
1

C'est délicat. Voici ce que j'ai imaginé; il peut être possible de le simplifier / rationaliser:

#!/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
  • Tout d'abord, démarrez un processus ( while true; do sleep 42; done &) qui s'endort / s'arrête pour toujours. Si vous êtes sûr que vos deux commandes se termineront dans un certain laps de temps (par exemple, une heure), vous pouvez changer cela en un seul sommeil qui dépassera cela (par exemple, sleep 3600). Vous pouvez ensuite modifier la logique suivante pour l'utiliser comme délai d'expiration; c'est-à-dire, tuez les processus s'ils sont toujours en cours d'exécution après ce temps. (Notez que le script ci-dessus ne fait actuellement pas cela.)
  • Démarrez les deux processus asynchrones (arrière-plan simultané).
    • Vous n'avez pas besoin ./pour cd.
    • command & echo "$!" > somewhere; wait "$!" est une construction délicate qui démarre un processus de manière asynchrone, capture son PID, puis l'attend; ce qui en fait une sorte de processus de premier plan (synchrone). Mais cela se produit dans une (…)liste qui est en arrière-plan dans son intégralité, de sorte que les gulpprocessus s'exécutent de manière asynchrone.
    • Une fois l'un des gulpprocessus terminé, écrivez son état dans un fichier temporaire et supprimez le processus «toujours en sommeil».
  • sleep 1 pour se protéger contre une condition de concurrence critique où le premier processus d'arrière-plan meurt avant que le second n'ait la possibilité d'écrire son PID dans le fichier.
  • Attendez la fin du processus «toujours en sommeil». Cela se produit après la fin de l'un des gulpprocessus, comme indiqué ci-dessus.
  • Voir le processus d'arrière-plan terminé. S'il échoue, tuez l'autre.
  • Si un processus a échoué et que nous avons tué l'autre, attendez que le second se termine et enregistrez son état dans un fichier. Si le premier processus s'est terminé avec succès, attendez que le second se termine.
  • Vérifiez les statuts des deux processus.
G-Man dit «Réintègre Monica»
la source
1

Pour être complet, voici ce que j'ai fini par utiliser:

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

Cela fonctionne pour moi sur Git pour Windows 2.5.3 64 bits. Les versions plus anciennes peuvent ne pas accepter l' -noption wait.

blah238
la source
1

Sur mon système (Centos), waitn'a pas -ndonc j'ai fait ceci:

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

Cela n'attend pas "non plus", attend plutôt le premier. Mais cela peut toujours aider si vous savez quel serveur sera arrêté en premier.

Ondra Žižka
la source
Que waitl' -noption soit ou non dépend du shell que vous utilisez, pas de la distribution Linux.
Kusalananda