83 votes

Exécuter une fonction shell avec timeout

Pourquoi cela fonctionnerait-il

timeout 10s echo "foo bar" # foo bar

mais ce ne serait pas

function echoFooBar {
  echo "foo bar"
}

echoFooBar # foo bar

timeout 10s echoFooBar # timeout: failed to run command `echoFooBar': No such file or directory

et comment faire pour que ça marche ?

4voto

Hemant Patel Points 41
function foo(){
    for i in {1..100};
    do 
        echo $i;  
        sleep 1;
    done;
}

cat <( foo ) # Will work 
timeout 3 cat <( foo ) # Will Work 
timeout 3 cat <( foo ) | sort # Wont work, As sort will fail 
cat <( timeout 3 cat <( foo ) ) | sort -r # Will Work

1voto

untore Points 163

Cette fonction n'utilise que des builtins

  • En fonction de vos besoins, vous pouvez envisager d'évaluer "$*" au lieu d'exécuter directement $@.

  • Il lance un travail avec la chaîne de commande spécifiée après le premier argument qui est la valeur du délai d'attente et surveille le pid du travail.

  • Il vérifie toutes les 1 secondes, bash supporte des timeouts jusqu'à 0.01 donc cela peut être ajusté

  • Aussi si votre script a besoin de stdin, read doit s'appuyer sur un fd dédié ( exec {tofd}<> <(:) )

  • Vous pouvez également modifier le signal d'arrêt (celui qui se trouve à l'intérieur de la boucle) qui est réglé par défaut sur -15 vous pourriez vouloir -9

    forking is evil

    timeout() { to=$1; shift $@ & local wp=$! start=0 while kill -0 $wp; do read -t 1 start=$((start+1)) if [ $start -ge $to ]; then kill $wp && break fi done }

1voto

TauPan Points 51

Je mets mon commentaire à la réponse de Tiago Lopo sous une forme plus lisible :

Je pense qu'il est plus lisible d'imposer un timeout sur le sous-shell le plus récent, de cette façon nous n'avons pas besoin d'évaluer une chaîne et l'ensemble du script peut être mis en évidence comme shell par votre éditeur préféré. Je mets simplement les commandes après le subshell avec eval s'est transformé en une fonction shell (testé avec zsh, mais devrait fonctionner avec bash) :

timeout_child () {
    trap -- "" SIGTERM
    child=$!
    timeout=$1
    (
            sleep $timeout
            kill $child
    ) &
    wait $child
}

Exemple d'utilisation :

( while true; do echo -n .; sleep 0.1; done) & timeout_child 2

Et de cette façon, cela fonctionne également avec une fonction shell (si elle s'exécute en arrière-plan) :

 print_dots () {
     while true
     do
         sleep 0.1
         echo -n .
     done
 }

 > print_dots & timeout_child 2
 [1] 21725
 [3] 21727
 ...................[1]    21725 terminated  print_dots
 [3]  + 21727 done       ( sleep $timeout; kill $child; )

1voto

CristianCantoro Points 151

J'ai une légère modification de la réponse de @Tiago Lopo qui peut gérer les commandes avec plusieurs arguments. J'ai également testé la solution de TauPan, mais elle ne fonctionne pas si vous l'utilisez plusieurs fois dans un script, alors que celle de Tiago le fait.

function timeout_cmd { 
  local arr
  local cmd
  local timeout

  arr=( "$@" )

  # timeout: first arg
  # cmd: the other args
  timeout="${arr[0]}"
  cmd=( "${arr[@]:1}" )

  ( 
    eval "${cmd[@]}" &
    child=$!

    echo "child: $child"
    trap -- "" SIGTERM 
    (       
      sleep "$timeout"
      kill "$child" 2> /dev/null 
    ) &     
    wait "$child"
  )
}

Voici un script entièrement fonctionnel que vous pouvez utiliser pour tester la fonction ci-dessus :

$ ./test_timeout.sh -h
Usage:
  test_timeout.sh [-n] [-r REPEAT] [-s SLEEP_TIME] [-t TIMEOUT]
  test_timeout.sh -h

Test timeout_cmd function.

Options:
  -n              Dry run, do not actually sleep. 
  -r REPEAT       Reapeat everything multiple times [default: 1].
  -s SLEEP_TIME   Sleep for SLEEP_TIME seconds [default: 5].
  -t TIMEOUT      Timeout after TIMEOUT seconds [default: no timeout].

Par exemple, vous pouvez lancer le programme comme ceci :

$ ./test_timeout.sh -r 2 -s 5 -t 3
Try no: 1
  - Set timeout to: 3
child: 2540
    -> retval: 143
    -> The command timed out
Try no: 2
  - Set timeout to: 3
child: 2593
    -> retval: 143
    -> The command timed out
Done!

#!/usr/bin/env bash

#shellcheck disable=SC2128
SOURCED=false && [ "$0" = "$BASH_SOURCE" ] || SOURCED=true

if ! $SOURCED; then
  set -euo pipefail
  IFS=$'\n\t'
fi

#################### helpers
function check_posint() {
  local re='^[0-9]+$'
  local mynum="$1"
  local option="$2"

  if ! [[ "$mynum" =~ $re ]] ; then
     (echo -n "Error in option '$option': " >&2)
     (echo "must be a positive integer, got $mynum." >&2)
     exit 1
  fi

  if ! [ "$mynum" -gt 0 ] ; then
     (echo "Error in option '$option': must be positive, got $mynum." >&2)
     exit 1
  fi
}
#################### end: helpers

#################### usage
function short_usage() {
  (>&2 echo \
"Usage:
  test_timeout.sh [-n] [-r REPEAT] [-s SLEEP_TIME] [-t TIMEOUT]
  test_timeout.sh -h"
  )
}

function usage() {
  (>&2 short_usage )
  (>&2 echo \
"
Test timeout_cmd function.

Options:
  -n              Dry run, do not actually sleep. 
  -r REPEAT       Reapeat everything multiple times [default: 1].
  -s SLEEP_TIME   Sleep for SLEEP_TIME seconds [default: 5].
  -t TIMEOUT      Timeout after TIMEOUT seconds [default: no timeout].
")
}
#################### end: usage

help_flag=false
dryrun_flag=false
SLEEP_TIME=5
TIMEOUT=-1
REPEAT=1

while getopts ":hnr:s:t:" opt; do
  case $opt in
    h)
      help_flag=true
      ;;    
    n)
      dryrun_flag=true
      ;;
    r)
      check_posint "$OPTARG" '-r'

      REPEAT="$OPTARG"
      ;;
    s)
      check_posint "$OPTARG" '-s'

      SLEEP_TIME="$OPTARG"
      ;;
    t)
      check_posint "$OPTARG" '-t'

      TIMEOUT="$OPTARG"
      ;;
    \?)
      (>&2 echo "Error. Invalid option: -$OPTARG.")
      (>&2 echo "Try -h to get help")
      short_usage
      exit 1
      ;;
    :)
      (>&2 echo "Error.Option -$OPTARG requires an argument.")
      (>&2 echo "Try -h to get help")
      short_usage
      exit 1
      ;;
  esac
