Salta al contenuto principale

timeout: un limite di tempo per i comandi

Inviato da tuxsa il

Un controllo di rete che a volte non risponde mai. Un comando che dovrebbe durare un secondo e resta lì per ore, bloccando uno script o una copia di sicurezza notturna. Il problema è sempre lo stesso: qualcosa che non finisce. timeout gli dà un limite di tempo: se il comando non finisce entro quel tempo, lo ferma.

Cos'è

timeout fa parte del pacchetto coreutils, quindi è presente su ogni distribuzione. È un "involucro": si scrive davanti a un altro comando, e vigila su di esso. Non conosce il comando che sorveglia e non ne cambia il funzionamento: lo avvia, guarda l'orologio e interviene solo se il tempo scade.

Cosa fa

Avvia il comando e aspetta. Se il comando finisce prima del limite, timeout finisce con lo stesso risultato. Se il tempo scade, invia al comando un segnale (di norma TERM, "terminati": vedi l'articolo su ps e kill) e termina con un codice di uscita speciale, 124, che dice "il tempo è scaduto".

Come si usa

$ timeout [opzioni] durata comando [argomenti]

La durata è un numero, con un'unità facoltativa: s secondi (predefinita), m minuti, h ore, d giorni; sono ammessi i decimali. Gli esempi sono stati provati in un sandbox; i nomi e i percorsi del server web01 sono illustrativi.

Esempio 1: il comando di base

$ timeout 5 sleep 10
$ echo $?
124
$ timeout 5 sleep 1
$ echo $?
0
$ timeout 1.5 sleep 3; echo $?
124
$ timeout 2m /home/enzo/bin/backup.sh
$ timeout 1h rsync -a /var/www/ /mnt/dati/www/

Il primo sleep (una pausa di 10 secondi) viene interrotto dopo 5 e il codice di uscita (la variabile $?, vedi gli articoli su echo e export) è 124. Il secondo finisce da solo in tempo: il codice è quello del comando (0, tutto bene). La durata può essere decimale (1.5) e usare le unità: 2m sono due minuti, 1h un'ora. Se scrivi 0, il limite è disattivato.

Esempio 2: i codici di uscita

$ timeout 5 true;  echo $?
0
$ timeout 5 false; echo $?
1
$ timeout 1 sleep 5; echo $?
124
$ timeout 5 nonesiste; echo $?
timeout: failed to run command 'nonesiste': No such file or directory
127
$ timeout abc true; echo $?
timeout: invalid time interval 'abc'
125
$ timeout -s KILL 1 sleep 5; echo $?
137

Il codice di uscita permette agli script di distinguere i casi:

  • 0 e gli altri: il comando è finito in tempo, e timeout restituisce il suo codice (0 per riuscito, 1 per fallito, ecc.);
  • 124: il tempo è scaduto e il comando è stato fermato;
  • 125: è timeout stesso che ha avuto un problema (per esempio una durata scritta male);
  • 126 e 127: il comando esiste ma non si può eseguire, oppure non esiste;
  • 137 (128 + 9): il comando è stato ucciso dal segnale KILL (numero 9). Allo stesso modo, 143 è TERM (128 + 15).

Con l'opzione --preserve-status il codice di uscita è sempre quello del comando, anche in caso di scadenza (con TERM diventa 143 e non più 124): utile se non vuoi distinguere i due casi.

Esempio 3: quando il comando ignora il segnale, con -k

$ cat testardo.sh
#!/bin/bash
trap '' TERM
for i in 1 2 3 4 5 6 7 8; do sleep 1; done
$ timeout 1 ./testardo.sh; echo $?
124
$ timeout -k 1 1 ./testardo.sh; echo $?
Killed
137

Il segnale TERM è una richiesta educata: un programma può ignorarla o gestirla a modo suo (come fa lo script dell'esempio con trap ''). Nella prima prova, timeout ha mandato TERM dopo 1 secondo, ma lo script l'ha ignorato ed è andato avanti per i suoi 8 secondi; solo allora timeout ha restituito 124. Con -k 1 (kill-after) timeout aspetta ancora 1 secondo dopo il primo segnale e poi manda KILL, che non può essere ignorato: il comando muore dopo 2 secondi, e il codice è 137. È la combinazione consigliata per i comandi che potrebbero non rispondere.

Esempio 4: scegliere il segnale, con -s, e vedere cosa succede, con -v

$ timeout -v 1 sleep 3
timeout: sending signal TERM to command 'sleep'
$ timeout -s INT 1 sleep 3; echo $?
124
$ timeout -s HUP 1 sleep 3; echo $?
124
$ timeout -s KILL 1 sleep 3; echo $?
137

Con -s (signal) si cambia il segnale (INT è lo stesso di Ctrl+C, HUP quello dell'articolo su nohup, KILL l'uccisione immediata). Con -v (verbose) timeout scrive su quale comando sta intervenendo: utile per capire cosa è successo in uno script. Se il comando ha rispettato il segnale scelto, il codice resta 124.

Esempio 5: comandi di rete che potrebbero non rispondere

$ timeout 10 curl -s https://example.org/ -o pagina.html
$ echo $?
124
$ curl -s --max-time 10 https://example.org/ -o pagina.html
$ echo $?
28
$ timeout 20 ssh enzo@203.0.113.10 uptime
$ ssh -o ConnectTimeout=5 enzo@203.0.113.10 uptime
$ timeout 5 ping 192.168.1.1

Molti programmi hanno già un loro limite: curl --max-time (vedi l'articolo su curl; il codice di uscita in questo caso è 28), ssh -o ConnectTimeout= (limita solo il tempo della connessione: vedi l'articolo su ssh), ping -c e -w, rsync --timeout. Dove esiste, il limite integrato è più preciso. timeout ha il vantaggio di funzionare con qualunque comando, e di limitare il tempo complessivo: un ssh che si collega ma poi resta appeso a un comando che non finisce, il suo ConnectTimeout non lo fermerebbe. Ho provato con un finto server che accetta la connessione e non risponde: timeout 2 curl è stato interrotto dopo 2 secondi.

Esempio 6: guardare per un tempo limitato

$ timeout 30 tail -f /var/log/nginx/access.log
$ timeout 60 journalctl -f -u nginx
$ timeout 5m watch -n 5 'df -h /mnt/dati'
$ timeout 10 sh -c 'while true; do date +%T; sleep 1; done'

Alcuni comandi non finiscono mai per scelta: tail -f, journalctl -f (vedi l'articolo su journalctl), watch (vedi l'articolo su watch), i cicli infiniti. Con timeout si seguono per un tempo preciso, e poi lo schermo (o lo script) torna libero: comodo per raccogliere 30 secondi di registri in un file, senza dover premere Ctrl+C al momento giusto. Nel mio test, timeout 2 tail -f è terminato con codice 124, come previsto.

Esempio 7: agire in base al risultato, negli script

#!/bin/bash
timeout -k 5 30 /home/enzo/bin/controlla-sito.sh
STATO=$?

case $STATO in
    0)   echo "Controllo riuscito" ;;
    124) echo "Il controllo ha superato i 30 secondi" >&2 ;;
    137) echo "Il controllo è stato ucciso" >&2 ;;
    *)   echo "Controllo fallito (codice $STATO)" >&2 ;;
