ProcessBuilder:メインスレッドをブロックせずに、開始されたプロセスのstdoutおよびstderrを転送する


93

次のように、ProcessBuilderを使用してJavaでプロセスを構築しています。

ProcessBuilder pb = new ProcessBuilder()
        .command("somecommand", "arg1", "arg2")
        .redirectErrorStream(true);
Process p = pb.start();

InputStream stdOut = p.getInputStream();

今私の問題は次のとおりです。そのプロセスのstdoutやstderrを通過しているものをすべてキャプチャし、System.out非同期にリダイレクトしたいと思います。プロセスとその出力リダイレクトをバックグラウンドで実行する必要があります。これまでのところ、これを行うことがわかった唯一の方法は、継続的に読み取りをstdOut行い、適切なのwrite()メソッドを呼び出す新しいスレッドを手動で生成することですSystem.out

new Thread(new Runnable(){
    public void run(){
        byte[] buffer = new byte[8192];
        int len = -1;
        while((len = stdOut.read(buffer)) > 0){
            System.out.write(buffer, 0, len);
        }
    }
}).start();

そのアプローチは一種の作品ですが、それは少し汚い感じがします。その上、正しく管理および終了するためのスレッドがもう1つあります。これを行うより良い方法はありますか?


2
呼び出し元のスレッドをブロックすることはオプションだった場合は、さえのJava 6で非常に簡単な解決策があると思います:org.apache.commons.io.IOUtils.copy(new ProcessBuilder().command(commandLine) .redirectErrorStream(true).start().getInputStream(), System.out);
oberlies

回答:


69

Java 6以前の方法でのみ、いわゆるStreamGobbler(作成を開始した)を使用します。

StreamGobbler errorGobbler = new StreamGobbler(p.getErrorStream(), "ERROR");

// any output?
StreamGobbler outputGobbler = new StreamGobbler(p.getInputStream(), "OUTPUT");

// start gobblers
outputGobbler.start();
errorGobbler.start();

...

private class StreamGobbler extends Thread {
    InputStream is;
    String type;

    private StreamGobbler(InputStream is, String type) {
        this.is = is;
        this.type = type;
    }

    @Override
    public void run() {
        try {
            InputStreamReader isr = new InputStreamReader(is);
            BufferedReader br = new BufferedReader(isr);
            String line = null;
            while ((line = br.readLine()) != null)
                System.out.println(type + "> " + line);
        }
        catch (IOException ioe) {
            ioe.printStackTrace();
        }
    }
}

Java 7については、Evgeniy Dorofeevの回答を参照してください。


1
StreamGobblerはすべての出力をキャッチしますか?出力の一部が失われる可能性はありますか?また、プロセスが停止すると、StreamGobblerスレッドは自動的に終了しますか?
LordOfThePigs 2013年

1
その瞬間から、InputStreamは終了しました。それも終了します。
2013年

7
Java 6以前では、これが唯一の解決策のようです。Java 7以降については、ProcessBuilder.inheritIO()に関する他の回答を参照してください
LordOfThePigs

@asgothプロセスに入力を送信する方法はありますか?これが私の質問です:stackoverflow.com/questions/28070841/…、誰かが問題の解決を手伝ってくれるとありがたいです。
DeepSidhu1313、2015年

144

を使用するとProcessBuilder.inheritIO、サブプロセスの標準I / Oのソースと宛先が、現在のJavaプロセスのものと同じになるように設定されます。

Process p = new ProcessBuilder().inheritIO().command("command1").start();

Java 7がオプションでない場合

public static void main(String[] args) throws Exception {
    Process p = Runtime.getRuntime().exec("cmd /c dir");
    inheritIO(p.getInputStream(), System.out);
    inheritIO(p.getErrorStream(), System.err);

}

private static void inheritIO(final InputStream src, final PrintStream dest) {
    new Thread(new Runnable() {
        public void run() {
            Scanner sc = new Scanner(src);
            while (sc.hasNextLine()) {
                dest.println(sc.nextLine());
            }
        }
    }).start();
}

srcEOFになるため、スレッドはサブプロセスの終了時に自動的に終了します。


1
Java 7には、stdout、stderr、およびstdinを処理するための興味深いメソッドが多数追加されていることがわかります。かなりいい。次回、Java 7プロジェクトinheritIO()でこれらの便利なredirect*(ProcessBuilder.Redirect)メソッドを使用する必要があると思います。残念ながら、私のプロジェクトは、Java 6である
LordOfThePigs

ああ、OKは私の1.6バージョンを追加しました
Evgeniy Dorofeev

sc閉鎖する必要がありますか?
ほとほと2017年

あなたはstackoverflow.com/questions/43051640/…を手伝ってくれる?
gstackoverflow 2017年

