1. Очікування завершення процесу: waitFor()
Коли ви запускаєте зовнішній процес із Java, він живе своїм життям: щось рахує, пише в консоль, інколи зависає (привіт, ping на мільйон пакетів). Але іноді нам потрібно знати, коли цей процес завершився, як він завершився (успіх чи провал), і — якщо щось пішло не так — уміти його «вбити». Загалом, усе як у реальному житті: якщо ви доручили комусь задачу, непогано б дізнатися, чи впорався він, і мати змогу його зупинити, якщо він раптом почав нескінченно варити каву.
Коли ви запускаєте процес через ProcessBuilder, ви отримуєте об’єкт типу Process. Цей об’єкт — як пульт керування: через нього можна чекати завершення процесу, отримати код повернення, завершити процес, а також керувати потоками введення-виведення.
Як чекати завершення процесу?
Для цього є метод waitFor():
ProcessBuilder builder = new ProcessBuilder("ping", "google.com", "-c", "3");
Process process = builder.start();
System.out.println("Чекаємо завершення процесу...");
int exitCode = process.waitFor(); // Блокує поточний потік до завершення процесу
System.out.println("Процес завершився з кодом: " + exitCode);
Що відбувається?
- Програма запускає зовнішній процес (ping).
- Потім потік, у якому викликано waitFor(), «засинає» доти, доки зовнішній процес не завершиться.
- Щойно зовнішній процес завершив роботу, waitFor() повертає код повернення (зазвичай 0 — успіх, не 0 — помилка).
Як дізнатися код повернення?
Метод waitFor() повертає цей код. Також його можна отримати окремо через exitValue():
int code = process.exitValue();
Але будьте обережні: якщо процес іще не завершився, виклик exitValue() кине виняток IllegalThreadStateException. Тому зазвичай спершу чекають завершення через waitFor(), а потім дивляться код.
2. Переривання процесу: destroy() і destroyForcibly()
Іноді зовнішній процес починає жити занадто довго або зависає. Наприклад, хтось запустив ping на 1000 пакетів, а у вас дедлайн через 5 секунд. Потрібна «червона кнопка».
М’яке завершення: destroy()
Метод destroy() надсилає процесу сигнал завершення (SIGTERM в Unix, CTRL-BREAK у Windows), і процесу дається шанс завершитися коректно.
Process process = new ProcessBuilder("ping", "google.com").start();
Thread.sleep(2000); // Нехай попрацює 2 секунди
process.destroy(); // Попросимо завершитися
Жорстке завершення: destroyForcibly()
Якщо процес не реагує на ввічливі прохання, можна використати метод destroyForcibly(). Це як висмикнути вилку з розетки.
process.destroyForcibly();
Важливо: Після виклику destroy() або destroyForcibly() потрібно дочекатися завершення процесу через waitFor(). Іноді процесу потрібен час, щоб «усвідомити» свою смерть.
3. Очікування процесу з тайм-аутом
У Java 8+ з’явився метод із тайм-аутом:
boolean finished = process.waitFor(5, java.util.concurrent.TimeUnit.SECONDS);
if (finished) {
System.out.println("Процес завершився вчасно!");
} else {
System.out.println("Процес не завершився, знищуємо...");
process.destroy();
}
- Якщо процес завершився за 5 секунд, метод поверне true.
- Якщо ні — поверне false, і ви можете вжити заходів (наприклад, завершити процес).
Приклад: Запустимо довгу команду й спробуємо її «вбити»:
ProcessBuilder builder = new ProcessBuilder("ping", "google.com", "-c", "10");
Process process = builder.start();
boolean finished = process.waitFor(2, java.util.concurrent.TimeUnit.SECONDS);
if (!finished) {
System.out.println("Процес працює занадто довго, знищуємо...");
process.destroy();
// Можна дочекатися повного завершення
process.waitFor();
}
System.out.println("Код повернення: " + process.exitValue());
4. Практика: мініутиліта для запуску команди з тайм-аутом
Напишімо просту програму, яка запускає зовнішню команду з тайм-аутом, чекає її завершення, а у разі перевищення часу — завершує процес.
import java.io.IOException;
import java.util.Scanner;
import java.util.concurrent.TimeUnit;
public class ProcessTimeoutDemo {
public static void main(String[] args) throws IOException, InterruptedException {
Scanner scanner = new Scanner(System.in);
System.out.println("Введіть команду для запуску (наприклад, ping google.com):");
String commandLine = scanner.nextLine();
String[] command = commandLine.split(" ");
ProcessBuilder builder = new ProcessBuilder(command);
Process process = builder.start();
System.out.println("Скільки секунд чекати завершення?");
int timeout = scanner.nextInt();
boolean finished = process.waitFor(timeout, TimeUnit.SECONDS);
if (finished) {
System.out.println("Процес завершився сам. Код повернення: " + process.exitValue());
} else {
System.out.println("Час вийшов! Завершуємо процес...");
process.destroy();
process.waitFor();
System.out.println("Процес завершено примусово. Код повернення: " + process.exitValue());
}
}
}
Пояснення:
- Користувач вводить команду, яку потрібно запустити.
- Програма чекає зазначений час.
- Якщо процес не завершився — знищує його.
Лайфхак: У Windows ping за замовчуванням пінгує 4 рази, а в Linux — нескінченно. Для Linux/Mac додайте параметр -c 5 (наприклад, ping google.com -c 5).
5. Отримання коду повернення: що це і навіщо?
У світі командних рядків прийнято: якщо команда завершилася успішно, вона повертає код 0. Якщо щось пішло не так — будь-який інший код.
- В Unix-подібних системах: 0 = успіх, 1 і більше = помилка.
- У Windows — приблизно так само.
Ви можете використовувати цей код для прийняття рішень у Java-програмі:
int exitCode = process.waitFor();
if (exitCode == 0) {
System.out.println("Успішно!");
} else {
System.out.println("Помилка! Код: " + exitCode);
}
6. Завершення процесів, що зависли
Іноді процес може просто зависнути — наприклад, чекає, що ви щось введете, або не може під’єднатися до мережі. Якщо залишити його без уваги, він висітиме в системі й спокійно споживатиме ресурси.
Щоб не допустити цього, варто задати процесу межу терпіння. Метод waitFor(timeout, unit) дозволяє чекати його завершення лише певний час. Якщо процес не встиг — сміливо завершуйте його.
Після виклику destroy() важливо не просто махнути рукою, а переконатися, що процес справді завершився. Для цього знову викликають waitFor(). А якщо й це не допомогло — є крайній захід, destroyForcibly(). Він перериває все без зайвих розмов.
Приклад із подвійним знищенням
boolean finished = process.waitFor(5, TimeUnit.SECONDS);
if (!finished) {
process.destroy();
if (!process.waitFor(2, TimeUnit.SECONDS)) {
process.destroyForcibly();
}
}
Особливості роботи з Process: що важливо пам’ятати
Коли процес завершився, не поспішайте про нього забути. Загляньте в його виведення — і звичайне, і те, де зберігаються помилки. Там може виявитися щось важливе: повідомлення про збій, попередження або просто підтвердження, що все пройшло успішно.
Після цього обов’язково закрийте всі пов’язані потоки — InputStream, OutputStream і ErrorStream. Це як вимкнути світло після виходу: заощаджує ресурси та підтримує порядок у системі.
І пам’ятайте: якщо ви зупинили процес примусово, він може не встигнути коректно записати своє виведення. Нічого страшного — це природна поведінка для таких ситуацій.
7. Приклад: запуск тривалої команди та її завершення
Припустімо, ми хочемо запустити тривалий процес (наприклад, ping з великою кількістю пакетів) і завершити його через 3 секунди.
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.concurrent.TimeUnit;
public class KillLongProcessDemo {
public static void main(String[] args) throws IOException, InterruptedException {
String[] command = {"ping", "google.com"};
// Для Linux/Mac: {"ping", "google.com", "-c", "100"}
ProcessBuilder builder = new ProcessBuilder(command);
Process process = builder.start();
// Читаємо вивід процесу в окремому потоці
Thread readerThread = new Thread(() -> {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
// Ігноруємо
}
});
readerThread.start();
// Чекаємо 3 секунди
if (!process.waitFor(3, TimeUnit.SECONDS)) {
System.out.println("Процес працює занадто довго, знищуємо...");
process.destroy();
process.waitFor();
}
readerThread.join(); // Дочекатися читання виводу
System.out.println("Процес завершено. Код повернення: " + process.exitValue());
}
}
8. Типові помилки під час керування процесами
Помилка № 1: Не дочікуватися завершення процесу. Якщо ви запустили процес і просто забули про нього (не викликали waitFor()), то процес може залишитися «висячим» у системі навіть після завершення вашої Java-програми. Потоки введення/виведення можуть не бути закриті, що призведе до витоків пам’яті та файлових дескрипторів.
Помилка № 2: Неправильне завершення процесу. Викликали destroy(), але не дочекалися завершення процесу через waitFor(). У підсумку процес може залишитися «зомбі» (особливо в Unix-системах).
Помилка № 3: Очікування коду повернення до завершення процесу. Виклик exitValue() до завершення процесу призводить до винятку IllegalThreadStateException.
Помилка № 4: Не обробляти винятки. Методи роботи з процесами можуть кидати IOException і InterruptedException. Не забувайте їх перехоплювати або оголошувати в сигнатурі методу.
Помилка № 5: Не читати виведення процесу. Якщо не читати stdout і stderr, процес може зависнути через переповнення буфера виведення.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