Jak mogę zabijać i czekać na zakończenie procesów w tle w skrypcie powłoki, gdy Ctrl + C go?


24

Próbuję skonfigurować skrypt powłoki, aby uruchamiał procesy w tle, a kiedy Ctrlcwykonuję skrypt powłoki, zabija dzieci, a następnie kończy działanie.

Najlepsze, co udało mi się wymyślić, to to. Wygląda na to, że kill 0 -INTrównież zabija skrypt, zanim nastąpi oczekiwanie, więc skrypt powłoki umiera przed ukończeniem przez dzieci.

Jakieś pomysły na to, jak sprawić, by ten skrypt powłoki czekał na śmierć dzieci po wysłaniu INT?

#!/bin/bash
trap 'killall' INT

killall() {
    echo "**** Shutting down... ****"
    kill 0 -INT
    wait # Why doesn't this wait??
    echo DONE
}

process1 &
process2 &
process3 &

cat # wait forever


Znalazłem to pytanie, wyszukując „przełącznik zabijania przechwytywania powłoki”, dzięki za udostępnienie polecenia pułapki, bardzo przydatne jest wykonanie polecenia po wyjściu z aplikacji za pomocą Ctrl + C, gdy aplikacja nie wychodzi poprawnie za pomocą przycisku zamykania GUI.
baptx

Odpowiedzi:


22

Twoje killpolecenie jest cofnięte.

Podobnie jak wiele poleceń UNIX, opcje zaczynające się od minus muszą być na pierwszym miejscu, przed innymi argumentami.

Jeśli napiszesz

kill -INT 0

widzi to -INTjako opcję i wysyła SIGINTna 0( 0to specjalny numer oznaczający wszystkie procesy w bieżącej grupie procesów).

Ale jeśli napiszesz

kill 0 -INT

widzi 0, decyduje, że nie ma więcej opcji, więc używa SIGTERMdomyślnie. I wysyła to do bieżącej grupy procesów, tak samo jak gdybyś to zrobił

kill -TERM 0 -INT    

(próbowałby również wysłać SIGTERMdo -INT, co spowodowałoby błąd składniowy, ale wysyła SIGTERMdo 0pierwszego i nigdy nie dociera tak daleko).

Więc twój główny skrypt dostaje, SIGTERMzanim będzie mógł uruchomić waiti echo DONE.

Dodaj

trap 'echo got SIGTERM' TERM

u góry, zaraz po

trap 'killall' INT

i uruchom go ponownie, aby to udowodnić.

Jak wskazuje Stephane Chazelas, dzieci w tle ( process1itp.) SIGINTDomyślnie będą ignorować .

W każdym razie myślę, że wysyłanie SIGTERMbyłoby bardziej sensowne.

Wreszcie nie jestem pewien, czy kill -process groupgwarantuje się, że najpierw pójdzie do dzieci. Ignorowanie sygnałów podczas wyłączania może być dobrym pomysłem.

Spróbuj tego:

#!/bin/bash
trap 'killall' INT

killall() {
    trap '' INT TERM     # ignore INT and TERM while shutting down
    echo "**** Shutting down... ****"     # added double quotes
    kill -TERM 0         # fixed order, send TERM not INT
    wait
    echo DONE
}

./process1 &
./process2 &
./process3 &

cat # wait forever

Dzięki, to działa! To wydaje się być problemem.
slipheed

9

Niestety polecenia uruchamiane w tle są ustawiane przez powłokę tak, aby ignorowały SIGINT, a co gorsza, nie można ich zignorować trap. W przeciwnym razie wszystko, co musisz zrobić, to

(trap - INT; exec process1) &
(trap - INT; exec process2) &
trap '' INT
wait

Ponieważ proces1 i proces2 otrzymałyby SIGINT po naciśnięciu Ctrl-C, ponieważ są one częścią tej samej grupy procesów, która jest grupą procesów pierwszego planu w terminalu.

Powyższy kod będzie działał z pdksh i zsh, które pod tym względem nie są zgodne z POSIX.

W przypadku innych powłok należy użyć czegoś innego, aby przywrócić domyślny moduł obsługi dla SIGINT, na przykład:

