web-dev-qa-db-fra.com

Impossible d'arrêter un script bash avec Ctrl + C

J'ai écrit un simple script bash avec une boucle pour imprimer la date et le ping sur une machine distante:

#!/bin/bash
while true; do
    #     *** DATE: Thu Sep 17 10:17:50 CEST 2015  ***
    echo -e "\n*** DATE:" `date` " ***";
    echo "********************************************"
    ping -c5 $1;
done

Lorsque je l'exécute à partir d'un terminal, je ne peux pas l'arrêter avec Ctrl+C. Il semble qu'il envoie le ^C au terminal, mais le script ne s'arrête pas.

MacAir:~ tomas$ ping-tester.bash www.google.com

*** DATE: Thu Sep 17 23:58:42 CEST 2015  ***
********************************************
PING www.google.com (216.58.211.228): 56 data bytes
64 bytes from 216.58.211.228: icmp_seq=0 ttl=55 time=39.195 ms
64 bytes from 216.58.211.228: icmp_seq=1 ttl=55 time=37.759 ms
^C                                                          <= That is Ctrl+C press
--- www.google.com ping statistics ---
2 packets transmitted, 2 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 40.887/59.699/78.510/18.812 ms

*** DATE: Thu Sep 17 23:58:48 CEST 2015  ***
********************************************
PING www.google.com (216.58.211.196): 56 data bytes
64 bytes from 216.58.211.196: icmp_seq=0 ttl=55 time=37.460 ms
64 bytes from 216.58.211.196: icmp_seq=1 ttl=55 time=37.371 ms

Peu importe combien de fois j'appuie dessus ou à quelle vitesse je le fais. Je ne peux pas l'arrêter.
Faites le test et réalisez par vous-même.

Comme solution secondaire, je l’arrête avec Ctrl+Z, cela l'arrête, puis kill %1.

Que se passe-t-il exactement ici avec ^C?

42
nephewtom

Ce qui se passe, c'est que bash et ping reçoivent le SIGINT (bash étant pas interactif, à la fois ping et bash s'exécute dans le même groupe de processus qui a été créé et défini comme groupe de processus de premier plan du terminal par le shell interactif à partir duquel vous avez exécuté ce script).

Cependant, bash gère ce SIGINT de manière asynchrone, uniquement après la fin de la commande en cours d'exécution. bash ne se ferme qu'après avoir reçu ce SIGINT si la commande en cours d'exécution meurt d'un SIGINT (c'est-à-dire que son état de sortie indique qu'il a été tué par SIGINT).

$ bash -c 'sh -c "trap exit\ 0 INT; sleep 10; :"; echo here'
^Chere

Ci-dessus, bash, sh et sleep reçoivent SIGINT lorsque j'appuie sur Ctrl-C, mais sh sort normalement avec un code de sortie 0, donc bash ignore le SIGINT, c'est pourquoi nous voyons "ici".

ping, au moins celui d'iputils, se comporte comme ça. Lorsqu'il est interrompu, il imprime des statistiques et sort avec un état de sortie 0 ou 1 selon que ses pings ont été répondus ou non. Ainsi, lorsque vous appuyez sur Ctrl-C pendant que ping est en cours d'exécution, bash note que vous avez appuyé sur Ctrl-C dans ses gestionnaires SIGINT, mais puisque ping se ferme normalement, bash ne se ferme pas.

Si vous ajoutez un sleep 1 dans cette boucle et appuyez sur Ctrl-C pendant que sleep est en cours d'exécution, car sleep n'a pas de gestionnaire spécial sur SIGINT, il mourra et signalera à bash qu'il est mort d'un SIGINT, et dans ce cas bash se fermera (il se tuera en fait avec SIGINT pour signaler l'interruption à son parent).

Quant à savoir pourquoi bash se comporte comme ça, je ne suis pas sûr et je note que le comportement n'est pas toujours déterministe. Je viens de demander la question sur la liste de diffusion bash development ( Update : @Jilles has now cloué la raison en sa réponse ).

Le seul autre Shell que j'ai trouvé qui se comporte de la même manière est ksh93 (Update, comme mentionné par @Jilles, FreeBSD sh ). Là, SIGINT semble être clairement ignoré. Et ksh93 se ferme chaque fois qu'une commande est supprimée par SIGINT.

Vous obtenez le même comportement que bash ci-dessus mais aussi:

ksh -c 'sh -c "kill -INT \$\$"; echo test'

Ne produit pas de "test". Autrement dit, il se ferme (en se tuant avec SIGINT là-bas) si la commande qu'il attendait meurt de SIGINT, même si elle-même n'a pas reçu ce SIGINT.

Une solution de contournement consisterait à ajouter:

trap 'exit 130' INT

En haut du script pour forcer bash à quitter lors de la réception d'un SIGINT (notez que dans tous les cas, SIGINT ne sera pas traité de manière synchrone, uniquement après la fin de la commande en cours d'exécution).

