JavaRush /Курсы /JAVA 25 SELF /Чтение вывода процесса, работа со stdin/stdout

Чтение вывода процесса, работа со stdin/stdout

JAVA 25 SELF
61 уровень , 1 лекция
Открыта

1. Потоки ввода/вывода процесса

Когда вы запускаете внешний процесс из Java, между ним и вашей программой открывается настоящий мини-диалог — три канала связи.

Первый канал — это стандартный вывод. Всё, что внешний процесс хочет показать «на экран», вы можете поймать через Process.getInputStream(). Второй канал — поток ошибок. Если процесс решит пожаловаться или выдать сообщение об ошибке, вы узнаете об этом через Process.getErrorStream(). И наконец, третий — стандартный ввод. Через него вы можете отправить процессу какие-то данные, если он чего-то от вас ждёт. Делается это с помощью Process.getOutputStream().

Немного сбивает с толку, что названия потоков будто перепутаны. У процесса InputStream — это то, что вы читаете (его вывод), а OutputStream — то, куда вы пишете (его ввод). Но если смотреть глазами вашей Java-программы, всё становится логичным: вы читаете то, что процесс выдаёт, и пишете то, что хотите ему передать.

Схема потоков процесса

+-------------------+           +---------------------+
|   Ваша программа  | <-------> |   Внешний процесс   |
+-------------------+           +---------------------+
        |                                |
        |  getOutputStream()  -------->  stdin
        |  getInputStream()   <--------  stdout
        |  getErrorStream()   <--------  stderr

2. Чтение вывода процесса (stdout)

Давайте научимся читать то, что внешний процесс выводит на экран. Это может быть результат команды, версия программы, сообщение об успехе — всё, что угодно.

Пример 1: Чтение вывода команды java -version

Сначала запустим процесс, а потом прочитаем его вывод.

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.IOException;

public class ProcessOutputExample {
    public static void main(String[] args) throws IOException {
        // Создаём процесс: команда зависит от вашей ОС!
        ProcessBuilder pb = new ProcessBuilder("java", "-version");
        Process process = pb.start();

        // Читаем поток ошибок (java -version обычно пишет туда!)
        try (BufferedReader reader = new BufferedReader(
                new InputStreamReader(process.getErrorStream()))) {
            String line;
            while ((line = reader.readLine()) != null) {
                System.out.println("Process output: " + line);
            }
        }
    }
}

Внимание! Команда java -version пишет свой результат не в обычный stdout, а в поток ошибок stderr. Это не баг, а особенность Java. Поэтому мы читаем process.getErrorStream(). Для большинства других команд (например, echo, ls, dir) — используйте getInputStream().

Пример 2: Чтение вывода команды echo

ProcessBuilder pb = new ProcessBuilder("echo", "Hello, Java!");
Process process = pb.start();

try (BufferedReader reader = new BufferedReader(
        new InputStreamReader(process.getInputStream()))) {
    String line;
    while ((line = reader.readLine()) != null) {
        System.out.println("Process output: " + line);
    }
}

Почему нужен BufferedReader?

Стандартный поток ввода — это байтовый поток (InputStream). Чтобы читать его как строки, используем связку InputStreamReader (преобразует байты в символы) + BufferedReader (удобно читать построчно).

3. Запись во входной поток процесса (stdin)

Иногда внешний процесс ждёт от нас данных. Например, если вы запускаете скрипт, который просит ввести имя пользователя, или калькулятор, который ждёт числа.

Для этого используйте process.getOutputStream() — да, именно так: вы «пишете» в поток процесса.

Пример 3: Передача данных процессу

Допустим, у вас есть простой Python-скрипт, который читает строку и печатает её обратно:

echo_input.py:

# echo_input.py
line = input()
print("You said:", line)

Запустим его из Java и отправим строку:

import java.io.*;

public class ProcessStdinExample {
    public static void main(String[] args) throws IOException {
        ProcessBuilder pb = new ProcessBuilder("python", "echo_input.py");
        Process process = pb.start();

        // Пишем в stdin процесса
        try (BufferedWriter writer = new BufferedWriter(
                new OutputStreamWriter(process.getOutputStream()))) {
            writer.write("Привет из Java!\n");
            writer.flush();
        }

        // Читаем stdout процесса
        try (BufferedReader reader = new BufferedReader(
                new InputStreamReader(process.getInputStream()))) {
            String line;
            while ((line = reader.readLine()) != null) {
                System.out.println("Ответ процесса: " + line);
            }
        }
    }
}

Не забывайте про flush() — иначе данные могут «застрять» в буфере и не попасть в процесс.

4. Обработка ошибок: чтение stderr

Внешний процесс может не только радовать нас результатами, но и ругаться на жизнь — писать ошибки. Если вы не читаете поток ошибок, можете пропустить важную информацию или даже получить зависание (deadlock), если буфер ошибок переполнится.

Пример 4: Чтение stderr процесса

ProcessBuilder pb = new ProcessBuilder("java", "-version");
Process process = pb.start();

// Читаем ошибки
try (BufferedReader errorReader = new BufferedReader(
        new InputStreamReader(process.getErrorStream()))) {
    String line;
    while ((line = errorReader.readLine()) != null) {
        System.out.println("Ошибка процесса: " + line);
    }
}