done

if $help_flag; then
  usage
  exit 0
fi

#################### utils
if $dryrun_flag; then
  function wrap_run() {
    ( echo -en "[dry run]\\t" )
    ( echo "$@" )
  }
else
  function wrap_run() { "$@"; }
fi

# Execute a shell function with timeout
# https://stackoverflow.com/a/24416732/2377454
function timeout_cmd { 
  local arr
  local cmd
  local timeout

  arr=( "$@" )

  # timeout: first arg
  # cmd: the other args
  timeout="${arr[0]}"
  cmd=( "${arr[@]:1}" )

  ( 
    eval "${cmd[@]}" &
    child=$!

    echo "child: $child"
    trap -- "" SIGTERM 
    (       
      sleep "$timeout"
      kill "$child" 2> /dev/null 
    ) &     
    wait "$child"
  )
}
####################

function sleep_func() {
  local secs
  local waitsec

  waitsec=1
  secs=$(($1))
  while [ "$secs" -gt 0 ]; do
   echo -ne "$secs\033[0K\r"
   sleep "$waitsec"
   secs=$((secs-waitsec))
  done

}

command=("wrap_run" \
         "sleep_func" "${SLEEP_TIME}"
         )

for i in $(seq 1 "$REPEAT"); do
  echo "Try no: $i"

  if [ "$TIMEOUT" -gt 0 ]; then
    echo "  - Set timeout to: $TIMEOUT"
    set +e
    timeout_cmd "$TIMEOUT" "${command[@]}"
    retval="$?"
    set -e

    echo "    -> retval: $retval"
    # check if (retval % 128) == SIGTERM (== 15)
    if [[ "$((retval % 128))" -eq 15 ]]; then
      echo "    -> The command timed out"
    fi
  else
    echo "  - No timeout"
    "${command[@]}"
    retval="$?"
  fi
done

echo "Done!"

exit 0

1voto

Aidan Walton Points 11

Cette petite modification à la réponse de TauPan ajoute une protection utile. Si le processus enfant qui est attendu s'est déjà éteint avant que le sleep $timeout ne se termine. La commande kill tente de tuer un processus qui n'existe plus. Ceci est probablement inoffensif, mais il n'y a pas de garantie absolue que le même PID n'a pas été réassigné. Pour éviter cela, une vérification rapide est effectuée pour s'assurer que le PID enfant existe et que son parent est le shell à partir duquel il a été bifurqué. De plus, essayer de tuer un processus inexistant génère des erreurs qui, si elles ne sont pas supprimées, peuvent facilement remplir les journaux.

J'ai également utilisé un kill -9 plus agressif. C'est le seul moyen de tuer un processus qui ne se bloque pas sur la commande shell mais sur le système de fichiers par exemple. read < named_pipe .
Une conséquence de ceci est que le kill -9 $child envoie son signal kill de manière asynchrone au processus et génère donc un message dans le shell appelant. Ceci peut être supprimé en redirigeant la commande wait $child > /dev/null 2>&1 . Avec des conséquences évidentes pour le débogage.

#!/bin/bash

function child_timeout () {
        child=$!
        timeout=$1
        (
        #trap -- "" SIGINT

        sleep $timeout
        if [ $(ps -o pid= -o comm= --ppid $$ | grep -o $child) ]; then
                kill -9 $child
        fi
        ) &
wait $child > /dev/null 2>&1

}

( tail -f /dev/null ) & child_timeout 10

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X