Timeout in uno script di shell


53

Ho uno script di shell che legge da input standard . In rare circostanze, non ci sarà nessuno pronto a fornire input e lo script deve scadere . In caso di timeout, lo script deve eseguire un codice di pulizia. Qual è il modo migliore per farlo?

Questo script deve essere molto portatile , compresi i sistemi unix del 20 ° secolo senza un compilatore C e i dispositivi integrati che eseguono busybox, quindi Perl, bash, qualsiasi linguaggio compilato e persino l'intero POSIX.2 non possono essere invocati. In particolare, $PPID, read -ttrappole e perfettamente conforme a POSIX non sono disponibili. È anche esclusa la scrittura su un file temporaneo; lo script potrebbe essere eseguito anche se tutti i filesystem sono montati in sola lettura.

Solo per rendere le cose più difficili, voglio anche che la sceneggiatura sia ragionevolmente veloce quando non scade. In particolare, utilizzo anche lo script in Windows (principalmente in Cygwin), dove fork ed exec sono particolarmente bassi, quindi voglio ridurre al minimo il loro utilizzo.

In poche parole, ho

trap cleanup 1 2 3 15
foo=`cat`

e voglio aggiungere un timeout. Non posso sostituirlo catcon quello readintegrato. In caso di timeout, voglio eseguire la cleanupfunzione.


Sfondo: questo script indovina la codifica del terminale stampando alcuni caratteri a 8 bit e confrontando la posizione del cursore prima e dopo. L'inizio dello script verifica che stdout sia collegato a un terminale supportato, ma a volte l'ambiente si trova (ad esempio plinkimposta TERM=xtermanche se viene chiamato conTERM=dumb ). La parte rilevante dello script è simile alla seguente:

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

Come posso modificare lo script in modo tale che se trnon ha letto alcun input dopo 2 secondi, viene ucciso e lo script esegue la cleanupfunzione?


Risposte:


33

Che dire di questo:

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

Cioè: esegui il comando di produzione dell'output e sleepnello stesso gruppo di processi, un gruppo di processi solo per loro. Qualunque comando restituisca per primo uccide l'intero gruppo di processi.

Qualcuno si chiederebbe: Sì, la pipa non viene utilizzata; viene bypassato usando i reindirizzamenti. Il solo scopo è far eseguire alla shell i due processi nello stesso gruppo di processi.


Come ha sottolineato Gilles nel suo commento, questo non funzionerà in uno script di shell perché il processo di script verrebbe ucciso insieme ai due sottoprocessi.

Un modo¹ per forzare l'esecuzione di un comando in un gruppo di processi separato è quello di avviare una nuova shell interattiva:

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

Ma potrebbero esserci avvertenze con questo (ad esempio quando lo stdin non è un tty?). Il reindirizzamento stderr è lì per sbarazzarsi del messaggio "Terminato" quando la shell interattiva viene uccisa.

Testato con zsh, bashe dash. E i vecchi?

B98 suggerisce la seguente modifica, lavorando su Mac OS X, con GNU bash 3.2.57 o Linux con trattino:

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

-
1. diverso da quello setsidche sembra non standard.


1
Mi piace molto, non avevo pensato di usare un gruppo di processi in quel modo. È piuttosto semplice e non ci sono condizioni di gara. Devo testarlo ancora per la portabilità, ma questo sembra un vincitore.
Gilles 'SO- smetti di essere malvagio' il

Sfortunatamente, questo fallisce in modo spettacolare non appena lo inserisco in uno script: la pipeline nella sostituzione dei comandi non viene eseguita nel proprio gruppo di processi e kill 0finisce anche per uccidere il chiamante dello script. Esiste un modo portatile per forzare la pipeline nel proprio gruppo di processi?
Gilles 'SO- smetti di essere malvagio' il

