7

Sto scrivendo un'applicazione Java che deve utilizzare un'applicazione esterna a riga di comando utilizzando la libreria Apache Commons Exec. L'applicazione che devo eseguire ha un tempo di caricamento abbastanza lungo, quindi sarebbe preferibile mantenere un'istanza attiva invece di creare un nuovo processo ogni volta. Il modo in cui funziona l'applicazione è molto semplice. Una volta avviato, attende alcuni nuovi input e genera alcuni dati come output, entrambi utilizzano l'I/O standard dell'applicazione.Problemi nel fornire più input a un comando utilizzando Apache Commons Exec ed estrazione dell'output

Quindi l'idea sarebbe di eseguire CommandLine e quindi utilizzare PumpStreamHandler con tre flussi separati (output, errore e input) e utilizzare tali flussi per interagire con l'applicazione. Finora, ho avuto questo lavoro in scenari di base in cui ho un input, un output e l'applicazione quindi si spegne. Ma non appena sto provando ad avere una seconda transazione, qualcosa va storto.

dopo aver creato il mio CommandLine, Creo il mio esecutore e lanciarlo in questo 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); 

ho quindi procedere al lancio di tre diversi fili, ciascuno di una gestione di un flusso diverso. Ho anche tre SynchronousQueues che gestiscono input e output (uno usato come input per il flusso di input, uno per informare l'outputQueue che è stato lanciato un nuovo comando e uno per l'output). Ad esempio, il filo flusso di input è simile al seguente:

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

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

    IOQueue.put(input); 
} 

Se rimuovo il ciclo while e proprio eseguo per questa volta, tutto sembra funzionare perfettamente. Ovviamente, se provo ad eseguirlo di nuovo, PumpStreamHandler genera un'eccezione perché è stata letta da due thread differenti.

Il problema qui è che sembra che il processoInput non sia veramente svuotato fino alla fine del thread. Quando viene eseguito il debug, l'applicazione della riga di comando riceve il proprio input solo quando il thread termina, ma non lo ottiene mai se il ciclo while viene mantenuto. Ho provato molte cose diverse per far sì che processInput venisse svuotato, ma nulla sembra funzionare.

Qualcuno ha tentato qualcosa di simile prima? C'è qualcosa che mi manca? Qualsiasi aiuto sarebbe molto apprezzato!

+0

dovresti aggiungere un tag java al tuo post, agli "occhi" più esperti sul tuo problema. In bocca al lupo. – shellter

risposta

8

Alla fine ho scoperto un modo per farlo funzionare. Guardando all'interno del codice della libreria di Commons Exec, ho notato che gli StreamPumper usati da PumpStreamHandler non scaricavano ogni volta che avevano nuovi dati in arrivo. Questo è il motivo per cui il codice ha funzionato quando l'ho eseguito solo una volta, poiché ha automaticamente svuotato e chiuso lo stream. Così ho creato classi che ho chiamato AutoFlushingStreamPumper e AutoFlushingPumpStreamHandler. Più tardi è lo stesso di un normale PumpStreamHandler ma utilizza AutoFlushingStreamPumpers invece dei soliti. AutoFlushingStreamPumper fa lo stesso di un StreamPumper standard, ma svuota il flusso di output ogni volta che scrive qualcosa.

L'ho testato abbastanza estesamente e sembra funzionare bene. Grazie a tutti coloro che hanno cercato di capirlo!

+2

Aiuta un fratello a uscire? Ho lo stesso problema, potresti farmi un "solido" e postare il codice che hai scritto (AutoFlushingStreamPumper e AutoFlushingPumpStreamHandler) qui o in un gist o qualcosa del genere? Non ha senso reinventare quella ruota ... Grazie per il tuo post! – GroovyCakes

+2

Questo ha aiutato immensamente, ma come @GroovyCakes ha menzionato, un Gist avrebbe aiutato di più - quindi eccone uno. Nota questo è quello che sto usando, non necessariamente quello che l'OP ha usato. https://gist.github.com/4653381 –

+0

Esiste un esempio che utilizza AutoFlushingStreamPumper e AutoFlushingPumpStreamHandler? – Johan

1

Per i miei scopi, risulta necessario solo eseguire l'override di "ExecuteStreamHandler". Ecco la mia soluzione, che cattura stderr in uno StringBuilder, e ti permette di trasmettere cose da stdin e ricevere cose da stdout:

class SendReceiveStreamHandler implements ExecuteStreamHandler 

Si può vedere l'intera classe come un succo su GitHub here.

+0

Nel codice, Receiver, TransferCompleteEvent e DataReceivedEvent non sono inclusi nelle importazioni. Quale pacchetto contiene queste classi? – Yeti

0

Per essere in grado di scrivere più di un comando nel STDIN del processo, ho creare un nuovo

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(); 
     } 
    } 

} 

Per utilizzare questa nuova esecutore, continuo il flusso di sottofondo all'interno del PumpStreamHandler per evitare che la STDIN per essere vicino a 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); 

È possibile utilizzare il metodo executorine write() o crearne uno personalizzato.

Problemi correlati