Timing out dans un script shell

53

J'ai un script shell qui lit à partir d'une entrée standard . Dans de rares cas, personne ne sera prêt à apporter une contribution et le script doit expirer . En cas de dépassement de délai, le script doit exécuter du code de nettoyage. Quelle est la meilleure façon de faire ça?

Ce script doit être très portable , y compris pour les systèmes unix du XXe siècle sans compilateur C et pour les périphériques intégrés exécutant busybox. Perl, bash, tous les langages compilés et même le POSIX.2 complet ne peuvent pas être fiables. En particulier, $PPID, read -tet parfaitement pièges conformes POSIX ne sont pas disponibles. L'écriture dans un fichier temporaire est également exclue; le script peut s'exécuter même si tous les systèmes de fichiers sont montés en lecture seule.

Juste pour rendre les choses plus difficiles, je veux aussi que le script soit raisonnablement rapide quand il n’expire pas. En particulier, j'utilise également le script sous Windows (principalement sous Cygwin), où fork et exec sont particulièrement faibles, je souhaite donc limiter leur utilisation au minimum.

En un mot, j'ai

trap cleanup 1 2 3 15
foo=`cat`

et je veux ajouter un délai d'attente. Je ne peux pas remplacer catpar la fonction readintégrée. En cas de dépassement de délai, je veux exécuter la cleanupfonction.


