使 bash 中的命令超时,避免不必要的延迟
-
22-08-2019 - |
题
提出了一种 1 行方法来使 bash 命令行中长时间运行的命令超时:
( /path/to/slow command with options ) & sleep 5 ; kill $!
但给定的“长时间运行”命令可能会在超时之前完成。(我们称其为“通常长时间运行但有时很快”的命令,或者 特鲁布斯夫 为了娱乐。)
所以这个漂亮的单行方法有几个问题。首先, sleep
不是有条件的,因此这对序列完成所需的时间设置了一个不希望的下限。考虑 30 秒或 2m 甚至 5m 的睡眠时间,当 特鲁布斯夫 命令在 2 秒内完成——这是非常不受欢迎的。其次, kill
是无条件的,因此该序列将尝试终止未运行的进程并抱怨它。
所以...
有没有办法 使通常长时间运行但有时很快的程序超时(“tlrbsf”)命令
- 有一个 bash 实现(另一个问题已经有 Perl 和 C 答案)
- 将在两者中较早的一个终止: 特鲁布斯夫 程序终止或超时
- 不会杀死不存在/未运行的进程(或者,可选:将不会 抱怨 关于一次糟糕的杀戮)
- 不一定是 1-liner
- 可以在Cygwin或Linux下运行
...并且,为了奖励积分,运行 特鲁布斯夫 前台的命令和后台的任何“睡眠”或额外进程,这样的 stdin/stdout/stderr 特鲁布斯夫 命令可以重定向,就像直接运行一样吗?
如果是这样,请分享您的代码。如果不是,请解释原因。
我花了一段时间尝试破解上述示例,但我已经达到了 bash 技能的极限。
解决方案
我认为这正是您所要求的:
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 <Dmitry.Golovashkin@sas.com>
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 "$@"
其他提示
您可能正在寻找 timeout
coreutils 中的命令。由于它是 coreutils 的一部分,因此从技术上来说它是一个 C 解决方案,但它仍然是 coreutils。 info timeout
更多细节。这是一个例子:
timeout 5 /path/to/slow/command with options
无论 bash 监视器模式如何,该解决方案都有效。您可以使用适当的信号来终止 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
观察者在给定的超时后终止 your_command ;该脚本等待缓慢的任务并终止观察者。注意 wait
不适用于不同 shell 的子进程。
例子:
- your_command 运行超过 2 秒并被终止
your_command 被中断
( 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
- your_command 在超时(20 秒)之前完成
你的命令完成
( 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
就这样:
timeout --signal=SIGINT 10 /path/to/slow command with options
你可以改变 SIGINT
和 10
如你所愿;)
我更喜欢“timelimit”,它至少在 debian 中有一个包。
http://devel.ringlet.net/sysutils/timelimit/
它比 coreutils“超时”好一点,因为它在终止进程时打印一些内容,并且默认情况下它还会在一段时间后发送 SIGKILL。
你完全可以用 bash 4.3
以上:
_timeout() { ( set +b; sleep "$1" & "${@:2}" & wait -n; r=$?; kill -9 `jobs -p`; exit $r; ) }
- 例子:
_timeout 5 longrunning_command args
- 例子:
{ _timeout 5 producer || echo KABOOM $?; } | consumer
- 例子:
producer | { _timeout 5 consumer1; consumer2; }
例子:
{ while date; do sleep .3; done; } | _timeout 5 cat | less
需要 Bash 4.3
wait -n
- 如果命令被杀死,则返回 137,否则返回命令的返回值。
- 适用于管道。(您不需要在这里进入前台!)
- 也可与内部 shell 命令或函数一起使用。
- 在子 shell 中运行,因此没有变量导出到当前 shell,抱歉。
如果您不需要返回代码,这可以变得更简单:
_timeout() { ( set +b; sleep "$1" & "${@:2}" & wait -n; kill -9 `jobs -p`; ) }
笔记:
严格来说,您不需要
;
在; )
, ,但是它使事情更加一致; }
-案件。还有set +b
也许也可以把它放在一边,但安全总比后悔好。除了
--forground
(可能)你可以实现所有变体timeout
支持。--preserve-status
不过有点困难。这是留给读者的练习;)
这个配方可以“自然”地在外壳中使用(就像自然一样) flock fd
):
(
set +b
sleep 20 &
{
YOUR SHELL CODE HERE
} &
wait -n
kill `jobs -p`
)
但是,如上所述,您自然不能以这种方式将环境变量重新导出到封闭的 shell 中。
编辑:
现实世界的例子:暂停 __git_ps1
如果需要太长时间(例如缓慢的 SSHFS 链接):
eval "__orig$(declare -f __git_ps1)" && __git_ps1() { ( git() { _timeout 0.3 /usr/bin/git "$@"; }; _timeout 0.3 __orig__git_ps1 "$@"; ) }
编辑2:错误修正。我注意到 exit 137
不需要并使得 _timeout
同时也不可靠。
编辑3: git
是个顽固分子,所以需要双招才能满意地工作。
编辑4:忘记了一个 _
在第一个 _timeout
以现实世界的 GIT 为例。
另请参阅 http://www.pixelbeat.org/scripts/timeout 其功能已集成到较新的 coreutils 中的脚本
有点老套,但它确实有效。如果您有其他前台进程,则不起作用(请帮我解决此问题!)
sleep TIMEOUT & SPID=${!}; (YOUR COMMAND HERE; kill ${SPID}) & CPID=${!}; fg 1; kill ${CPID}
实际上,我认为你可以扭转它,满足你的“奖金”标准:
(YOUR COMMAND HERE & SPID=${!}; (sleep TIMEOUT; kill ${SPID}) & CPID=${!}; fg 1; kill ${CPID}) < asdf > fdsa
暂停 可能是第一个尝试的方法。如果超时,您可能需要通知或执行其他命令。经过大量的搜索和实验,我想出了这个 巴什 脚本:
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
脚本简单,代码清晰。存到 /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
使运行时间过长的命令超时:
$ run 2 sleep 10
Timed out after 2 seconds
Terminated
$
对于完成以下命令的命令立即结束:
$ run 10 sleep 2
$
使超时 slowcommand
1秒后:
timeout 1 slowcommand || echo "I failed, perhaps due to time out"
如果您已经知道程序的名称(假设 program
)超时后终止(例如 3
秒),我可以提供一个简单但有点肮脏的替代解决方案:
(sleep 3 && killall program) & ./program
如果我使用系统调用来调用基准流程,这将非常有效。
还有 cratimeout
作者:Martin Cracauer(用 C 语言编写,适用于 Unix 和 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'
OS X 尚未使用 bash 4,也没有 /usr/bin/timeout,因此这里有一个无需 home-brew 或 macports 即可在 OS X 上运行的函数,类似于 /usr/bin/timeout (基于 Tino 的回答)。参数验证、帮助、用法和对其他信号的支持是读者的练习。
# 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
) }
我遇到了一个保留 shell 上下文并允许超时的问题,唯一的问题是它将在超时时停止脚本执行 - 但这符合我提出的需求:
#!/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
与输出:
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
当然我假设有一个目录叫 scripts
#! /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 "$@"
我的问题可能有点不同:我通过 ssh 在远程计算机上启动命令,并且希望在命令挂起时终止 shell 和子进程。
我现在使用以下内容:
ssh server '( sleep 60 && kill -9 0 ) 2>/dev/null & my_command; RC=$? ; sleep 1 ; pkill -P $! ; exit $RC'
这样,当超时时,命令会返回 255,或者成功时会返回命令的返回码
请注意,从 ssh 会话终止进程的处理方式与交互式 shell 不同。但您也可以使用 ssh 的 -t 选项来分配伪终端,因此它的作用就像交互式 shell
这是一个不依赖于生成子进程的版本 - 我需要一个嵌入此功能的独立脚本。它还执行分数轮询间隔,因此您可以更快地轮询。超时本来是首选 - 但我被困在旧服务器上
# 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
建立在 @loup的回答...
如果您想要使进程超时并静默终止作业/pid 输出,请运行:
( (sleep 1 && killall program 2>/dev/null) &) && program --version
这会将后台进程放入子 shell 中,这样您就看不到作业输出。
我有一个调用 php 脚本的 cron 作业,有时它会卡在 php 脚本上。这个解决方案对我来说非常完美。
我用:
scripttimeout -t 60 /script.php
在 99% 的情况下,答案是不实现任何超时逻辑。超时逻辑几乎在任何情况下都是一个红色警告信号,表明某些事情 别的 是错误的,应该被修复 反而.
您的进程有时会在 n 秒后挂起或中断吗?然后找出原因并解决它。
顺便说一句,要正确执行 strager 的解决方案,您需要使用 wait "$SPID" 而不是 fg 1,因为在脚本中您没有作业控制(并且尝试打开它是愚蠢的)。此外,fg 1 依赖于这样一个事实:您之前没有在脚本中启动任何其他作业,这是一个错误的假设。
一个非常简单的方法:
# command & sleep 5; pkill -9 -x -f "command"
和 杀戮 (选项 -F)您可以使用参数终止特定命令或指定 -n 以避免终止旧进程。