perl -e '$SIG{INT}=DEFAULT; exec "process1"' &

lub użyj innego sygnału, takiego jak SIGTERM.


2

Dla tych, którzy chcą zabić proces i czekać na jego śmierć, ale nie na czas nieokreślony :

Czeka maksymalnie 60 sekund na typ sygnału.

Ostrzeżenie: ta odpowiedź nie jest w żaden sposób związana z przechwytywaniem sygnału zabicia i wysyłaniem go.

# close_app_sub GREP_STATEMENT SIGNAL DURATION_SEC
# GREP_STATEMENT must not match itself!
close_app_sub() {
    APP_PID=$(ps -x | grep "$1" | grep -oP '^\s*\K[0-9]+' --color=never)
    if [ ! -z "$APP_PID" ]; then
        echo "App is open. Trying to close app (SIGNAL $2). Max $3sec."
        kill $2 "$APP_PID"
        WAIT_LOOP=0
        while ps -p "$APP_PID" > /dev/null 2>&1; do
            sleep 1
            WAIT_LOOP=$((WAIT_LOOP+1))
            if [ "$WAIT_LOOP" = "$3" ]; then
                break
            fi
        done
    fi
    APP_PID=$(ps -x | grep "$1" | grep -oP '^\s*\K[0-9]+' --color=never)
    if [ -z "$APP_PID" ]; then return 0; else return "$APP_PID"; fi
}

close_app() {
    close_app_sub "$1" "-HUP" "60"
    close_app_sub "$1" "-TERM" "60"
    close_app_sub "$1" "-SIGINT" "60"
    close_app_sub "$1" "-KILL" "60"
    return $?
}

close_app "[f]irefox"

Wybiera aplikację do zabicia według nazwy lub argumentów. Zachowaj nawiasy kwadratowe dla pierwszej litery nazwy aplikacji, aby uniknąć dopasowania samego grep.

Z pewnymi zmianami możesz bezpośrednio użyć PID lub prostszego pidof process_namezamiastps instrukcji.

Szczegóły kodu: Ostatecznym grepem jest uzyskanie PID bez końcowych spacji.


1

Jeśli chcesz zarządzać niektórymi procesami w tle, dlaczego nie korzystać z funkcji kontroli zadań bash?

$ gedit &
[1] 2581
$ emacs &
[2] 2594
$ jobs
[1]-  Running                 gedit &
[2]+  Running                 emacs &
$ jobs -p
2581
2594
$ kill -2 `jobs -p`
$ jobs
[1]-  Interrupt               gedit
[2]+  Done                    emacs

0

Więc majstrowałem przy tym również. W Bash możesz definiować funkcje i robić z nimi wymyślne rzeczy. Ja i moi współpracownicy używamy terminator, więc do wykonania wsadowego zwykle odradzamy całą masę okien terminatora, jeśli chcemy zobaczyć wyniki (mniej eleganckie niż tmuxtabulatory, ale można użyć haha ​​bardziej przypominającego GUI).

Oto konfiguracja, którą wymyśliłem, a także przykład rzeczy, które możesz uruchomić:

#!/bin/bash
custom()
{
    terun 'something cool' 'yes'
    run 'xclock'
    terun 'echoes' 'echo Hello; echo Goodbye; read'
    func()
    {
        local i=0
        while :
        do
            echo "Iter $i"
            let i+=1
            sleep 0.25
        done
    }
    export -f func
    terun 'custom func' 'func'
}

# [ Setup ]
run()
{
    "$@" &
    # Give process some time to start up so windows are sequential
    sleep 0.05
}
terun()
{
    run terminator -T "$1" -e "$2"
}
finish()
{
    procs="$(jobs -p)"
    echo "Kill: $procs"
    # Ignore process that are already dead
    kill $procs 2> /dev/null
}
trap 'finish' 2

custom

echo 'Press <Ctrl+C> to kill...'
wait

Uruchamiam to

$ ./program_spawner.sh 
Press <Ctrl+C> to kill...
^CKill:  9835
9840
9844
9850
$ 

Edytuj @Maxim, właśnie zobaczyłem twoją sugestię, która sprawia, że ​​ton jest prostszy! Dzięki!

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.