シェルスクリプトでのタイムアウト


53

私が持っているシェルスクリプト標準入力からの読み込みを。まれに、入力の準備ができている人がいないため、スクリプトがタイムアウトする必要があります。タイムアウトの場合、スクリプトは何らかのクリーンアップコードを実行する必要があります。それを行う最良の方法は何ですか?

このスクリプトは、Cコンパイラを使用しない20世紀のUNIXシステムやbusyboxを実行する組み込みデバイスなど、非常に移植性高い必要があります。そのため、Perl、bash、コンパイル済み言語、およびPOSIX.2全体にさえ依存できません。特に、$PPIDread -t、完全にPOSIX準拠のトラップは使用できません。一時ファイルへの書き込みも除外されます。すべてのファイルシステムが読み取り専用でマウントされている場合でも、スクリプトが実行される場合があります。

物事をさらに難しくするために、タイムアウトにならないときにスクリプトを適度に高速にすることも必要です。特に、forkとexecが特に低いWindows(主にCygwin)でスクリプトを使用するため、使用を最小限に抑えたいと思います。

一言で言えば、私は持っています

trap cleanup 1 2 3 15
foo=`cat`

タイムアウトを追加したいです。私は置き換えることはできませんcatread内蔵しています。タイムアウトの場合、cleanup関数を実行したいです。


背景:このスクリプトは、いくつかの8ビット文字を出力し、前後のカーソル位置を比較することにより、端末のエンコーディングを推測しています。スクリプトの最初は、stdoutがサポートされている端末に接続されていることをテストしますが、環境が嘘をついている場合があります(たとえば、で呼び出されてもplink設定されますTERM=xtermTERM=dumb)。スクリプトの関連部分は次のようになります。

text='Éé'  # UTF-8; shows up as Ãé on a latin1 terminal
csi='␛['; dsr_cpr="${csi}6n"; dsr_ok="${csi}5n"  # ␛ is an escape character
stty_save=`stty -g`
cleanup () { stty "$stty_save"; }
trap 'cleanup; exit 120' 0 1 2 3 15     # cleanup code
stty eol 0 eof n -echo                # Input will end with `0n`
# echo-n is a function that outputs its argument without a newline
echo-n "$dsr_cpr$dsr_ok"              # Ask the terminal to report the cursor position
initial_report=`tr -dc \;0123456789`  # Expect ␛[42;10R␛[0n for y=42,x=10
echo-n "$text$dsr_cpr$dsr_ok"
final_report=`tr -dc \;0123456789`
cleanup
# Compute and return initial_x - final_x

tr2秒後に入力を読み取らなかった場合にスクリプトが変更され、スクリプトがcleanup機能を実行するようにスクリプトを変更するにはどうすればよいですか?


2
シェルスクリプトのタイムアウトを導入する方法も参照してください移植性の低いソリューションの場合
ジル「SO-悪であるのをやめる」

回答:


33

これはどうですか:

foo=`{ { cat 1>&3; kill 0; } | { sleep 2; kill 0; } } 3>&1`

つまり、出力を生成するコマンドを実行sleepし、同じプロセスグループで、それらだけのプロセスグループを実行します。どのコマンドが最初に返されても、プロセスグループ全体が強制終了されます。

誰もが疑問に思うでしょう:はい、パイプは使用されていません。リダイレクトを使用してバイパスされます。唯一の目的は、シェルに同じプロセスグループ内の2つのプロセスを実行させることです。


Gillesが彼のコメントで指摘したように、これはシェルスクリプトでは機能しません。スクリプトプロセスが2つのサブプロセスとともに終了するためです。

別のプロセスグループでコマンドを強制的に実行する1つの方法¹は、新しい対話型シェルを開始することです。

#!/bin/sh
foo=`sh -ic '{ cat 1>&3; kill 0; } | { sleep 2; kill 0; }' 3>&1 2>/dev/null`
[ -n "$foo" ] && echo got: "$foo" || echo timeouted

ただし、これには注意事項があります(たとえば、stdinがttyではない場合)。stderrリダイレクションは、対話型シェルが終了したときに「Terminated」メッセージを取り除くためにあります。

でテストされzshbashそしてdash。しかし、オールディーズはどうですか?

B98は、Mac OS XでGNU bash 3.2.57を使用して、またはLinuxをダッシュ​​で使用して、次の変更を提案しています。

foo=`sh -ic 'exec 3>&1 2>/dev/null; { cat 1>&3; kill 0; } | { sleep 2; kill 0; }'`


1. setsid非標準と思われるもの以外。


1
私はこれがとても好きで、プロセスグループをそのように使用することを考えていませんでした。それは非常にシンプルで、競合状態はありません。移植性についてもう少しテストする必要がありますが、これは勝者のようです。
ジル 'SO-悪であるのをやめる'

