[ruby-list:49598] IO.popen に不具合?

From: 尾川敏也 <ogw@...>
Date: 2013-09-24 13:11:26 UTC
List: ruby-list #49598
たびたびお騒がせします。尾川と申します。

ruby-2.0.0-p247 を使っているのですが、IO.popen がどうも挙動不審のよ
うに思えます。

IO.popen(cmndline, "w+") で外部コマンドの標準入出力をパイプにつなげ
た際、外部コマンドの出力のパイプへの書き込みが途中で切れてしまい、
それでも Ruby は知らんぷりして先に進む、ということが「たまに」起き
ているようです。

あれこれ試すなかで、IO.popen を使わずに Open3.pipeline_rw を使うよ
うにしてみたら、このような不具合は(今のところ)無くなりました。

こういうものなのでしょうか?それともひょっとしてバグなのでしょうか?
何となく釈然としないので、報告させて頂く次第です。


以下はもう少し詳細です。

環境は以下のとおりです。

Ruby は、32bit Windows XP 上の MS VisualStudio 2008 で自分でビルド
した ruby-2.0.0-p247 を Windows 7 (32bit) で使っています。

ビルドは win32/README.ja の説明にそって

    win32\configure, nmake, nmake test, nmake install

しただけです。拡張ライブラリの類は何も入れていません。

問題が起きたのは、以下のようなスクリプトです。

#--------------------------------------------------
class String
  def |(cmdline)
    IO.popen(cmdline, "w+") do |pipe|
      Thread.fork {
        pipe.write self
        pipe.close_write
      }
      pipe.read
    end
  end
end

result = "".|("myprog1 datafile.txt").|("myprog2").|("myprog3")

File.open("result.txt", "w") do |f|
  f.write result
end
#--------------------------------------------------

これで、datafile.txt として相応のサイズ(例えば、1 行 400 byte 程度
で 3000 行程度、サイズにして 1MB 程度)のファイルを指定した場合に問
題が起きました。

同じ入力ファイルで同じ事を繰り返し実行してみると、最後までうまく行
くこともありますが、数回〜十数回に1回程度、myprog2 か myprog3 が、
本来受け取るはずの行数のデータを受け取れなくてエラーになります。

エラーで終了するのは外部コマンドですが、myprog2 と myprog3 のどちら
がエラーになるかは一定していません。

また、外部コマンドの出しているメッセージを見ると、読み取り損ねたデー
タのサイズはエラーが発生するたびに毎回微妙に異なります。

ただ、大体、3000 行のうちの 2800 行前後読んだところでエラーになるこ
とが多いような気配です。

という訳で、再現性の無い不審な挙動を示します。

何とかして、せめてメインテナーの方に問題の発生を再現して頂けるよう
なサンプルを用意しようとしたのですが、以下のような事が判っただけで、
よいサンプルは作れず、断念しました。

(1) 入力データが小さい(例えば 10 kB)ときには問題は起きない。

(2) 外部コマンドが単純 (cat 相当の即席の C プログラムとか) な場合に
    は、例えば 20MB 程度の大きなデータを渡しても問題は起きない。

以上が問題の様子です。

それで、苦し紛れに IO.popen の替わりに  Open3.pipeline_rw を使って
以下のように書き換えてみたら、問題無く動くようになりました。

IO.popen では問題が起きたデータで 40 回ほど繰り返し実行してみても問
題は起きませんでしたので、多分大丈夫なのだろうと思います。

#----------------------------------------------------------
require "open3"
class String
  def |(cmdline)
    Open3.pipeline_rw(cmdline) do |stdin, stdout, wait_thrs|
      Thread.fork {
        stdin.write self
        stdin.close
      }
      stdout.read 
    end
  end
end
#----------------------------------------------------------

以上のような次第で、Open3.pipeline_rw では問題無く動くのに、IO.popen
では動いたり動かなかったりというのが、どうにも釈然とせず、ひょっと
して IO.popen にバグでもあるのではないかと思った次第です。

以上、長文で失礼しました。


P.S.

もし、この先 Open3.pipeline_rw でも不具合が起きるような事があったら、
次は以下のように書き換えてしのごうかと思っています。

20 年前の 16bit の MS-DOS のパイプみたいで、スマートな Ruby らしく
なくて不細工ですが、極めて安定して動くようです(笑)。

#----------------------------------------------------------
class String
  def |(cmdline)
    tmp_i = "tmp_#{$$}_i_fake_pipe.txt"
    tmp_o = "tmp_#{$$}_o_fake_pipe.txt"
    File.open(tmp_i, "w") { |f| f.write self }
    system(cmdline + " < " + tmp_i + " > " + tmp_o)
    File.read(tmp_o)
  ensure
    File.delete tmp_i, tmp_o
  end
end
#----------------------------------------------------------

-- 
尾川敏也 ogw@shizuokanet.ne.jp
http://www6.shizuokanet.ne.jp/ogw/

In This Thread