Dati due comandi in background, termina quello rimanente quando uno dei due esce


14

Ho un semplice script bash che avvia due server:

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

Se il secondo comando termina, sembra che il primo comando continui a essere eseguito.

Come posso cambiarlo in modo che se uno dei comandi esce, l'altro viene terminato?

Si noti che non è necessario controllare i livelli di errore dei processi in background, solo se sono usciti.


Perché no gulp ./fronend/serve && gulp ./backend/serve --verbose?
heemayl,

serveè un argomento, non un file, quindi è necessario impostare la directory corrente.
blah238,

1
Anche questi sono processi a lungo termine che devono essere eseguiti contemporaneamente, scusate se non era chiaro.
blah238,

Risposte:


22

Questo avvia entrambi i processi, attende il primo che termina e quindi uccide l'altro:

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

Come funziona

  1. Inizio:

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

    I due comandi precedenti avviano entrambi i processi in background.

  2. Aspettare

    wait -n

    Ciò attende la fine di entrambi i processi in background.

    A causa -ndell'opzione, questo richiede bash 4.3 o superiore.

  3. Uccidere

    pkill -P $$

    Questo uccide qualsiasi lavoro per il quale il processo corrente è il genitore. In altre parole, questo uccide qualsiasi processo in background che è ancora in esecuzione.

    Se il tuo sistema non ha pkill, prova a sostituire questa riga con:

    kill 0

    che uccide anche l'attuale gruppo di processi .

Esempio facilmente testabile

Modificando lo script, possiamo testarlo anche senza gulpinstallato:

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

Lo script sopra può essere eseguito come bash script.sh 1 3e il primo processo termina per primo. In alternativa, è possibile eseguirlo come bash script.sh 3 1e il secondo processo terminerà per primo. In entrambi i casi, si può vedere che questo funziona come desiderato.


Questo sembra fantastico. Sfortunatamente quei comandi non funzionano nel mio ambiente bash (msysgit). Questo è il mio male per non averlo specificato. Lo proverò su un vero box Linux.
blah238,

(1) Non tutte le versioni bashsupportano l' -nopzione al waitcomando. (2) Concordo al 100% con la prima frase: la tua soluzione avvia due processi, attende che il primo finisca e quindi uccide l'altro. Ma la domanda dice "... se uno dei due comandi è in errore , l'altro è terminato?" Credo che la tua soluzione non sia quella che vuole l'OP. (3) Perché sei passato (…) &a { …; } &? Le &forze della lista (comando di gruppo) per l'esecuzione in una subshell comunque. IMHO, hai aggiunto personaggi e forse introdotto confusione (ho dovuto guardarlo due volte per capirlo) senza alcun vantaggio.
G-Man dice "Reinstate Monica" il

1
John ha ragione, si tratta di server Web che normalmente dovrebbero continuare a funzionare a meno che non si verifichi un errore o venga segnalato di terminare. Quindi non penso che dobbiamo controllare il livello di errore di ogni processo, solo se è ancora in esecuzione.
blah238,

2
pkillnon è disponibile per me ma kill 0sembra avere lo stesso effetto. Inoltre ho aggiornato il mio ambiente Git per Windows e sembra wait -nfunzionare ora, quindi sto accettando questa risposta.
blah238

1
Interessante. Anche se vedo che questo comportamento è documentato per alcune versioni di kill. la documentazione sul mio sistema non la menziona. Tuttavia, kill 0funziona comunque. Buona scoperta!
Giovanni 1024,

1

Questo è difficile. Ecco cosa ho ideato; potrebbe essere possibile semplificarlo / semplificarlo:

#!/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
  • Innanzitutto, avvia un processo ( while true; do sleep 42; done &) che dorme / mette in pausa per sempre. Se sei sicuro che i tuoi due comandi termineranno entro un certo periodo di tempo (ad es. Un'ora), puoi cambiarlo in un singolo sonno che lo supererà (ad es sleep 3600.). È quindi possibile modificare la seguente logica per utilizzarla come timeout; cioè, uccidi i processi se sono ancora in esecuzione dopo così tanto tempo. (Nota che attualmente lo script sopra non lo fa.)
  • Avviare i due processi asincroni (sfondo simultaneo).
    • Non è necessario ./per cd.
    • command & echo "$!" > somewhere; wait "$!" è un costrutto complicato che avvia un processo in modo asincrono, acquisisce il suo PID e quindi lo attende; rendendolo una specie di processo in primo piano (sincrono). Ma questo accade all'interno di un (…)elenco che è in background nella sua interezza, quindi i gulpprocessi vengono eseguiti in modo asincrono.
    • Dopo la chiusura di uno dei gulpprocessi, scrivere il suo stato in un file temporaneo e terminare il processo di "sospensione per sempre".
  • sleep 1 per proteggere da una race condition in cui il primo processo in background termina prima che il secondo abbia la possibilità di scrivere il suo PID nel file.
  • Attendere il termine del processo di "sospensione permanente". Ciò accade dopo la chiusura di uno dei gulpprocessi, come indicato sopra.
  • Scopri quale processo in background è terminato. Se fallisce, uccidi l'altro.
  • Se un processo ha esito negativo e abbiamo ucciso l'altro, attendere che il secondo si concluda e salvare il suo stato in un file. Se il primo processo è stato completato correttamente, attendere il completamento del secondo.
  • Controllare gli stati dei due processi.

1

Per completezza ecco cosa ho finito per usare:

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

Questo funziona per me su Git per Windows 2.5.3 a 64 bit. Le versioni precedenti potrebbero non accettare l' -nopzione attivata wait.


1

Sul mio sistema (Centos), waitnon ho -nquindi ho fatto questo:

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

Questo non aspetta "neanche", ma aspetta il primo. Tuttavia, può essere utile sapere quale server verrà arrestato per primo.


Se waitl' -nopzione ha o meno dipende dalla shell che stai usando, non dalla distribuzione Linux.
Kusalananda
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.