J'ai un script bash qui lance un processus enfant qui se bloque (se bloque) de temps en temps et sans raison apparente (source fermée, donc je ne peux rien faire à ce sujet). En conséquence, j'aimerais pouvoir lancer ce processus pendant un laps de temps donné et le tuer s'il échouait après un certain laps de temps.
Existe-t-il un moyen simple et robuste pour y parvenir en utilisant bash?
PS: dites-moi si cette question convient mieux à serverfault ou au super-utilisateur.
(Comme on le voit dans: BASH FAQ entrée # 68: "Comment puis-je exécuter une commande et la faire abandonner (délai d'attente) après N secondes? " )
Si cela ne vous dérange pas de télécharger quelque chose, utilisez timeout
(Sudo apt-get install timeout
) et utilisez-le comme: (la plupart des systèmes l’ont déjà installé, sinon utilisez Sudo apt-get install coreutils
)
timeout 10 ping www.goooooogle.com
Si vous ne voulez pas télécharger quelque chose, faites ce que timeout fait en interne:
( cmdpid=$BASHPID; (sleep 10; kill $cmdpid) & exec ping www.goooooogle.com )
Au cas où vous voudriez faire un timeout pour un code bash plus long, utilisez la deuxième option en tant que telle:
( cmdpid=$BASHPID;
(sleep 10; kill $cmdpid) \
& while ! ping -w 1 www.goooooogle.com
do
echo crap;
done )
# Spawn a child process:
(dosmth) & pid=$!
# in the background, sleep for 10 secs then kill that process
(sleep 10 && kill -9 $pid) &
ou pour obtenir les codes de sortie également:
# Spawn a child process:
(dosmth) & pid=$!
# in the background, sleep for 10 secs then kill that process
(sleep 10 && kill -9 $pid) & waiter=$!
# wait on our worker process and return the exitcode
exitcode=$(wait $pid && echo $?)
# kill the waiter subshell, if it still runs
kill -9 $waiter 2>/dev/null
# 0 if we killed the waiter, cause that means the process finished before the waiter
finished_gracefully=$?
sleep 999&
t=$!
sleep 10
kill $t
J'avais aussi cette question et trouvais deux choses très utiles:
J'utilise donc quelque chose comme ceci sur la ligne de commande (OSX 10.9):
ping www.goooooogle.com & PING_PID=$(pgrep 'ping'); SECONDS=0; while pgrep -q 'ping'; do sleep 0.2; if [ $SECONDS = 10 ]; then kill $PING_PID; fi; done
Comme il s’agit d’une boucle, j’ai inclus un "sommeil 0.2" pour garder le CPU au frais. ;-)
(BTW: le ping est de toute façon un mauvais exemple, vous utiliseriez simplement l’option "-t" (timeout) intégrée.)
En supposant que vous avez (ou pouvez facilement créer) un fichier pid pour suivre le pid de l'enfant, vous pouvez ensuite créer un script qui vérifie l'heure de modification du fichier pid et tue/respawns le processus selon les besoins. Ensuite, placez le script dans crontab pour qu’il s’exécute approximativement à la période souhaitée.
Faites-moi savoir si vous avez besoin de plus de détails. Si cela ne vous convient pas, qu'en est-il de pstart?
Une solution consiste à exécuter le programme dans un sous-shell et à communiquer avec ce dernier via un canal nommé à l'aide de la commande read
. De cette façon, vous pouvez vérifier l’état de sortie du processus en cours d’exécution et le communiquer à travers le canal.
Voici un exemple de dépassement de la commande yes
après 3 secondes. Il obtient le PID du processus en utilisant pgrep
(ne fonctionne probablement que sous Linux). L’utilisation d’un canal pose également un problème, puisqu’un processus ouvrant un canal en lecture reste suspendu jusqu’à ce qu’il soit également ouvert en écriture, et inversement. Donc, pour éviter que la commande read
ne soit suspendue, j'ai "bloqué" l'ouverture du tube pour lecture avec un sous-fond. (Un autre moyen d’empêcher le gel d’ouvrir le tuyau en lecture-écriture, c’est-à-dire read -t 5 <>finished.pipe
- cependant, cela peut aussi ne pas fonctionner sauf avec Linux.)
rm -f finished.pipe
mkfifo finished.pipe
{ yes >/dev/null; echo finished >finished.pipe ; } &
SUBSHELL=$!
# Get command PID
while : ; do
PID=$( pgrep -P $SUBSHELL yes )
test "$PID" = "" || break
sleep 1
done
# Open pipe for writing
{ exec 4>finished.pipe ; while : ; do sleep 1000; done } &
read -t 3 FINISHED <finished.pipe
if [ "$FINISHED" = finished ] ; then
echo 'Subprocess finished'
else
echo 'Subprocess timed out'
kill $PID
fi
rm finished.pipe
Voici une tentative visant à éviter de tuer un processus après sa sortie, ce qui réduit les chances de tuer un autre processus avec le même ID de processus (même s’il est probablement impossible d’éviter complètement ce type d’erreur).
run_with_timeout ()
{
t=$1
shift
echo "running \"$*\" with timeout $t"
(
# first, run process in background
(exec sh -c "$*") &
pid=$!
echo $pid
# the timeout Shell
(sleep $t ; echo timeout) &
waiter=$!
echo $waiter
# finally, allow process to end naturally
wait $pid
echo $?
) \
| (read pid
read waiter
if test $waiter != timeout ; then
read status
else
status=timeout
fi
# if we timed out, kill the process
if test $status = timeout ; then
kill $pid
exit 99
else
# if the program exited normally, kill the waiting Shell
kill $waiter
exit $status
fi
)
}
Utilisez comme run_with_timeout 3 sleep 10000
, qui exécute sleep 10000
mais le termine au bout de 3 secondes.
Cela ressemble à d'autres réponses qui utilisent un processus de délai d'attente en arrière-plan pour tuer le processus enfant après un délai. Je pense que cela est presque identique à la réponse étendue de Dan ( https://stackoverflow.com/a/5161274/135198 ), sauf que le délai d'expiration Shell ne sera pas tué s'il est déjà terminé.
Une fois ce programme terminé, il restera quelques processus de "sommeil" en cours d'exécution, mais ils devraient être inoffensifs.
C'est peut-être une meilleure solution que mon autre réponse, car elle n'utilise pas la fonctionnalité non portable de Shell read -t
et n'utilise pas pgrep
.
Voici la troisième réponse que j'ai soumise ici. Celui-ci gère les interruptions de signal et nettoie les processus en arrière-plan lorsque SIGINT
est reçu. Il utilise le $BASHPID
et exec
astuce utilisée dans le réponse du haut pour obtenir le PID d’un processus (dans ce cas, $$
dans une invocation sh
). Il utilise un FIFO pour communiquer avec un sous-shell responsable de la destruction et du nettoyage. (Cela ressemble au tuyau dans ma deuxième réponse , mais avec un tuyau nommé signifie que le gestionnaire de signal peut aussi y écrire.)
run_with_timeout ()
{
t=$1 ; shift
trap cleanup 2
F=$$.fifo ; rm -f $F ; mkfifo $F
# first, run main process in background
"$@" & pid=$!
# sleeper process to time out
( sh -c "echo \$\$ >$F ; exec sleep $t" ; echo timeout >$F ) &
read sleeper <$F
# control Shell. read from fifo.
# final input is "finished". after that
# we clean up. we can get a timeout or a
# signal first.
( exec 0<$F
while : ; do
read input
case $input in
finished)
test $sleeper != 0 && kill $sleeper
rm -f $F
exit 0
;;
timeout)
test $pid != 0 && kill $pid
sleeper=0
;;
signal)
test $pid != 0 && kill $pid
;;
esac
done
) &
# wait for process to end
wait $pid
status=$?
echo finished >$F
return $status
}
cleanup ()
{
echo signal >$$.fifo
}
J'ai essayé d'éviter autant que possible les conditions de course. Cependant, l'une des sources d'erreur que je n'ai pas pu supprimer est la fin du processus à peu près au même moment que l'expiration du délai. Par exemple, run_with_timeout 2 sleep 2
ou run_with_timeout 0 sleep 0
. Pour moi, ce dernier donne une erreur:
timeout.sh: line 250: kill: (23248) - No such process
comme il essaie de tuer un processus qui est déjà sorti de lui-même.