web-dev-qa-db-fra.com

Expiration d'une commande en bash sans délai inutile

Cette réponse à Commande en ligne de commande pour supprimer automatiquement une commande après un certain temps

propose une méthode sur 1 ligne pour expirer une commande longue depuis la ligne de commande bash:

( /path/to/slow command with options ) & sleep 5 ; kill $!

Mais il est possible qu'une commande "longue" donnée se termine avant l'expiration du délai. (Appelons-la une commande "typiquement-longue-courante-mais-parfois-rapide", ou tlrbsf par plaisir.)

Donc, cette approche astucieuse à une ligne a quelques problèmes. Premièrement, la variable sleep n'est pas conditionnelle, ce qui définit une limite inférieure indésirable sur le temps pris pour terminer la séquence. Considérez 30 ou 2 m, voire 5 m pour le sommeil, lorsque la commande tlrbsf se termine en 2 secondes, ce qui est hautement indésirable. Deuxièmement, la kill étant inconditionnelle, cette séquence va tenter de tuer un processus qui ne fonctionne pas et de se plaindre.

Alors...

Existe-t-il un moyen de temporiser une commande généralement longue ("tlrbsf")

  • a une implémentation bash (l'autre question a déjà des réponses Perl et C)
  • se terminera au plus tôt des deux: tlrbsf fin du programme ou délai écoulé
  • ne tue pas les processus inexistants/non-exécutés (ou, facultativement: ne sera pas se plaindre à propos d'une destruction sérieuse)
  • ne doit pas être un 1-liner
  • peut fonctionner sous Cygwin ou Linux

... et, pour les points bonus, exécute la commande tlrbsf au premier plan et tout processus "veille" ou supplémentaire en arrière-plan, tel que le stdin/stdout/stderr du tlrbsf La commande peut être redirigée, comme si elle avait été lancée directement?

Si oui, merci de partager votre code. Si non, s'il vous plaît expliquer pourquoi.

J'ai passé un certain temps à essayer de pirater l'exemple susmentionné, mais je suis à la limite de mes compétences bash.

248
system PAUSE

Je pense que c'est précisément ce que vous demandez:

http://www.bashcookbook.com/bashinfo/source/bash-4.0/examples/scripts/timeout3

#!/bin/bash
#
# The Bash Shell script executes a command with a time-out.
# Upon time-out expiration SIGTERM (15) is sent to the process. If the signal
# is blocked, then the subsequent SIGKILL (9) terminates it.
#
# Based on the Bash documentation example.

# Hello Chet,
# please find attached a "little easier"  :-)  to comprehend
# time-out example.  If you find it suitable, feel free to include
# anywhere: the very same logic as in the original examples/scripts, a
# little more transparent implementation to my taste.
#
# Dmitry V Golovashkin <[email protected]>

scriptName="${0##*/}"

declare -i DEFAULT_TIMEOUT=9
declare -i DEFAULT_INTERVAL=1
declare -i DEFAULT_DELAY=1

# Timeout.
declare -i timeout=DEFAULT_TIMEOUT
# Interval between checks if the process is still alive.
declare -i interval=DEFAULT_INTERVAL
# Delay between posting the SIGTERM signal and destroying the process by SIGKILL.
declare -i delay=DEFAULT_DELAY

function printUsage() {
    cat <<EOF

Synopsis
    $scriptName [-t timeout] [-i interval] [-d delay] command
    Execute a command with a time-out.
    Upon time-out expiration SIGTERM (15) is sent to the process. If SIGTERM
    signal is blocked, then the subsequent SIGKILL (9) terminates it.

    -t timeout
        Number of seconds to wait for command completion.
        Default value: $DEFAULT_TIMEOUT seconds.

    -i interval
        Interval between checks if the process is still alive.
        Positive integer, default value: $DEFAULT_INTERVAL seconds.

    -d delay
        Delay between posting the SIGTERM signal and destroying the
        process by SIGKILL. Default value: $DEFAULT_DELAY seconds.

As of today, Bash does not support floating point arithmetic (sleep does),
therefore all delay/time values must be integers.
EOF
}

# Options.
while getopts ":t:i:d:" option; do
    case "$option" in
        t) timeout=$OPTARG ;;
        i) interval=$OPTARG ;;
        d) delay=$OPTARG ;;
        *) printUsage; exit 1 ;;
    esac
done
shift $((OPTIND - 1))

