Java-процесс с потоком ввода / вывода - PullRequest
87 голосов
/ 05 сентября 2010

У меня есть следующий пример кода ниже.Таким образом, вы можете ввести команду для оболочки 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);
}

Ответы [ 3 ]

131 голосов
/ 05 сентября 2010

Во-первых, я бы порекомендовал заменить строку

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. Он не вернет конец файла, если в данный момент не будет больше выходных данных процесса. Вместо этого он будет ожидать следующей строки вывода процесса и не вернется, пока не получит следующую строку.

Поскольку перед отправкой в ​​этот цикл вы отправляете две строки ввода в процесс, первый из этих двух циклов зависнет, если процесс не завершился после этих двух строк ввода. Он будет сидеть в ожидании прочтения другой строки, но другой строки для чтения не будет.

Я скомпилировал ваш исходный код (сейчас я на 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 сообщает о синтаксической ошибке и завершается, если вы вводите какой-либо текст с несогласованным ).

Эти пункты могут не иметь значения для вас, если все, что вы думаете о выполнении в качестве запланированной задачи, будет ограничено командой или небольшим набором команд, которые никогда не будут вести себя таким патологическим образом.

EDIT : улучшена обработка выхода и другие незначительные изменения после запуска этого в Linux.

4 голосов
/ 02 февраля 2012

Я думаю, что вы можете использовать поток, подобный демон-нити, для чтения ваших входных данных, и ваш читатель вывода уже будет в цикле 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();

и вы можете читатель будет таким же, как указано выше, т.е.

while ((line = reader.readLine ()) != null) {
    System.out.println ("Stdout: " + line);
}

сделайте вашего писателя окончательным, иначе он не будет доступен для внутреннего класса.

1 голос
/ 05 сентября 2010

В вашем коде writer.close();. Таким образом, bash получает EOF на stdin и завершает работу. Затем вы получаете Broken pipe при попытке прочитать из stdout несуществующего bash.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...