在超时的情况下执行shell函数



为什么能工作

timeout 10s echo "foo bar" # foo bar

但这不是

function echoFooBar {
  echo "foo bar"
}
echoFooBar # foo bar
timeout 10s echoFooBar # timeout: failed to run command `echoFooBar': No such file or directory

我该如何让它发挥作用?

正如Douglas Leeder所说,您需要一个单独的超时进程来向其发出信号。通过将函数导出到子shell并手动运行子shell来解决问题。

export -f echoFooBar
timeout 10s bash -c echoFooBar

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

给出的命令timeout作为timeout的子进程执行,timeout是shell的一个子进程。

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

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

或者fork,在子shell中执行函数——在原始流程中,监控进度,如果花费太长时间,就会终止子流程。

还有一个内联替代方案也在启动bash-shell的子流程:


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

您可以创建一个函数,该函数允许您执行与超时相同的操作,但也可以用于其他函数:

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命令和函数超时的优雅解决方案

如果你只想添加timeout作为整个现有脚本的附加选项,你可以让它测试timeout选项,然后让它在没有该选项的情况下自递归地调用它。

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

此函数仅使用内置

  • 也许可以考虑根据您的需求评估"$*",而不是直接运行$@

  • 它使用在第一个参数(即超时值)之后指定的命令字符串启动作业,并监视作业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
}

将我对Tiago Lopo答案的评论转化为更可读的形式:

我认为在最新的子shell上加一个超时更可读,这样我们就不需要评估字符串,整个脚本可以由您最喜欢的编辑器突出显示为shell。我只需在带有eval的子shell派生成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; )

我对@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=$'nt'
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 "$secs33[0Kr"
   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

对TauPan答案的这个小修改增加了一些有用的保护。如果正在等待的子进程在sleep$timeout完成之前已经退出。kill命令试图终止已不存在的进程。这可能是无害的,但不能绝对保证没有重新分配相同的PID。为了避免这种情况,进行了一次快速检查,以测试子PID是否存在,以及其父PID是否是从中派生的shell。试图杀死一个不存在的进程也会产生错误,如果不加以抑制,这些错误很容易填满日志。

我还使用了一个更具攻击性的击杀-9。这是杀死不是在shell命令上而是从文件系统(例如read < named_pipe)阻塞的进程的唯一方法
这样做的结果是kill -9 $child命令将其终止信号异步发送给进程,从而在调用shell中生成一条消息。这可以通过重新定向CCD_ 14来抑制。具有明显的调试后果。

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

这一行将在10s 后退出您的Bash会话

$ TMOUT=10 && echo "foo bar"

相关内容

  • 没有找到相关文章

最新更新