Wie kann ich in einem Shell-Skript abbrechen und warten, bis die Hintergrundprozesse beendet sind, wenn ich Strg + C drücke?

24

Ich versuche, ein Shell-Skript so einzurichten, dass Hintergrundprozesse ausgeführt werden. Wenn ich Ctrlcdas Shell-Skript verwende, werden die untergeordneten Elemente beendet und dann beendet.

Das Beste, was mir gelungen ist, ist das hier. Es scheint, dass das kill 0 -INTSkript vor dem Warten ebenfalls beendet wird, sodass das Shell-Skript abstirbt, bevor die Kinder fertig sind.

Irgendwelche Ideen, wie ich dieses Shell-Skript dazu bringen kann, darauf zu warten, dass die Kinder nach dem Senden sterben 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
Slipheed
quelle
Ich fand diese Frage bei der Suche nach "Shell Intercept Kill Switch". Dank der Freigabe des Trap-Befehls ist es sehr nützlich, einen Befehl nach dem Beenden einer App mit Strg + C auszuführen, wenn die App nicht ordnungsgemäß über die Schaltfläche zum Schließen der GUI beendet wird.
Taufe

Antworten:

22

Ihr killBefehl ist rückwärts.

Wie bei vielen UNIX-Befehlen müssen Optionen, die mit einem Minus beginnen, vor anderen Argumenten stehen.

Wenn du schreibst

kill -INT 0

Es sieht das -INTals eine Option und sendet SIGINTan 0( 0ist eine spezielle Nummer, die alle Prozesse in der aktuellen Prozessgruppe bezeichnet).

Aber wenn du schreibst

kill 0 -INT

Es sieht die 0, entscheidet, dass es keine weiteren Optionen gibt, also verwendet es SIGTERMstandardmäßig. Und sendet das an die aktuelle Prozessgruppe, genauso wie Sie es getan haben

kill -TERM 0 -INT    

(es würde auch versuchen , zu senden SIGTERMan -INT, die einen Syntaxfehler verursachen würde, aber es sendet SIGTERMan 0ersten und wird nie so weit.)

Also Ihr Haupt - Skript ein bekommt , SIGTERMbevor es wird die laufen waitund echo DONE.

Hinzufügen

trap 'echo got SIGTERM' TERM

an der Spitze gleich danach

trap 'killall' INT

und führen Sie es erneut aus, um dies zu beweisen.

Wie Stephane Chazelas betont, werden Ihre Hintergrundkinder ( process1usw.) SIGINTstandardmäßig ignoriert .

In jedem Fall halte ich das Senden SIGTERMfür sinnvoller.

Schließlich bin ich mir nicht sicher, ob kill -process groupes garantiert ist, zuerst zu den Kindern zu gehen. Das Ignorieren von Signalen beim Herunterfahren ist möglicherweise eine gute Idee.

Also versuche folgendes:

#!/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
Mikel
quelle
Danke, das funktioniert! Das scheint ein Problem gewesen zu sein.
Slipheed
9

Leider werden Befehle, die im Hintergrund gestartet werden, von der Shell so eingestellt, dass sie SIGINT ignorieren. Schlimmer noch, sie können sie nicht mit ignorieren trap. Ansonsten müssten Sie nur noch

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

Weil process1 und process2 den SIGINT erhalten würden, wenn Sie Ctrl-C drücken, da sie Teil derselben Prozessgruppe sind, die die Vordergrundprozessgruppe des Terminals ist.

Der obige Code funktioniert mit pdksh und zsh, die in dieser Hinsicht nicht POSIX-konform sind.

Bei anderen Shells müssten Sie etwas anderes verwenden, um den Standardhandler für SIGINT wiederherzustellen:

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

oder verwenden Sie ein anderes Signal wie SIGTERM.

Stéphane Chazelas
quelle
2

Für diejenigen, die nur einen Prozess beenden und darauf warten möchten, dass er stirbt, aber nicht auf unbestimmte Zeit :

Es wartet maximal 60 Sekunden pro Signaltyp.

Warnung: Diese Antwort hat nichts mit dem Abfangen und Versenden eines Kill-Signals zu tun.

# 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"

Es wählt die zu tötende App nach Namen oder Argumenten aus. Behalten Sie die Klammern für den ersten Buchstaben des App-Namens bei, um eine Übereinstimmung mit grep selbst zu vermeiden.

Mit einigen Änderungen können Sie direkt die PID oder eine einfachere pidof process_nameanstelle der psAnweisung verwenden.

Code-Details: Das letzte grep ist, die PID ohne die nachfolgenden Leerzeichen zu erhalten.

KrisWebDev
quelle
1

Wenn Sie Hintergrundprozesse verwalten möchten, können Sie die Funktionen zur Bash-Job-Steuerung verwenden.

$ 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
Maxime
quelle
0

Also habe ich auch daran herumgebastelt. In Bash können Sie Funktionen definieren und damit ausgefallene Dinge tun. Meine Kollegen und ich verwenden terminator, also erzeugen wir für Stapelausführungen normalerweise eine ganze Reihe von Abschlussfenstern, wenn wir die Ausgabe anzeigen möchten (weniger elegant als tmuxRegisterkarten, aber Sie können eine GUI-ähnliche Oberfläche verwenden, haha).

Hier ist das Setup, das ich mir ausgedacht habe, sowie ein Beispiel für Dinge, die Sie ausführen können:

#!/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

Laufen es

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

Edit @Maxim, habe gerade deinen Vorschlag gesehen, das macht es eine Tonne einfacher! Vielen Dank!

eacousineau
quelle