Pourquoi ne puis-je pas tuer un timeout appelé à partir d'un script Bash avec une frappe?

11

[Modifier: Cela ressemble à d'autres questions demandant comment tuer tous les processus générés - les réponses semblent toutes être d'utiliser pkill. Donc, le cœur de ma question peut être: existe-t-il un moyen de propager Ctrl-C / Z à tous les processus générés par un script?]

Lors de l'appel d'un SoX recavec la timeoutcommande de coreutils (discuté ici ), il ne semble pas y avoir de moyen de le tuer avec une frappe une fois qu'il a été invoqué depuis un script Bash.

Exemples:

timeout 10 rec test.wav

... peut être tué avec Ctrl+ Cou Ctrl+ Zdepuis bash, mais pas quand il est appelé depuis l'intérieur d'un script.

timeout 10 ping nowhere

... peut être tué avec Ctrl+ Cou Ctrl+ Zdepuis bash, et avec Ctrl+ Zquand il est exécuté depuis l'intérieur d'un script.

Je peux trouver l'ID de processus et le tuer de cette façon, mais pourquoi ne puis-je pas utiliser une touche de pause standard? Et existe-t-il un moyen de structurer mon script pour que je puisse?

meetar
la source
2
Ctrl + Z ne tue pas les processus, les suspend uniquement. Ils continueront de fonctionner si vous donnez les commandes bgof fg. Quoi qu'il en soit, y a-t-il une différence entre vos 1er et 3e exemples?
terdon
Je n'ai pas timeoutsur mon système, mais tuer sleepfonctionne, qu'il soit tapé directement sur la ligne de commande, sourcé, exécuté ou passé explicitement par l'interpréteur
Kevin
@terdon Merci, j'ai clarifié les exemples.
meetar

Réponses:

18

Les touches de signal telles que Ctrl+ Cenvoient un signal à tous les processus du groupe de processus de premier plan .

Dans le cas typique, un groupe de processus est un pipeline. Par exemple, dans head <somefile | sort, le processus en cours d'exécution headet le processus en cours d'exécution sortsont dans le même groupe de processus, tout comme le shell, de sorte qu'ils reçoivent tous le signal. Lorsque vous exécutez un travail en arrière-plan ( somecommand &), ce travail se trouve dans son propre groupe de processus, donc appuyer sur Ctrl+ Cne l'affecte pas.

Le timeoutprogramme se place dans son propre groupe de processus. Du code source:

/* 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);

Lorsqu'un délai d'expiration se produit, timeoutpasse par le simple moyen de tuer le groupe de processus dont il est membre. Puisqu'il s'est placé dans un groupe de processus séparé, son processus parent ne sera pas dans le groupe. L'utilisation d'un groupe de processus ici garantit que si l'application enfant se transforme en plusieurs processus, tous ses processus recevront le signal.

Lorsque vous exécutez timeoutdirectement sur la ligne de commande et appuyez sur Ctrl+ C, le SIGINT résultant est reçu à la fois par timeoutet par le processus enfant, mais pas par le shell interactif qui est timeoutle processus parent. Quand timeoutest appelé à partir d'un script, seul le shell exécutant le script reçoit le signal: timeoutne l'obtient pas car il se trouve dans un groupe de processus différent.

Vous pouvez définir un gestionnaire de signal dans un script shell avec la fonction trapintégrée. Malheureusement, ce n'est pas si simple. Considère ceci:

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

Si vous appuyez sur Ctrl+ Caprès 2 secondes, cela attend toujours les 5 secondes complètes, puis imprimez le message «Interrompu». En effet, le shell s'abstient d'exécuter le code d'interruption lorsqu'un travail de premier plan est actif.

Pour y remédier, exécutez le travail en arrière-plan. Dans le gestionnaire de signaux, appelez killpour relayer le signal au timeoutgroupe de processus.

#!/bin/sh
trap 'kill -INT -$pid' INT
timeout 5 sleep 10 &
pid=$!
wait $pid
Gilles 'SO- arrête d'être méchant'
la source
Très sournois - fonctionnait magnifiquement! Je suis beaucoup plus intelligent maintenant, merci!
meetar
13

S'appuyant sur l'excellente réponse fournie par Gilles. La commande timeout a une option de premier plan, si elle est utilisée, un CTRL + C quittera la commande timeout.

#!/bin/sh
trap 'echo caught interrupt and exiting;exit' INT
date
timeout --foreground 5 sleep 10
date
Marwan Alsabbagh
la source
C'est une excellente réponse - plus simple que la réponse acceptée et a bien fonctionné pour moi, en utilisant timeoutde GNU coreutils.
RichVel
1

Fondamentalement, Ctrl+ Cenvoie un SIGINTsignal, tandis que Ctrl+ Z envoie un SIGTSTPsignal.

SIGTSTParrête simplement le processus, un SIGCONTle continuera.

Cela fonctionne sur le processus de premier plan qui a été bifurqué sur la ligne de commande.

Si votre processus est un processus d'arrière-plan, vous devrez envoyer ce signal aux processus d'une manière différente. killFera cela. En théorie, un opérateur "-" sur ce kill devrait également signaler les processus enfants mais cela fonctionne rarement comme prévu.

Pour en savoir plus: Unix-Signals

Nils
la source
C'est vrai, au moins jusqu'à «fonctionne rarement comme prévu» (qui dépend de vos attentes - vous devriez lire des informations sur les groupes de processus), mais complètement hors de propos. La question ne trahit aucune confusion concernant SIGINT et SIGTSTP.
Gilles 'SO- arrête d'être méchant'
0

Amélioration de la réponse de @Gilles en fournissant une approche "trouver et remplacer" pour les scripts existants:

  1. Ajoutez cet extrait au début de votre code:
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. Ajoutez (ou fusionnez) le code suivant dans votre INTgestionnaire:
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. Remplacez les timeoutcommandes par une my_timeoutfonction dans votre script.

Exemple

Voici un exemple de script:

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