RubyのOpen3.popen3を使って外部プログラムを起動すると、Ctrl+C押下時に外部プログラムが終了しちゃう問題
puppeteer-rubyでは、Open3.popen3を使ってChromeを起動しているが、
binding.pryなどで処理を止めて、そこで不用意にCtrl+Cを押下するとChromeが終了して以後使えなくなるという問題が発覚。
原因をたどっていくと、どうもOpen3.popen3自体がそういう動くをするっぽいことがわかった。
at_exit do
puts "-------exit"
end
trap(:INT) do
puts "-------SIGINT"
exit 0
end
sleep 5
require 'open3'
Open3.popen3('ruby sigint_echo.rb') do |stdin, stdout, stderr, thread|
stdin.close
Thread.new(stdout) do |_stdout|
stdout.each do |line|
puts "[stdout]=> #{line}"
end
end
Thread.new(stderr) do |_stderr|
_stderr.each do |line|
puts "[stderr]=> #{line}"
end
end
puts "さあCtrl+Cを押してみよう"
trap(:INT) { puts "10秒まってくれ" }
sleep 10
ensure
stdout.close
stderr.close
end
$ ruby popen3_play.rb
さあCtrl+Cを押してみよう
^C10秒まってくれ
[stdout]=> -------SIGINT
[stdout]=> -------exit
^C10秒まってくれ
^C10秒まってくれ
^C10秒まってくれ
^C10秒まってくれ
^C10秒まってくれ
^C10秒まってくれ
^C10秒まってくれ
^C10秒まってくれ
^C10秒まってくれ
Ferrumはどうか?
irb(main):001:0> require 'ferrum'
=> true
irb(main):002:0> browser = Ferrum::Browser.new
=>
#<Ferrum::Browser:0x00007f979ad338d8
...
irb(main):003:0> browser.go_to("https://google.com")
=> "FF958D70BE6403E27D4E9E175FAD4FCE"
irb(main):004:0>
^C
irb(main):004:0> browser.go_to("https://google.com")
=> "FF958D70BE6403E27D4E9E175FAD4FCE"
irb(main):005:0>
^C
irb(main):005:0> browser.go_to("https://google.com")
=> "FF958D70BE6403E27D4E9E175FAD4FCE"
irb(main):006:0> exit
Ctrl+Cを押下しても、Chromeは生きている!
Ferrumのソースを見ると、open3ではなくspawnを使用しており、そのパラメータに pgroup: true
というものを渡している。
open3でspanに渡しているパラメータを見てみる
こいつが見たい。モンキーパッチを作って見てみる。
module Open3Patch
def popen_run(cmd, opts, child_io, parent_io)
puts "opts=#{opts}"
super
end
end
Open3.singleton_class.prepend(Open3Patch)
$ ruby popen3_play.rb
opts={:in=>#<IO:fd 9>, :out=>#<IO:fd 12>, :err=>#<IO:fd 14>}
さあCtrl+Cを押してみよう
pgroupは指定がない。
んじゃ、試しにpgroupをつけてみると・・・?
module Open3Patch
def popen_run(cmd, opts, child_io, parent_io)
opts[:pgroup] = true
puts "opts=#{opts}"
super
end
end
Open3.singleton_class.prepend(Open3Patch)
$ ruby popen3_play.rb
opts={:in=>#<IO:fd 9>, :out=>#<IO:fd 12>, :err=>#<IO:fd 14>, :pgroup=>true}
さあCtrl+Cを押してみよう
^C10秒まってくれ
^C10秒まってくれ
^C10秒まってくれ
^C10秒まってくれ
[stdout]=> -------exit
^C10秒まってくれ
^C10秒まってくれ
意図したとおりに動いた!!
{ pgroup: true }
指定すればいいだけ
open3の最後のパラメータに よくよくOpen3のソースを見てみたら、
モンキーパッチなんて使わなくても、仕様としてあった。
Open3.popen3('ruby sigint_echo.rb')
↓
Open3.popen3('ruby sigint_echo.rb', pgroup: true)
これで解決!
なぜ解決するのか?
Open3.popen3はデフォルトでは同じプロセスグループに別プロセスを作る。
Ctrl+Cはプロセスグループ内のすべてのプロセスに対して作用するため、popen3で起動したプログラムもSIGINTを受け取ることになる。
呼ばれる側のプログラムで明示的にsetpgid(0, 0) のようにしてプロセスグループを独立にすれば、この問題を避けることができる。
呼ぶ側のプログラムで明示的に指定するオプションが、さっきの pgroup: true
.
spawnのリファレンスに書いてある内容はこうだ。
:pgroup
引数に true or 0 を渡すと新しいプロセスグループを作成し、そこで動きます。整数を渡すと、指定したプロセスグループに属します。 nil を渡すとプロセスグループを変更しません。デフォルトは nil です。
pid, pgidの概念を理解すれば、そういうことか。と納得できる。