Runtime#exec でコマンドが発行できないときの対処

java.lang.Runtimeにハマリました。ということで、めもめも。
結局は、Java1.5で導入されたjava.lang.ProcessBuilderを使えばよかったんですが。。

JavaからLinux(RHEL4)上でコマンドを発行したかったのですが、Runtime#execでメソッドが戻らなくなってしまう現象になりました。
同じコマンドをコンソールにコピペすれば、問題なく実行されます。。。

いろいろ調べてみると、Runtimeを使うには次のポイントがあるようです。

1.コマンドをシェル経由で実行すべし
これは、リダイレクトを使っているコマンドが動作しないときに有効なようです。
たとえば、次のようなコマンドです。

ls -l > dirlist.txt

Runtime#execを次のように呼び出します。

String[] cmd = {"/bin/sh", "-c", "ls -l > dirlist.txt"};
process = runtime.exec(cmd);

2.Runtime#execの結果をProcess#waitForする前にストリームから読み出すべし
これは、java.lang.Procesの実装にてプロセス実行結果をためておくバッファが小さいためにおきてしまう問題らしいです。参考:http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4109888
ということで、次のような感じで対応しました。

public class CommandExecuteService {
      
   public String exec(String[] command) {
      try {
         Runtime r = Runtime.getRuntime();
         Process p = null;
         
         //コマンド実行
         p = r.exec(command);
         
         //標準出力用Stream
         BufferedReader r_std = new BufferedReader(
               new InputStreamReader(p.getInputStream()));
         StringBuffer stdOutput = new StringBuffer();
         String line_std = null;
         while ((line_std = r_std.readLine()) != null) {
            stdOutput.append(line_std + "\n");
         }
         return stdOutput.toString();
      } catch (Exception e) {
         e.printStackTrace();
      }
      return null;
   }
}

が、これを対応しても、メソッドが戻ってこない。。。
重ねて書きますが、同じコマンドをコピペしてコンソールで実行すれば実行できるわけです。


実はそのコマンドは標準エラー出力に、メッセージを出していました。


実行結果が別にエラーというわけではないのですが。どこぞのメーカーさんが作ったメンテナンス用のコマンドだったわけで、そういう作りで泣かされました。。。
以下、このコマンドは「hoge」という名前で呼びますね。
ということで、hogeコマンドの標準エラー出力を標準出力にリダイレクトしたら、問題なく動いたという次第でした。

どういうことかというと、これは駄目で

String[] cmd = {"/bin/sh", "-c", "hoge"};
process = runtime.exec(cmd);

これは成功します。

String[] cmd = {"/bin/sh", "-c", "hoge 2>&1"};
process = runtime.exec(cmd);

これで完成でも良かったのですが、今後のことを考えて、標準エラー出力と標準出力の両方を同時に読み出すために、Threadで読み込むように処理を書き換えたのです。こんな感じにです。

public class CommandExecuteService {
   
   //標準エラー出力の結果を保持します
   private StringBuffer errOutput = new StringBuffer();
   
   public String exec(String[] command) {
      try {
         Runtime r = Runtime.getRuntime();
         Process p = null;
         
         //コマンド実行
         p = r.exec(command);
         
         //標準出力用Stream
         BufferedReader r_std = new BufferedReader(
               new InputStreamReader(p.getInputStream()));
         //標準エラー出力用Stream
         final BufferedReader r_err = new BufferedReader(
               new InputStreamReader(p.getErrorStream()));
         //標準エラー出力をThreadで同時に読み出すようにする
         Thread th_err = new Thread() {
            @Override
            public void run() {
               String line_err = null;
               try {
                  while ((line_err = r_err.readLine()) != null) {
                     errOutput.append(line_err + "\n");
                  }
               } catch (IOException e) {
                  e.printStackTrace();
               }
            }
         };
         //th_err.start();//Thread起動
         th_err.run();
         //標準出力の読み出しは元のThreadで行う
         p.waitFor();
         StringBuffer stdOutput = new StringBuffer();
         String line_std = null;
         while ((line_std = r_std.readLine()) != null) {
            stdOutput.append(line_std + "\n");
         }
         //標準エラー出力のThread終了を待ち合わせる
         th_err.join();
         return stdOutput.toString() + errOutput.toString();
      } catch (Exception e) {
         e.printStackTrace();
      }
      return null;
   }
}

が、もっといい方法が、さらにあとでわかりました。。。
それはJava1.5から追加されたらしいjava.lang.ProcessBuilderを使う方法でした。これなら、標準エラー出力をマージして出力できてしまいます。うわ。こんなに簡単にかけるとは。orz

public class CommandExecuteUtil {
   
   public static String execute(String[] command) throws Exception {
      ProcessBuilder b = new ProcessBuilder(command);
      //標準エラー出力をマージして出力する
      b.redirectErrorStream(true);
      Process p = b.start();
      BufferedReader reader = new BufferedReader(
            new InputStreamReader(p.getInputStream()));
      StringBuffer ret = new StringBuffer();
      String line = null;
      //標準エラー出力が標準出力にマージして出力されるので、標準出力だけ読み出せばいい
      while ((line = reader.readLine()) != null) {
         ret.append(line + "\n");
      }
      return ret.toString();
   }
}


Java1.5からは、Runtimeの実装は内部的にはProcessBuilderを利用するそうで、実際にソースを見るとそうなっていました。
ということで、今の時代RuntimeじゃなくてProcessBuilderを使うんですねぇ。。。