残念ながら、これをスクリプトに入れるとすぐに失敗します。コマンド置換のパイプラインは独自のプロセスグループで実行されずkill 0、スクリプトの呼び出し元も強制終了します。パイプラインを独自のプロセスグループに強制するポータブルな方法はありますか?
ジル 'SO-悪であるのをやめる'

@Gilles:いいね!今のところsetprgp()なしsetsidに方法を見つけることができませんでした:-(
ステファンギメネス

1
私はこのトリックが大好きなので、賞金を授与しています。Linuxで動作するようです。他のシステムでテストする時間はまだありません。
ジル 'SO-悪であるのをやめる'

2
@Zac:最初のプロセスのみがstdinにアクセスできるため、元の場合の順序を逆にすることはできません。
ステファンギメネス

6
me=$$
(sleep 2; kill $me >/dev/null 2>&1) & nuker=$!
# do whatever
kill $nuker >/dev/null 2>&1

すでに15(の数値バージョンでありSIGTERMkill特に指示がない限り送信される)をトラップしているので、すでに準備ができているはずです。とはいえ、POSIX以前を見る場合、シェル関数も存在しない可能性があることに注意してください(これらはSystem Vのシェルから来たものです)。


それほど単純ではありません。シグナルをトラップすると、多くのシェル(dash、bash、pdksh、zsh…おそらくATT kshを除くすべて)はcat、終了を待っている間、それを無視します。私はすでに少し実験をしましたが、今のところ満足できるものは見つかりませんでした。
ジル「SO-悪であるのをやめる」

移植性について:ありがたいことに、どこにでも機能があります。よくわからない$!、私はめったに使用しないマシンのいくつかはジョブ制御を持たない、$!普遍的に利用できると思う?
ジル「SO-悪であるのをやめる」

@Gilles:ええ、ええ。私bashはかつての虐待に関与したいくつかの本当に恐ろしく特異的なことを思い出し-o monitorます。それを書いたとき、私は本当に古代のシェルの観点から考えていました(v7で機能しました)。そうは言っても、他の2つのことのいずれかを行うことができると思います:(1)バックグラウンド "whatever"およびwait $!、または(2)送信SIGCLD/ SIGCHLD... III / V、後者のBSD、およびV7にはどちらもありませんでした)。
ギコサウルス

@Gilles:$!少なくとも、V7に戻り、確かに以前からsh長い時間のために、実際にジョブ制御について何を(知っていた様/bin/shジョブ制御をしませんでしたBSDのを、あなたが持っていた実行するためにcshそれを得るために-しかし$!だったが)。
ギーコサウルス

私は「何であれ」を背景にすることはできません、その出力が必要です。についての情報をありがとう$!
ジル 'SO-悪である停止

4

バージョン7.0のcoretuilsにはタイムアウトコマンドが含まれていますが、それがないいくつかの環境について言及しました。幸いなことに、pixelbeat.orgにはタイムアウトスクリプトが記述されていshます。

何度か使用したことがありますが、非常にうまく機能します。

http://www.pixelbeat.org/scripts/timeout注:以下のスクリプトは、pixelbeat.orgのものからわずかに変更されています。この回答の下のコメントを参照してください。)

#!/bin/sh

# Execute a command with a timeout

# Author:
#    http://www.pixelbeat.org/
# Notes:
#    Note there is a timeout command packaged with coreutils since v7.0
#    If the timeout occurs the exit status is 124.
#    There is an asynchronous (and buggy) equivalent of this
#    script packaged with bash (under /usr/share/doc/ in my distro),
#    which I only noticed after writing this.
#    I noticed later again that there is a C equivalent of this packaged
#    with satan by Wietse Venema, and copied to forensics by Dan Farmer.
# Changes:
#    V1.0, Nov  3 2006, Initial release
#    V1.1, Nov 20 2007, Brad Greenlee <brad@footle.org>
#                       Make more portable by using the 'CHLD'
#                       signal spec rather than 17.
#    V1.3, Oct 29 2009, Ján Sáreník <jasan@x31.com>
#                       Even though this runs under dash,ksh etc.
#                       it doesn't actually timeout. So enforce bash for now.
#                       Also change exit on timeout from 128 to 124
#                       to match coreutils.
#    V2.0, Oct 30 2009, Ján Sáreník <jasan@x31.com>
#                       Rewritten to cover compatibility with other
#                       Bourne shell implementations (pdksh, dash)

if [ "$#" -lt "2" ]; then
    echo "Usage:   `basename $0` timeout_in_seconds command" >&2
    echo "Example: `basename $0` 2 sleep 3 || echo timeout" >&2
    exit 1