# $# should be at least 1 (the command to execute), however it may be strictly
# greater than 1 if the command itself has options.
if (($# == 0 || interval <= 0)); then
    printUsage
    exit 1
fi

# kill -0 pid   Exit code indicates if a signal may be sent to $pid process.
(
    ((t = timeout))

    while ((t > 0)); do
        sleep $interval
        kill -0 $$ || exit 0
        ((t -= interval))
    done

    # Be Nice, post SIGTERM first.
    # The 'exit 0' below will be executed if any preceeding command fails.
    kill -s SIGTERM $$ && kill -0 $$ || exit 0
    sleep $delay
    kill -s SIGKILL $$
) 2> /dev/null &

exec "$@"
135
Juliano

Vous recherchez probablement la commande timeout dans coreutils. Étant donné qu’il fait partie de coreutils, c’est techniquement une solution en C, mais c’est toujours coreutils. info timeout pour plus de détails ..__ Voici un exemple:

timeout 5 /path/to/slow/command with options
464
yingted

Cette solution fonctionne quel que soit le mode de surveillance bash. Vous pouvez utiliser le bon signal pour terminer your_command

#!/bin/sh
( your_command ) & pid=$!
( sleep $TIMEOUT && kill -HUP $pid ) 2>/dev/null & watcher=$!
wait $pid 2>/dev/null && pkill -HUP -P $watcher

L'observateur tue votre_commande après l'expiration du délai imparti; le script attend la tâche lente et met fin à l'observateur. Notez que wait ne fonctionne pas avec les processus qui sont les enfants d'un autre shell.

Exemples:

  • votre_commande s'exécute plus de 2 secondes et a été terminée

votre_commande interrompue

( sleep 20 ) & pid=$!
( sleep 2 && kill -HUP $pid ) 2>/dev/null & watcher=$!
if wait $pid 2>/dev/null; then
    echo "your_command finished"
    pkill -HUP -P $watcher
    wait $watcher
else
    echo "your_command interrupted"
fi
  • votre_commande est terminée avant la fin du délai (20 secondes)

votre_commande terminée

( sleep 2 ) & pid=$!
( sleep 20 && kill -HUP $pid ) 2>/dev/null & watcher=$!
if wait $pid 2>/dev/null; then
    echo "your_command finished"
    pkill -HUP -P $watcher
    wait $watcher
else
    echo "your_command interrupted"
fi
33
Dmitry

Voilà:

timeout --signal=SIGINT 10 /path/to/slow command with options

vous pouvez changer la SIGINT et le 10 comme vous le souhaitez;)

18

Je préfère "timelimit", qui contient un paquet au moins dans debian.

http://devel.ringlet.net/sysutils/timelimit/

Il est un peu plus agréable que le délai d'attente de coreutils, car il imprime quelque chose lorsque vous tuez le processus et envoie également SIGKILL après un certain temps par défaut.

17
maxy

Vous pouvez le faire entièrement avec bash 4.3 et plus:

_timeout() { ( set +b; sleep "$1" & "${@:2}" & wait -n; r=$?; kill -9 `jobs -p`; exit $r; ) }
  • Exemple: _timeout 5 longrunning_command args
  • Exemple: { _timeout 5 producer || echo KABOOM $?; } | consumer
  • Exemple: producer | { _timeout 5 consumer1; consumer2; }
  • Exemple: { while date; do sleep .3; done; } | _timeout 5 cat | less

  • Nécessite Bash 4.3 pour wait -n

  • Donne 137 si la commande a été supprimée, sinon la valeur de retour de la commande.
  • Fonctionne pour les pipes. (Vous n'avez pas besoin d'aller au premier plan ici!)
  • Fonctionne également avec les commandes ou fonctions internes du shell.
  • S'exécute dans un sous-shell, donc aucune exportation de variable dans le shell en cours, désolé.

Si vous n'avez pas besoin du code de retour, vous pouvez le simplifier encore davantage:

_timeout() { ( set +b; sleep "$1" & "${@:2}" & wait -n; kill -9 `jobs -p`; ) }

Remarques:

  • Strictement parlant, vous n'avez pas besoin du ; dans ; ), mais cela rend les choses plus cohérentes avec le ; }-. Et le set +b peut probablement être laissé de côté aussi, mais mieux vaut prévenir que guérir.

  • À l'exception de --forground (probablement), vous pouvez implémenter toutes les variantes supportées par timeout. --preserve-status est un peu difficile, cependant. Ceci est laissé comme un exercice pour le lecteur;)

Cette recette peut être utilisée "naturellement" dans la coque (aussi naturelle que pour flock fd):

(
set +b
sleep 20 &
{
YOUR Shell CODE HERE
} &
wait -n
kill `jobs -p`
)

Toutefois, comme expliqué ci-dessus, vous ne pouvez pas réexporter naturellement les variables d'environnement dans le Shell englobant.

Modifier:

Exemple concret: Time out __git_ps1 au cas où cela prendrait trop de temps (pour des choses comme les liens SSHFS lents):

eval "__orig$(declare -f __git_ps1)" && __git_ps1() { ( git() { _timeout 0.3 /usr/bin/git "$@"; }; _timeout 0.3 __orig__git_ps1 "$@"; ) }

Edit2: correction de bug. J'ai remarqué que exit 137 n'est pas nécessaire et rend _timeout non fiable en même temps.

Edit3: git est un disque dur, il a donc besoin d'un double tour pour fonctionner de manière satisfaisante.

Edit4: Oublié un _ dans le premier _timeout pour l'exemple GIT du monde réel.

14
Tino

Voir aussi le http://www.pixelbeat.org/scripts/timeout script dont la fonctionnalité a été intégrée dans les nouveaux coreutils.

9
pixelbeat

timeout est probablement la première approche à essayer. Vous aurez peut-être besoin d'une notification ou d'une autre commande à exécuter si le délai est écoulé. Après avoir fait quelques recherches et expérimenté, j'ai créé ce script bash :

if 
    timeout 20s COMMAND_YOU_WANT_TO_EXECUTE;
    timeout 20s AS_MANY_COMMANDS_AS_YOU_WANT;
then
    echo 'OK'; #if you want a positive response
else
    echo 'Not OK';
    AND_ALTERNATIVE_COMMANDS
fi
8
user2099484

Un peu hacky, mais ça marche. Ne fonctionne pas si vous avez d'autres processus de premier plan (aidez-moi s'il vous plaît à résoudre ce problème!)