Arrière-plan: ce script consiste à deviner l'encodage du terminal en imprimant des caractères de 8 bits et en comparant la position du curseur avant et après. Le début du script vérifie que stdout est connecté à un terminal pris en charge, mais parfois l'environnement est menteur (par exemple, des plinkensembles TERM=xtermmême s'il est appelé avecTERM=dumb ). La partie pertinente du script ressemble à ceci:

text='Éé'  # UTF-8; shows up as Ãé on a latin1 terminal
csi='␛['; dsr_cpr="${csi}6n"; dsr_ok="${csi}5n"  # ␛ is an escape character
stty_save=`stty -g`
cleanup () { stty "$stty_save"; }
trap 'cleanup; exit 120' 0 1 2 3 15     # cleanup code
stty eol 0 eof n -echo                # Input will end with `0n`
# echo-n is a function that outputs its argument without a newline
echo-n "$dsr_cpr$dsr_ok"              # Ask the terminal to report the cursor position
initial_report=`tr -dc \;0123456789`  # Expect ␛[42;10R␛[0n for y=42,x=10
echo-n "$text$dsr_cpr$dsr_ok"
final_report=`tr -dc \;0123456789`
cleanup
# Compute and return initial_x - final_x

Comment puis-je modifier le script pour que si traucune entrée ne soit lue après 2 secondes, elle soit supprimée et que le script exécute la cleanupfonction?

Gilles, arrête de faire le mal
la source
2
Voir aussi Comment introduire le délai d'attente pour les scripts shell? pour des solutions moins portables
Gilles 'SO, arrête d'être méchant'

Réponses:

33

Et ça:

foo=`{ { cat 1>&3; kill 0; } | { sleep 2; kill 0; } } 3>&1`

C'est-à-dire: exécutez la commande produisant une sortie et sleepdans le même groupe de processus, un groupe de processus juste pour eux. Quelle que soit la commande renvoyée, le groupe de processus entier est tué en premier.

Quelqu'un pourrait-il se demander: oui, le tuyau n'est pas utilisé; il est contourné en utilisant les redirections. Son seul objectif est que le shell exécute les deux processus dans le même groupe de processus.


Comme Gilles l'a souligné dans son commentaire, cela ne fonctionnera pas dans un script shell car le processus de script serait tué en même temps que les deux sous-processus.

Une façon¹ de forcer une commande à s'exécuter dans un groupe de processus distinct consiste à démarrer un nouveau shell interactif:

#!/bin/sh
foo=`sh -ic '{ cat 1>&3; kill 0; } | { sleep 2; kill 0; }' 3>&1 2>/dev/null`
[ -n "$foo" ] && echo got: "$foo" || echo timeouted

Mais il pourrait y avoir des réserves à ce sujet (par exemple, quand stdin n’est pas un tty?). La redirection stderr est là pour supprimer le message "Terminated" lorsque le shell interactif est tué.

Testé avec zsh, bashet dash. Mais qu'en est-il des vieux?

B98 suggère la modification suivante, fonctionnant sous Mac OS X, avec GNU bash 3.2.57 ou sous Linux avec tiret:

foo=`sh -ic 'exec 3>&1 2>/dev/null; { cat 1>&3; kill 0; } | { sleep 2; kill 0; }'`

-
1. autre que setsidce qui semble être non standard.

Stéphane Gimenez
la source
1
J'aime assez ça, je n'avais pas pensé à utiliser un groupe de processus de cette façon. C'est assez simple et il n'y a pas de condition de course. Je dois tester la portabilité un peu plus, mais cela semble être un gagnant.
Gilles, arrête de faire le mal '14
Malheureusement, cela échoue de façon spectaculaire dès que je le mets dans un script: le pipeline dans la substitution de commande ne s'exécute pas dans son propre groupe de processus et kill 0finit également par tuer l'appelant du script. Existe-t-il un moyen portable de forcer le pipeline dans son propre groupe de processus?
Gilles 'SO- arrête d'être méchant'
@ Gilles: Eek! impossible de trouver un moyen de se setprgp()passer de setsidpour l'instant :-(
Stéphane Gimenez
1
J'aime beaucoup le truc, alors j'attribue la prime. Cela semble fonctionner sous Linux, je n'ai pas encore eu le temps de le tester sur d'autres systèmes.
Gilles 'SO- arrête d'être méchant'
2
@Zac: Il n'est pas possible d'inverser l'ordre dans le cas d'origine, car seul le premier processus a accès à stdin.
Stéphane Gimenez le
6
me=$$
(sleep 2; kill $me >/dev/null 2>&1) & nuker=$!
# do whatever
kill $nuker >/dev/null 2>&1

Vous êtes déjà au piège de 15 (la version numérique de SIGTERM, qui est ce qui killenvoie sauf indication contraire), alors vous devriez déjà être prêt à partir. Cela dit, si vous examinez les versions antérieures à POSIX, sachez que les fonctions du shell peuvent également ne pas exister (elles proviennent du shell de System V).

geekosaur
la source
Ce n'est pas aussi simple. Si vous interceptez le signal, de nombreux shells (dash, bash, pdksh, zsh… probablement tous sauf ATT ksh) l'ignorent pendant qu'ils attendent catde quitter. J'ai déjà expérimenté un peu mais je n'ai rien trouvé qui me satisfasse jusqu'à présent.
Gilles 'SO- arrête d'être méchant'
Sur la portabilité: heureusement, j'ai des fonctions partout. Je ne suis pas sûr $!, je pense que certaines de ces machines que j'utilise rarement n'ont pas de contrôle du travail, sont-elles $!universellement disponibles?
Gilles 'SO- arrête d'être méchant'
@ Gilles: Hm, yeh. Je me souviens de bashchoses vraiment hideuses et spécifiques que j'ai faites une fois impliquant l'abus de -o monitor. Je pensais en termes de coquilles vraiment anciennes quand j'ai écrit cela (cela a fonctionné dans la v7). Cela dit, je pense que vous pouvez faire l'une des deux choses suivantes: (1) fond "quels que soient" et wait $!, ou (2) aussi envoyer SIGCLD/ SIGCHLD... mais sur des machines assez anciennes, la dernière est inexistante ou non portable (la première est System III / V, ce dernier BSD et V7 n’ont ni l’un ni l’autre).
geekosaur
@Gilles: $!remonte au moins à la V7 et est certainement antérieur à un shtype qui connaissait le contrôle des tâches (en fait, pendant longtemps /bin/sh, BSD ne contrôlait pas les tâches; il fallait courir cshpour l'obtenir, mais $!il y en avait )
geekosaur
Je ne peux pas “peu importe”, j'ai besoin de sa sortie. Merci pour l'info sur $!.
Gilles 'SO- arrête d'être méchant'
4

Bien que coretuils à partir de la version 7.0 inclue une commande de dépassement de temps, vous avez mentionné certains environnements qui ne l’auraient pas. Heureusement, pixelbeat.org a écrit un script de temporisation sh.

Je l'ai déjà utilisé à plusieurs reprises et cela fonctionne très bien.

http://www.pixelbeat.org/scripts/timeout ( Remarque: le script ci-dessous a été légèrement modifié par rapport à celui de pixelbeat.org, voir les commentaires sous cette réponse.)

#!/bin/sh

# Execute a command with a timeout

# Author:
#    http://www.pixelbeat.org/
# Notes:
#    Note there is a timeout command packaged with coreutils since v7.0
#    If the timeout occurs the exit status is 124.
#    There is an asynchronous (and buggy) equivalent of this
#    script packaged with bash (under /usr/share/doc/ in my distro),
#    which I only noticed after writing this.
#    I noticed later again that there is a C equivalent of this packaged
#    with satan by Wietse Venema, and copied to forensics by Dan Farmer.
# Changes:
#    V1.0, Nov  3 2006, Initial release
#    V1.1, Nov 20 2007, Brad Greenlee <[email protected]>
#                       Make more portable by using the 'CHLD'
#                       signal spec rather than 17.
#    V1.3, Oct 29 2009, Ján Sáreník <[email protected]>
#                       Even though this runs under dash,ksh etc.
#                       it doesn't actually timeout. So enforce bash for now.
#                       Also change exit on timeout from 128 to 124
#                       to match coreutils.
#    V2.0, Oct 30 2009, Ján Sáreník <[email protected]>
#                       Rewritten to cover compatibility with other
#                       Bourne shell implementations (pdksh, dash)

if [ "$#" -lt "2" ]; then
    echo "Usage:   `basename $0` timeout_in_seconds command" >&2
    echo "Example: `basename $0` 2 sleep 3 || echo timeout" >&2
    exit 1
fi

cleanup()
{
    trap - ALRM               #reset handler to default
    kill -ALRM $a 2>/dev/null #stop timer subshell if running
    kill $! 2>/dev/null &&    #kill last job
      exit 124                #exit with 124 if it was running
}

watchit()
{
    trap "cleanup" ALRM
    sleep $1& wait
    kill -ALRM $$
}

watchit $1& a=$!         #start the timeout
shift                    #first param was timeout for sleep
trap "cleanup" ALRM INT  #cleanup after timeout
"$@" < /dev/tty & wait $!; RET=$?    #start the job wait for it and save its return value
kill -ALRM $a            #send ALRM signal to watchit
wait $a                  #wait for watchit to finish cleanup
exit $RET                #return the value
bahamat
la source
Il semble que cela ne permette pas de récupérer le résultat de la commande. Voir "Je ne peux pas" quelle que soit "" le commentaire de Gilles dans l'autre réponse.
Stéphane Gimenez
Ah, intéressant. J'ai modifié le script (afin qu'il ne corresponde plus à celui sur pixelbeat) pour rediriger / dev / stdin dans la commande. Cela semble fonctionner lors de mes tests.
bahamat
Cela ne fonctionne pas si la commande est lue depuis l'entrée standard, sauf (curieusement) dans bash. </dev/stdinest un no-op. </dev/ttylui permettrait de lire depuis le terminal, ce qui est suffisant pour mon cas d'utilisation.
Gilles 'SO- arrête d'être méchant'
@Giles: cool, je vais faire cette mise à jour.
Bahamat
Cela ne peut pas fonctionner sans beaucoup plus d'efforts: je dois récupérer le résultat de la commande et je ne peux pas le faire si la commande est en arrière-plan.
Gilles 'SO- arrête d'être méchant'
3

Qu'en est-il (ab) utiliser NC pour cette

Comme;

   $ nc -l 0 2345 | cat &  # output come from here
   $ nc -w 5 0 2345   # input come from here and times out after 5 sec

Ou réunis dans une seule ligne de commande;

   $ foo=`nc -l 0 2222 | nc -w 5 0 2222`

La dernière version, bien que semblant étrange, fonctionne réellement lorsque je la teste sur un système linux - ma meilleure hypothèse est que cela devrait fonctionner sur n’importe quel système. Sinon, une variation de la redirection de sortie pourrait résoudre le problème de la portabilité. L'avantage ici est qu'aucun processus en arrière-plan n'est impliqué.

Soren
la source
Pas assez portable. Je n'ai pas ncsur de vieux boîtiers Unix, ni sur beaucoup de Linux embarqué.
Gilles 'SO- arrête d'être méchant'
0

Une autre façon d'exécuter le pipeline dans son propre groupe de processus consiste à s'exécuter sh -c '....'dans un pseudo-terminal à l'aide de la scriptcommande (qui applique implicitement la setsidfonction).

#!/bin/sh
stty -echo -onlcr
# GNU script
foo=`script -q -c 'sh -c "{ cat 1>&3; kill 0; } | { sleep 5; kill 0; }" 3>&1 2>/dev/null' /dev/null`
# FreeBSD script
#foo=`script -q /dev/null sh -c '{ cat 1>&3; kill 0; } | { sleep 5; kill 0; }' 3>&1 2>/dev/null`
stty echo onlcr
echo "foo: $foo"


# alternative without: stty -echo -onlcr
# cr=`printf '\r'`
# foo=`script -q -c 'sh -c "{ { cat 1>&3; kill 0; } | { sleep 5; kill 0; } } 3>&1 2>/dev/null"' /dev/null | sed -e "s/${cr}$//" -ne 'p;N'`  # GNU
# foo=`script -q /dev/null sh -c '{ { cat 1>&3; kill 0; } | { sleep 5; kill 0; } } 3>&1 2>/dev/null' | sed -e "s/${cr}$//" -ne 'p;N'`  # FreeBSD
# echo "foo: $foo"
pete
la source
Pas assez portable. Je n'ai pas scriptsur de vieux boîtiers Unix, ni sur beaucoup de Linux embarqué.
Gilles, arrête de faire le mal
0

La réponse dans https://unix.stackexchange.com/a/18711 est très agréable.

Je voulais obtenir un résultat similaire, mais sans avoir à appeler explicitement à nouveau le shell car je voulais invoquer des fonctions de shell existantes.

En utilisant bash, il est possible de faire ce qui suit:

eval 'set -m ; ( ... ) ; '"$(set +o)"

Supposons donc que j'ai déjà une fonction shell f:

f() { date ; kill 0 ; }

echo Before
eval 'set -m ; ( f ) ; '"$(set +o)"
echo After

En exécutant ceci, je vois:

$ sh /tmp/foo.sh
Before
Mon 14 Mar 2016 17:22:41 PDT
/tmp/foo.sh: line 4: 17763 Terminated: 15          ( f )
After
comte
la source
-2
timeout_handler () { echo Timeout. goodbye.;  exit 1; }
trap timeout_handler ALRM
( sleep 60; kill -ALRM $$ ) &
Cory Schwartz
la source
Cela ne fonctionne pas pour la même raison que la réponse de geekausor . J'ai besoin d'obtenir le résultat d'une commande externe et le signal est ignoré pendant l'exécution de cette commande.
Gilles 'SO- arrête d'être méchant'