No se puede detener un script bash con Ctrl + C

42

Escribí un script bash simple con un bucle para imprimir la fecha y hacer ping a una máquina remota:

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

Cuando lo ejecuto desde una terminal no puedo detenerlo Ctrl+C. Parece que envía el ^Cal terminal, pero el script no se detiene.

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

No importa cuántas veces lo presione o qué tan rápido lo haga. No puedo detenerlo.
Haz la prueba y date cuenta por ti mismo.

Como solución secundaria, lo estoy deteniendo Ctrl+Z, eso lo detiene y luego kill %1.

¿Qué está pasando exactamente aquí con ^C?

Nephewtom
fuente

Respuestas:

26

Lo que pasa es que tanto bashy pingreciben el SIGINT ( bashsiendo no interactiva, tanto pingy bashejecutarse en el mismo grupo de procesos que se ha creado y se establece como grupo de procesos en primer plano de la terminal por el intérprete de comandos interactivo que ejecutó el guión de).

Sin embargo, bashmaneja ese SIGINT de forma asíncrona, solo después de que el comando actualmente en ejecución haya salido. bashsolo sale al recibir ese SIGINT si el comando actualmente en ejecución muere de un SIGINT (es decir, su estado de salida indica que SIGINT lo ha matado).

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

Por encima, bash, shy sleeprecibir SIGINT al pulsar Ctrl-C, pero shlas salidas normalmente con un código de salida de 0, por lo que bashignora el SIGINT, que es por eso que vemos "aquí".

ping, al menos el de iputils, se comporta así. Cuando se interrumpe, imprime estadísticas y sale con un estado de salida 0 o 1 dependiendo de si se respondieron o no sus pings. Entonces, cuando presiona Ctrl-C mientras se pingestá ejecutando, las bashnotas que presionó Ctrl-Cen sus controladores SIGINT, pero como pingsale normalmente, bashno salen.

Si agrega un sleep 1en ese bucle y presiona Ctrl-Cmientras se sleepestá ejecutando, porque sleepno tiene un controlador especial en SIGINT, morirá e informará bashque murió de un SIGINT, y en ese caso bashsaldrá (en realidad se suicidará con SIGINT como para informar la interrupción a su padre).

En cuanto a por qué se bashcomporta así, no estoy seguro y noto que el comportamiento no siempre es determinista. Acabo de hacer la pregunta en la bashlista de correo de desarrollo ( Actualización : @Jilles ahora ha aclarado la razón en su respuesta ).

El único otro shell que encontré que se comporta de manera similar es ksh93 (Actualización, como lo menciona @Jilles, también lo hace FreeBSDsh ). Allí, SIGINT parece ser simplemente ignorado. Y ksh93sale cada vez que un comando es asesinado por SIGINT.

Obtiene el mismo comportamiento que el bashanterior pero también:

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

No emite "prueba". Es decir, sale (al suicidarse con SIGINT allí) si el comando que estaba esperando muere de SIGINT, incluso si no recibió ese SIGINT.

Una solución sería agregar un:

trap 'exit 130' INT

En la parte superior de la secuencia de comandos para forzar la bashsalida al recibir un SIGINT (tenga en cuenta que, en cualquier caso, SIGINT no se procesará de forma síncrona, solo después de que el comando actualmente en ejecución haya salido).

Idealmente, nos gustaría informar a nuestros padres que morimos de un SIGINT (de modo que si se trata de otro bashscript, por ejemplo, ese bashscript también se interrumpe). Hacer un exit 130no es lo mismo que morir de SIGINT (aunque algunos proyectiles se establecerán $?en el mismo valor para ambos casos), sin embargo, a menudo se usa para informar una muerte por SIGINT (en sistemas donde SIGINT es 2, que es el más).

Sin embargo bash, ksh93o FreeBSD sh, eso no funciona. SIGINT no considera el estado de salida 130 como una muerte y un script principal no abortaría allí.

Entonces, una alternativa posiblemente mejor sería matarnos con SIGINT al recibir SIGINT:

trap '
  trap - INT # restore default INT handler
  kill -s INT "$$"
