Proceso Java con flujo de entrada / salida

90

Tengo el siguiente ejemplo de código a continuación. Por lo que puede ingresar un comando en el shell bash, es decir, echo testy hacer que el resultado se haga eco. Sin embargo, después de la primera lectura. ¿Otros flujos de salida no funcionan?

¿Por qué es esto o estoy haciendo algo mal? Mi objetivo final es crear una tarea programada subprocesada que ejecute un comando periódicamente para / bash para que OutputStreamy InputStreamtengan que trabajar en conjunto y no dejar de funcionar. También he estado experimentando el error ¿ java.io.IOException: Broken pipealguna idea?

Gracias.

String line;
Scanner scan = new Scanner(System.in);

Process process = Runtime.getRuntime ().exec ("/bin/bash");
OutputStream stdin = process.getOutputStream ();
InputStream stderr = process.getErrorStream ();
InputStream stdout = process.getInputStream ();

BufferedReader reader = new BufferedReader (new InputStreamReader(stdout));
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(stdin));

String input = scan.nextLine();
input += "\n";
writer.write(input);
writer.flush();

input = scan.nextLine();
input += "\n";
writer.write(input);
writer.flush();

while ((line = reader.readLine ()) != null) {
System.out.println ("Stdout: " + line);
}

input = scan.nextLine();
input += "\n";
writer.write(input);
writer.close();

while ((line = reader.readLine ()) != null) {
System.out.println ("Stdout: " + line);
}
James Moore
fuente
"Tubería rota" probablemente significa que el proceso hijo ha salido. No he examinado completamente el resto de su código para ver cuáles son los otros problemas.
vanza
1
use hilos separados, funcionará bien
Johnydep

Respuestas:

139

En primer lugar, recomendaría reemplazar la línea

Process process = Runtime.getRuntime ().exec ("/bin/bash");

con las lineas

ProcessBuilder builder = new ProcessBuilder("/bin/bash");
builder.redirectErrorStream(true);
Process process = builder.start();

ProcessBuilder es nuevo en Java 5 y facilita la ejecución de procesos externos. En mi opinión, su mejora más significativa Runtime.getRuntime().exec()es que le permite redirigir el error estándar del proceso hijo a su salida estándar. Esto significa que solo tienes unoInputStream para leer. Antes de esto, necesitaba tener dos subprocesos separados, uno de lectura stdouty otro de lectura stderr, para evitar que el búfer de error estándar se llene mientras el búfer de salida estándar estaba vacío (lo que provocaba que el proceso hijo se bloqueara), o viceversa.

A continuación, los bucles (de los cuales tienes dos)

while ((line = reader.readLine ()) != null) {
    System.out.println ("Stdout: " + line);
}

solo salir cuando el reader , que lee de la salida estándar del proceso, devuelve el final del archivo. Esto solo sucede cuando el bashproceso finaliza. No devolverá el final del archivo si actualmente no hay más resultados del proceso. En su lugar, esperará la siguiente línea de salida del proceso y no regresará hasta que tenga la siguiente línea.

Como está enviando dos líneas de entrada al proceso antes de llegar a este bucle, el primero de estos dos bucles se bloqueará si el proceso no ha salido después de estas dos líneas de entrada. Se quedará allí esperando a que se lea otra línea, pero nunca habrá otra línea para leer.

Compilé su código fuente (estoy en Windows en este momento, así que lo reemplacé /bin/bashconcmd.exe , pero los principios deben ser los mismos), y me encontré con lo siguiente:

  • después de escribir dos líneas, aparece la salida de los dos primeros comandos, pero luego el programa se cuelga,
  • si escribo, digamos, echo testy luego exit, el programa sale del primer ciclo desde que el cmd.exeproceso ha finalizado. Luego, el programa solicita otra línea de entrada (que se ignora), salta directamente el segundo ciclo ya que el proceso hijo ya ha salido y luego sale por sí mismo.
  • si escribo exity luego echo test, obtengo una IOException quejándose de que una tubería está cerrada. Esto es de esperar: la primera línea de entrada provocó la salida del proceso y no hay ningún lugar para enviar la segunda línea.

