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();

그 접근 방식이 작동하지만 약간 더러워 보입니다. 또한 올바르게 관리하고 종료 할 수있는 스레드가 하나 더 있습니다. 이 작업을 수행하는 더 좋은 방법이 있습니까?


2
호출 스레드를 차단하는 것이 옵션이라면 Java 6에서도 매우 간단한 솔루션이있을 것입니다.org.apache.commons.io.IOUtils.copy(new ProcessBuilder().command(commandLine) .redirectErrorStream(true).start().getInputStream(), System.out);
2013-08-09

답변:


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

1
그 순간부터 InputStream이 종료되고 종료됩니다.
asgoth

7
Java 6 및 이전 버전의 경우 이것이 유일한 솔루션 인 것 같습니다. Java 7 이상의 경우 ProcessBuilder.inheritIO ()에 대한 다른 답변을 참조하십시오
LordOfThePigs

@asgoth 프로세스에 입력을 보내는 방법이 있습니까? 내 질문은 다음과 같습니다. stackoverflow.com/questions/28070841/… , 누군가가 문제를 해결하도록 도와 주면 감사하겠습니다.
DeepSidhu1313

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)방법 중 하나를 사용할 것이라고 생각 합니다. 불행하게도 내 프로젝트는 자바 6입니다
LordOfThePigs

아, OK 내 1.6 버전 추가
에브 게니 Dorofeev

sc폐쇄해야합니까?
hotohoto


System.out 스트림이 아닌 상위 JVM의 OS 파일 설명 자로 설정됩니다. 따라서 이것은 부모의 콘솔 또는 셸 리디렉션에 쓰기는 좋지만 스트림 로깅에는 작동하지 않습니다. 여전히 펌프 스레드가 필요합니다 (하지만 stderr을 stdin으로 리디렉션 할 수 있으므로 스레드 하나만 필요합니다.
eckes

20

Consumer출력을 한 줄씩 처리 (예 : 로그 기록) 하는를 제공 할 수있는 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.


이것을 어떻게 사용할 것인지 확장 해 주시겠습니까?
크리스 터너

@Robert 해당 입력 / 오류 스트림이 닫히면 스레드가 자동으로 중지됩니다. forEach()run()스트림이 열릴 때까지 방법은 다음 행을 기다리고, 차단한다. 스트림이 닫히면 종료됩니다.
Adam Michalik 19

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 에서 main 및 asyncOut 스레드의 출력을 모두 볼 수 있습니다.


이것은 질문에 대답하지 않습니다. 해당 프로세스의 stdout 및 / 또는 stderr를 통해 진행되는 모든 것을 캡처하고 비동기 적으로 System.out으로 리디렉션하고 싶습니다. 프로세스와 출력 리디렉션이 백그라운드에서 실행되기를 원합니다.
Adam Michalik

@AdamMichalik, 당신 말이 맞아요-처음에는 요점을 이해하지 못했습니다. 보여 주셔서 감사합니다.
nike.laos

이것은 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의 스레드 스캐너 구현을 사용했습니다. 내 코드에서 프로세스가 완료된 후 각각 리디렉션 된 출력을 비교하는 두 개의 다른 프로세스를 즉시 실행해야합니다 (stdout 및 stderr이 축복받은 것과 동일한 지 확인하기위한 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.