Idéalement, nous voudrions signaler à notre parent que nous sommes morts d'un SIGINT (de sorte que s'il s'agit d'un autre script bash par exemple, ce script bash est également interrompu). Faire un exit 130 n'est pas la même chose que mourir de SIGINT (bien que certains shells définissent $? à la même valeur pour les deux cas), mais il est souvent utilisé pour signaler un décès par SIGINT (sur les systèmes où SIGINT vaut 2, ce qui est le plus).

Cependant pour bash, ksh93 ou FreeBSD sh, cela ne fonctionne pas. Ce statut de sortie 130 n'est pas considéré comme une mort par SIGINT et un script parent serait pas abandonné là.

Donc, une meilleure alternative serait de se suicider avec SIGINT lors de la réception de SIGINT:

trap '
  trap - INT # restore default INT handler
  kill -s INT "$$"
' INT
26
Stéphane Chazelas

L'explication est que bash implémente WCE (attente et sortie coopérative) pour SIGINT et SIGQUIT par http://www.cons.org/cracauer/sigint.html . Cela signifie que si bash reçoit SIGINT ou SIGQUIT en attendant la fin d'un processus, il attendra la fin du processus et se fermera lui-même si le processus se termine sur ce signal. Cela garantit que les programmes qui utilisent SIGINT ou SIGQUIT dans leur interface utilisateur fonctionneront comme prévu (si le signal n'a pas provoqué l'arrêt du programme, le script continuera normalement).

Un inconvénient apparaît avec les programmes qui capturent SIGINT ou SIGQUIT mais se terminent à cause de cela mais en utilisant une sortie normale () au lieu de se renvoyer le signal à eux-mêmes. Il peut ne pas être possible d'interrompre les scripts qui appellent de tels programmes. Je pense que la vraie solution est dans de tels programmes tels que ping et ping6.

Un comportement similaire est implémenté par ksh93 et ​​/ bin/sh de FreeBSD, mais pas par la plupart des autres shells.

13
jilles

Comme vous le supposez, cela est dû au fait que le SIGINT est envoyé au processus subordonné et que le shell continue après la fin de ce processus.

Pour mieux gérer cela, vous pouvez vérifier l'état de sortie des commandes en cours d'exécution. Le code de retour Unix code à la fois la méthode par laquelle un processus s'est arrêté (appel système ou signal) et quelle valeur a été transmise à exit() ou quel signal a mis fin au processus. Tout cela est assez compliqué, mais le moyen le plus rapide de l'utiliser est de savoir qu'un processus qui s'est terminé par signal aura un code retour non nul. Ainsi, si vous vérifiez le code retour dans votre script, vous pouvez quitter vous-même si le processus enfant a été interrompu, supprimant ainsi le besoin d'inélégations comme les appels inutiles sleep. Un moyen rapide de le faire tout au long de votre script consiste à utiliser set -e, bien qu'il puisse nécessiter quelques ajustements pour les commandes dont l'état de sortie est une valeur non nulle attendue.

5
Tom Hunt

Le terminal remarque le contrôle-c et envoie un signal INT au groupe de processus de premier plan, qui inclut ici le shell, car ping n'a pas créé de nouveau groupe de processus de premier plan. Ceci est facile à vérifier en piégeant INT.

#! /bin/bash
trap 'echo oh, I am slain; exit' INT
while true; do
  ping -c5 127.0.0.1
done

