1. Взаємне блокування під час читання/запису потоків
Одна з найнеприємніших помилок — коли процес зависає й не завершується, хоча здається, що все зроблено правильно. Часта причина — переповнення буфера стандартного виведення або потоку помилок зовнішнього процесу. Якщо не читати обидва потоки (stdout і stderr), процес може «заблокуватися» під час спроби щось вивести, тому що ніхто не забирає дані з його буфера.
Приклад проблеми
ProcessBuilder builder = new ProcessBuilder("java", "-version");
Process process = builder.start();
// Читаємо лише stdout, а stderr ігноруємо!
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
process.waitFor();
Якщо команда пише щось у stderr (наприклад, java -version майже завжди пише туди!), цей потік переповнюється, і процес зависає.
Як правильно
Читати обидва потоки паралельно (окремими потоками або через ExecutorService):
ProcessBuilder builder = new ProcessBuilder("java", "-version");
Process process = builder.start();
// Читаємо stdout
Thread stdoutThread = new Thread(() -> {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println("[stdout] " + line);
}
} catch (IOException e) {
e.printStackTrace();
}
});
// Читаємо stderr
Thread stderrThread = new Thread(() -> {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getErrorStream()))) {
String line;
while ((line = reader.readLine()) != null) {
System.err.println("[stderr] " + line);
}
} catch (IOException e) {
e.printStackTrace();
}
});
stdoutThread.start();
stderrThread.start();
process.waitFor();
stdoutThread.join();
stderrThread.join();
Висновок:
Якщо не читати stderr — процес може зависнути.
Якщо не читати stdout — також може зависнути.
Читай обидва потоки — і буде тобі щастя!
2. Проблеми з кодуванням
Зовнішні процеси можуть використовувати інше кодування для виведення тексту, ніж ваш Java‑застосунок за замовчуванням. Якщо не вказати правильне кодування, отримаєте «кракозябри» замість тексту (особливо помітно з кирилицею).
Приклад помилки
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
Цей код використовує системне кодування за замовчуванням. Але якщо зовнішній процес пише, наприклад, у UTF-8, а у вас — Windows-1251, кирилиця перетвориться на абракадабру.
Як правильно
Явно передавайте потрібне кодування, якщо ви його знаєте:
BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8)
);
Якщо не впевнені — перегляньте документацію до програми або спробуйте кілька варіантів.
Порада: у Windows консольні утиліти часто використовують CP866 або Windows-1251, а в Linux — UTF-8.
3. Платформні відмінності
Команди, що працюють в одній ОС, можуть бути відсутні в іншій. Наприклад, ls є в Linux/macOS, але не в Windows (там — dir). Відрізняються синтаксис команд, розділювачі шляхів і лапки.
Приклад помилки
ProcessBuilder builder = new ProcessBuilder("ls", "-l");
builder.start(); // У Windows: "ls" не знайдено!
Як правильно
String os = System.getProperty("os.name").toLowerCase();
ProcessBuilder builder;
if (os.contains("win")) {
builder = new ProcessBuilder("cmd", "/c", "dir");
} else {
builder = new ProcessBuilder("ls", "-l");
}
Шляхи до файлів: використовуйте File.separator замість «/» або «\», щоб не потрапити в халепу з шляхами.
4. Проблеми з правами доступу
Деякі команди вимагають прав адміністратора (видалення системних файлів, зміна мережевих налаштувань тощо). За недостатніх прав команда завершиться з помилкою або взагалі не запуститься.
Приклад
ProcessBuilder builder = new ProcessBuilder("rm", "-rf", "/root/secret.txt");
Process process = builder.start();
// ... чекаємо помилку Permission denied
Як правильно
- Перевіряйте, чи потрібні підвищені права для вашої команди.
- Опрацьовуйте код завершення процесу через process.exitValue().
- Читайте stderr — там найчастіше є причина помилки (наприклад, «Permission denied»).
5. Витоки ресурсів
Якщо не закривати потоки процесу (InputStream, OutputStream, ErrorStream), вони можуть «висіти», займати ресурси й навіть блокувати завершення. Аналогічно, якщо не завершити сам процес, він може стати «зомбі» в системі.
Приклад помилки
Process process = builder.start();
// ... працюємо, але не закриваємо потоки!
Як правильно
Використовуйте try-with-resources для потоків:
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()))) {
// Читаємо виведення
}
Після завершення роботи з процесом коректно його зупиняйте:
process.destroy(); // Завершити процес (якщо він ще живий)
Увага: якщо не закрити потоки — можливі витоки пам’яті, зависання та системні проблеми (особливо за масового запуску процесів).
6. Взаємне блокування під час інтерактивної взаємодії
Під час інтерактивного обміну легко потрапити в ситуацію взаємного блокування: Java чекає відповіді, а зовнішній процес — ваших даних. У підсумку обидва «мовчать». Або ви надіслали повідомлення, але не читаєте відповідь — у певний момент буфер зовнішньої програми переповнюється, і вона припиняє запис.
Щоб цього уникнути, розділяйте обов’язки: один потік відповідає за читання, інший — за запис. Тоді взаємодія відбувається паралельно, і ніхто нікого не блокує. Також не залишайте відкриті потоки після завершення — закрийте їх, щоб система не тримала зайві ресурси.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