ProcessBuilder: Пересилання stdout та stderr запущених процесів без блокування основного потоку


93

Я будую процес на Java за допомогою ProcessBuilder наступним чином:

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);
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, див. Відповідь Євгена Дорофєєва.


1
Чи вловить StreamGobbler весь результат? Чи є якийсь шанс, що він втратить деякі результати? Також потік StreamGobbler загине самостійно, як тільки процес зупиниться?
LordOfThePigs

1
З того моменту, як InputStream закінчився, він теж закінчиться.
асгот

7
Для Java 6 та попередніх версій, схоже, це єдине рішення. Для java 7 і новіших версій дивіться іншу відповідь про ProcessBuilder.inheritIO ()
LordOfThePigs

@asgoth Чи є спосіб надіслати вхідні дані до процесу? Ось моє запитання: stackoverflow.com/questions/28070841/… , я буду вдячний, якщо хтось допоможе мені вирішити проблему.
DeepSidhu1313

144

Використовуйте ProcessBuilder.inheritIO, він встановлює джерело та призначення для стандартного вводу-виводу підпроцесу такими ж, як у поточного процесу 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

О, добре, додав мою версію 1.6
Євген Дорофєєв

scпотрібно закрити?
hotohoto

чи можете ви допомогти з stackoverflow.com/questions/43051640/… ?
gstackoverflow

Зверніть увагу, що він встановлює його на дескриптор файлу ОС батьківського JVM, а не на потоки System.out. Тому це нормально писати на консоль або перенаправлення оболонки батьківського, але це не буде працювати для потоків реєстрації. Тим все ще потрібна нитка насоса (однак ви можете принаймні перенаправити 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()in run()блокує, поки потік не буде відкритий, очікуючи наступного рядка. Він вийде, коли потік буде закритий.
Адам Міхалік

13

Це так просто:

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

.redirectErrorStream (true) ви повідомляєте процесу злиття помилок і вихідного потоку, а потім .redirectOutput (файл) перенаправляєте об'єднаний вихід у файл.

Оновлення:

Мені вдалося зробити це наступним чином:

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
}

Тепер ви можете бачити як вихідні дані з основних і asyncOut-потоків у System.out


Це не відповідає на запитання: я хотів би зафіксувати все, що проходить через stdout та / або stderr цього процесу, і переадресувати його до System.out асинхронно. Я хочу, щоб процес та його перенаправлення на вихід працювали у фоновому режимі.
Адам Міхалік

@AdamMichalik, ти маєш рацію - спочатку я не зрозумів суті. Дякую за показ.
nike.laos

Це має таку ж проблему, що і наслідування Io (), він буде писати в поарент 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. Ця бібліотека може робити саме те, про що ви просите, і багато іншого.

Ось як би виглядав ваш код із zt-exec замість ProcessBuilder:

додати залежність:

<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 однакові з блаженними).

Потоки сканера не закінчуються досить швидко, навіть якщо я чекаю завершення процесу (). Щоб код працював коректно, я повинен переконатися, що нитки об’єднані після завершення процесу.

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

За замовчуванням створений підпроцес не має власного терміналу чи консолі. Усі його стандартні операції вводу-виводу (тобто 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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.