Biorąc pod uwagę dwa polecenia w tle, zakończ pozostałe po wyjściu z nich

14

Mam prosty skrypt bash, który uruchamia dwa serwery:

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

Jeśli drugie polecenie zakończy działanie, wydaje się, że pierwsze polecenie nadal działa.

Jak mogę to zmienić, aby jedno z poleceń zakończyło się, a drugie zostanie zakończone?

Pamiętaj, że nie musimy sprawdzać poziomów błędów procesów w tle, tylko czy zakończyły się.

blah238
źródło
Dlaczego nie gulp ./fronend/serve && gulp ./backend/serve --verbose?
heemayl
servejest argumentem, a nie plikiem, dlatego należy ustawić bieżący katalog.
blah238,
1
Są to również długotrwałe procesy, które muszą działać równolegle, przepraszam, jeśli nie było to jasne.
blah238,

Odpowiedzi:

22

To uruchamia oba procesy, czeka na pierwszy, który się kończy, a następnie zabija drugi:

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

Jak to działa

  1. Początek:

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

    Powyższe dwa polecenia uruchamiają oba procesy w tle.

  2. Czekać

    wait -n

    To czeka na zakończenie zadania w tle.

    Z powodu tej -nopcji wymaga to wersji bash 4.3 lub nowszej.

  3. Zabić

    pkill -P $$

    To zabija każde zadanie, dla którego bieżący proces jest nadrzędny. Innymi słowy, zabija to cały działający proces w tle.

    Jeśli twój system nie ma pkill, spróbuj zastąpić ten wiersz:

    kill 0

    co również zabija bieżącą grupę procesów .

Łatwo testowalny przykład

Zmieniając skrypt, możemy go przetestować nawet bez gulpinstalacji:

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

Powyższy skrypt można uruchomić jako, bash script.sh 1 3a pierwszy proces kończy się jako pierwszy. Alternatywnie można uruchomić go jako, bash script.sh 3 1a drugi proces zakończy się jako pierwszy. W obu przypadkach widać, że działa to zgodnie z potrzebami.

John1024
źródło
To wygląda świetnie. Niestety te polecenia nie działają w moim środowisku bash (msysgit). To moje złe, że nie sprecyzowałem tego. Wypróbuję to jednak na prawdziwym Linux-ie.
blah238,
(1) Nie wszystkie wersje bashobsługują -nopcję waitpolecenia. (2) Zgadzam się w 100% z pierwszym zdaniem - twoje rozwiązanie rozpoczyna dwa procesy, czeka na zakończenie pierwszego, a następnie zabija drugie. Ale pytanie brzmi „… jeśli jedno z poleceń się nie powiedzie , drugie zostanie zakończone?” Uważam, że twoje rozwiązanie nie jest tym, czego chce PO. (3) Dlaczego zmieniłeś się (…) &na { …; } &? Te &siły lista (komenda grupa), aby uruchomić w podpowłoce tak. IMHO, dodałeś postacie i prawdopodobnie wprowadziłeś zamieszanie (musiałem na to spojrzeć dwa razy, aby to zrozumieć) bez żadnej korzyści.
G-Man mówi „Reinstate Monica”
1
John ma rację, są to serwery sieciowe, które normalnie powinny oba nadal działać, chyba że wystąpi błąd lub zostaną zasygnalizowane zakończenie pracy. Więc nie sądzę, że musimy sprawdzać poziom błędu każdego procesu, tylko czy nadal działa.
blah238,
2
pkillnie jest dla mnie dostępny, ale kill 0wydaje się mieć ten sam efekt. Zaktualizowałem także środowisko Git dla Windows i wygląda na to, że wait -nteraz działa, więc akceptuję tę odpowiedź.
blah238,
1
Ciekawy. Chociaż widzę, że to zachowanie jest udokumentowane na niektórych wersjach o kill. dokumentacja mojego systemu nie wspomina o tym. Jednak i kill 0tak działa. Dobre znalezisko!
John1024,
1

To trudne. Oto, co wymyśliłem; możliwe jest uproszczenie / usprawnienie:

#!/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
  • Najpierw uruchom proces ( while true; do sleep 42; done &), który śpi / zatrzymuje się na zawsze. Jeśli masz pewność, że twoje dwa polecenia zakończą się w określonym czasie (np. Godzinie), możesz zmienić to na pojedynczy sen, który go przekroczy (np sleep 3600.). Następnie możesz zmienić następującą logikę, aby użyć tego jako limitu czasu; tzn. zabij procesy, jeśli nadal działają po tak długim czasie. (Uwaga: powyższy skrypt obecnie tego nie robi).
  • Uruchom dwa procesy asynchroniczne (współbieżne w tle).
    • Nie trzeba ./za cd.
    • command & echo "$!" > somewhere; wait "$!" jest trudną konstrukcją, która uruchamia proces asynchronicznie, przechwytuje swój PID, a następnie czeka na niego; dzięki czemu jest to rodzaj procesu pierwszego planu (synchronicznego). Ale dzieje się tak na (…)liście, która jest w całości w tle, więc gulpprocesy przebiegają asynchronicznie.
    • Po wyjściu jednego z gulpprocesów zapisz jego status do pliku tymczasowego i zabij proces „na zawsze”.
  • sleep 1 aby zabezpieczyć się przed wyścigiem, w którym pierwszy proces w tle umiera, zanim drugi ma szansę zapisać swój PID w pliku.
  • Poczekaj na zakończenie procesu „wiecznego snu”. Dzieje się tak po wyjściu jednego z gulpprocesów, jak wspomniano powyżej.
  • Zobacz, który proces w tle został zakończony. Jeśli to się nie powiedzie, zabij drugiego.
  • Jeśli jeden proces się nie powiedzie, a drugi zabijemy, poczekaj, aż drugi się zakończy i zapisze swój status w pliku. Jeśli pierwszy proces zakończy się pomyślnie, poczekaj na zakończenie drugiego.
  • Sprawdź statusy dwóch procesów.
G-Man mówi „Przywróć Monikę”
źródło
1

Dla kompletności oto, co skończyło się na użyciu:

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

Działa to dla mnie na Git dla Windows 2.5.3 64-bit. Starsze wersje mogą nie akceptować tej -nopcji wait.

blah238
źródło
1

W moim systemie (Centos), waitnie mam, -nwięc zrobiłem to:

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

To nie czeka na „jedno”, a raczej czeka na pierwsze. Ale nadal może pomóc, jeśli wiesz, który serwer zostanie zatrzymany jako pierwszy.

Ondra Žižka
źródło
To, czy waitma taką -nopcję, zależy od używanej powłoki, a nie od dystrybucji Linuksa.
Kusalananda