Si la commande en cours d'exécution a créé un nouveau groupe de processus de premier plan, le contrôle-c ira à ce groupe de processus et non au shell. Dans ce cas, le shell devra inspecter les codes de sortie, car il ne sera pas signalé par le terminal.

(INT la manipulation dans des shells peut être fabuleusement compliquée, soit dit en passant, car le Shell a parfois besoin d'ignorer le signal, et parfois non. Source plonge si vous êtes curieux, ou réfléchissez: tail -f /etc/passwd; echo foo)

4
thrig

Eh bien, j'ai essayé d'ajouter un sleep 1 au script bash, et bang!
Maintenant je peux l'arrêter avec deux Ctrl+C.

En appuyant sur Ctrl+C, un signal SIGINT est envoyé au processus en cours d'exécution, laquelle commande a été exécutée à l'intérieur de la boucle. Ensuite, le processus subshell continue d'exécuter la commande suivante dans la boucle, qui démarre un autre processus. Pour pouvoir arrêter le script, il est nécessaire d'envoyer deux signaux SIGINT, l'un pour interrompre la commande en cours d'exécution et l'autre pour interrompre le processus subshell.

Dans le script sans l'appel sleep, appuyez sur Ctrl+C vraiment rapide et plusieurs fois ne semble pas fonctionner, et il n'est pas possible de sortir de la boucle. Je suppose que presser deux fois n'est pas assez rapide pour arriver juste au bon moment entre l'interruption du processus exécuté en cours et le début du suivant. Chaque Ctrl+C pressé enverra un SIGINT à un processus exécuté à l'intérieur de la boucle, mais ni au sous-shell.

Dans le script avec sleep 1, cet appel suspendra l'exécution pendant une seconde, et lorsqu'il sera interrompu par le premier Ctrl+C (premier SIGINT), le sous-shell prendra plus de temps pour exécuter la commande suivante. Alors maintenant, le deuxième Ctrl+C (second SIGINT) ira dans le sous-shell, et l'exécution du script se terminera.

2
nephewtom

Si quelqu'un est intéressé par un correctif pour cette fonctionnalité bash, et pas tellement dans la philosophie derrière , voici une proposition:

N'exécutez pas directement la commande problématique, mais à partir d'un wrapper qui a) attend qu'elle se termine b) ne joue pas avec les signaux et c) ne pas implémente le mécanisme WCE lui-même, mais meurt simplement lors de la réception d'un SIGINT.

Un tel wrapper pourrait être fait avec awk + sa fonction system().

$ while true; do awk 'BEGIN{system("ping -c5 localhost")}'; done
PING localhost(localhost (::1)) 56 data bytes
64 bytes from localhost (::1): icmp_seq=1 ttl=64 time=0.082 ms
64 bytes from localhost (::1): icmp_seq=2 ttl=64 time=0.087 ms
^C
--- localhost ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1022ms
rtt min/avg/max/mdev = 0.082/0.084/0.087/0.009 ms
[3]-  Terminated              ping -c5 localhost

Mettez dans un script comme OP:

#!/bin/bash
while true; do
        echo -e "\n*** DATE:" `date` " ***";
        echo "********************************************"
        awk 'BEGIN{system(ARGV[1])}' "ping -c5 ${1-localhost}"
done
1
mosvy
pgrep -f process_name > any_file_name
sed -i 's/^/kill /' any_file_name
chmod 777 any_file_name
./any_file_name

par exemple pgrep -f firefox récupérera le PID de l'exécution de firefox et enregistrera ce PID dans un fichier appelé any_file_name. La commande 'sed' ajoutera le kill au début du numéro PID dans le fichier 'any_file_name'. La troisième ligne va any_file_name fichier exécutable. Maintenant, la quatrième ligne va tuer le PID disponible dans le fichier any_file_name. Écrire les quatre lignes ci-dessus dans un fichier et exécuter ce fichier peut faire Control-C. Ça marche très bien pour moi.

0
user2176228

Essaye ça:

#!/bin/bash
while true; do
   echo "Ctrl-c works during sleep 5"
   sleep 5
   echo "But not during ping -c 5"
   ping -c 5 127.0.0.1
done

Maintenant, changez la première ligne en:

#!/bin/sh

et réessayez - voyez si le ping est maintenant interruptible.

0
Sparrow