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, процесс может зависнуть из-за переполнения буфера вывода.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