sleep TIMEOUT & SPID=${!}; (YOUR COMMAND HERE; kill ${SPID}) & CPID=${!}; fg 1; kill ${CPID}

En fait, je pense que vous pouvez l’inverser, en répondant à vos critères de «bonus»:

(YOUR COMMAND HERE & SPID=${!}; (sleep TIMEOUT; kill ${SPID}) & CPID=${!}; fg 1; kill ${CPID}) < asdf > fdsa
7
strager

Script simple avec la clarté du code. Enregistrer dans /usr/local/bin/run:

#!/bin/bash

# run
# Run command with timeout $1 seconds.

# Timeout seconds
timeout_seconds="$1"
shift

# PID
pid=$$

# Start timeout
(
  sleep "$timeout_seconds"
  echo "Timed out after $timeout_seconds seconds"
  kill -- -$pid &>/dev/null
) &
timeout_pid=$!

# Run
"$@"

# Stop timeout
kill $timeout_pid &>/dev/null

Expire une commande trop longue:

$ run 2 sleep 10
Timed out after 2 seconds
Terminated
$

Se termine immédiatement pour une commande qui se termine:

$ run 10 sleep 2
$
3
Lycan

Si vous connaissez déjà le nom du programme (supposons que program) se termine après l'expiration du délai (par exemple 3 secondes), je peux proposer une solution de rechange simple et quelque peu sale:

(sleep 3 && killall program) & ./program

Cela fonctionne parfaitement si j'appelle des processus de référence avec des appels système.

3
loup

Pour expirer la slowcommand après 1 seconde:

timeout 1 slowcommand || echo "I failed, perhaps due to time out"

2
lance.dolan

Il y a aussi cratimeout de Martin Cracauer (écrit en C pour les systèmes Unix et Linux).

# cf. http://www.cons.org/cracauer/software.html
# usage: cratimeout timeout_in_msec cmd args
cratimeout 5000 sleep 1
cratimeout 5000 sleep 600
cratimeout 5000 tail -f /dev/null
cratimeout 5000 sh -c 'while sleep 1; do date; done'
2
max32

OS X n'utilise pas encore bash 4, pas plus que/usr/bin/timeout. Voici donc une fonction qui fonctionne sous OS X sans home-brass ou macports, similaire à/usr/bin/timeout réponse). La validation des paramètres, l'aide, l'utilisation et la prise en charge d'autres signaux constituent un exercice pour le lecteur.

# implement /usr/bin/timeout only if it doesn't exist
[ -n "$(type -p timeout 2>&1)" ] || function timeout { (
    set -m +b
    sleep "$1" &
    SPID=${!}
    ("${@:2}"; RETVAL=$?; kill ${SPID}; exit $RETVAL) &
    CPID=${!}
    wait %1
    SLEEPRETVAL=$?
    if [ $SLEEPRETVAL -eq 0 ] && kill ${CPID} >/dev/null 2>&1 ; then
      RETVAL=124
      # When you need to make sure it dies
      #(sleep 1; kill -9 ${CPID} >/dev/null 2>&1)&
      wait %2
    else
      wait %2
      RETVAL=$?
    fi
    return $RETVAL
) }
1
NerdMachine

