サブプロセスのタイムアウト

うーん、意図した様に動いてくれません。

昔作った C のプログラムをデバッグしなければならず、検証用に Ruby のテストコードを書いて、そこから呼び出そうとしています。

当初、Ruby/DL を利用して、公開している API を直接呼び出すことを検討していたのですが、DL::PtrData が上手く使えず、ポインタを渡して変更された内容を参照することができなかったため、今回は断念しました。
一応、コマンドベースでも検証できるプログラムなので、API でなくても良いのが救いでした。
それに、良く考えてみると、C プログラム側は、32bit/64bit のそれぞれを提供しているので、それらの両方をテストする場合に DL で API 越しにやろうとすると、Ruby 自体も 32/64 bit の両方を用意しなければなりません (よね?)。そうなるとちょっと面倒なので、コマンドベースに逃げることにしました。

しかし、コマンドベースで検証するためにも実は課題が残っています。
今回のバグでは、副次的な挙動として、コマンドが loop してしまって戻らなくなるという事象も発生しており、そのため、検証時に実行した C プログラムが戻って来ないことがあり得るので、ruby 側で popen して実行した後, select でタイムアウトを設定してやろうと思った訳です。

で、単純に、popen した後で select したのですが、パイプから何も読み込めないときにも select で block されません。

$ irb
irb(main):001:0> p RUBY_VERSION
"1.8.4"
nil
irb(main):002:0> pi = IO.popen("sleep 30 && uname", "r")
#<IO:0x100385a18>
irb(main):003:0> re = IO.select([pi], nil, nil, 5)
[[#<IO:0x100385a18>], [], []]     <--- これがブロックされずにすぐ返ってくる。
irb(main):004:0> p pi.eof?
false
nil
irb(main):005:0> print pi.gets
SunOS                             <--- ここでブロックされる。
nil
irb(main):006:0> pi.close
nil
irb(main):007:0> quit

となって、実際の pipe からの読み取り時にブロックされてしまい、select で待つ意味がないのです。

げ、popen って select で待てないんだっけ? と、C で書いて確認してみたところ、

$ cat wait-proc.c
#ifdef TO_COPILE_AND_USE_THIS_FILE /* Type "sh wait-proc.c"
out=`echo $0|sed 's/\..$//g'`
command="${CC:-cc} -g -xs $0 -o $out && ./$out && rm -f $out; exit"
echo $command
eval $command
*/
#endif

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <limits.h>
#include <sys/select.h>

int main(int argc, char *argv[])
{
  char str[PIPE_BUF];
  char * command = "sleep 5 && uname";
  int result = 0;
  FILE * pi;
  fd_set rdfds;

  struct timeval tv;
  tv.tv_sec = 0;
  tv.tv_usec = 500000;

  printf ("execute command: <%s>\n", command);
  pi = popen (command, "r");
  printf ("waiting."); fflush (stdout);

  while (1) {
    FD_ZERO(&rdfds);
    FD_SET(fileno(pi), &rdfds);

    result = select (fileno(pi) + 1,
                     &rdfds, (fd_set *)NULL, (fd_set *)NULL, &tv);
    if (result < 0)
      perror("select()");
    if (FD_ISSET(fileno(pi), &rdfds))
      break;
    printf ("."); fflush (stdout);
  }

  fread (str, 1, PIPE_BUF, pi);
  fclose (pi);

  printf ("\nresult str: %s\n", str);

  return (EXIT_SUCCESS);
}
/* wait-proc.c ends here. */
$ sh wait-proc.c
cc -g wait-proc.c -o wait-proc && ./wait-proc && rm -f wait-proc; exit
execute command: <sleep 5 && uname>
waiting..........
result str: SunOS

と、手抜きのコードですが、どうやら popen だからといって、select で block されない訳ではなさそうです。そりゃそうですよね……

実は、今回のプラットフォームは SPARC (しかも sparcv9 で 64bit) でして、今回、安定版の 1.8.4 を `-mcpu=v9 -m64' を指定して build しています。それにより、

$ file `type -p ruby`
ruby: ELF 64-ビット MSB 実行可能 SPARCV9 バージョン 1[動的にリンクされています][取り除かれていません]

という具合になってます。
さて、64 bit が問題なのか、そもそも sparc だとダメなのか、安定版の 1.8.4 ではダメなのか……
いいえ、多分、sparcv9 上での 64 bit バイナリの build に失敗している気がします……

気を取り直して、Linux (Debian/Sarge) 上の Ruby で確認してみました。すると、

$ irb
irb(main):001:0> p RUBY_VERSION
"1.8.2"
=> nil
irb(main):002:0> pi = IO.popen("sleep 30 && uname", "r")
=> #<IO:0xb7d69114>
irb(main):003:0> re = IO.select([pi], nil, nil, 5)
=> nil                   <--- ちゃんと select がブロックされ、timeout している。
irb(main):004:0> re = IO.select([pi], nil, nil, 5)
=> nil
irb(main):005:0> re = IO.select([pi], nil, nil, 5)
=> nil
irb(main):006:0> re = IO.select([pi], nil, nil, 5)
=> nil
irb(main):007:0> re = IO.select([pi], nil, nil, 5)
=> [[#<IO:0xb7d69114>], [], []]        <--- ここでブロックが解ける。
irb(main):008:0> p pi.eof?
false
=> nil
irb(main):009:0> print pi.gets
Linux
=> nil
irb(main):010:0> p pi.eof?
true
=> nil
irb(main):011:0> pi.close
=> nil
irb(main):012:0> quit

と、意図した通りの動きをしてくれます。

兎に角、現状の sparcv9 環境だとダメだと言うことは判りました。
しかし、64 bit だからなのか、sparc だとダメなのか、そもそも build に失敗してんじゃないの? といったところまでは切り分けできていません。1.8.4 を `-m32' で build し直すなどの手当てで動くのかもしれませんが、それらを確認している時間はありませんでした。

という訳で最新の安定版 snapshot を 32 bit で build したものに入れ替えてみました。折角なので最新にしてしまおうという腹積もりもあっての選択です。

入れ替えた結果、

$ irb
irb(main):001:0> p RUBY_VERSION
"1.9.0"
=> nil
irb(main):002:0> pi = IO.popen("sleep 5 && uname", "r")
=> #<IO:0x112248>
irb(main):003:0> print "." until IO.select([pi], nil, nil, 1)
....=> nil                       <--- 4回ブロックされている。
irb(main):004:0> p pi.eof?
false
=> nil
irb(main):005:0> print pi.gets
SunOS
=> nil
irb(main):006:0> p pi.eof?
true
=> nil
irb(main):007:0> pi.close
=> nil
irb(main):008:0> quit

と、こちらも意図した通りに動作する様になりました。

こうなると、冒頭に書いた DL::PtrData が上手く使えなかった件も、build し直した今なら何とか行くのかもしれません。時間ができたらやってみようかな。

ともあれ、これでテストに入れます。
しかし、勢い余ってここに書いてしまいましたが、結局、非常に情報量の乏しいエントリとなってしまいました。まあ、いつものことですが。

また、お気付きになった方もいらっしゃるかと思いますが、実は、安定版の snapshot を入れるつもりが、CVS HEAD の snapshot を入れてしまっています。はあ、また入れ直しかなあ……