CodeGym /Cursos /JAVA 25 SELF /Gestión del ciclo de vida de un proceso

Gestión del ciclo de vida de un proceso

JAVA 25 SELF
Nivel 61, Lección 2
Disponible

1. Esperar a que termine el proceso: waitFor()

Cuando lanzas un proceso externo desde Java, vive su propia vida: hace cálculos, escribe en la consola, a veces se bloquea (hola, ping con un millón de paquetes). Pero a veces necesitamos saber cuándo ha terminado, cómo ha terminado (éxito o fallo) y —si algo salió mal— poder «matarlo». En general, como en la vida real: si le encargaste una tarea a alguien, no está de más saber si cumplió y tener la posibilidad de detenerlo si de repente empieza a preparar café eternamente.

Cuando ejecutas un proceso mediante ProcessBuilder, obtienes un objeto de tipo Process. Este objeto es como un mando a distancia: con él puedes esperar a que el proceso termine, obtener el código de salida, finalizar el proceso y gestionar los flujos de entrada/salida.

¿Cómo esperar a que termine un proceso?

Para ello existe el método waitFor():

ProcessBuilder builder = new ProcessBuilder("ping", "google.com", "-c", "3");
Process process = builder.start();

System.out.println("Esperamos a que termine el proceso...");
int exitCode = process.waitFor(); // Bloquea el hilo actual hasta que el proceso termine
System.out.println("El proceso terminó con el código: " + exitCode);

¿Qué ocurre?

  • El programa lanza un proceso externo (ping).
  • Después, el hilo en el que se invoca waitFor() «se duerme» hasta que el proceso externo termina.
  • En cuanto el proceso externo termina, waitFor() devuelve el código de salida (normalmente 0 = éxito; distinto de 0 = error).

¿Cómo obtener el código de salida?

El método waitFor() devuelve ese código. También puedes obtenerlo por separado mediante exitValue():

int code = process.exitValue();

Pero cuidado: si el proceso aún no ha terminado, llamar a exitValue() lanzará la excepción IllegalThreadStateException. Por eso, normalmente primero se espera con waitFor() y luego se consulta el código.

2. Interrupción del proceso: destroy() y destroyForcibly()

A veces un proceso externo dura demasiado o se bloquea. Por ejemplo, alguien lanzó ping de 1000 paquetes y tu plazo vence en 5 segundos. Necesitas el «botón rojo».

Finalización suave: destroy()

El método destroy() envía al proceso una señal de finalización (SIGTERM en Unix, CTRL-BREAK en Windows) y se le da la oportunidad de terminar correctamente.

Process process = new ProcessBuilder("ping", "google.com").start();

Thread.sleep(2000); // Dejémosle trabajar 2 segundos
process.destroy(); // Le pediremos que termine

Finalización forzada: destroyForcibly()

Si el proceso no responde a las peticiones amables, puedes usar el método destroyForcibly(). Es como desenchufar el cable de la toma.

process.destroyForcibly();

Importante: tras llamar a destroy() o destroyForcibly() hay que esperar la terminación del proceso con waitFor(). A veces el proceso necesita tiempo para «asumir» su muerte.

3. Esperar al proceso con tiempo de espera

A partir de Java 8 existe un método con tiempo de espera:

boolean finished = process.waitFor(5, java.util.concurrent.TimeUnit.SECONDS);
if (finished) {
    System.out.println("¡El proceso terminó a tiempo!");
} else {
    System.out.println("El proceso no terminó; lo destruimos...");
    process.destroy();
}
  • Si el proceso termina en 5 segundos, el método devuelve true.
  • Si no — devuelve false y puedes actuar (por ejemplo, finalizar el proceso).

Ejemplo: Lancemos un comando largo e intentemos «matarlo»:

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("El proceso está tardando demasiado; lo destruimos...");
    process.destroy();
    // Se puede esperar a la finalización completa
    process.waitFor();
}
System.out.println("Código de salida: " + process.exitValue());

4. Práctica: miniutilidad para ejecutar un comando con tiempo de espera

Escribamos un programa sencillo que ejecute un comando externo con un tiempo de espera, espere su finalización y, si se supera el tiempo, finalice el proceso.

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("Introduce el comando a ejecutar (por ejemplo, ping google.com):");
        String commandLine = scanner.nextLine();
        String[] command = commandLine.split(" ");

        ProcessBuilder builder = new ProcessBuilder(command);
        Process process = builder.start();

        System.out.println("¿Cuántos segundos hay que esperar?");
        int timeout = scanner.nextInt();

        boolean finished = process.waitFor(timeout, TimeUnit.SECONDS);
        if (finished) {
            System.out.println("El proceso terminó por sí solo. Código de salida: " + process.exitValue());
        } else {
            System.out.println("¡Se acabó el tiempo! Finalizamos el proceso...");
            process.destroy();
            process.waitFor();
            System.out.println("El proceso se ha finalizado por la fuerza. Código de salida: " + process.exitValue());
        }
    }
}