esac

Lo script lancia un controllo (qui un programma immaginario) con un limite di 30 secondi, e con -k 5 lo uccide se non risponde al primo segnale. Salva subito il codice di uscita in STATO e lo valuta con case, distinguendo il successo, la scadenza (124) e l'uccisione (137). Salvare subito $? in una variabile è importante: il comando successivo lo sovrascriverebbe.

Esempio 8: pipe, funzioni e comandi composti

$ timeout 5 comando1 | comando2
$ timeout 5 sh -c 'comando1 | comando2'
$ f() { echo "inizio"; sleep 3; echo "fine"; }
$ timeout 1 f
timeout: failed to run command 'f': No such file or directory
$ export -f f
$ timeout 1 bash -c f
inizio

Due trappole classiche. La prima: timeout controlla un solo comando, quello che segue; in una pipe (|) il limite vale solo per il primo, per metterlo su tutta la catena si usa sh -c '...' tra apici (come per nohup). La seconda: una funzione della shell non è un programma, e timeout non la vede (codice 127, "failed to run command"). Si risolve con export -f funzione (vedi l'articolo su export) e lanciandola con bash -c funzione: così timeout avvia una nuova bash che la conosce. Lo stesso vale per gli alias (vedi l'articolo su alias): timeout non li vede.

Esempio 9: proteggere i lavori programmati

30 3 * * * timeout -k 60 2h /home/enzo/bin/backup.sh >> /home/enzo/backup.log 2>&1
$ echo "timeout 1h /home/enzo/bin/rielabora.sh" | at 03:00
$ nohup timeout 6h ./elabora-statistiche.sh > stat.log 2>&1 < /dev/null &

