Java : processbuilder 標準出力 タイムアウト

1つ前の投稿から続けて)

Java(1.8)で外部シェルを実行するというお話、その2。

いったん作成したが、「タイムアウトとかほしいよね」とのことでタイムアウト判定を入れることになった。

単純にタイムアウトだけであれば タイムアウト付きwaitFor() が使えたと思う。
ただ、この関数は標準出力バッファが詰まった時点で、エラー扱いで処理が戻ってきた。

理想のタイムアウトの挙動は以下であった。

  • タイムアウトになるまではずーっと標準出力を受け取っていてほしい。
  • タイムアウト時点で、そこまでの標準出力を全部受け取っていて、最後に「と、ここまでやったけど、ここでタイムアウトしました」ぐらいの結果返却。


というわけで、前回の「標準出力バッファを吸い出しながら処理するコード」を元にして、タイムアウト処理を追加する形で実装。

コードは以下。

ちなみに、タイマーに内部クラスを使用しているのは好みです。
当然、System.currentTimeMillis() と if文でも同様のことが出来ますし、ここでは一つの例ということで。

// cmd[] は とりあえずこんな → [0]:ユーザー用コマンド名称、[1]:処理開始日時、[2]:外部実行のシェル名、[3]~ : 引数情報(可変長)
private List<String> cmdExecute(String[] cmd) throws Exception {

  // タイムアウト用タイマー : 内部クラス
  class CmdTimer extends TimerTask {

    private boolean isTimeout = false;
    private Process p = null;

    public CmdTimer( Process p ) {
      this.p = p;
    }

    public boolean getIsTimeOut() {
      return this.isTimeout;
    }

    @Override
    public void run() {
      this.isTimeout = true;
      try {
        p.waitFor();
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
  }

  Timer timer = new Timer();

  // 標準出力受け取り用の配列。
  List<String> resultStr = new ArrayList<String>();
  resultStr.add("Command start time [" + cmd[1] + "]");
  resultStr.add("========================================");
  resultStr.add("");

  // ■準備 : タイムアウト間隔の格納(例 : 5000ms = 5s)
  long timeout = 600000;  //とりあえず10分

  // ■準備
  ProcessBuilder pb = new ProcessBuilder(Arrays.copyOfRange(cmd, 2, cmd.length)); //配列は、シェル名、引数~ の順序で固定。
  // 標準出力と標準エラー出力を同一視
  pb.redirectErrorStream(true);

  // ■準備2
  Process process = null;
  InputStreamReader isr = null;
  BufferedReader reader = null;
  boolean isTimeOut = false;

  try {

    // プロセススタート
    process = pb.start();

    // タイマー設定
    CmdTimer task = new CmdTimer(process);
    timer.schedule((TimerTask) task, timeout);

    // 標準出力受取用のストリーム取得
    isr = new InputStreamReader(process.getInputStream(), "UTF-8");
    reader = new BufferedReader(isr);

    // 標準出力受取用ループ。
    // ストリーム内で文字列が4Kバイト?を超えるとプロセスが停止するため、定期的にストリームから抜き出しておく。
    // 抜き取り間隔は下のThread.sleepで指定。ここが遅いと、抜き取る前にたくさん貯まるので、値を小さくしたりすること。
    while (true) {
      try {
        Thread.sleep(100);
      } catch (InterruptedException e) {
      }

      // プロセス終了、もしくはタイム・アウトしている場合、受取用ループはお役御免なので終了する。
      if ( !process.isAlive() || task.getIsTimeOut() ) {
        isTimeOut = task.getIsTimeOut();
        break;
      }

      // 標準出力のバッファが貯まるとprocessBuilderが停止するので、抜き出す。
      if (reader.ready()) {
        resultStr.add(reader.readLine());
      }
    }
  } catch (Exception e) {
    // たとえば sh が見つからない時などはここに来る。
    resultStr.add("コマンド処理中にエラーが発生しました。");
    resultStr.add(e.getMessage());

  } finally {

    // タイムアウトかどうか。
    if ( isTimeOut ) {
      // タイムアウトで終わった場合 : 後続の標準出力は無いので、抜ける。
      resultStr.add("コマンドがタイムアウトしました。");
      resultStr.add("詳細はシステム担当者にお問い合わせください。");
      resultStr.add("現在のタイムアウト間隔 : " + timeout + "(ms)");
    } else {
      // タイムアウト以外(正常終了)

      // エラー判定 : リーダーがnullという状況は異常で、上部のcatchを経由してきた状況なのでそのまま返却。
      if ( reader == null ) {
        // ここはtimer.cancel()が必要かも。
        return resultStr;
      }
      // 標準出力結果を取得。
      // ここまでループ内で細かく取得しており、最後に1回抜き取る。
      String tempLine = "";
      while (true) {
        tempLine = reader.readLine();
        if (tempLine == null) {
          break;
        } else {
          resultStr.add(tempLine);
        }
      }

      // 結果用文言の付与 : 0なら正常終了、それ以外は異常終了ってことで。sh次第だけど、いちおう一般的。
      resultStr.add("");
      if ( process.exitValue() == 0 ) {
        // 0 : 正常終了
        resultStr.add("コマンドが正常終了しました。");
      } else {
        // !0 : 異常終了
        resultStr.add("コマンドが異常終了しました。");
      }
    }

    // 各種close
    reader.close();
    isr.close();
  }

  // 返却
  timer.cancel();
  return resultStr;
}

コメント

このブログの人気の投稿

windows10 で nvidia のグラボのcode43現象を解決した

GTX560Ti がおかしい(code 43が出る)(2018年)→解決しました(2019)