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. Завжди обробляйте винятки, щоб програма не завершилася аварійно.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