I lavori che girano senza nessuno davanti sono quelli che hanno più bisogno di un limite. La prima riga è una voce di crontab (vedi l'articolo relativo): la copia notturna parte alle 3:30 e, se dopo due ore non è finita, viene fermata, con un minuto di tolleranza prima dell'uccisione. Senza il limite, una copia bloccata potrebbe restare lì fino al giorno dopo, e la copia successiva partire sopra la prima. Lo stesso vale per at (vedi l'articolo su at) e per i lavori lanciati con nohup.

Esempio 10: il limite per l'input, con read -t

$ read -t 5 -p "Continuare? [s/N] " RISPOSTA || RISPOSTA=n
Continuare? [s/N]
$ echo "${RISPOSTA:-n}"
n

Per un input da tastiera esiste il comando interno di bash read -t secondi, che smette di aspettare allo scadere del tempo: restituisce un codice di uscita maggiore di 128 (nei miei test, 142) e la variabile resta vuota. È il modo giusto di scrivere una domanda con risposta predefinita in uno script, senza bloccarlo se nessuno risponde.

Opzioni utili

  • timeout durata comando: ferma il comando dopo la durata (s, m, h, d; decimali ammessi; 0 = nessun limite).
  • -s SEGNALE: sceglie il segnale (TERM predefinito; INT, HUP, KILL, anche in numero).
  • -k durata: se il comando è ancora vivo dopo questo tempo, lo uccide con KILL.
  • --preserve-status: restituisce sempre il codice del comando, anche alla scadenza.
  • -v: scrive quale segnale viene inviato; --foreground: per i comandi che devono leggere dal terminale.
  • sleep, read -t, curl --max-time, ssh -o ConnectTimeout, ping -w, rsync --timeout: limiti già presenti in altri programmi.
  • Codici di uscita: 124 scaduto, 125 errore di timeout, 126/127 comando non eseguibile o assente, 137 ucciso con KILL.

Attenzione a

  • Il codice 124 può essere ambiguo: se il comando stesso restituisce 124 per un motivo suo, non lo distingui da una scadenza. Per i comandi che usano questo codice, o per capire cosa è successo, aggiungi -v.
  • Interrompere a metà può lasciare danni: un file copiato solo in parte, un archivio tar troncato, un'esportazione di un database incompleta. Dopo una scadenza non fidarti del risultato: controlla, o scrivi in un file temporaneo e rinomina solo se tutto è riuscito (vedi l'articolo su mv).
  • TERM si può ignorare: un programma ostinato continua a girare, e timeout aspetta che finisca da solo (esempio 3). Se il comando deve essere fermato in ogni caso, aggiungi sempre -k.
  • I processi figli possono restare vivi: timeout ferma il comando che ha lanciato, ma non è detto che si fermino anche i programmi lanciati a loro volta. Nel mio sandbox, un timeout 1 bash -c 'sleep 37; echo dopo' ha fermato la bash ma ha lasciato il sleep in esecuzione. Dopo una scadenza verifica con pgrep (vedi l'articolo su ps), e se serve lancia direttamente il programma che vuoi limitare, o usa exec nella bash -c (timeout 1 bash -c 'exec sleep 36' funziona bene).
  • Non vale per le funzioni e per gli alias: né per le catene con pipe, che vanno racchiuse in sh -c (esempio 8).
  • I comandi che chiedono qualcosa al terminale (la password di ssh, una conferma): timeout li esegue in modo che possano non riuscire a leggere dalla tastiera. Se serve, aggiungi --foreground (con la limitazione che i processi figli non ricevono il limite). Per le domande negli script, usa read -t.
  • Il tempo è quello dell'orologio, non del processore: timeout non limita il consumo di CPU o di memoria, ma solo la durata. Un comando fermo ad aspettare consuma tempo come uno che lavora. Per limitare le risorse servono altri strumenti (ulimit, o le impostazioni di systemd).
  • Un limite troppo stretto causa falsi allarmi: un server sotto carico (vedi l'articolo su uptime) può rallentare un comando che di solito è rapido. Scegli un tempo con un margine, e registra le scadenze (per esempio con logger: vedi l'articolo su journalctl).
  • Prima di un comando distruttivo, pensa due volte: timeout 5 rm -rf ... non protegge da niente, e se il tempo scade a metà lascia il lavoro incompleto. timeout è fatto per i comandi che potrebbero bloccarsi, non per quelli pericolosi.

Riepilogo

ComandoCosa fa
timeout 10 comandoFerma il comando dopo 10 secondi (codice 124)
timeout 2m comandoLimite di 2 minuti (anche h, d, decimali)
timeout -k 5 30 comandoDopo 30 s richiede la fine, dopo altri 5 s uccide (137)
timeout -s KILL 10 comandoUsa subito il segnale KILL
timeout -v 10 comandoDice quale segnale invia
timeout --preserve-status 10 comandoRestituisce il codice del comando, anche se scaduto
timeout 10 sh -c 'a | b'Limita tutta una pipe
export -f f; timeout 5 bash -c fLimita una funzione di bash
timeout 30 tail -f fileSegue un file per un tempo limitato
read -t 5 -p "..." VARAspetta una risposta per 5 secondi
curl --max-time 10 URLLimite integrato di curl (codice 28)