1. Oczekiwanie na zakończenie procesu: waitFor()
Gdy uruchamiasz proces zewnętrzny z Javy, żyje on własnym życiem: coś oblicza, wypisuje do konsoli, czasem się zawiesza (pozdrawiamy ping na milion pakietów). Ale czasem musimy wiedzieć, kiedy ten proces się zakończył, jak się zakończył (sukces czy porażka) i – jeśli coś poszło nie tak – potrafić go „zabić”. Krótko mówiąc, jak w prawdziwym życiu: jeśli komuś zlecisz zadanie, dobrze wiedzieć, czy sobie poradził, i mieć możliwość go zatrzymać, jeśli nagle zacznie bez końca parzyć kawę.
Gdy uruchamiasz proces przez ProcessBuilder, otrzymujesz obiekt typu Process. Ten obiekt to jak pilot: dzięki niemu możesz czekać na zakończenie procesu, pobrać kod wyjścia, zakończyć proces, a także zarządzać strumieniami wejścia/wyjścia.
Jak czekać na zakończenie procesu?
Służy do tego metoda waitFor():
ProcessBuilder builder = new ProcessBuilder("ping", "google.com", "-c", "3");
Process process = builder.start();
System.out.println("Czekamy na zakończenie procesu...");
int exitCode = process.waitFor(); // Blokuje bieżący wątek do zakończenia procesu
System.out.println("Proces zakończył się z kodem: " + exitCode);
Co się dzieje?
- Program uruchamia proces zewnętrzny (ping).
- Następnie wątek, w którym wywołano waitFor(), „zasypia” do czasu zakończenia procesu zewnętrznego.
- Gdy tylko proces zewnętrzny zakończy pracę, waitFor() zwraca kod wyjścia (zwykle 0 – sukces, różne od 0 – błąd).
Jak poznać kod wyjścia?
Metoda waitFor() zwraca ten kod. Można go też pobrać osobno przez exitValue():
int code = process.exitValue();
Uwaga: jeśli proces jeszcze się nie zakończył, wywołanie exitValue() wyrzuci wyjątek IllegalThreadStateException. Dlatego zwykle najpierw czeka się na zakończenie przez waitFor(), a potem sprawdza kod.
2. Przerywanie procesu: destroy() i destroyForcibly()
Czasem proces zewnętrzny działa zbyt długo albo się zawiesza. Na przykład ktoś uruchomił ping na 1000 pakietów, a termin macie za 5 sekund. Potrzebny jest „czerwony przycisk”.
Łagodne zakończenie: destroy()
Metoda destroy() wysyła do procesu sygnał zakończenia (SIGTERM w Unix, CTRL-BREAK w Windows), dając mu szansę na poprawne zamknięcie.
Process process = new ProcessBuilder("ping", "google.com").start();
Thread.sleep(2000); // Niech popracuje 2 sekundy
process.destroy(); // Poprośmy o zakończenie
Twarde zakończenie: destroyForcibly()
Jeśli proces nie reaguje na uprzejme prośby, można użyć metody destroyForcibly(). To jak wyciągnięcie wtyczki z gniazdka.
process.destroyForcibly();
Ważne: Po wywołaniu destroy() lub destroyForcibly() należy poczekać na zakończenie procesu przez waitFor(). Czasem proces potrzebuje chwili, żeby „uświadomić sobie” swoją śmierć.
3. Oczekiwanie na proces z limitem czasu
W Javie 8+ pojawiła się metoda z limitem czasu:
boolean finished = process.waitFor(5, java.util.concurrent.TimeUnit.SECONDS);
if (finished) {
System.out.println("Proces zakończył się na czas!");
} else {
System.out.println("Proces się nie zakończył, przerywamy...");
process.destroy();
}
- Jeśli proces zakończy się w ciągu 5 sekund, metoda zwróci true.
- Jeśli nie – zwróci false i możesz podjąć działania (np. zakończyć proces).
Przykład: Uruchommy długotrwałe polecenie i spróbujmy je „zabić”:
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("Proces działa zbyt długo, przerywamy...");
process.destroy();
// Można poczekać na pełne zakończenie
process.waitFor();
}
System.out.println("Kod wyjścia: " + process.exitValue());
4. Praktyka: mini-narzędzie do uruchamiania polecenia z limitem czasu
Napiszmy prosty program, który uruchamia zewnętrzne polecenie z limitem czasu, czeka na jego zakończenie, a po przekroczeniu czasu – kończy proces.
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("Wpisz polecenie do uruchomienia (np. ping google.com):");
String commandLine = scanner.nextLine();
String[] command = commandLine.split(" ");
ProcessBuilder builder = new ProcessBuilder(command);
Process process = builder.start();
System.out.println("Ile sekund czekać na zakończenie?");
int timeout = scanner.nextInt();
boolean finished = process.waitFor(timeout, TimeUnit.SECONDS);
if (finished) {
System.out.println("Proces zakończył się sam. Kod wyjścia: " + process.exitValue());
} else {
System.out.println("Czas minął! Kończymy proces...");
process.destroy();
process.waitFor();
System.out.println("Proces został zakończony przymusowo. Kod wyjścia: " + process.exitValue());
}
}
}
Wyjaśnienie:
- Użytkownik wpisuje polecenie, które należy uruchomić.
- Program czeka podany czas.
- Jeśli proces się nie zakończył – przerywa go.
Wskazówka: W Windows ping domyślnie pinguje 4 razy, a w Linux – bez końca. Dla Linux/Mac dodaj parametr -c 5 (np. ping google.com -c 5).
5. Kod wyjścia: co to jest i po co?
W świecie wiersza poleceń przyjęto, że jeśli polecenie zakończyło się pomyślnie, zwraca kod 0. Jeśli coś poszło nie tak – dowolny inny kod.
- W systemach uniksowych: 0 = sukces, 1 i więcej = błąd.
- W Windows – zasadniczo tak samo.
Możesz użyć tego kodu do podejmowania decyzji w programie w Javie:
int exitCode = process.waitFor();
if (exitCode == 0) {
System.out.println("Sukces!");
} else {
System.out.println("Błąd! Kod: " + exitCode);
}
6. Kończenie zawieszonych procesów
Czasem proces może po prostu się zawiesić – np. czeka na dane wejściowe albo nie może połączyć się z siecią. Jeśli zostawisz go bez opieki, będzie wisiał w systemie i zupełnie spokojnie pożerał zasoby.
Aby do tego nie dopuścić, warto wyznaczyć procesowi granicę cierpliwości. Metoda waitFor(timeout, unit) pozwala czekać na jego zakończenie tylko określony czas. Jeśli proces się nie wyrobi – śmiało go zakańczaj.
Po wywołaniu destroy() ważne jest nie tylko machnąć na to ręką, lecz upewnić się, że proces rzeczywiście się zakończył. W tym celu ponownie wywołuje się waitFor(). A jeśli i to nie pomogło – jest środek ostateczny, destroyForcibly(). Przerywa wszystko bez zbędnych ceregieli.
Przykład z podwójnym zakończeniem
boolean finished = process.waitFor(5, TimeUnit.SECONDS);
if (!finished) {
process.destroy();
if (!process.waitFor(2, TimeUnit.SECONDS)) {
process.destroyForcibly();
}
}
Cechy pracy z Process: o czym warto pamiętać
Gdy proces się zakończy, nie spiesz się, by o nim zapomnieć. Zajrzyj do jego wyjść – zarówno zwykłego, jak i tego z błędami. Może się tam znaleźć coś istotnego: komunikat o awarii, ostrzeżenie lub po prostu potwierdzenie, że wszystko poszło pomyślnie.
Następnie koniecznie zamknij wszystkie powiązane strumienie – InputStream, OutputStream i ErrorStream. To jak zgaszenie światła po wyjściu: oszczędza zasoby i utrzymuje porządek w systemie.
I pamiętaj: jeśli zatrzymasz proces przymusowo, może on nie zdążyć poprawnie zapisać swojego wyjścia. Nic strasznego – to naturalne zachowanie w takich sytuacjach.
7. Przykład: uruchomienie długiego polecenia i jego zakończenie
Załóżmy, że chcemy uruchomić długotrwały proces (np. ping z dużą liczbą pakietów) i zakończyć go po 3 sekundach.
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"};
// Dla Linux/Mac: {"ping", "google.com", "-c", "100"}
ProcessBuilder builder = new ProcessBuilder(command);
Process process = builder.start();
// Czytamy wyjście procesu w osobnym wątku
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) {
// Ignorujemy
}
});
readerThread.start();
// Czekamy 3 sekundy
if (!process.waitFor(3, TimeUnit.SECONDS)) {
System.out.println("Proces działa zbyt długo, przerywamy...");
process.destroy();
process.waitFor();
}
readerThread.join(); // Poczekaj na odczyt wyjścia
System.out.println("Proces zakończony. Kod wyjścia: " + process.exitValue());
}
}
8. Typowe błędy przy zarządzaniu procesami
Błąd nr 1: Nie czekać na zakończenie procesu. Jeśli uruchomiłeś proces i po prostu o nim zapomniałeś (nie wywołałeś waitFor()), to proces może pozostać „wiszący” w systemie nawet po zakończeniu twojego programu w Javie. Strumienie wejścia/wyjścia mogą nie zostać zamknięte, co doprowadzi do wycieków pamięci i deskryptorów plików.
Błąd nr 2: Nieprawidłowe zakończenie procesu. Wywołano destroy(), ale nie poczekano na zakończenie procesu przez waitFor(). W rezultacie proces może pozostać „zombie” (zwłaszcza w systemach uniksowych).
Błąd nr 3: Oczekiwanie na kod wyjścia przed zakończeniem procesu. Wywołanie exitValue() przed zakończeniem procesu prowadzi do wyjątku IllegalThreadStateException.
Błąd nr 4: Brak obsługi wyjątków. Metody pracy z procesami mogą wyrzucać IOException i InterruptedException. Nie zapominaj ich przechwytywać albo zadeklarować w sygnaturze metody.
Błąd nr 5: Nieczytanie wyjścia procesu. Jeśli nie czytasz stdout i stderr, proces może się zawiesić z powodu przepełnienia bufora wyjścia.
GO TO FULL VERSION