7

Piszę aplikację Java, która musi korzystać z zewnętrznej aplikacji wiersza poleceń przy użyciu biblioteki Apache Commons Exec. Aplikacja, którą muszę uruchomić, ma dość długi czas ładowania, dlatego lepiej byłoby utrzymać jedną instancję przy życiu, zamiast tworzyć nowy proces za każdym razem. Sposób działania aplikacji jest bardzo prosty. Po uruchomieniu oczekuje na kilka nowych danych wejściowych i generuje dane jako dane wyjściowe, z których oba korzystają ze standardowego wejścia/wyjścia aplikacji.Kłopoty z wprowadzaniem wielu poleceń do polecenia za pomocą Apache Commons Exec i wypakowywanie wyjścia

Ideą byłoby wykonanie CommandLine, a następnie użycie PumpStreamHandler z trzema oddzielnymi strumieniami (wyjście, błąd i dane wejściowe) i wykorzystanie tych strumieni do interakcji z aplikacją. Do tej pory miałem tę pracę w podstawowych scenariuszach, w których mam jedno wejście, jedno wyjście, a następnie aplikacja wyłącza się. Ale gdy tylko próbuję przeprowadzić drugą transakcję, coś idzie nie tak.

Po stworzył mój CommandLine, tworzę Executor i uruchomić go tak:

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

I następnie przystąpić do uruchomienia trzy różne tematy, każdy z jednym magazynowe inny strumień. Mam również trzy SynchronousQueues, które obsługują wejście i wyjście (jeden używany jako wejście dla strumienia wejściowego, jeden do poinformowania outputQueue, że nowe polecenie zostało uruchomione i jedno dla wyjścia). Na przykład, nić strumień wejściowy wygląda następująco:

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

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

    IOQueue.put(input); 
} 

Jeśli usunąć pętli while i wykonać tylko ten jeden raz, wszystko wydaje się działać idealnie. Oczywiście, jeśli spróbuję wykonać to ponownie, PumpStreamHandler zgłasza wyjątek, ponieważ dostęp do niego miały dwa różne wątki.

Problem polega na tym, że wygląda na to, że processInput nie jest naprawdę przepłukiwany, dopóki wątek się nie skończy. Po debugowaniu aplikacja wiersza poleceń naprawdę otrzymuje swoje wejście dopiero po zakończeniu wątku, ale nigdy nie otrzyma go, jeśli pętla while będzie zachowana. Próbowałem wiele różnych rzeczy, aby proces processInput się spłukał, ale nic nie działa.

Czy ktoś wcześniej próbował czegoś podobnego? Czy jest coś, czego mi brakuje? Każda pomoc będzie bardzo ceniona!

+0

powinieneś dodać tag java do swojego posta, do najbardziej doświadczonych "oczu" na temat twojego problemu. Powodzenia. – shellter

Odpowiedz

8

Skończyło się na zastanawianiu, jak to zrobić. Przeglądając kod biblioteki Commons Exec, zauważyłem, że StreamPumpers używane przez PumpStreamHandler nie przepłukiwały się za każdym razem, gdy otrzymywały nowe dane. Dlatego kod zadziałał, gdy wykonałem go tylko raz, ponieważ automatycznie przepłukał i zamknął strumień. Stworzyłem więc klasy, które nazwałem AutoFlushingStreamPumper i AutoFlushingPumpStreamHandler. Później jest taki sam jak normalny PumpStreamHandler, ale używa AutoFlushingStreamPumpers zamiast zwykłych. AutoFlushingStreamPumper robi to samo, co standardowy StreamPumper, ale wypróżnia swój strumień wyjściowy za każdym razem, gdy coś do niego napisze.

Przetestowałem to dość szeroko i wydaje się, że działa dobrze. Dziękuję wszystkim, którzy próbowali to rozgryźć!

+2

Pomóż bratu? Mam ten sam problem, czy możesz zrobić mi "solidną" i opublikować kod, który napisałeś (AutoFlushingStreamPumper i AutoFlushingPumpStreamHandler) tutaj lub do sedna czy coś takiego? Nie ma sensu wymyślać tego koła ... Dzięki za twój post! – GroovyCakes

+2

Pomogło to niezmiernie, ale jak wspomniał @GroovyCakes, Gist pomógłby więcej - więc tutaj jest jeden. Zauważ, że to jest to, czego używam, niekoniecznie co używał OP. https://gist.github.com/4653381 –

+0

Czy istnieje przykład korzystający z AutoFlushingStreamPumper i AutoFlushingPumpStreamHandler? – Johan

1

Dla moich celów okazało się, że wystarczy zastąpić "ExecuteStreamHandler". Oto moje rozwiązanie, które przechwytuje stderr do StringBuilder i pozwala przesyłać rzeczy do standardowego wejścia i odbierać rzeczy z stdout:

class SendReceiveStreamHandler implements ExecuteStreamHandler 

Można zobaczyć całą klasę jako GIST na GitHub here.

+0

W kodzie odbiorca, TransferCompleteEvent i DataReceivedEvent nie znajduje się w imporcie. Który pakiet zawiera te klasy? – Yeti

0

Aby móc napisać więcej niż jedno polecenie w STDIN procesu, muszę utworzyć nowy

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

} 

Aby korzystać z tej nowej Executor, trzymam rurami strumień w PumpStreamHandler aby uniknąć że STDIN, aby być blisko przez 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); 

Można użyć metody executor writeLine() lub utworzyć własną.

Powiązane problemy