Comment puis-je tuer et attendre la fin des processus d'arrière-plan dans un script shell lorsque je le Ctrl + C?

24

J'essaie de mettre en place un script shell pour qu'il exécute des processus d'arrière-plan, et quand je Ctrlcle script shell, il tue les enfants, puis se ferme.

Le meilleur que j'ai réussi à trouver est celui-ci. Il semble que lekill 0 -INT tue également le script avant l'attente, de sorte que le script shell meurt avant la fin des enfants.

Toutes les idées sur la façon dont je peux faire attendre ce script shell pour que les enfants meurent après l'envoi 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
la source
J'ai trouvé cette question en recherchant "shell intercept kill switch", merci de partager la commande trap, il est très utile d'exécuter une commande après avoir quitté une application avec Ctrl + C lorsque l'application ne se ferme pas correctement via le bouton Fermer GUI.
baptx

Réponses:

22

Votre killcommande est à l'envers.

Comme de nombreuses commandes UNIX, les options commençant par un moins doivent venir en premier, avant les autres arguments.

Si vous écrivez

kill -INT 0

il voit l' -INToption comme une option et envoie SIGINTà 0(0 est un nombre spécial signifiant tous les processus dans le groupe de processus actuel).

Mais si vous écrivez

kill 0 -INT

il voit le 0, décide qu'il n'y a plus d'options, donc utilise SIGTERMpar défaut. Et envoie cela au groupe de processus actuel, comme si vous l'aviez fait

kill -TERM 0 -INT    

(il essaierait également d'envoyer SIGTERMà -INT, ce qui provoquerait une erreur de syntaxe, mais il enverrait SIGTERMà0 premier, et n'atteint jamais aussi loin.)

Donc, votre script principal obtient un SIGTERMavant de pouvoir exécuter le waitetecho DONE .

Ajouter

trap 'echo got SIGTERM' TERM

au sommet, juste après

trap 'killall' INT

et l'exécuter à nouveau pour le prouver.

Comme le souligne Stéphane Chazelas, vos enfants issus de milieux ( process1, etc.) ignorerontSIGINT par défaut.

En tout cas, je pense que l'envoi SIGTERMaurait plus de sens.

Enfin, je ne sais pas s'il kill -process groupest garanti d'aller en premier aux enfants. Ignorer les signaux lors de l'arrêt peut être une bonne idée.

Essayez donc ceci:

#!/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
la source
Merci, ça marche! Cela semble avoir posé problème.
slipheed
9

Malheureusement, les commandes démarrées en arrière-plan sont définies par le shell pour ignorer SIGINT, et pire, elles ne peuvent pas le ignorer avec trap. Sinon, il vous suffirait de faire

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

Parce que process1 et process2 obtiendraient le SIGINT lorsque vous appuyez sur Ctrl-C car ils font partie du même groupe de processus qui est le groupe de processus de premier plan du terminal.

Le code ci-dessus fonctionnera avec pdksh et zsh qui à cet égard ne sont pas conformes à POSIX.

Avec d'autres shells, vous devrez utiliser autre chose pour restaurer le gestionnaire par défaut pour SIGINT comme:

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

ou utilisez un signal différent comme SIGTERM.

Stéphane Chazelas
la source
2

Pour ceux qui veulent simplement tuer un processus et attendre qu'il meure, mais pas indéfiniment :

Il attend max 60 secondes par type de signal.

Avertissement: Cette réponse n'est en aucun cas liée à la capture d'un signal de mise à mort et à sa distribution.

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

Il sélectionne l'application à tuer par son nom ou ses arguments. Conservez les crochets pour la première lettre du nom de l'application pour éviter de faire correspondre grep lui-même.

Avec certaines modifications, vous pouvez utiliser directement le PID ou un simple pidof process_nameau lieu de l' psinstruction.

Détails du code: le grep final consiste à obtenir le PID sans les espaces de fin.

KrisWebDev
la source
1

Si vous souhaitez gérer certains processus d'arrière-plan, pourquoi ne pas utiliser les fonctionnalités de contrôle des tâches 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
Maxime
la source
0

J'ai donc aussi bricolé avec ça. Dans Bash, vous pouvez définir des fonctions et faire des choses fantaisistes avec celles-ci. Mes collègues et moi utilisons terminator, donc pour les exécutions par lots, nous générons normalement tout un tas de fenêtres de terminaison si nous voulons afficher la sortie (moins élégant que les tmuxonglets, mais vous pouvez utiliser une interface plus proche de l'interface graphique haha).

Voici la configuration que j'ai trouvée, ainsi qu'un exemple de choses que vous pouvez exécuter:

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

L'exécuter

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

Modifiez @Maxim, je viens de voir votre suggestion, cela rend les choses plus simples! Merci!

eacousineau
la source