¿Cómo puedo manejar la trampa SIGINT con un mensaje de usuario en un script de shell?

¿Cómo puedo manejar la trampa SIGINT con un mensaje de usuario en un script de shell?

Estoy tratando de manejar SIGINT( CtrlC) de tal manera que si un usuario presiona accidentalmente ctrl-c, aparecerá un mensaje: "¿Desea salir? (s/n)". Si ingresa sí, entonces salga del script. Si no, continúe desde donde ocurrió la interrupción. Básicamente, necesito CtrlCtrabajar de manera similar a SIGTSTP( CtrlZ) pero de una manera ligeramente diferente. He probado varias formas de lograr esto pero no obtuve los resultados esperados. A continuación se muestran algunos escenarios que probé.

Caso 1

Guion:play.sh

#!/bin/sh
function stop()
{
while true; do 
    read -rep $'\nDo you wish to stop playing?(y/n)' yn
    case $yn in
        [Yy]* ) echo "Thanks for playing !!!"; exit 1;;
        [Nn]* ) break;;
        * ) echo "Please answer (y/n)";;
    esac
done
} 
trap 'stop' SIGINT 
echo "going to sleep"
for i in {1..100}
do
  echo "$i"
  sleep 3   
done
echo "end of sleep"

Cuando ejecuto el script anterior, obtengo los resultados esperados.

Producción:

$ play.sh 
going to sleep
1
^C
Do you wish to stop playing?(y/n)y
Thanks for playing !!!

$ play.sh 
going to sleep
1
2
^C
Do you wish to stop playing?(y/n)n
3
4
^C
Do you wish to stop playing?(y/n)y
Thanks for playing !!! 
$  

Caso: 2

Moví el forbucle a un nuevo script loop.sh, por lo que play.shse convierte en el proceso padre y loop.shel proceso hijo.

Guion:play.sh

#!/bin/sh
function stop()
{
while true; do 
    read -rep $'\nDo you wish to stop playing?(y/n)' yn
    case $yn in
        [Yy]* ) echo "Thanks for playing !!!"; exit 1;;
        [Nn]* ) break;;
        * ) echo "Please answer (y/n)";;
    esac
done
}
trap 'stop' SIGINT 
loop.sh

Guion:loop.sh

#!/bin/sh
echo "going to sleep"
for i in {1..100}
do
  echo "$i"
  sleep 3   
done
echo "end of sleep"

El resultado en este caso no es el esperado.

Producción:

$ play.sh 
going to sleep
1
2
^C
Do you wish to stop playing?(y/n)y
Thanks for playing !!!

$ play.sh 
going to sleep
1
2
3
4
^C
Do you wish to stop playing?(y/n)n
$

Entiendo que cuando un proceso recibe una SIGINTseñal, la propaga a todos los procesos secundarios, por lo que mi segundo caso falla. ¿Hay alguna manera de evitar que SIGINTse propague a procesos secundarios y así hacer que el loop.shtrabajo funcione exactamente como funcionó en el primer caso?

Nota: Este es sólo un ejemplo de mi aplicación real. La aplicación en la que estoy trabajando tiene varios scripts secundarios en play.shy loop.sh. Debo asegurarme de que la aplicación, al recibirla SIGINT, no finalice, sino que muestre al usuario un mensaje.

Respuesta1

¡Gran pregunta clásica sobre la gestión de trabajos y señales con buenos ejemplos! He desarrollado un script de prueba simplificado para centrarme en la mecánica del manejo de la señal.

Para lograr esto, después de iniciar los hijos (loop.sh) en segundo plano, llame waity al recibir la señal INT, killel grupo de procesos cuyo PGID es igual a su PID.

  1. Para el script en cuestión, play.shesto se puede lograr de la siguiente manera:

  2. En la stop()función reemplazar exit 1con

    kill -TERM -$$  # note the dash, negative PID, kills the process group
    
  3. Comience loop.shcomo un proceso en segundo plano (aquí se pueden iniciar y administrar múltiples procesos en segundo plano play.sh)

    loop.sh &
    
  4. Agregue waital final del guión esperar a todos los niños.

    wait
    

Cuando su secuencia de comandos inicia un proceso, ese hijo se convierte en miembro de un grupo de procesos con PGID igual al PID del proceso principal que se encuentra $$en el shell principal.

Por ejemplo, el script trap.shinició tres sleepprocesos en segundo plano y ahora waitlos está ejecutando; observe que la columna de ID del grupo de procesos (PGID) es la misma que el PID del proceso principal:

  PID  PGID STAT COMMAND
17121 17121 T    sh trap.sh
17122 17121 T    sleep 600
17123 17121 T    sleep 600
17124 17121 T    sleep 600

En Unix y Linux, puede enviar una señal a cada proceso en ese grupo de procesos llamando killcon el valor negativo de PGID. Si proporciona killun número negativo, se utilizará como -PGID. Dado que el PID () del script $$es el mismo que el PGID, puede crear killsu grupo de procesos en el shell con

kill -TERM -$$    # note the dash before $$

debe proporcionar un número o nombre de señal; de lo contrario, algunas implementaciones de kill le indicarán "opción ilegal" o "especificación de señal no válida".

El código simple a continuación ilustra todo esto. Establece un trapcontrolador de señales, genera 3 hijos, luego entra en un ciclo de espera sin fin, esperando suprimirse mediante el killcomando del grupo de procesos en el controlador de señales.

$ cat trap.sh
#!/bin/sh

