执行带有超时的shell函数


Answers:


64

timeout是一个命令-因此它正在bash shell的子进程中执行。因此,它无权访问在当前shell中定义的函数。

timeout给定的命令是作为超时的子进程执行的-外壳程序的子进程。

您可能会感到困惑,因为echo它既是内置的shell又是单独的命令。

您可以做的是将函数放入其自己的脚本文件中,将其chmod可执行,然后使用执行timeout

或者,在子外壳中执行功能,并在原始进程中执行分叉-监视进度,如果花费太长时间则终止子进程。


谢谢您的解决方案!但是,由于我想添加超时作为现有脚本的附加选项,因此仅拥有用于超时功能的文件是非常不便的。这是唯一的解决方案吗?
speendo 2012年

6
@speendo认为timeout通过向进程发送信号来杀死进程是可行的,这是您只能对进程执行的操作。因此,无论您超时运行什么,都必须是它自己的过程。
道格拉斯·里德

2
@speendo另请注意,bash(AFAIK)是单线程的,因此如果线程正在执行您的函数,超时功能怎么办?
道格拉斯·里德

62

正如道格拉斯·里德(Douglas Leeder)所说,您需要一个单独的超时信号通知流程。通过将功能导出到子shell并手动运行子shell的解决方法。

export -f echoFooBar
timeout 10s bash -c echoFooBar

28

有一个内联的替代方案,它还会启动bash shell的子进程:


timeout 10s bash <<EOT
function echoFooBar {
  echo foo
}

echoFooBar
sleep 20
EOT


在这里,Document子流程不知道父流程功能(即“找不到命令”错误),因此请确保export -f parent_func(或set -o allexport针对所有功能)在父Shell流程中。
Noam Manos

10

您可以创建一个函数,该函数不仅可以执行超时操作,还可以执行其他功能:

function run_cmd { 
    cmd="$1"; timeout="$2";
    grep -qP '^\d+$' <<< $timeout || timeout=10

    ( 
        eval "$cmd" &
        child=$!
        trap -- "" SIGTERM 
        (       
                sleep $timeout
                kill $child 2> /dev/null 
        ) &     
        wait $child
    )
}

并可以如下运行:

run_cmd "echoFooBar" 10

注意:该解决方案来自于我的问题之一: 优雅的解决方案,用于为bash命令和功能实现超时


难道不应该杀死最里面的子壳wait $child吗?它没有做任何有害的事情(除了等待),但即使孩子已经完成,它仍然在计数
phil294 '17

这非常有用。我个人觉得在脚本中可读性更高,因为它对最近的子进程有超时,而不是执行eval $ cmd。所以对我来说,它看起来像这样: timeout_child () { trap -- "" SIGTERM; child=$!; timeout=$1; ( sleep $timeout; kill $child; ) & wait $child; } 和用法: ( while true; do echo -n .; sleep 0.1; done) & timeout_child 2
TauPan

6

如果您只想将超时作为整个现有脚本的附加选项添加,则可以使其对timeout-option进行测试,然后使其在没有该选项的情况下以递归方式对其进行调用。

example.sh:

#!/bin/bash
if [ "$1" == "-t" ]; then
  timeout 1m $0 $2
else
  #the original script
  echo $1
  sleep 2m
  echo YAWN...
fi

运行此脚本而不会超时:

$./example.sh -other_option # -other_option
                            # YAWN...

一分钟超时运行它:

$./example.sh -t -other_option # -other_option

4
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 

1

此函数仅使用内置函数

  • 也许考虑根据您的需要逃避“ $ *”而不是直接运行$ @

  • 它使用在第一个arg之后指定的命令字符串(它是超时值)启动作业,并监视作业pid

  • 它每1秒检查一次,bash支持的超时降低到0.01,因此可以进行调整

  • 另外,如果您的脚本需要stdin,read则应依赖专用的fd(exec {tofd}<> <(:)

  • 您可能还需要调整击杀信号(循环内的一个),这是默认-15,你可能想-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
}

1

将我对Tiago Lopo的回答的评论更易读:

我认为在最近的子shell上加上超时更为容易理解,这样我们就不需要评估一个字符串,整个脚本可以由您喜欢的编辑器突出显示为shell。我只是简单地将命令放在subshel​​leval生成了shell函数之后(已通过zsh测试,但应该与bash一起使用):

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

用法示例:

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

这样,它也可以与Shell函数一起使用(如果它在后台运行):

 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; )

1
我真的很喜欢这种方法,但是如果我在脚本中多次使用此方法,则只能在第一次使用。@Tiago Lopo的解决方案可以多次使用。
CristianCantoro

1

我对@Tiago Lopo的答案做了些微修改,可以处理带有多个参数的命令。我还测试了TauPan的解决方案,但是如果您在脚本中多次使用它,则该方法不起作用,而Tiago却可以。

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"
  )
}

这是一个功能全面的脚本,可用于测试上述功能:

$ ./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].

例如,您可以像下面这样启动:

$ ./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

-1

这支班轮将在10秒后退出您的Bash会话

$ TMOUT=10 && echo "foo bar"

1
这完全在10秒后退出了父bash会话,这根本不是OP所要求的
danielpops
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.