fi

cleanup()
{
    trap - ALRM               #reset handler to default
    kill -ALRM $a 2>/dev/null #stop timer subshell if running
    kill $! 2>/dev/null &&    #kill last job
      exit 124                #exit with 124 if it was running
}

watchit()
{
    trap "cleanup" ALRM
    sleep $1& wait
    kill -ALRM $$
}

watchit $1& a=$!         #start the timeout
shift                    #first param was timeout for sleep
trap "cleanup" ALRM INT  #cleanup after timeout
"$@" < /dev/tty & wait $!; RET=$?    #start the job wait for it and save its return value
kill -ALRM $a            #send ALRM signal to watchit
wait $a                  #wait for watchit to finish cleanup
exit $RET                #return the value

これでは、コマンドの出力を取得できないようです。他の回答では、Gillesによる「 "whatever"を背景にできない」コメントを参照してください。
ステファンギメネス

ああ、面白い。/ dev / stdinをコマンドにリダイレクトするようにスクリプトを変更しました(したがって、pixelbeatのスクリプトと一致しなくなります)。私のテストではうまくいくようです。
バハマ

これは、(奇妙なことに)bashを除いて、コマンドを標準入力から読み取るためには機能しません。</dev/stdinノーオペレーションです。</dev/ttyターミナルからの読み取りが可能になります。これは、私のユースケースには十分です。
ジル 'SO-悪であるのをやめる'

@Giles:クール、私はその更新を行います。
バハマ

これはかなりの労力なしでは機能しません。コマンドの出力を取得する必要があり、コマンドがバックグラウンドにある場合は実行できません。
ジル 'SO-悪であるのをやめる'

3

NCの(ab)use NCはどうですか

好む;

   $ nc -l 0 2345 | cat &  # output come from here
   $ nc -w 5 0 2345   # input come from here and times out after 5 sec

または、1つのコマンドラインにロールアップしました。

   $ foo=`nc -l 0 2222 | nc -w 5 0 2222`

最後のバージョンは、Linuxシステムでテストすると実際に動作しますが、どのシステムでも動作するはずです。そうでない場合は、出力リダイレクトのバリエーションが移植性を解決する可能性があります。ここでの利点は、バックグラウンドプロセスが関与しないことです。


移植性が十分ではありません。私は持っていないncいくつかの古いUnixのツゲの上に、また多くの組み込みLinux上で。
ジル 'SO-悪であるのをやめる' 14

0

独自のプロセスグループでパイプラインを実行する別の方法はsh -c '....'scriptコマンドを使用して擬似ターミナルで実行することです(これは暗黙的にsetsid関数を適用します)。

#!/bin/sh
stty -echo -onlcr
# GNU script
foo=`script -q -c 'sh -c "{ cat 1>&3; kill 0; } | { sleep 5; kill 0; }" 3>&1 2>/dev/null' /dev/null`
# FreeBSD script
#foo=`script -q /dev/null sh -c '{ cat 1>&3; kill 0; } | { sleep 5; kill 0; }' 3>&1 2>/dev/null`
stty echo onlcr
echo "foo: $foo"


# alternative without: stty -echo -onlcr
# cr=`printf '\r'`
# foo=`script -q -c 'sh -c "{ { cat 1>&3; kill 0; } | { sleep 5; kill 0; } } 3>&1 2>/dev/null"' /dev/null | sed -e "s/${cr}$//" -ne 'p;N'`  # GNU
# foo=`script -q /dev/null sh -c '{ { cat 1>&3; kill 0; } | { sleep 5; kill 0; } } 3>&1 2>/dev/null' | sed -e "s/${cr}$//" -ne 'p;N'`  # FreeBSD
# echo "foo: $foo"

移植性が十分ではありません。私は持っていないscriptいくつかの古いUnixのツゲの上に、また多くの組み込みLinux上で。
ジル 'SO-悪であるのをやめる' 14

0

https://unix.stackexchange.com/a/18711の答えはとてもいいです。

同様の結果を達成したかったのですが、既存のシェル関数を呼び出したいため、シェルを再度明示的に呼び出す必要はありませんでした。

bashを使用すると、次のことが可能です。

eval 'set -m ; ( ... ) ; '"$(set +o)"

だから私はすでにシェル関数を持っていると仮定しますf

f() { date ; kill 0 ; }

echo Before
eval 'set -m ; ( f ) ; '"$(set +o)"
echo After

これを実行すると、私は見る:

$ sh /tmp/foo.sh
Before
Mon 14 Mar 2016 17:22:41 PDT
/tmp/foo.sh: line 4: 17763 Terminated: 15          ( f )
After

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.