System.outストリームではなく、親JVMのOSファイル記述子に設定されることに注意してください。したがって、これは親のコンソールまたはシェルのリダイレクトに書き込むことはできますが、ロギングストリームには機能しません。それらはまだあなたが一つのスレッドだけを必要とする(ただし、あなた標準入力に少なくともリダイレクト標準エラー出力できるため、ポンプのスレッドを必要としています。
eckes

20

Consumer出力を1行ずつ処理する(ログに記録するなど)を提供できるJava 8ラムダを使用した柔軟なソリューション。run()チェックされた例外がスローされないワンライナーです。を実装RunnableするThread代わりに、他の回答が示唆するように、代わりに拡張することができます。

class StreamGobbler implements Runnable {
    private InputStream inputStream;
    private Consumer<String> consumeInputLine;

    public StreamGobbler(InputStream inputStream, Consumer<String> consumeInputLine) {
        this.inputStream = inputStream;
        this.consumeInputLine = consumeInputLine;
    }

    public void run() {
        new BufferedReader(new InputStreamReader(inputStream)).lines().forEach(consumeInputLine);
    }
}

その後、たとえば次のように使用できます。

public void runProcessWithGobblers() throws IOException, InterruptedException {
    Process p = new ProcessBuilder("...").start();
    Logger logger = LoggerFactory.getLogger(getClass());

    StreamGobbler outputGobbler = new StreamGobbler(p.getInputStream(), System.out::println);
    StreamGobbler errorGobbler = new StreamGobbler(p.getErrorStream(), logger::error);

    new Thread(outputGobbler).start();
    new Thread(errorGobbler).start();
    p.waitFor();
}

ここでは、出力ストリームがリダイレクトされSystem.out、エラーストリームがによってエラーレベルで記録されますlogger


これをどのように使用するかを詳しく教えていただけますか?
Chris Turner

@Robert対応する入力/エラーストリームが閉じられると、スレッドは自動的に停止します。メソッドのforEach()inはrun()、ストリームが開くまでブロックし、次の行を待ちます。ストリームが閉じられると終了します。
Adam Michalik

13

次のように簡単です。

    File logFile = new File(...);
    ProcessBuilder pb = new ProcessBuilder()
        .command("somecommand", "arg1", "arg2")
    processBuilder.redirectErrorStream(true);
    processBuilder.redirectOutput(logFile);

.redirectErrorStream(true)によって、プロセスにエラーと出力ストリームマージするよう指示し、次に.redirectOutput(file)によって、マージされた出力をファイルにリダイレクトします。

更新:

私はこれを次のようにしてなんとかしました:

public static void main(String[] args) {
    // Async part
    Runnable r = () -> {
        ProcessBuilder pb = new ProcessBuilder().command("...");
        // Merge System.err and System.out
        pb.redirectErrorStream(true);
        // Inherit System.out as redirect output stream
        pb.redirectOutput(ProcessBuilder.Redirect.INHERIT);
        try {
            pb.start();
        } catch (IOException e) {
            e.printStackTrace();
        }
    };
    new Thread(r, "asyncOut").start();
    // here goes your main part
}

これで、System.outのメインスレッドとasyncOutスレッドの両方の出力を確認できます。


これは質問の答えにはなりません。そのプロセスのstdoutやstderrを通過しているものをすべてキャプチャして、非同期でSystem.outにリダイレクトしたいと思います。プロセスとその出力リダイレクトをバックグラウンドで実行する必要があります。
Adam Michalik 2017年

@AdamMichalik、あなたは正しい-最初は要点を理解していなかった。表示していただきありがとうございます。
nike.laos 2017年

これには、inheritIO()と同じ問題があります。ポアレントJVM FD1に書き込みますが、(ロガーアダプターのような)代わりのSystem.out OutputStreamsには書き込みません。
eckes

3

を使用して出力とリアクティブ処理の両方をキャプチャするシンプルなjava8ソリューションCompletableFuture

static CompletableFuture<String> readOutStream(InputStream is) {
    return CompletableFuture.supplyAsync(() -> {
        try (
                InputStreamReader isr = new InputStreamReader(is);
                BufferedReader br = new BufferedReader(isr);
        ){
            StringBuilder res = new StringBuilder();
            String inputLine;
            while ((inputLine = br.readLine()) != null) {
                res.append(inputLine).append(System.lineSeparator());
            }
            return res.toString();
        } catch (Throwable e) {
            throw new RuntimeException("problem with executing program", e);
        }
    });
}

そして使用法:

Process p = Runtime.getRuntime().exec(cmd);
CompletableFuture<String> soutFut = readOutStream(p.getInputStream());
CompletableFuture<String> serrFut = readOutStream(p.getErrorStream());
CompletableFuture<String> resultFut = soutFut.thenCombine(serrFut, (stdout, stderr) -> {
         // print to current stderr the stderr of process and return the stdout
        System.err.println(stderr);
        return stdout;
        });
// get stdout once ready, blocking
String result = resultFut.get();

このソリューションは非常に簡単です。また、ロガーにリダイレクトする方法を間接的に示します。例として、私の答えを見てください。
keocra

3

より優れたProcessBuilder、zt-execを提供するライブラリがあります。このライブラリーは、あなたが求めていることを正確に行うことができます。

ProcessBuilderの代わりにzt-execを使用すると、コードは次のようになります。

依存関係を追加します。

<dependency>
  <groupId>org.zeroturnaround</groupId>
  <artifactId>zt-exec</artifactId>
  <version>1.11</version>
</dependency>

コード :

new ProcessExecutor()
  .command("somecommand", "arg1", "arg2")
  .redirectOutput(System.out)
  .redirectError(System.err)
  .execute();

ライブラリのドキュメントはこちら:https : //github.com/zeroturnaround/zt-exec/


2

私もJava 6しか使用できません。@ EvgeniyDorofeevのスレッドスキャナー実装を使用しました。私のコードでは、プロセスが終了した後、リダイレクトされた出力をそれぞれ比較する他の2つのプロセスを直ちに実行する必要があります(stdoutとstderrがblessのものと同じであることを確認するためのdiffベースのユニットテスト)。

プロセスが完了するまでwaitFor()を実行しても、スキャナースレッドはすぐには終了しません。コードを正しく機能させるには、プロセスの終了後にスレッドが結合されるようにする必要があります。

public static int runRedirect (String[] args, String stdout_redirect_to, String stderr_redirect_to) throws IOException, InterruptedException {
    ProcessBuilder b = new ProcessBuilder().command(args);
    Process p = b.start();
    Thread ot = null;
    PrintStream out = null;
    if (stdout_redirect_to != null) {
        out = new PrintStream(new BufferedOutputStream(new FileOutputStream(stdout_redirect_to)));
        ot = inheritIO(p.getInputStream(), out);
        ot.start();
    }
    Thread et = null;
    PrintStream err = null;
    if (stderr_redirect_to != null) {
        err = new PrintStream(new BufferedOutputStream(new FileOutputStream(stderr_redirect_to)));
        et = inheritIO(p.getErrorStream(), err);
        et.start();
    }
    p.waitFor();    // ensure the process finishes before proceeding
    if (ot != null)
        ot.join();  // ensure the thread finishes before proceeding
    if (et != null)
        et.join();  // ensure the thread finishes before proceeding
    int rc = p.exitValue();
    return rc;
}

private static Thread inheritIO (final InputStream src, final PrintStream dest) {
    return new Thread(new Runnable() {
        public void run() {
            Scanner sc = new Scanner(src);
            while (sc.hasNextLine())
                dest.println(sc.nextLine());
            dest.flush();
        }
    });
}

1

msangel 回答への追加として、次のコードブロックを追加したいと思います。

private static CompletableFuture<Boolean> redirectToLogger(final InputStream inputStream, final Consumer<String> logLineConsumer) {
        return CompletableFuture.supplyAsync(() -> {
            try (
                InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
                BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
            ) {
                String line = null;
                while((line = bufferedReader.readLine()) != null) {
                    logLineConsumer.accept(line);
                }
                return true;
            } catch (IOException e) {
                return false;
            }
        });
    }

プロセスの入力ストリーム(stdout、stderr)を他のコンシューマーにリダイレクトできます。これは、System.out :: printlnまたはその他の文字列を消費するものです。

使用法:

...
Process process = processBuilder.start()
CompletableFuture<Boolean> stdOutRes = redirectToLogger(process.getInputStream(), System.out::println);
CompletableFuture<Boolean> stdErrRes = redirectToLogger(process.getErrorStream(), System.out::println);
System.out.println(stdOutRes.get());
System.out.println(stdErrRes.get());
System.out.println(process.waitFor());

0
Thread thread = new Thread(() -> {
      new BufferedReader(
          new InputStreamReader(inputStream, 
                                StandardCharsets.UTF_8))
              .lines().forEach(...);
    });
    thread.start();

あなたのカスタムコードは ...


-2

デフォルトでは、作成されたサブプロセスには独自の端末またはコンソールがありません。すべての標準I / O(つまり、stdin、stdout、stderr)操作は親プロセスにリダイレクトされ、そこでgetOutputStream()、getInputStream()、およびgetErrorStream()メソッドを使用して取得されたストリームを介してアクセスできます。親プロセスはこれらのストリームを使用して、サブプロセスに入力を供給し、サブプロセスから出力を取得します。一部のネイティブプラットフォームは標準の入力および出力ストリームに限られたバッファーサイズしか提供しないため、サブプロセスの入力ストリームの書き込みまたは出力ストリームの読み取りに失敗すると、サブプロセスがブロックしたり、デッドロックが発生したりすることがあります。

https://www.securecoding.cert.org/confluence/display/java/FIO07-J.+Do+not+let+external+processes+block+on+IO+buffers

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.