@Gilles: Eek! non riusciva a trovare un modo per setprgp()senza setsidper il momento :-(
Stéphane Gimenez

1
Mi piace molto il trucco, quindi assegnerò la taglia. Sembra funzionare su Linux, non ho ancora avuto il tempo di testarlo su altri sistemi.
Gilles 'SO- smetti di essere malvagio' il

2
@Zac: non è possibile invertire l'ordine nel caso originale, poiché solo il primo processo ha accesso a stdin.
Stéphane Gimenez,

6
me=$$
(sleep 2; kill $me >/dev/null 2>&1) & nuker=$!
# do whatever
kill $nuker >/dev/null 2>&1

Stai già intrappolando 15 (la versione numerica di SIGTERM, che è ciò che killinvia se non diversamente specificato), quindi dovresti già essere pronto a partire. Detto questo, se stai guardando pre-POSIX, tieni presente che le funzioni di shell potrebbero non esistere (provenivano dalla shell di System V).


Non è così semplice. Se intrappoli il segnale, molte shell (trattino, bash, pdksh, zsh ... probabilmente tutte tranne ATT ksh) lo ignorano mentre stanno aspettando catl'uscita. Ho già sperimentato un po ', ma finora non ho trovato nulla di cui sono soddisfatto.
Gilles 'SO- smetti di essere malvagio' il

Sulla portabilità: per fortuna ho funzioni ovunque. Non ne sono sicuro $!, penso che alcune di quelle macchine che utilizzo raramente non abbiano il controllo del lavoro, sono $!universalmente disponibili?
Gilles 'SO- smetti di essere malvagio' il

@Gilles: Hm, yeh. Ricordo alcune cose davvero orribili e bashspecifiche che una volta ho fatto comportando abusi -o monitor. Stavo pensando in termini di conchiglie veramente antiche quando l'ho scritto (ha funzionato in v7). Detto questo, penso che tu possa fare una delle altre due cose: (1) lo sfondo "qualunque" e wait $!, o (2) anche inviare SIGCLD/ SIGCHLD... ma su macchine abbastanza vecchie quest'ultima è o inesistente o non portabile (la prima è Sistema III / V, quest'ultimo BSD e V7 non avevano nessuno dei due).
geekosaur,

@Gilles: $!risale al V7 almeno, e certamente anteriore una sh-come che sapesse qualcosa di controllo del lavoro (in realtà, per lungo tempo /bin/shsu BSD non ha fatto il controllo dei job, si doveva correre cshper farlo - ma $!era lì ).
geekosaur,

Non riesco a scrivere "qualunque cosa", ho bisogno del suo output. Grazie per le informazioni su $!.
Gilles 'SO- smetti di essere malvagio' il

4

Sebbene coretuils a partire dalla versione 7.0 includa un comando di timeout hai menzionato alcuni ambienti che non lo avranno. Fortunatamente pixelbeat.org ha uno script di timeout scritto sh.

L'ho usato prima in diverse occasioni e funziona molto bene.

http://www.pixelbeat.org/scripts/timeout ( Nota: lo script seguente è stato leggermente modificato da quello su pixelbeat.org, vedere i commenti sotto questa risposta.)

#!/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 <brad@footle.org>
#                       Make more portable by using the 'CHLD'
#                       signal spec rather than 17.
#    V1.3, Oct 29 2009, Ján Sáreník <jasan@x31.com>
#                       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 <jasan@x31.com>
#                       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

Sembra che ciò non consenta di recuperare l'output del comando. Vedi il commento "Non posso" qualunque cosa "" di Gilles nell'altra risposta.
Stéphane Gimenez,

Ah, interessante. Ho modificato lo script (in modo che non corrisponda più a quello di pixelbeat) per reindirizzare / dev / stdin nel comando. Sembra funzionare nei miei test.
bahamat,

Questo non funziona per avere il comando letto dallo standard input, tranne (stranamente) in bash. </dev/stdinè una no-op. </dev/ttyconsentirebbe a leggere dal terminale, che è abbastanza buono per il mio caso d'uso.
Gilles 'SO- smetti di essere malvagio' il

@Giles: figo, farò quell'aggiornamento.
bahamat,

Questo non può funzionare senza uno sforzo considerevolmente maggiore: devo recuperare l'output del comando e non posso farlo se il comando è in background.
Gilles 'SO- smetti di essere malvagio' il

3

Che dire di (ab) usare NC per questo

Piace;

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

O arrotolato in un'unica riga di comando;

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

L'ultima versione, anche se in realtà sembra stange funziona quando lo collaudo su un sistema Linux - la mia ipotesi migliore è che dovrebbe funzionare su qualsiasi sistema, e se non una variazione del reindirizzamento dell'output potrebbe risolvere la portabilità. il vantaggio qui è che non sono coinvolti processi in background.


Non abbastanza portatile. Non ho ncsu alcune vecchie scatole Unix, né su molti Linux embedded.
Gilles 'SO- smetti di essere malvagio' il

0

Un altro modo per eseguire la pipeline nel proprio gruppo di processi è eseguire sh -c '....'in uno pseudo-terminale utilizzando il scriptcomando (che applica implicitamente la setsidfunzione).

#!/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"

Non abbastanza portatile. Non ho scriptsu alcune vecchie scatole Unix, né su molti Linux embedded.
Gilles 'SO- smetti di essere malvagio' il

0

La risposta in https://unix.stackexchange.com/a/18711 è molto bella.

Volevo ottenere un risultato simile, ma senza dover richiamare esplicitamente la shell perché volevo invocare le funzioni di shell esistenti.

Utilizzando bash, è possibile effettuare le seguenti operazioni:

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

Supponiamo quindi che abbia già una funzione shell f:

f() { date ; kill 0 ; }

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

In esecuzione di questo vedo:

$ sh /tmp/foo.sh
Before
Mon 14 Mar 2016 17:22:41 PDT
/tmp/foo.sh: line 4: 17763 Terminated: 15          ( f )
After

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.