¿Por qué no puedo eliminar un tiempo de espera llamado desde un script Bash con una pulsación de tecla?

11

[Editar: Esto se parece a algunas otras preguntas sobre cómo matar todos los procesos generados; todas las respuestas parecen ser el uso de pkill. Entonces, el núcleo de mi pregunta puede ser: ¿Hay alguna forma de propagar Ctrl-C / Z a todos los procesos generados por un script?]

Al llamar a un SoX reccon el timeoutcomando de coreutils (discutido aquí ), no parece haber ninguna forma de matarlo con una pulsación de tecla una vez que se invoca desde un script Bash.

Ejemplos:

timeout 10 rec test.wav

... se puede matar con Ctrl+ Co Ctrl+ Zdesde bash, pero no cuando se ha llamado desde dentro de un script.

timeout 10 ping nowhere

... se puede matar con Ctrl+ Co Ctrl+ Zdesde bash, y con Ctrl+ Zcuando se ejecuta desde un script.

Puedo encontrar la ID del proceso y matarlo de esa manera, pero ¿por qué no puedo usar una tecla de interrupción estándar? ¿Y hay alguna forma de estructurar mi script para que pueda?

meetar
fuente
2
Ctrl + Z no elimina los procesos, solo los detiene. Se mantendrán en funcionamiento si se le da el bgde fgcomandos. De todos modos, ¿hay alguna diferencia entre tus ejemplos primero y 3d?
terdon
No tengo timeouten mi sistema, pero matar sleepfunciona si se escribe directamente en la línea de comando, se obtiene, se ejecuta o se pasa explícitamente por el intérprete
Kevin
@terdon Gracias, he aclarado los ejemplos.
meetar

Respuestas:

18

Las teclas de señal como Ctrl+ Cenvían una señal a todos los procesos en el grupo de procesos en primer plano .

En el caso típico, un grupo de procesos es una tubería. Por ejemplo, en head <somefile | sort, el proceso en ejecución heady el proceso en ejecución sortestán en el mismo grupo de procesos, al igual que el shell, por lo que todos reciben la señal. Cuando ejecuta un trabajo en segundo plano ( somecommand &), ese trabajo está en su propio grupo de procesos, por lo que presionar Ctrl+ Cno lo afecta.

El timeoutprograma se coloca en su propio grupo de procesos. Del código fuente:

/* Ensure we're in our own group so all subprocesses can be killed.
   Note we don't just put the child in a separate group as
   then we would need to worry about foreground and background groups
   and propagating signals between them.  */
setpgid (0, 0);

Cuando se produce un tiempo de espera, timeoutpasa por el simple recurso de eliminar el grupo de procesos del que es miembro. Como se ha colocado en un grupo de proceso separado, su proceso padre no estará en el grupo. El uso de un grupo de procesos aquí garantiza que si la aplicación secundaria se bifurca en varios procesos, todos sus procesos recibirán la señal.

Cuando se ejecuta timeoutdirectamente en la línea de comando y presiona Ctrl+ C, el timeoutproceso secundario recibe el SIGINT resultante , pero no el shell interactivo, que es timeoutel proceso principal. Cuando timeoutse llama desde un script, solo el shell que ejecuta el script recibe la señal: timeoutno la recibe ya que está en un grupo de proceso diferente.

Puede establecer un controlador de señal en un script de shell con el trapincorporado. Lamentablemente, no es tan simple. Considera esto:

#!/bin/sh
trap 'echo Interrupted at $(date)' INT
date
timeout 5 sleep 10
date

Si presiona Ctrl+ Cdespués de 2 segundos, esto todavía esperará los 5 segundos completos, luego imprima el mensaje "Interrumpido". Esto se debe a que el shell se abstiene de ejecutar el código de captura mientras un trabajo en primer plano está activo.

Para remediar esto, ejecute el trabajo en segundo plano. En el controlador de señal, llame killpara retransmitir la señal al timeoutgrupo de procesos.

#!/bin/sh
trap 'kill -INT -$pid' INT
timeout 5 sleep 10 &
pid=$!
wait $pid
Gilles 'SO- deja de ser malvado'
fuente
Muy astuto, ¡funcionó maravillosamente! ¡Soy mucho más inteligente ahora, merci!
meetar
13

Sobre la base de la excelente respuesta proporcionada por Gilles. El comando de tiempo de espera tiene una opción de primer plano; si se usa, CTRL + C saldrá del comando de tiempo de espera.