He visto un truco que hace algo similar a lo que parece querer, en un programa en el que solía trabajar. Este programa mantuvo alrededor de una serie de shells, ejecutó comandos en ellos y leyó la salida de estos comandos. El truco utilizado fue escribir siempre una línea "mágica" que marque el final de la salida del comando de shell y usarla para determinar cuándo había terminado la salida del comando enviado al shell.

Tomé su código y reemplacé todo después de la línea que se asigna writercon el siguiente bucle:

while (scan.hasNext()) {
    String input = scan.nextLine();
    if (input.trim().equals("exit")) {
        // Putting 'exit' amongst the echo --EOF--s below doesn't work.
        writer.write("exit\n");
    } else {
        writer.write("((" + input + ") && echo --EOF--) || echo --EOF--\n");
    }
    writer.flush();

    line = reader.readLine();
    while (line != null && ! line.trim().equals("--EOF--")) {
        System.out.println ("Stdout: " + line);
        line = reader.readLine();
    }
    if (line == null) {
        break;
    }
}

Después de hacer esto, podría ejecutar de manera confiable algunos comandos y hacer que la salida de cada uno me regresara individualmente.

Los dos echo --EOF--comandos en la línea enviados al shell están ahí para garantizar que la salida del comando termine --EOF--incluso en el resultado de un error del comando.

Por supuesto, este enfoque tiene sus limitaciones. Estas limitaciones incluyen:

  • si ingreso un comando que espera la entrada del usuario (por ejemplo, otro shell), el programa parece bloquearse,
  • asume que cada proceso ejecutado por el shell finaliza su salida con una nueva línea,
  • se confunde un poco si el comando que está ejecutando el shell escribe una línea --EOF--.
  • bashinforma un error de sintaxis y sale si ingresa algún texto con un ).

Es posible que estos puntos no le importen si lo que sea que esté pensando en ejecutar como una tarea programada se limitará a un comando o un pequeño conjunto de comandos que nunca se comportarán de forma tan patológica.

EDITAR : mejore el manejo de salidas y otros cambios menores después de ejecutar esto en Linux.

Luke Woodward
fuente
Gracias por la respuesta completa. Sin embargo, creo que he identificado el verdadero caso de mis problemas. Me llamó la atención en su publicación. stackoverflow.com/questions/3645889/… . Gracias.
James Moore
1
reemplazar / bin / bash con cmd realmente no se comportó igual, ya que hice un programa que hizo lo mismo pero tenía problemas con bash. En mi caso, abrir tres subprocesos separados para cada entrada / salida / err funciona mejor sin ningún problema para los comandos interactivos de sesión larga.
Johnydep
@AlexMills: ¿su segundo comentario significa que ha resuelto su problema? No hay suficientes detalles en su primer comentario para decir cuál es el problema, ni por qué está recibiendo excepciones 'de repente'.
Luke Woodward
@ Luke, el enlace que proporcioné justo encima de tu comentario resuelve mi problema
Alexander Mills
4

Creo que puede usar un hilo como demon-thread para leer su entrada y su lector de salida ya estará en el bucle while en el hilo principal para que pueda leer y escribir al mismo tiempo. Puede modificar su programa de esta manera:

Thread T=new Thread(new Runnable() {

    @Override
    public void run() {
        while(true)
        {
            String input = scan.nextLine();
            input += "\n";
            try {
                writer.write(input);
                writer.flush();
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }

        }

    }
} );
T.start();

y puede que el lector sea el mismo que el anterior, es decir

while ((line = reader.readLine ()) != null) {
    System.out.println ("Stdout: " + line);
}

Haga que su escritor sea definitivo, de lo contrario, no podrá acceder a él la clase interna.

Shashank Jain
fuente
1

Tienes writer.close();en tu código. Entonces, bash recibe EOF stdiny sale. Entonces obtienes Broken pipeal intentar leer desde stdoutel difunto bash.

gpeche
fuente