signal_handler() {
        echo
        read -p 'Interrupt: ignore? (y/n) [Y] >' answer
        case $answer in
                [nN]) 
                        kill -TERM -$$  # negative PID, kill process group
                        ;;
        esac
}

trap signal_handler INT 

for i in 1 2 3
do
    sleep 600 &
done

wait  # don't exit until process group is killed or all children die

Aquí hay una ejecución de muestra:

$ ps -o pid,pgid,stat,args
  PID  PGID STAT COMMAND
 8073  8073 Ss   /bin/bash
17111 17111 R+   ps -o pid,pgid,stat,args
$ 

OK, no se están ejecutando procesos adicionales. Inicie el script de prueba, interrumpa ( ^C), elija ignorar la interrupción y luego suspenda ( ^Z):

$ sh trap.sh 
^C
Interrupt: ignore? (y/n) [Y] >y
^Z
[1]+  Stopped                 sh trap.sh
$

Verifique los procesos en ejecución, anote los números de grupo de procesos ( PGID):

$ ps -o pid,pgid,stat,args
  PID  PGID STAT COMMAND
 8073  8073 Ss   /bin/bash
17121 17121 T    sh trap.sh
17122 17121 T    sleep 600
17123 17121 T    sleep 600
17124 17121 T    sleep 600
17143 17143 R+   ps -o pid,pgid,stat,args
$

Traiga nuestro script de prueba al primer plano ( fg) e interrumpa ( ^C) nuevamente, esta vez elijanoignorar:

$ fg
sh trap.sh
^C
Interrupt: ignore? (y/n) [Y] >n
Terminated
$

Verifique los procesos en ejecución, no más dormir:

$ ps -o pid,pgid,stat,args
  PID  PGID STAT COMMAND
 8073  8073 Ss   /bin/bash
17159 17159 R+   ps -o pid,pgid,stat,args
$ 

Nota sobre su caparazón:

Tuve que modificar su código para que se ejecutara en mi sistema. Tiene #!/bin/shla primera línea en sus scripts, pero los scripts usan extensiones (de bash o zsh) que no están disponibles en /bin/sh.

Respuesta2

"Entiendo que cuando un proceso recibe una señal SIGINT, propaga la señal a todos los procesos hijos"

¿De dónde sacaste esta idea incorrecta?

$ perl -E '$SIG{INT}=sub { say "ouch $$" }; if (fork()) { say "parent $$"; sleep 3; kill 2, $$ } else { say "child $$"; sleep 99 }'
parent 25831
child 25832
ouch 25831
$

Si hubiera propagación, como se afirma, uno podría haber esperado un "ay" del proceso hijo.

En realidad:

"Cada vez que escribimos la tecla de interrupción de nuestro terminal (a menudo BORRAR o Control-C) o la tecla de salir (a menudo Control-barra invertida), esto provoca que la señal de interrupción o la señal de salida se envíe a todos los procesos en el grupo de procesos de primer plano" -- W .Richard Stevens. "Programación Avanzada en el Entorno UNIX®". Addison-Wesley. 1993. p.246.

Esto se puede observar a través de:

$ perl -E '$SIG{INT}=sub { say "ouch $$" }; fork(); sleep 99'
^Couch 25971
ouch 25972
$

Dado que todos los procesos del grupo de procesos de primer plano consumirán un SIGINTdel Control+C, deberá diseñar todos los procesos para manejar o ignorar adecuadamente esa señal, según corresponda, o posiblemente hacer que los subprocesos se conviertan en el nuevo grupo de procesos de primer plano para que el padre ( ej., el shell) no ve la señal, debido a que ya no está en el grupo de procesos en primer plano.

Respuesta3

En play.shla fuente el archivo de bucle es así:

source ./loop.sh

Una subcapa de la que se salió debido a una trampa no se puede volver a ella al salir de la trampa.

Respuesta4

El problema es sleep.

Cuando ustedCTRL+Cmatas a un sleep- no a tush (incluso si la sintaxis en tu #!/bin/shes un poco sospechosa). Sin embargo, todo el grupo de procesos recibe la señal, debido a que no instala su controlador loop.sh, que es una subcapa, termina inmediatamente después.

Necesitas una trampa en loop.sh. Las trampas se eliminan para cada subcapa iniciada a menos que se indique explícitamente.trap ''SIGignorado por los padres.

También necesita una -msupervisión controlada por el trabajo de su padre para que pueda realizar un seguimiento de sus hijos. wait, por ejemplo, sólo funciona con control de tareas. -mEl modo monitor es cómo los shells interactúan con los terminales.

sh  -cm '  . /dev/fd/3' 3<<""                     # `-m`onitor is important
     sh -c ' kill -INT "$$"'&                     # get the sig#
     wait %%;INT=$?                               # keep it in $INT
     trap  " stty $(stty -g;stty -icanon)" EXIT   # lose canonical input
     loop()( trap    exit INT                     # this is a child
             while   [ "$#" -le 100 ]             # same as yours
             do      echo "$#"                    # writing the iterator
                     set "" "$@"                  # iterating
                     sleep 3                      # sleeping
             done
     )
     int(){  case    $1      in                   # handler fn
             ($INT)  printf  "\33[2K\rDo you want to stop playing? "
                     case    $(dd bs=1 count=1; echo >&3)  in
                     ([Nn])  return
             esac;   esac;   exit "$1"
     }       3>&2    >&2     2>/dev/null
     while   loop ||
             int "$?"
     do :;   done

0
1
2
3
4
Do you want to stop playing? n
0
1
2
3
Do you want to stop playing? N
0
1
Do you want to stop playing? y
[mikeserv@desktop tmp]$

información relacionada