Проблема с множественным вводом команды с использованием Apache Commons Exec и извлечением вывода
Я пишу Java-приложение, которое должно использовать внешнее приложение командной строки с использованием библиотеки Apache Commons Exec. Приложение, которое мне нужно запустить, имеет довольно длительное время загрузки, поэтому было бы предпочтительнее сохранять один экземпляр живым, а не каждый раз создавать новый процесс. Способ работы приложения очень прост. После запуска он ожидает некоторого нового ввода и генерирует некоторые данные в качестве вывода, оба из которых используют стандартный ввод-вывод приложения.
Поэтому идея состоит в том, чтобы выполнить CommandLine, а затем использовать PumpStreamHandler с тремя отдельными потоками (вывод, ошибка и ввод) и использовать эти потоки для взаимодействия с приложением. До сих пор у меня была эта работа в основных сценариях, когда у меня есть один вход, один выход, и приложение затем закрывается. Но как только я пытаюсь провести вторую транзакцию, что-то идет не так.
После создания моей CommandLine я создаю свой Executor и запускаю его так:
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);
Затем я приступаю к запуску трех разных потоков, каждый из которых обрабатывает отдельный поток. У меня также есть три SynchronousQueues, которые обрабатывают ввод и вывод (один используется в качестве ввода для входного потока, один для информирования outputQueue о запуске новой команды и один для вывода). Например, поток входного потока выглядит так:
while (!killThreads) {
String input = inputQueue.take();
processInput.write(input.getBytes());
processInput.flush();
IOQueue.put(input);
}
Если я удаляю цикл while и просто выполняю его один раз, кажется, что все работает отлично. Очевидно, что если я попытаюсь выполнить его снова, PumpStreamHandler выдает исключение, потому что к нему обращались два разных потока.
Проблема здесь в том, что кажется, что processInput не очищается до тех пор, пока поток не закончится. При отладке приложение командной строки действительно получает свои входные данные только после завершения потока, но никогда не получает их, если цикл while сохраняется. Я пробовал много разных вещей, чтобы сбросить processInput, но, похоже, ничего не работает.
Кто-нибудь пробовал что-то подобное раньше? Есть что-то, что я пропускаю? Любая помощь будет принята с благодарностью!
3 ответа
В конце концов я придумал способ сделать эту работу. Просматривая код библиотеки Commons Exec, я заметил, что StreamPumpers, используемые PumpStreamHandler, не сбрасываются каждый раз, когда поступают новые данные. Вот почему код работал, когда я выполнял его только один раз, поскольку он автоматически сбрасывал и закрывал поток. Поэтому я создал классы, которые я назвал AutoFlushingStreamPumper и AutoFlushingPumpStreamHandler. Последний аналогичен обычному PumpStreamHandler, но использует AutoFlushingStreamPumpers вместо обычных. AutoFlushingStreamPumper делает то же самое, что и стандартный StreamPumper, но сбрасывает свой выходной поток каждый раз, когда что-то записывает в него.
Я довольно тщательно его протестировал, и, похоже, он хорошо работает. Спасибо всем, кто пытался это понять!
Оказывается, для моих целей мне нужно было только переопределить "ExecuteStreamHandler". Вот мое решение, которое захватывает stderr в StringBuilder и позволяет вам передавать данные в stdin и получать данные из stdout:
class SendReceiveStreamHandler implements ExecuteStreamHandler
Вы можете увидеть весь класс в GitHub здесь.
Чтобы иметь возможность написать более одной команды в STDIN процесса, я должен создать новую
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();
}
}
}
Чтобы использовать этого нового Executor, я сохраняю поток по конвейеру в PumpStreamHandler, чтобы избежать того, чтобы STDIN был закрыт 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);
Вы можете использовать метод writeLine() исполнителя или создать свой собственный.