Rubyから外部コマンドを起動して、一定時間が経過しても終了しなかったらkillする、という処理をしようと思って次のようなコードを書いてみた(実際にはSTDOUT, STDERRを取るなどもっと複雑)。
#!/bin/sh
# Rubyから呼び出される外部コマンド
echo "do something"
sleep 30
echo "done something"
# 外部コマンドを実行してタイムアウトしたらkillする
require "timeout"
cmd = "/tmp/heavy.sh"
pid = spawn(cmd)
thr = Process.detach(pid)
begin
Timeout.timeout(3) do
thr.join
end
rescue Timeout::Error
puts "execution expired"
Process.kill(:TERM, pid)
end
status = thr.value
p status
で、早速実行してみると、一見うまく動いているように見えるんだけど、実際にはheavy.shの中で実行しているコマンド(ここではsleep 30)は生き残っている。
実行中
$ ps xjf
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
1 1599 1599 1599 ? -1 Ss 500 6:43 tmux
1599 7765 7765 7765 pts/6 9309 Ss 500 0:00 \_ /usr/local/bin/zsh
7765 9309 9309 7765 pts/6 9309 Sl+ 500 0:00 \_ ruby run_command.rb
9309 9311 9309 7765 pts/6 9309 S+ 500 0:00 \_ /bin/sh /tmp/heavy.sh
9311 9315 9309 7765 pts/6 9309 S+ 500 0:00 \_ sleep 30
killされた後(heavy.shは終了したけどsleep 30は生きてる)
$ ps xjf
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
1 9315 9309 7765 pts/6 7765 S 500 0:00 sleep 30
これはどういうことかというと、UNIX系のOSでは親プロセスが死んで孤児になったプロセスはinitの養子になることが決まっているから(sleep 30のPPIDが1=initになっている)。
道連れになって親と一緒に死んだりはしない。
で、子だけ生き残って処理が続行されるのは都合が悪いという場合は、次のようなアプローチが考えられる。
- killしようとしているプロセスの子プロセスを列挙して全部killする
- 新しいプロセスグループにしてまとめてkillする
幸いにもRubyのspawnには:pgroupというプロセスグループを指定するオプションがあるので、これを指定して新しいプロセスグループで実行させることができる。
そしてProcess.killのpidに負数を指定すると、そのIDのプロセスグループ全体に対してシグナルを投げることができる(ちなみにkillコマンドも同じ仕様)。
通常、プロセスグループIDはグループリーダー、つまりそのグループの最初のプロセスIDになるので、ここではsapwnで返ってきたpidをマイナスにした値(pid=1234なら-1234)を使えばよいことになる。
というわけで、先のRubyスクリプトをこんな感じに修正することで、タイムアウトしたら外部コマンドとそこからさらに実行されたコマンド全てをkillすることができましたとさ。めでたしめでたし。
require "timeout"
cmd = "/tmp/heavy.sh"
pid = spawn(cmd, :pgroup => true) # :pgroup => trueを追加
thr = Process.detach(pid)
begin
Timeout.timeout(3) do
thr.join
end
rescue Timeout::Error
puts "execution expired"
Process.kill(:TERM, -pid) # -pidに変更
end
status = thr.value
p status
参考
0 件のコメント:
コメントを投稿