A6.B Hablar con el proceso hijo¶
Un hijo tiene tres flujos: entrada (stdin), salida (stdout) y salida de error (stderr). Desde el punto de vista del padre, la dirección es la contraria a la que parece:
| Flujo del hijo | El padre usa | Dirección |
|---|---|---|
stdin (lo que el hijo lee) |
getOutputStream() o outputWriter() |
padre → hijo |
stdout (lo que el hijo escribe) |
getInputStream() o inputReader() |
hijo → padre |
stderr (los errores del hijo) |
getErrorStream() o errorReader() |
hijo → padre |
El nombre confunde
getInputStream() lee la salida del hijo: «input» es entrada para el padre. Y getOutputStream() sirve para enviar datos al hijo.
Leer lo que escribe el hijo¶
Lo habitual es leer línea a línea. Con redirectErrorStream(true) la salida de error se mezcla con la normal, y basta con leer un solo flujo:
import java.io.BufferedReader;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
public class LeerLineas {
public static void main(String[] args) throws IOException, InterruptedException {
String java = Path.of(System.getProperty("java.home"), "bin", "java").toString();
ProcessBuilder pb = new ProcessBuilder(java, "Charlatan.java");
pb.redirectErrorStream(true); // mezcla la salida de error con la salida normal
Process p = pb.start();
try (BufferedReader lector = p.inputReader(StandardCharsets.UTF_8)) {
String linea;
while ((linea = lector.readLine()) != null) {
System.out.println("[hijo] " + linea);
}
}
System.out.println("Código: " + p.waitFor());
}
}
Salida:
[hijo] linea 1 (salida normal)
[hijo] ERROR: algo ha ido mal
[hijo] linea 2 (salida normal)
[hijo] ERROR: otro fallo
[hijo] linea 3 (salida normal)
Código: 0
Si prefieres separarlas, se puede mandar una de ellas a un fichero con redirectError(...):
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
public class Separados {
public static void main(String[] args) throws IOException, InterruptedException {
String java = Path.of(System.getProperty("java.home"), "bin", "java").toString();
ProcessBuilder pb = new ProcessBuilder(java, "Charlatan.java");
pb.redirectError(new File("errores.log")); // la salida de error va a un fichero
Process p = pb.start();
try (BufferedReader lector = p.inputReader(StandardCharsets.UTF_8)) {
String linea;
while ((linea = lector.readLine()) != null) {
System.out.println("normal: " + linea);
}
}
p.waitFor();
for (String linea : Files.readAllLines(Path.of("errores.log"))) {
System.out.println("error: " + linea);
}
}
}
Salida:
normal: linea 1 (salida normal)
normal: linea 2 (salida normal)
normal: linea 3 (salida normal)
error: ERROR: algo ha ido mal
error: ERROR: otro fallo
Enviar datos al hijo¶
Para darle datos al hijo se escribe en su stdin. Hay que cerrar el flujo cuando se terminan los datos: el hijo Eco sigue leyendo hasta que no hay más, y eso solo ocurre al cerrar.
import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
public class Enviar {
public static void main(String[] args) throws IOException, InterruptedException {
String java = Path.of(System.getProperty("java.home"), "bin", "java").toString();
Process p = new ProcessBuilder(java, "Eco.java").start();
try (BufferedWriter escritor = p.outputWriter(StandardCharsets.UTF_8)) {
escritor.write("pera\nmanzana\nkiwi\n");
} // cerrar el flujo = «ya no hay más datos»
System.out.print(new String(p.getInputStream().readAllBytes(), StandardCharsets.UTF_8));
p.waitFor();
}
}
Salida:
ECO: PERA
ECO: MANZANA
ECO: KIWI
(fin de los datos)
Si no se cierra el flujo, el hijo espera para siempre
Aquí se envía un dato pero no se cierra el flujo. El hijo no sabe que no habrá más y se queda esperando; el padre lo comprueba con un límite de tiempo y lo detiene:
import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.concurrent.TimeUnit;
public class SinCerrar {
public static void main(String[] args) throws IOException, InterruptedException {
String java = Path.of(System.getProperty("java.home"), "bin", "java").toString();
Process p = new ProcessBuilder(java, "Eco.java").start();
BufferedWriter escritor = p.outputWriter(StandardCharsets.UTF_8);
escritor.write("pera\n");
escritor.flush(); // ¡se envía, pero NO se cierra el flujo!
boolean terminado = p.waitFor(3, TimeUnit.SECONDS);
System.out.println("¿Ha terminado el hijo? " + terminado);
p.destroy();
}
}
Salida:
¿Ha terminado el hijo? false
Redirecciones¶
Además de leer los flujos a mano, ProcessBuilder puede conectarlos directamente a ficheros o a la consola:
| Orden | Qué hace |
|---|---|
inheritIO() |
El hijo usa la consola del padre |
redirectOutput(File) |
La salida va a un fichero y lo sobrescribe |
redirectOutput(Redirect.appendTo(File)) |
La salida se añade al final del fichero |
redirectError(...), redirectInput(File) |
Lo mismo para la salida de error y para la entrada |
redirectErrorStream(true) |
Mezcla la salida de error con la normal |
Redirect.DISCARD |
Descarta la salida |
En este ejemplo el hijo se lanza dos veces: la salida normal se sobrescribe (queda con las líneas de una ejecución) y los errores se añaden (dos ejecuciones):
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
public class Redirecciones {
public static void main(String[] args) throws IOException, InterruptedException {
String java = Path.of(System.getProperty("java.home"), "bin", "java").toString();
for (int vez = 1; vez <= 2; vez++) {
ProcessBuilder pb = new ProcessBuilder(java, "Charlatan.java");
pb.redirectOutput(new File("salida.log")); // SOBRESCRIBE
pb.redirectError(ProcessBuilder.Redirect.appendTo(new File("errores.log"))); // AÑADE al final
pb.start().waitFor();
}
List<String> salida = Files.readAllLines(Path.of("salida.log"));
List<String> errores = Files.readAllLines(Path.of("errores.log"));
System.out.println("salida.log: " + salida.size() + " líneas");
System.out.println("errores.log: " + errores.size() + " líneas");
}
}
Salida:
salida.log: 3 líneas
errores.log: 4 líneas
Dos trampas clásicas¶
1. Si no lees la salida del hijo, puede quedarse bloqueado. Las tuberías tienen un búfer pequeño: cuando se llena, el hijo se detiene hasta que alguien lea. Aquí el hijo escribe cien mil líneas y el padre no lee ninguna:
import java.io.IOException;
import java.nio.file.Path;
import java.util.concurrent.TimeUnit;
public class BufferLleno {
public static void main(String[] args) throws IOException, InterruptedException {
String java = Path.of(System.getProperty("java.home"), "bin", "java").toString();
Process p = new ProcessBuilder(java, "Parlanchin.java").start(); // escribe 100 000 líneas
// ... y el padre NO lee su salida
boolean terminado = p.waitFor(4, TimeUnit.SECONDS);
System.out.println("¿Ha terminado el hijo? " + terminado);
p.destroyForcibly();
}
}
Salida:
¿Ha terminado el hijo? false
Por eso: lee la salida antes de llamar a waitFor() (o mándala a un fichero con redirectOutput).
2. Nunca pegues datos del usuario a una orden de la shell. Si el padre construye la orden con "echo " + dato, quien escriba hola & echo INYECTADO consigue ejecutar otra orden. Es la inyección de órdenes. Lo seguro es que el dato viaje como un argumento de un programa, sin pasar por la shell:
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
public class Inyeccion {
static String ejecutar(ProcessBuilder pb) throws IOException, InterruptedException {
pb.redirectErrorStream(true);
Process p = pb.start();
String salida = new String(p.getInputStream().readAllBytes(), StandardCharsets.UTF_8).trim();
p.waitFor();
return salida;
}
public static void main(String[] args) throws IOException, InterruptedException {
String java = Path.of(System.getProperty("java.home"), "bin", "java").toString();
String dato = "hola & echo INYECTADO"; // lo que escribe el usuario
System.out.println("--- MAL: el dato se pega a una orden de la shell (cmd /c) ---");
System.out.println(ejecutar(new ProcessBuilder("cmd", "/c", "echo " + dato)));
System.out.println("--- BIEN: el dato viaja como UN argumento de un programa ---");
System.out.println(ejecutar(new ProcessBuilder(java, "Contexto.java", dato)));
}
}
Salida:
--- MAL: el dato se pega a una orden de la shell (cmd /c) ---
hola
INYECTADO
--- BIEN: el dato viaja como UN argumento de un programa ---
MODO = null
Carpeta de trabajo: C:\Users\ana\proyecto
Argumentos: hola & echo INYECTADO
La primera forma ejecuta dos órdenes (la segunda no la ha escrito el programador). En la segunda, & es solo texto. La shell (cmd /c, bash -c) solo se usa cuando de verdad se necesitan tuberías, comodines u órdenes internas, y nunca con datos sin controlar.
Qué recordar¶
getInputStream()lee la salida del hijo;getOutputStream()le envía datos.- Para enviar datos hay que cerrar el flujo al terminar.
- Lee antes de
waitFor(): si no, el hijo puede bloquearse. redirectOutputsobrescribe;Redirect.appendToañade.- Datos del usuario: siempre como argumentos, nunca pegados a una orden de shell.
Siguiente: A6.C Controlar y observar procesos.