7

Estoy escribiendo una aplicación Java que necesita usar una aplicación de línea de comando externa usando la biblioteca Apache Commons Exec. La aplicación que necesito ejecutar tiene un tiempo de carga bastante largo, por lo que sería preferible mantener viva una instancia en lugar de crear un proceso nuevo cada vez. La forma en que funciona la aplicación es muy simple. Una vez iniciado, espera una nueva entrada y genera algunos datos como salida, los cuales usan la E/S estándar de la aplicación.Problema al proporcionar entrada múltiple a un comando usando Apache Commons Exec y extraer la salida

Así que la idea sería ejecutar CommandLine, y luego usar PumpStreamHandler con tres flujos separados (salida, error y entrada) y usar esas transmisiones para interactuar con la aplicación. Hasta ahora, he tenido este trabajo en escenarios básicos donde tengo una entrada, una salida y luego la aplicación se apaga. Pero tan pronto como trato de tener una segunda transacción, algo sale mal.

Después de haber creado mi CommandLine, creo mi Ejecutor y poner en marcha este modo:

this.executor = new DefaultExecutor(); 

PipedOutputStream stdout = new PipedOutputStream(); 
PipedOutputStream stderr = new PipedOutputStream(); 
PipedInputStream stdin = new PipedInputStream(); 
PumpStreamHandler streamHandler = new PumpStreamHandler(stdout, stderr, stdin); 

this.executor.setStreamHandler(streamHandler); 

this.processOutput = new BufferedInputStream(new PipedInputStream(stdout)); 
this.processError = new BufferedInputStream(new PipedInputStream(stderr)); 
this.processInput = new BufferedOutputStream(new PipedOutputStream(stdin)); 

this.resultHandler = new DefaultExecuteResultHandler(); 
this.executor.execute(cmdLine, resultHandler); 

entonces proceder al lanzamiento de tres hilos diferentes, cada uno de manejo de una corriente diferente. También tengo tres SynchronousQueues que manejan entrada y salida (uno usado como entrada para el flujo de entrada, uno para informar al outputQueue que un nuevo comando ha sido lanzado y uno para salida). Por ejemplo, el hilo flujo de entrada tiene el siguiente aspecto:

while (!killThreads) { 
    String input = inputQueue.take(); 

    processInput.write(input.getBytes()); 
    processInput.flush(); 

    IOQueue.put(input); 
} 

Si quito el bucle de tiempo y acaba de ejecutar esta vez, todo parece funcionar perfectamente. Obviamente, si intento ejecutarlo de nuevo, PumpStreamHandler lanza una excepción porque se ha accedido por dos hilos diferentes.

El problema aquí es que parece que processInput no se vacía realmente hasta que termina el hilo. Cuando se depura, la aplicación de línea de comandos solo recibe su entrada una vez que termina el hilo, pero nunca la obtiene si se mantiene el ciclo while. He intentado muchas cosas diferentes para que el processInput se descargue pero nada parece funcionar.

¿Alguien ha intentado algo similar antes? ¿Hay algo que me falta? ¡Cualquier ayuda sería muy apreciada!

+0

debe agregar una etiqueta java a su publicación, a los 'ojos' más experimentados en su problema. Buena suerte. – shellter

Respuesta

8

Terminé descubriendo una forma de hacer que esto funcione. Al mirar dentro del código de la biblioteca Commons Exec, noté que los StreamPumpers utilizados por PumpStreamHandler no se descargaban cada vez que recibían datos nuevos. Es por eso que el código funcionó cuando lo ejecuté solo una vez, ya que se vació automáticamente y cerró la transmisión. Así que creé clases que llamé AutoFlushingStreamPumper y AutoFlushingPumpStreamHandler. Lo último es lo mismo que un PumpStreamHandler normal, pero usa AutoFlushingStreamPumpers en lugar de los habituales. AutoFlushingStreamPumper hace lo mismo que un StreamPumper estándar, pero vacía su flujo de salida cada vez que le escribe algo.

Lo he probado bastante extensamente y parece funcionar bien. ¡Gracias a todos los que han tratado de resolver esto!

+2

Ayuda a un hermano a salir? Tengo el mismo problema, ¿podría hacerme un "sólido" y publicar el código que escribió (AutoFlushingStreamPumper y AutoFlushingPumpStreamHandler) aquí o en una idea o algo así? No tiene sentido reinventar esa rueda ... ¡Gracias por tu publicación! – GroovyCakes

+2

Esto ayudó inmensamente, pero como mencionó @GroovyCakes, un Gist hubiera ayudado más, así que aquí hay uno. Tenga en cuenta que esto es lo que estoy usando, no necesariamente lo que utilizó el OP. https://gist.github.com/4653381 –

+0

¿Existe un ejemplo que utiliza AutoFlushingStreamPumper y AutoFlushingPumpStreamHandler? – Johan

1

Para mis propósitos, resulta que solo tuve que anular el "ExecuteStreamHandler". Aquí está mi solución, que captura stderr en un StringBuilder, y le permite transmitir cosas a la entrada estándar y recibir las cosas desde la salida estándar:

class SendReceiveStreamHandler implements ExecuteStreamHandler 

se puede ver toda la clase como una esencia en GitHub here.

+0

En su código, Receiver, TransferCompleteEvent y DataReceivedEvent no están en sus importaciones. ¿Qué paquete contiene estas clases? – Yeti

0

Para ser capaz de escribir más de un comando en el STDIN del proceso, tengo que crear una nueva

import java.io.BufferedWriter; 
import java.io.File; 
import java.io.IOException; 
import java.io.OutputStreamWriter; 
import java.util.Map; 

import org.apache.commons.exec.CommandLine; 
import org.apache.commons.exec.DefaultExecutor; 
import org.apache.commons.lang3.CharEncoding; 

public class ProcessExecutor extends DefaultExecutor { 

    private BufferedWriter processStdinput; 

    @Override 
    protected Process launch(CommandLine command, Map env, File dir) throws IOException { 
     Process process = super.launch(command, env, dir); 
     processStdinput = new BufferedWriter(new OutputStreamWriter(process.getOutputStream(), CharEncoding.UTF_8)); 
     return process; 
    } 

    /** 
    * Write a line in the stdin of the process. 
    * 
    * @param line 
    *   does not need to contain the carriage return character. 
    * @throws IOException 
    *    in case of error when writing. 
    * @throws IllegalStateException 
    *    if the process was not launched. 
    */ 
    public void writeLine(String line) throws IOException { 
     if (processStdinput != null) { 
      processStdinput.write(line); 
      processStdinput.newLine(); 
      processStdinput.flush(); 
     } else { 
      throw new IllegalStateException(); 
     } 
    } 

} 

Para utilizar este nuevo Ejecutor, sigo la corriente por tubería dentro de la PumpStreamHandler para evitar que la STDIN para estar cerca de PumpStreamHandler.

ProcessExecutor executor = new ProcessExecutor(); 
executor.setExitValue(0); 
executor.setWorkingDirectory(workingDirectory); 
executor.setWatchdog(new ExecuteWatchdog(ExecuteWatchdog.INFINITE_TIMEOUT)); 
executor.setStreamHandler(new PumpStreamHandler(outHanlder, outHanlder, new PipedInputStream(new PipedOutputStream()))); 
executor.execute(commandLine, this); 

Puede utilizar el método executor writeLine() o crear uno propio.

Cuestiones relacionadas