Closed6

RubyのOpen3.popen3を使って外部プログラムを起動すると、Ctrl+C押下時に外部プログラムが終了しちゃう問題

Yusuke IwakiYusuke Iwaki

puppeteer-rubyでは、Open3.popen3を使ってChromeを起動しているが、
binding.pryなどで処理を止めて、そこで不用意にCtrl+Cを押下するとChromeが終了して以後使えなくなるという問題が発覚。

https://github.com/YusukeIwaki/puppeteer-ruby/issues/194

原因をたどっていくと、どうもOpen3.popen3自体がそういう動くをするっぽいことがわかった。

sigint_echo.rb
at_exit do
  puts "-------exit"
end
trap(:INT) do
  puts "-------SIGINT"
  exit 0
end
sleep 5
popen_play.rb
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秒まってくれ
Yusuke IwakiYusuke Iwaki

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は生きている!

Yusuke IwakiYusuke Iwaki

Ferrumのソースを見ると、open3ではなくspawnを使用しており、そのパラメータに pgroup: true というものを渡している。

Yusuke IwakiYusuke Iwaki

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秒まってくれ

意図したとおりに動いた!!

Yusuke IwakiYusuke Iwaki

open3の最後のパラメータに { pgroup: true } 指定すればいいだけ

よくよくOpen3のソースを見てみたら、

モンキーパッチなんて使わなくても、仕様としてあった。

Open3.popen3('ruby sigint_echo.rb')
 ↓
Open3.popen3('ruby sigint_echo.rb', pgroup: true)

これで解決!

Yusuke IwakiYusuke Iwaki

なぜ解決するのか?

Open3.popen3はデフォルトでは同じプロセスグループに別プロセスを作る。
Ctrl+Cはプロセスグループ内のすべてのプロセスに対して作用するため、popen3で起動したプログラムもSIGINTを受け取ることになる。

呼ばれる側のプログラムで明示的にsetpgid(0, 0) のようにしてプロセスグループを独立にすれば、この問題を避けることができる。

呼ぶ側のプログラムで明示的に指定するオプションが、さっきの pgroup: true.

spawnのリファレンスに書いてある内容はこうだ。

:pgroup
引数に true or 0 を渡すと新しいプロセスグループを作成し、そこで動きます。整数を渡すと、指定したプロセスグループに属します。 nil を渡すとプロセスグループを変更しません。デフォルトは nil です。

https://docs.ruby-lang.org/ja/latest/method/Kernel/m/spawn.html

pid, pgidの概念を理解すれば、そういうことか。と納得できる。

このスクラップは2022/02/02にクローズされました