Saltar al contenido principal

Procesos externos con ProcessBuilder

Una aplicación Java puede iniciar otro proceso y observar su salida, errores y código de terminación. ProcessBuilder no interpreta una línea de shell: recibe el ejecutable y cada argumento por separado.

Un proceso Java portable​

Para evitar depender de bash, cmd.exe, ls o PowerShell, usaremos como proceso hijo una clase Java controlada:

public class ProcesoHijo {
public static void main(String[] args) {
String nombre = args.length == 0 ? "mundo" : args[0];
System.out.println("Hola, " + nombre);
System.err.println("Diagnóstico del hijo");
}
}

La ruta del ejecutable se obtiene de la JVM actual:

String java = Path.of(
System.getProperty("java.home"), "bin", "java").toString();

ProcessBuilder builder = new ProcessBuilder(
java,
"-cp",
System.getProperty("java.class.path"),
"ProcesoHijo",
"Ada Lovelace"
);

"Ada Lovelace" es un único argumento porque ocupa un elemento. No construyas "java ProcesoHijo Ada Lovelace" esperando que ProcessBuilder lo divida como una shell.

Directorio y entorno​

builder.directory(Path.of(".").toFile());
builder.environment().put("MODO_HIJO", "prueba");

directory(...) fija el directorio de trabajo del hijo. El mapa de environment() modifica el entorno que heredará el proceso hijo; no cambia el entorno del proceso Java padre. No registres secretos del entorno.

Consumir stdout y stderr​

Los procesos escriben en pipes con buffers limitados. Si el padre espera sin consumir una salida abundante, el hijo puede bloquearse. Para un ejemplo sencillo podemos combinar ambas salidas y consumirlas mientras el proceso vive:

builder.redirectErrorStream(true);
Process process = builder.start();

try (BufferedReader reader = process.inputReader(StandardCharsets.UTF_8)) {
reader.lines().forEach(line -> System.out.println("HIJO> " + line));
}

int exitCode = process.waitFor();
System.out.println("Código: " + exitCode);

Con redirectErrorStream(true), stderr se incorpora a stdout. Si necesitas conservarlos separados, debes consumir ambos correctamente, por ejemplo con tareas distintas o redirigiéndolos a archivos. Leer enormes salidas solo después de waitFor() no es un patrón universalmente seguro.

También existen redirecciones explícitas:

builder.redirectOutput(Path.of("salida.log").toFile());
builder.redirectError(Path.of("error.log").toFile());

Código de salida​

waitFor() espera la terminación y devuelve el código de salida. Por convenio, cero suele indicar éxito, pero el significado lo define cada programa. No llames a exitValue() antes de saber que terminó: puede lanzar IllegalThreadStateException.

Timeout y política de terminación​

Process process = builder.start();
boolean termino = process.waitFor(2, TimeUnit.SECONDS);

if (!termino) {
process.destroy();
if (!process.waitFor(500, TimeUnit.MILLISECONDS)) {
process.destroyForcibly();
process.waitFor();
}
}

Si waitFor(timeout, ...) devuelve false, el proceso sigue vivo. destroy() solicita una terminación normal. destroyForcibly() es el último recurso si continúa vivo y la política lo requiere; incluso entonces esperamos a que finalice.

Portabilidad​

Un ejecutable externo debe existir en la máquina destino y puede comportarse de forma distinta según plataforma. El ejemplo principal reutiliza el ejecutable Java actual. Si utilizas ls, grep, cmd.exe o PowerShell, documenta expresamente esa dependencia; no ocultes una shell detrás de bash -c o cmd /c.

Experimento observable​

Compila ProcesoHijo y un lanzador. Antes de ejecutar, predice:

  • qué argumentos recibirá el hijo;
  • qué stream leerá el padre tras combinar errores;
  • cuándo estará disponible el código de salida.

Repite y separa evidencia de garantía. Está garantizado que cada argumento se conserva como elemento y que waitFor() retorna tras terminar. No está garantizado cuánto tardará ni cómo se intercalarían stdout y stderr si se consumieran por separado.

Práctica​

Corrige esta construcción:

new ProcessBuilder("java ProcesoHijo argumento 1");

Solución​

new ProcessBuilder(
java,
"-cp",
System.getProperty("java.class.path"),
"ProcesoHijo",
"argumento 1"
);

Errores habituales​

  • Pasar toda la orden como una cadena.
  • Depender de una shell sin documentarlo.
  • Esperar antes de consumir una salida potencialmente grande.
  • Consultar exitValue() antes de terminar.
  • Creer que un timeout mata el proceso.
  • Usar destroyForcibly() como primera opción.
  • Imprimir secretos del entorno.

En JV5-07 integraremos estos criterios con tareas, races, interrupción y cierre.