' INT
Stéphane Chazelas
fuente
1
La respuesta de Jilles explica el "por qué". Como un ejemplo ilustrativo , considere  for f in *.txt; do vi "$f"; cp "$f" newdir; done. Si el usuario escribe Ctrl + C mientras edita uno de los archivos, visolo muestra un mensaje. Parece razonable que el ciclo continúe después de que el usuario termine de editar el archivo. (Y sí, sé que se podría decir vi *.txt; cp *.txt newdir; solo estoy enviando el forbucle como ejemplo).
Scott,
@ Scott, buen punto. Aunque vi(bueno, vimal menos) deshabilita tty isigcuando edita (aunque obviamente no lo hace cuando ejecuta :!cmd, y eso se aplicaría mucho en ese caso).
Stéphane Chazelas
@Tim, mira mi edición para corregirla en tu edición.
Stéphane Chazelas
@ StéphaneChazelas Gracias. Entonces es porque pingsale con 0 después de recibir SIGINT. Encontré un comportamiento similar cuando un script bash contiene en sudolugar de ping, pero sudosale con 1 después de recibir SIGINT. unix.stackexchange.com/questions/479023/…
Tim
13

La explicación es que bash implementa WCE (espera y salida cooperativa) para SIGINT y SIGQUIT según http://www.cons.org/cracauer/sigint.html . Eso significa que si bash recibe SIGINT o SIGQUIT mientras espera que salga un proceso, esperará hasta que el proceso salga y se cerrará si el proceso salió de esa señal. Esto asegura que los programas que usan SIGINT o SIGQUIT en su interfaz de usuario funcionarán como se esperaba (si la señal no hizo que el programa terminara, el script continuará normalmente).

Una desventaja aparece con los programas que capturan SIGINT o SIGQUIT pero luego terminan por eso, pero usando una salida normal () en lugar de reenviar la señal a ellos mismos. Es posible que no sea posible interrumpir los scripts que llaman a dichos programas. Creo que la solución real que hay en programas como ping y ping6.

Comportamiento similar es implementado por ksh93 y FreeBSD's / bin / sh, pero no por la mayoría de los otros shells.

jilles
fuente
Gracias, eso tiene mucho sentido. Observo que FreeBSD sh tampoco aborta cuando el cmd sale con exit (130), que es una forma común de informar la muerte por SIGINT de un niño (mksh hace un exit(130)por ejemplo si interrumpe mksh -c 'sleep 10;:').
Stéphane Chazelas
5

Como supones, esto se debe a que SIGINT se está enviando al proceso subordinado y al shell que continúa después de que ese proceso finaliza.

Para manejar esto de una mejor manera, puede verificar el estado de salida de los comandos que se están ejecutando. El código de retorno de Unix codifica tanto el método por el cual salió un proceso (llamada o señal del sistema) como a qué valor se pasó exit()o qué señal finalizó el proceso. Todo esto es bastante complicado, pero la forma más rápida de usarlo es saber que un proceso que finalizó por señal tendrá un código de retorno distinto de cero. Por lo tanto, si verifica el código de retorno en su secuencia de comandos, puede salir si el proceso secundario finalizó, eliminando la necesidad de inelegitudes como sleepllamadas innecesarias . Una forma rápida de hacer esto a través de su script es usarlo set -e, aunque puede requerir algunos ajustes para los comandos cuyo estado de salida es distinto de cero.

Tom Hunt
fuente
1
Establecer -e no funciona correctamente en bash a menos que esté utilizando bash-4
schily
¿Qué significa "no funciona correctamente"? Lo he usado en bash 3 con éxito, pero probablemente hay algunos casos extremos.
Tom Hunt
En algunos casos simples, bash3 salió por error. Sin embargo, esto no sucedió en el caso general. Como resultado típico, make no se detuvo cuando la creación de un objetivo falló y esto fue de un makefile que funcionó en una lista de objetivos en subdirectorios. David Korn y yo tuvimos que enviar muchas semanas por correo al encargado de bash para convencerlo de que solucionara el error de bash4.
schily
44
Tenga en cuenta que el problema aquí es que pingregresa con un estado de salida 0 al recibir SIGINT y que bashluego ignora el SIGINT que recibió si ese es el caso. Agregar un "set -e" o verificar el estado de salida no ayudará aquí. Agregar una trampa explícita en SIGINT ayudaría.
Stéphane Chazelas
4

