Java-процесс с потоком ввода / вывода
У меня есть следующий пример кода ниже. С помощью которого вы можете ввести команду в оболочку bash, т.е. echo test
и получить результат echo'd обратно. Однако, после первого прочтения. Другие выходные потоки не работают?
Почему это или я делаю что-то не так? Моя конечная цель - создать потоковую запланированную задачу, которая периодически выполняет команду для / bash, поэтому OutputStream
и InputStream
должны были бы работать в тандеме и не переставать работать. Я также столкнулся с ошибкой java.io.IOException: Broken pipe
есть идеи?
Спасибо.
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);
}
Переведено автоматически
Ответ 1
Во-первых, я бы рекомендовал заменить строку
Process process = Runtime.getRuntime ().exec ("/bin/bash");
со строками
ProcessBuilder builder = new ProcessBuilder("/bin/bash");
builder.redirectErrorStream(true);
Process process = builder.start();
ProcessBuilder является новым в Java 5 и упрощает запуск внешних процессов. На мой взгляд, его наиболее значительным улучшением по сравнению с Runtime.getRuntime().exec()
является то, что он позволяет перенаправлять стандартную ошибку дочернего процесса в его стандартный вывод. Это означает, что у вас есть только один InputStream
для чтения. До этого вам нужно было иметь два отдельных потока, один для чтения из stdout
и один для чтения из stderr
, чтобы избежать заполнения стандартного буфера ошибок, в то время как стандартный буфер вывода был пуст (в результате чего дочерний процесс зависал), или наоборот.
Далее, циклы (которых у вас два)
while ((line = reader.readLine ()) != null) {
System.out.println ("Stdout: " + line);
}
завершается только тогда, когда reader
который считывает данные из стандартного вывода процесса, возвращает конец файла. Это происходит только при завершении bash
процесса. Он не вернет end-of-file, если в данный момент больше не будет выходных данных от процесса. Вместо этого он будет ждать следующей строки выходных данных от процесса и не вернется, пока не получит эту следующую строку.
Поскольку вы отправляете две строки ввода процессу до достижения этого цикла, первый из этих двух циклов зависнет, если процесс не завершится после этих двух строк ввода. Он будет сидеть там, ожидая чтения другой строки, но другой строки для чтения никогда не будет.
Я скомпилировал ваш исходный код (в данный момент я на Windows, поэтому заменил /bin/bash
на cmd.exe
, но принципы должны быть теми же), и я обнаружил, что:
- после ввода в двух строках появляется вывод первых двух команд, но затем программа зависает,
- если я введу, скажем,
echo test
, а затемexit
, программа завершит первый цикл с момента завершенияcmd.exe
процесса. Затем программа запрашивает другую строку ввода (которая игнорируется), сразу переходит ко второму циклу, поскольку дочерний процесс уже завершен, а затем завершает работу самостоятельно. - если я ввожу
exit
и затемecho test
, я получаю исключение IOException с жалобой на закрытие канала. Этого следовало ожидать - первая строка ввода привела к завершению процесса, а вторую строку отправить некуда.
Я видел трюк, который делает что-то похожее на то, что вы, кажется, хотите, в программе, над которой я раньше работал. Эта программа поддерживала несколько оболочек, запускала в них команды и считывала выходные данные этих команд. Используемый трюк заключался в том, чтобы всегда выписывать "волшебную" строку, которая отмечает конец вывода команды оболочки, и использовать это, чтобы определить, когда завершился вывод команды, отправленной в оболочку.
Я взял ваш код и заменил все после строки, которая присваивает значение writer
, следующим циклом:
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;
}
}
После выполнения этого я мог бы надежно запустить несколько команд, и результаты каждой из них возвращались бы мне по отдельности.
Две echo --EOF--
команды в строке, отправляемой в командную оболочку, предназначены для обеспечения завершения вывода из команды с помощью --EOF--
даже в результате ошибки из команды.
Конечно, этот подход имеет свои ограничения. Эти ограничения включают:
- если я ввожу команду, которая ожидает ввода пользователем (например, другой оболочки), программа, похоже, зависает,
- предполагается, что каждый процесс, запускаемый оболочкой, завершает свой вывод новой строкой,
- немного запутывается, если команда, выполняемая оболочкой, случайно записывает строку
--EOF--
. bash
сообщает о синтаксической ошибке и завершает работу, если вы вводите текст с несогласованным)
.
Эти моменты могут не иметь для вас значения, если то, что вы собираетесь запускать как запланированную задачу, будет ограничено командой или небольшим набором команд, которые никогда не будут вести себя таким патологическим образом.
РЕДАКТИРОВАТЬ: улучшена обработка выхода и другие незначительные изменения после запуска этого в Linux.
Ответ 2
Я думаю, вы можете использовать поток, подобный demon-thread, для чтения ваших входных данных, и ваш считыватель выходных данных уже будет находиться в цикле while в основном потоке, чтобы вы могли читать и записывать одновременно.Вы можете модифицировать свою программу следующим образом:
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();
и вы можете reader будет таким же, как указано выше, т.е.
while ((line = reader.readLine ()) != null) {
System.out.println ("Stdout: " + line);
}
сделайте ваш writer окончательным, иначе он не сможет быть доступен внутреннему классу.
Ответ 3
У вас есть writer.close();
в вашем коде. Итак, bash получает EOF на своем stdin
и завершает работу. Затем вы получаете Broken pipe
при попытке чтения из stdout
несуществующего bash.