Процесс Java с потоком ввода/вывода

Ниже приведен следующий пример кода. Если вы можете ввести команду в оболочку bash i.e. echo test и вернуть результат обратно. Однако после первого чтения. Другие выходные потоки не работают?

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

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

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

Ответ 2

Я думаю, вы можете использовать поток, например demon-thread, для чтения вашего ввода, и ваш считыватель вывода уже будет находиться в цикле в основном потоке, чтобы вы могли читать и писать одновременно. Вы можете изменить свою программу следующим образом:

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

и вы можете читать читатель так же, как и выше i.e.

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

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

Ответ 3

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