Voici une version qui ne repose pas sur la génération d'un processus enfant. J'avais besoin d'un script autonome intégrant cette fonctionnalité. Il effectue également un intervalle d'interrogation fractionnaire, ce qui vous permet d'interroger plus rapidement. le délai d'attente aurait été préféré - mais je suis bloqué sur un ancien serveur

# wait_on_command <timeout> <poll interval> command
wait_on_command()
{
    local timeout=$1; shift
    local interval=$1; shift
    $* &
    local child=$!

    loops=$(bc <<< "($timeout * (1 / $interval)) + 0.5" | sed 's/\..*//g')
    ((t = loops))
    while ((t > 0)); do
        sleep $interval
        kill -0 $child &>/dev/null || return
        ((t -= 1))
    done

    kill $child &>/dev/null || kill -0 $child &>/dev/null || return
    sleep $interval
    kill -9 $child &>/dev/null
    echo Timed out
}

slow_command()
{
    sleep 2
    echo Completed normally
}

# wait 1 sec in 0.1 sec increments
wait_on_command 1 0.1 slow_command

# or call an external command
wait_on_command 1 0.1 sleep 10
0
Neil McGill

J'ai un travail cron qui appelle un script php et, parfois, il reste bloqué sur un script php Cette solution était parfaite pour moi. 

J'utilise: 

scripttimeout -t 60 /script.php
0
Jehad Buria
#! /bin/bash
timeout=10
interval=1
delay=3
(
    ((t = timeout)) || :

    while ((t > 0)); do
        echo "$t"
        sleep $interval
        # Check if the process still exists.
        kill -0 $$ 2> /dev/null || exit 0
        ((t -= interval)) || :
    done

    # Be Nice, post SIGTERM first.
    { echo SIGTERM to $$ ; kill -s TERM $$ ; sleep $delay ; kill -0 $$ 2> /dev/null && { echo SIGKILL to $$ ; kill -s KILL $$ ; } ; }
) &

exec "$@"
0
eel ghEEz

Mon problème était peut-être un peu différent: je lance une commande via ssh sur une machine distante et je veux tuer le shell et les enfants si la commande se bloque.

J'utilise maintenant les éléments suivants:

ssh server '( sleep 60 && kill -9 0 ) 2>/dev/null & my_command; RC=$? ; sleep 1 ; pkill -P $! ; exit $RC'

De cette façon, la commande retourne 255 en cas de dépassement de délai ou le code de retour de la commande en cas de succès.

Veuillez noter que la suppression des processus d'une session ssh est traitée différemment d'un shell interactif. Mais vous pouvez également utiliser l'option -t de ssh pour allouer un pseudo-terminal, de sorte qu'il se comporte comme un shell interactif.

0
Franky_GT

Un problème m'a été présenté pour préserver le contexte du shell et autoriser les délais d'expiration. Le seul problème est que cela arrêtera l'exécution du script à l'expiration du délai imparti.

#!/usr/bin/env bash

safe_kill()
{
  ps aux | grep -v grep | grep $1 >/dev/null && kill ${2:-} $1
}

my_timeout()
{
  typeset _my_timeout _waiter_pid _return
  _my_timeout=$1
  echo "Timeout($_my_timeout) running: $*"
  shift
  (
    trap "return 0" USR1
    sleep $_my_timeout
    echo "Timeout($_my_timeout) reached for: $*"
    safe_kill $$
  ) &
  _waiter_pid=$!
  "$@" || _return=$?
  safe_kill $_waiter_pid -USR1
  echo "Timeout($_my_timeout) ran: $*"
  return ${_return:-0}
}

my_timeout 3 cd scripts
my_timeout 3 pwd
my_timeout 3 true  && echo true || echo false
my_timeout 3 false && echo true || echo false
my_timeout 3 sleep 10
my_timeout 3 pwd

avec les sorties:

Timeout(3) running: 3 cd scripts
Timeout(3) ran: cd scripts
Timeout(3) running: 3 pwd
/home/mpapis/projects/rvm/rvm/scripts
Timeout(3) ran: pwd
Timeout(3) running: 3 true
Timeout(3) ran: true
true
Timeout(3) running: 3 false
Timeout(3) ran: false
false
Timeout(3) running: 3 sleep 10
Timeout(3) reached for: sleep 10
Terminated

bien sûr, je suppose qu'il y avait un répertoire appelé scripts

0
mpapis

S'appuyant sur La réponse de @ loup ...

Si vous souhaitez expirer un processus et mettre au silence la sortie du travail temporaire/pid, exécutez:

( (sleep 1 && killall program 2>/dev/null) &) && program --version 

Cela place le processus en arrière-plan dans un sous-shell afin que vous ne voyiez pas la sortie du travail.

0
Cody A. Ray