El terminal nota el control-c y envía una INTseñal al grupo de procesos en primer plano, que aquí incluye el shell, ya que pingno ha creado un nuevo grupo de procesos en primer plano. Esto es fácil de verificar atrapando INT.

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

Si el comando que se está ejecutando ha creado un nuevo grupo de procesos en primer plano, entonces el control-c irá a ese grupo de procesos y no al shell. En ese caso, el shell deberá inspeccionar los códigos de salida, ya que el terminal no lo indicará.

( INTManejo de conchas puede ser complicado fabulosamente, por cierto, ya que la cáscara a veces necesita hacer caso omiso de la señal, ya veces no Fuente de buceo si curioso, o Ponder:. tail -f /etc/passwd; echo foo)

thrig
fuente
En este caso, el problema no es la señal de manipulación, pero el hecho de que hace bash JobControl en el guión aunque no debería, véase mi respuesta para más información
schily
Para que SIGINT vaya al nuevo grupo de procesos, el comando también tendría que hacer un ioctl () al terminal para convertirlo en el grupo de procesos en primer plano del terminal. pingno tiene ninguna razón para comenzar un nuevo grupo de procesos aquí y la versión de ping (iputils en Debian) con la que puedo reproducir el problema del OP no crea un grupo de procesos.
Stéphane Chazelas 18/09/2015
1
Tenga en cuenta que no es el terminal el que envía el SIGINT, es la disciplina de línea del dispositivo tty (el controlador (código en el núcleo) del dispositivo / dev / ttysomething) al recibir un carácter sin escape (por lo general, por lo general, ^ V) ^ C desde la terminal
Stéphane Chazelas
2

Bueno, traté de agregar un sleep 1script de bash y ¡bang!
Ahora puedo detenerlo con dos Ctrl+C.

Al presionar Ctrl+C, SIGINTse envía una señal al proceso ejecutado actualmente, cuyo comando se ejecutó dentro del bucle. Luego, el proceso de subshell continúa ejecutando el siguiente comando en el ciclo, que inicia otro proceso. Para poder detener el script, es necesario enviar dos SIGINTseñales, una para interrumpir el comando actual en ejecución y otra para interrumpir el proceso de subshell .

En el script sin la sleepllamada, presionar Ctrl+Cmuy rápido y muchas veces no parece funcionar, y no es posible salir del ciclo. Supongo que presionar dos veces no es lo suficientemente rápido como para hacerlo justo en el momento correcto entre la interrupción del proceso ejecutado actual y el inicio del siguiente. Cada Ctrl+Cpulsación enviará un SIGINTa un proceso ejecutado dentro del bucle, pero tampoco a la subshell .

En el script con sleep 1, esta llamada suspenderá la ejecución por un segundo, y cuando se interrumpe por el primero Ctrl+C(primero SIGINT), la subshell tardará más tiempo en ejecutar el siguiente comando. Así que ahora, el segundo Ctrl+C(segundo SIGINT) irá a la subshell y finalizará la ejecución del script.

Nephewtom
fuente
Estás equivocado, en un shell que funciona correctamente, un solo ^ C es suficiente, mira mi respuesta para el fondo.
schily
Bueno, considerando que ha sido rechazado y actualmente su respuesta tiene un puntaje de -1, no estoy muy convencido de que deba tomar su respuesta en serio.
nephewtom
El hecho de que algunas personas voten negativamente no siempre está relacionado con la calidad de una respuesta. Si necesita escribir dos veces ^ c, definitivamente es víctima de un error bash. ¿Intentaste una concha diferente? ¿Probaste el verdadero Bourne Shell?
schily
Si el shell funciona correctamente, ejecuta todo desde un script en el mismo grupo de procesos y luego un solo ^ c es suficiente.
schily
1
El comportamiento que @nephewtom describe en esta respuesta puede explicarse por diferentes comandos en el script que se comportan de manera diferente cuando reciben Ctrl-C. Si hay una suspensión, es muy probable que se reciba Ctrl-C mientras se ejecuta la suspensión (suponiendo que todo lo demás en el bucle sea rápido). El sueño se anula, con el valor de salida 130. El padre del sueño, un caparazón, se da cuenta de que el sueño fue anulado por la señalización y sale. Pero si el script no contiene suspensión, entonces Ctrl-C pasa al ping, que reacciona al salir con 0, por lo que el shell principal continúa ejecutando el siguiente comando.
Jonathan Hartley
0