#!/bin/sh
trap 'echo caught interrupt and exiting;exit' INT
date
timeout --foreground 5 sleep 10
date
Marwan Alsabbagh
fuente
Esta es una gran respuesta: más simple que la respuesta aceptada y funcionó bien para mí, usando timeoutGNU coreutils.
RichVel
1

Básicamente Ctrl+ Cenvía una SIGINTseñal, mientras que Ctrl+ Z envía una SIGTSTPseñal.

SIGTSTPsimplemente detiene el proceso, a SIGCONTlo continuará.

Esto funciona en el proceso de primer plano que se bifurcó en la línea de comandos.

Si su proceso es un proceso en segundo plano, deberá enviar esa señal a los procesos de una manera diferente. killlo haré. En teoría, un operador "-" en ese kill también debería indicar los procesos secundarios, pero esto rara vez funciona como se esperaba.

Para más información: Señales Unix

Nils
fuente
Esto es cierto, al menos hasta "rara vez funciona como se esperaba" (que depende de sus expectativas; debe leer sobre los grupos de procesos), pero es completamente irrelevante. La pregunta no revela ninguna confusión sobre SIGINT y SIGTSTP.
Gilles 'SO- deja de ser malvado'
0

Mejorando la respuesta de @Gilles proporcionando un enfoque de "buscar y reemplazar" para los scripts existentes

  1. Agregue este fragmento al comienzo de su código:
declare -a timeout_pids
exec 21>&1; exec 22>&2 # backup file descriptors, see /superuser//a/1446738/187576
my_timeout(){
    local args tp ret
    args="$@"
    timeout $args &
    tp=$!
    echo "pid of timeout: $tp"
    timeout_pids+=($tp)
    wait $tp
    ret=$?
    count=${#timeout_pids[@]}
    for ((i = 0; i < count; i++)); do
        if [ "${timeout_pids[i]}" = "$tp" ] ; then
            unset 'timeout_pids[i]'
        fi
    done
    return $ret
}
  1. Agregue (o combine) el siguiente código en su INTcontrolador:
pre_cleanup(){
    exec 1>&21; exec 2>&22 # restore file descriptors, see /superuser//a/1446738/187576
    echo "Executing pre-cleanup..."
    for i in "${timeout_pids[*]}"; do
        if [[ ! -z $i ]]; then
            #echo "Killing PID: $i"
            kill -INT -$i 2> /dev/null
        fi
    done
    exit
}

trap pre_cleanup INT
  1. Reemplace los timeoutcomandos con la my_timeoutfunción en su secuencia de comandos.

Ejemplo

Aquí hay un script de ejemplo:

#!/bin/bash

# see "killing timeout": /unix//a/57692/65781
declare -a timeout_pids
exec 21>&1; exec 22>&2 # backup file descriptors, see /superuser//a/1446738/187576
my_timeout(){
    local args tp ret
    args="$@"
    timeout $args &
    tp=$!
    echo "pid of timeout: $tp"
    timeout_pids+=($tp)
    wait $tp
    ret=$?
    count=${#timeout_pids[@]}
    for ((i = 0; i < count; i++)); do
        if [ "${timeout_pids[i]}" = "$tp" ] ; then
            unset 'timeout_pids[i]'
        fi
    done
    return $ret
}

cleanup(){
    echo "-----------------------------------------"
    echo "Restoring previous routing table settings"
}

pre_cleanup(){
    exec 1>&21; exec 2>&22 # restore file descriptors, see /superuser//a/1446738/187576
    echo "Executing pre-cleanup..."
    for i in "${timeout_pids[*]}"; do
        if [[ ! -z $i ]]; then
            echo "Killing PID: $i"
            kill -INT -$i 2> /dev/null
        fi
    done
    exit
}

trap pre_cleanup INT
trap cleanup EXIT

echo "Executing 5 temporary timeouts..."
unreachable_ip="192.168.44.5"
my_timeout 1s ping -c 1 "$unreachable_ip" &> /dev/null 
my_timeout 1s ping -c 1 "$unreachable_ip" &> /dev/null 
my_timeout 1s ping -c 1 "$unreachable_ip" &> /dev/null 
my_timeout 1s ping -c 1 "$unreachable_ip" &> /dev/null 
my_timeout 1s ping -c 1 "$unreachable_ip" &> /dev/null 
echo "ctrl+c now to execute cleanup"
my_timeout 9s ping -c 1 "$unreachable_ip" &> /dev/null 
ceremcem
fuente