Почему важно читать оба потока?

Если процесс пишет очень много в stdout или stderr, а вы не читаете эти потоки, буфер ОС может переполниться, и процесс «зависнет», ожидая, пока кто-то освободит место. Поэтому хороший стиль — читать оба потока параллельно (или хотя бы поочередно).

5. Практика: запуск команды, чтение вывода, обработка ошибок

Давайте объединим всё вышеописанное в одну небольшую утилиту, которая запускает внешнюю команду, читает её стандартный вывод и ошибки, и выводит всё на экран.

Пример 5: Универсальный запуск команды

import java.io.BufferedReader;
import java.io.InputStreamReader;

public class RunCommand {
    public static void main(String[] args) throws Exception {
        if (args.length == 0) {
            System.out.println("Укажите команду для запуска, например: java RunCommand ls -l");
            return;
        }

        var process = new ProcessBuilder(args).start();

        // Запускаем раздельные потоки чтения stdout и stderr
        var t1 = Thread.ofPlatform().start(() -> readStream(process.inputReader(), "[stdout]"));
        var t2 = Thread.ofPlatform().start(() -> readStream(process.errorReader(), "[stderr]"));

        int exitCode = process.waitFor(); // ждём завершения процесса
        t1.join();
        t2.join();

        System.out.println("Процесс завершился с кодом: " + exitCode);
    }

    private static void readStream(BufferedReader reader, String prefix) {
        try (reader) {
            reader.lines().forEach(line -> System.out.println(prefix + " " + line));
        } catch (Exception e) {
            System.err.println("Ошибка чтения " + prefix + ": " + e.getMessage());
        }
    }
}

Такой подход позволяет избежать deadlock'а и не потерять ни одну ошибку!

6. Коротко о кодировках

По умолчанию InputStreamReader использует системную кодировку. Если внешний процесс пишет в другой кодировке (например, UTF-8, а у вас Windows-1251), вывод может отображаться «кракозябрами». Укажите кодировку явно:

new InputStreamReader(process.getInputStream(), "UTF-8")

7. Пример: запуск Python-скрипта и передача ему данных

Допустим, у вас есть Python-скрипт, который ожидает несколько строк на входе, а затем выводит результат.

adder.py:

# adder.py
a = int(input())
b = int(input())
print(a + b)

Из Java вы можете отправить ему числа и прочитать ответ:

import java.io.BufferedReader;
import java.nio.charset.StandardCharsets;

public class PythonAdder {
    public static void main(String[] args) throws Exception {
        var process = new ProcessBuilder("python", "adder.py").start();

        // Отправляем два числа в stdin Python-скрипта
        try (var writer = process.outputWriter(StandardCharsets.UTF_8)) {
            writer.println("10");
            writer.println("25");
        }

        // Читаем результат из stdout с помощью inputReader
        try (BufferedReader reader = process.inputReader(StandardCharsets.UTF_8)) {
            reader.lines().forEach(line -> System.out.println("Ответ от Python: " + line));
        }

        int exitCode = process.waitFor();
        System.out.println("Python завершился с кодом: " + exitCode);
    }
}

8. Почему deadlock? Как его избежать

Deadlock — это ситуация, когда процесс и ваша программа ждут друг друга до бесконечности. Например, если буфер вывода процесса переполнен, а вы не читаете этот поток, процесс остановится. Если вы ждёте завершения процесса, а он ждёт, пока вы ему что-то напишете, — тоже deadlock.

Как избежать:

  • Всегда читайте оба потока (stdout и stderr).
  • Для долгих/многословных процессов используйте отдельные потоки для чтения.
  • Не блокируйте запись и чтение одновременно в одном потоке.

9. Типичные ошибки при работе с потоками ввода/вывода процесса

Ошибка №1: Чтение только одного потока (stdout или stderr).
Если читать только stdout, а процесс пишет ошибку в stderr, буфер ошибок может переполниться — процесс зависнет. Решение: читать оба потока (лучше в отдельных потоках).

Ошибка №2: Неправильная кодировка.
Если не указать нужную кодировку, вывод может быть нечитаемым. Всегда проверяйте, в какой кодировке пишет внешний процесс (например, явно задавайте "UTF-8" или используйте StandardCharsets.UTF_8).

Ошибка №3: Не закрыли потоки.
Открытые потоки могут привести к утечкам ресурсов. Используйте try-with-resources или явно закрывайте все потоки.

Ошибка №4: Не вызвали flush() при записи в stdin процесса.
Если забыть вызвать flush(), данные могут так и не попасть в процесс — он будет ждать ввода вечно.

Ошибка №5: Платформенные различия.
Команды и синтаксис могут отличаться между Windows и Linux/Mac. Проверяйте ОС через System.getProperty("os.name") и подбирайте команды соответственно.

Ошибка №6: Не обработали исключения.
Работа с процессами часто сопровождается IOException. Всегда обрабатывайте исключения, чтобы программа не завершилась аварийно.

1
Задача
JAVA 25 SELF, 61 уровень, 1 лекция
Недоступна
Тайное Послание 📩
Тайное Послание 📩
1
Задача
JAVA 25 SELF, 61 уровень, 1 лекция
Недоступна
Диспетчер Космической Станции 🚀
Диспетчер Космической Станции 🚀
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