Prueba esto:

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

Ahora cambie la primera línea a:

#!/bin/sh

e intente nuevamente: vea si el ping ahora es interrumpible.

Gorrión
fuente
0
pgrep -f process_name > any_file_name
sed -i 's/^/kill /' any_file_name
chmod 777 any_file_name
./any_file_name

por ejemplo pgrep -f firefox, grep el PID de ejecución firefoxy guardará este PID en un archivo llamado any_file_name. El comando 'sed' agregará al killcomienzo del número PID en el archivo 'any_file_name'. La tercera línea any_file_namearchivará el ejecutable. Ahora la cuarta línea eliminará el PID disponible en el archivo any_file_name. Escribir las cuatro líneas anteriores en un archivo y ejecutar ese archivo puede hacer el Control- C. Trabajando absolutamente bien para mí.

usuario2176228
fuente
0

Si alguien está interesado en una solución para esta bashfunción, y no tanto en la filosofía detrás de esto , aquí hay una propuesta:

No ejecute el comando problemático directamente, pero desde un contenedor que a) espera a que termine b) no se mete con las señales yc) no implementa el mecanismo WCE en sí, sino que simplemente muere al recibir a SIGINT.

Tal contenedor podría hacerse con awk+ su system()función.

$ 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

Poner en un script como OP:

#!/bin/bash
while true; do
        echo -e "\n*** DATE:" `date` " ***";
        echo "********************************************"
        awk 'BEGIN{system(ARGV[1])}' "ping -c5 ${1-localhost}"
done
Mosvy
fuente
-3

Eres víctima de un conocido error de bash. Bash hace control de trabajo para los scripts, lo cual es un error.

Lo que sucede es que bash ejecuta los programas externos en un grupo de procesos diferente al que usa para el script en sí. A medida que el grupo de proceso TTY se establece en el grupo de proceso del proceso en primer plano actual, solo este proceso en primer plano se anula y el bucle en el script de shell continúa.

Para verificar: Obtenga y compile un Bourne Shell reciente que implemente pgrp (1) como un programa integrado, luego agregue / bin / sleep 100 (o / usr / bin / sleep dependiendo de su plataforma) al bucle del script y luego inicie Bourne Shell. Después de usar ps (1) para obtener las ID de proceso para el comando sleep y el bash que ejecuta el script, llame pgrp <pid>y reemplace "<pid>" por el ID del proceso del sleep y el bash que ejecuta el script. Verá diferentes ID de grupo de proceso. Ahora llame a algo como pgrp < /dev/pts/7(reemplace el nombre tty por el tty utilizado por el script) para obtener el grupo de proceso tty actual. El grupo de proceso TTY es igual al grupo de proceso del comando de suspensión.

Para solucionarlo: use un shell diferente.

Las fuentes recientes de Bourne Shell están en mi paquete de herramientas schily que puedes encontrar aquí:

http://sourceforge.net/projects/schilytools/files/

astuto
fuente
¿De qué versión bashes esa? AFAIK bashsolo hace eso si pasa la opción -m o -i.
Stéphane Chazelas
Parece que esto ya no se aplica a bash4, pero cuando el OP tiene tales problemas, parece usar bash3
2015
No se puede reproducir con bash3.2.48 ni bash 3.0.16 ni bash-2.05b (probado con bash -c 'ps -j; ps -j; ps -j').
Stéphane Chazelas
Esto definitivamente sucede cuando llamas a bash como /bin/sh -ce. Tuve que agregar una solución fea smakeque mata explícitamente al grupo de procesos para un comando actualmente en ejecución a fin de permitir ^Cabortar una llamada en capas. ¿Verificó si bash cambió el grupo de procesos desde el id del grupo de procesos con el que se inició?
schily
ARGV0=sh bash -ce 'ps -j; ps -j; ps -j'informa el mismo pgid para ps y bash en todas las invocaciones de 3 ps. (ARGV0 = sh es la zshforma de pasar argv [0]).
Stéphane Chazelas