ShellScript
Bash
Linux

re-exec法(仮)によるスクリプトのタイムアウト実現

はじめに

何かの処理が完了しない場合に一定時間で打ち切りたい場合、timeoutというコマンドがあります。そのため、あるスクリプト自体をtimeout経由で実行すれば時間制限が設けられるのですが、ではスクリプト自身で完結させる場合どうするか。
ちょっとした方法ですが、re-exec法(仮) (適当に名前をつけただけ)という方法があるため、紹介します。

無難な方法

あるスクリプトmain.shがあり、これが一定時間(例えば10秒)で終わらない場合は打ち切りたいとします。それを実現したければ、次のように別スクリプトtmout.shを設ければ実現できます。

tmout.sh
#!/bin/bash

timeout 10 ./main.sh

しかし、それではタイムアウト処理のためだけにファイルを1つ増やすことになります。いえ、面倒でなければそれでも良いのですが。

re-exec法(仮)

そんな時にどうするか。メインの処理もタイムアウト処理も1つのファイルで完結させることを考えます。
とは言え、timeoutコマンド経由でスクリプトを実行するという部分は変えられません。そこで自分自身を再実行することでまとめてしまいます。これをre-exec法(仮)としておきます。
※別にこういう名前が実際流通している訳ではなく、名前がないと説明し辛いので、仮に名付けただけです。

ただそうすると、スクリプト側としては再実行が必要な状況なのか、それとも再実行されている状況なのかが区別できませんから、そこを何とかするために環境変数の定義の有無で判定させます。

典型的な実装としては次のようになります。

#!/bin/bash

TIMELIMIT=10
[ -z "$MY_SECOND_EXECUTION" ] && MY_SECOND_EXECUTION=1 exec timeout $TIMELIMIT bash $0 "$@"

# 以下メインの処理
(略)

ここではMY_SECOND_EXECUTIONとしていますが、この環境変数が定義されていなければ、初回実行と判断してtimeout経由で自身を再実行 ( その際環境変数を定義 )、定義されていれば再実行と判断して、メインの処理へ進む、ということです。

サンプル

以下、簡単なサンプルを紹介します。
countup.shというスクリプトで、引数に指定された回数のカウントアップを1秒おきで行うものですが、同時にこれに10秒の時間制限をかけます。

countup.sh
#!/bin/bash

TIMELIMIT=10
[ -z "$MY_SECOND_EXECUTION" ] && MY_SECOND_EXECUTION=1 exec timeout $TIMELIMIT bash $0 "$@"

# メイン
for ((i=0;i<"$1";i++)); do
  echo "** $i **"
  sleep 1
done
echo "** done **"

で、これを実行してみた結果です。カウント5回を指定した場合は時間内なので完走しますが、100回を指定すると10秒の制限に引っかかって打ち切られます。( 時間切れを示す timeout の終了ステータス124が返ってきます )

実行結果
$ ./countup.sh 5
** 0 **
** 1 **
** 2 **
** 3 **
** 4 **
** done **
$ echo $?
0
$ ./countup.sh 100
** 0 **
** 1 **
** 2 **
** 3 **
** 4 **
** 5 **
** 6 **
** 7 **
** 8 **
** 9 **
$ echo $?
124

おまけ: re-exec法(仮)の応用

今回はtimeoutを間に挟む目的で使用しましたが、他にもコマンドを間に挟んで実行させたいような場面であれば同じ手法が使えると思います。

例えば、最近はもう流行らない手法ではありますが、デーモン起動するプログラムです。メジャーなデーモンプログラム ( sshdやhttpdなど ) は、自前でデーモン化する機能を備えているのですが、そうでないものでもsetsidコマンドを経由することでデーモン化することができます。
これとre-exec法(仮)を組み合わせてみます。

という訳で、以下サンプルです。単に指定秒数sleepするだけのスクリプトですが、これをデーモン化してみます。
※本来の意味でのデーモン化はもっと込み入ったことをするのですが、ここでは最低限、標準入出力の切り離し(/dev/nullへの接続)とsetsid実行としています。

dsleep.sh
#!/bin/bash

[ -z "$MY_SECOND_EXECUTION" ] && MY_SECOND_EXECUTION=1 exec setsid bash $0 "$@" </dev/null >/dev/null 2>&1

# メイン
sleep "$1"

実行してみると、即座にコマンド実行したシェルの制御化を離れ、制御端末も切り離して(TTY=?)デーモン化されていることが分かります。

実行結果
$ ./dsleep.sh 1200
$ ps -ef
UID        PID  PPID  C STIME TTY          TIME CMD
(略)
angel        5     4  0 Jun30 pts/0    00:00:09 bash
angel     1502     1  2 22:17 ?        00:00:00 bash ./daemon.sh 1200
angel     1503  1502  0 22:17 ?        00:00:00 sleep 1200
(略)

終わりに

ちょっとした手法ですが、役に立つ場面もあるのではないでしょうか。