JavaRush /Курси /JAVA 25 SELF /Керування життєвим циклом процесу

Керування життєвим циклом процесу

JAVA 25 SELF
Рівень 61 , Лекція 2
Відкрита

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, процес може зависнути через переповнення буфера виведення.

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