Получение System.out из подпроцесса как в консоли

Я написал программу, которая запускает и слушает вывод подпроцесса, выводя его в консоль:

public static void main(String[] args) throws IOException {
    testProcess("java -Dfile.encoding=UTF-8 -Dconsole.encoding=UTF-8 -cp \"D:\\Users\\ZONT_\\Documents\" Test");
    System.out.println("");
    testProcess("python \"D:\\Users\\ZONT_\\Documents\\Test.py\"");
}

private static void testProcess(String command) throws IOException {
    Process process = Runtime.getRuntime().exec(command);
    Scanner scanner = new Scanner(process.getInputStream());
    Scanner scanner1 = new Scanner(process.getErrorStream());
    while (process.isAlive() || scanner.hasNext() || scanner1.hasNext()) {
        if (scanner.hasNext())
            System.out.printf("A:%s RL: %s\n", process.isAlive() ? "T" : "F", scanner.nextLine());
        if (scanner1.hasNext())
            System.err.printf("A:%s RL: %s\n", process.isAlive() ? "T" : "F", scanner1.nextLine());

    }
    System.out.println("Process dead.");
}

И тестовые программы на Java и Python:

Python

import time

print("Testing standard characters")
print("Waiting...")

for i in range(1, 10):
    print(i)
    time.sleep(1)

Java

public class Test {
    public static void main(String[] args) {
        System.out.println("Testing standard characters");
        System.out.println("Testing \u044E\u043D\u0438\u043A\u043E\u0434 characters \u3067\u3059");
        System.out.println("Waiting...");
        for (int i = 0; i < 10; i++) {
            System.out.println(i);
            try { Thread.sleep(1000); }
            catch (InterruptedException e) { e.printStackTrace(); }
        }
        System.out.println("Done");
    }
}

Вывод

Вывод родительской программы

Появилась проблема

Вывод в консоль получается корректный, но в Java приложении первая строка выводится сразу, а все остальные только когда подпроцесс завершается. В Python же не выводится ничего, пока подпроцесс не завершится.

Какое поведение программы я ожидаю

Мне нужно, что бы в консоль выводились все сообщения из подпроцессов в тот же момент, когда они и были отправлены (как если бы я их запустил просто из сmd, а не из Java-приложения), при этом очень желательно не изменять код этих подпроцессов, а только в родительской программе, в методе testProcess(). Как мне это сделать? Почему все строки появляются только одновременно, а не по мере их отправки подпроцессом?

UPD

Дебагом понял, что метод hasNext() в строке if (scanner1.hasNext()) блокирует программу до завершения подпроцесса. Как это избежать?


Ответы (1 шт):

Автор решения: ZONT_

Решил проблему следующим изобретением велосипеда

Описание проблемы

  1. Дело в том, что метод Scanner.hasNext() блокирует выполнение потока, пока процесс жив и не отправляет данные в свой stdout или stderr. Если бы я в цикле слушал только один InputStream, то проблемы бы не было, но так как в процессе не возникает ошибок, поток зависает в ожидании ничего из stderr, хоть и в другом stream'е есть данные. Следовтельно нужно слушать эти стримы в разных двух потоках, что бы они друг другу не мешали. Для этого я написал класс StreamListener, который слушает поток до гибели процесса и отправляет строки в callback onLineReceived.
  2. В отличии от Java, процесс python по умолчанию не совершает flush во время вызова функции print(). Но это лечится очень просто и без модификации скрипта - нужно при вызове python интерпретатора указывать опцию -u.

Итоговый рабочий код

public static void main(String[] args) throws IOException {
testProcess("java -Dfile.encoding=UTF-8 -Dconsole.encoding=UTF-8 -cp \"D:\\Users\\ZONT_\\Documents\" Test");
    System.out.println("");
    testProcess("python -u \"D:\\Users\\ZONT_\\Documents\\Test.py\"");
}

private static void testProcess(String command) throws IOException {
    Process process = Runtime.getRuntime().exec(command);
    StreamListener.create(process, process.getInputStream(),
            line -> System.out.printf("A:%s RL: %s\n", process.isAlive() ? "T" : "F", line));
    StreamListener.create(process, process.getErrorStream(),
            line -> System.err.printf("A:%s RL: %s\n", process.isAlive() ? "T" : "F", line));
    while (process.isAlive()) {
        try { Thread.sleep(10); }
        catch (InterruptedException e) { e.printStackTrace(); }
    }
    System.out.println("Process dead.");
}

private static class StreamListener extends Thread {
    private final Process process;
    private final InputStream stream;
    private final OnLineReceived listener;

    private static StreamListener create(Process process, InputStream stream, OnLineReceived listener) {
        return new StreamListener(process, stream, listener);
    }

    private StreamListener(Process process, InputStream stream, OnLineReceived listener) {
        this.process = process;
        this.stream = stream;
        this.listener = listener;
        start();
    }

    @Override
    public void run() {
        Scanner scanner = new Scanner(stream);
        while (process.isAlive() || scanner.hasNext()) {
            if (scanner.hasNext())
                listener.onLineReceived(scanner.nextLine());
        }
    }
}

private interface OnLineReceived {
    void onLineReceived(String line);
}
→ Ссылка