Explicación:

  • El usuario introduce el comando que se debe ejecutar.
  • El programa espera el tiempo indicado.
  • Si el proceso no termina, lo destruye.

Truco: En Windows, ping hace 4 pings por defecto, y en Linux lo hace indefinidamente. Para Linux/Mac añade el parámetro -c 5 (por ejemplo, ping google.com -c 5).

5. Obtener el código de salida: ¿qué es y para qué sirve?

En el mundo de la línea de comandos se asume que, si una orden finaliza correctamente, devuelve el código 0. Si algo salió mal, devuelve cualquier otro código.

  • En sistemas tipo Unix: 0 = éxito; 1 o superior = error.
  • En Windows — más o menos igual.

Puedes usar este código para tomar decisiones en tu programa Java:

int exitCode = process.waitFor();
if (exitCode == 0) {
    System.out.println("¡Correcto!");
} else {
    System.out.println("¡Error! Código: " + exitCode);
}

6. Finalizar procesos colgados

A veces un proceso puede simplemente colgarse —por ejemplo, espera a que introduzcas algo o no puede conectarse a la red—. Si lo dejas sin supervisión, seguirá colgado en el sistema consumiendo recursos.

Para evitarlo, conviene ponerle un límite de paciencia al proceso. El método waitFor(timeout, unit) permite esperar su finalización solo un tiempo determinado. Si no llega a tiempo, termínalo sin miramientos.

Tras llamar a destroy() no basta con mirar a otro lado: hay que asegurarse de que el proceso realmente terminó. Para ello se vuelve a llamar a waitFor(). Y si tampoco funciona, queda la medida extrema, destroyForcibly(). Corta por lo sano.

Ejemplo con doble finalización

boolean finished = process.waitFor(5, TimeUnit.SECONDS);
if (!finished) {
    process.destroy();
    if (!process.waitFor(2, TimeUnit.SECONDS)) {
        process.destroyForcibly();
    }
}

Particularidades al trabajar con Process: qué es importante recordar

Cuando el proceso haya terminado, no te olvides de él tan rápido. Revisa su salida —la estándar y la de errores—. Puede haber algo importante: un mensaje de fallo, una advertencia o simplemente la confirmación de que todo fue bien.

Después, cierra todos los flujos asociados — InputStream, OutputStream y ErrorStream. Es como apagar la luz al salir: ahorra recursos y mantiene el orden en el sistema.

Y recuerda: si detuviste el proceso por la fuerza, puede que no llegue a volcar correctamente su salida. No pasa nada — es el comportamiento natural en esas situaciones.

7. Ejemplo: lanzar un comando largo y finalizarlo

Supongamos que queremos lanzar un proceso largo (por ejemplo, ping con un número alto de paquetes) y finalizarlo a los 3 segundos.

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"};
        // Para Linux/Mac: {"ping", "google.com", "-c", "100"}
        ProcessBuilder builder = new ProcessBuilder(command);
        Process process = builder.start();

        // Leemos la salida del proceso en un hilo aparte
        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) {
                // Ignoramos
            }
        });
        readerThread.start();

        // Esperamos 3 segundos
        if (!process.waitFor(3, TimeUnit.SECONDS)) {
            System.out.println("El proceso está tardando demasiado; lo destruimos...");
            process.destroy();
            process.waitFor();
        }
        readerThread.join(); // Esperar a que termine la lectura de la salida
        System.out.println("Proceso finalizado. Código de salida: " + process.exitValue());
    }
}

8. Errores típicos al gestionar procesos

Error n.º 1: no esperar a que termine el proceso. Si lanzas un proceso y simplemente te olvidas de él (no llamas a waitFor()), el proceso puede quedarse colgado en el sistema incluso después de que termine tu programa Java. Los flujos de entrada/salida pueden quedar sin cerrar, lo que provocará fugas de memoria y de descriptores de archivo.

Error n.º 2: finalizar mal el proceso. Llamaste a destroy(), pero no esperaste su terminación con waitFor(). Como resultado, el proceso puede quedarse «zombi» (especialmente en sistemas Unix).

Error n.º 3: esperar el código de salida antes de que termine el proceso. Llamar a exitValue() antes de que termine el proceso provoca la excepción IllegalThreadStateException.

Error n.º 4: no gestionar las excepciones. Los métodos de trabajo con procesos pueden lanzar IOException e InterruptedException. No olvides capturarlas o declararlas en la firma del método.

Error n.º 5: no leer la salida del proceso. Si no lees stdout y stderr, el proceso puede quedarse bloqueado por el desbordamiento del búfer de salida.

1
Tarea
JAVA 25 SELF, nivel 61, lección 2
Bloqueada
Parada de emergencia del sistema 🚨
Parada de emergencia del sistema 🚨
1
Tarea
JAVA 25 SELF, nivel 61, lección 2
Bloqueada
Control de Lanzamiento del Cohete 🛰️
Control de Lanzamiento del Cohete 🛰️
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION