Процес Java із вхідним / вихідним потоком


90

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

"Поламана труба", ймовірно, означає, що дочірній процес завершено. Не повністю розглянув решту коду, щоб дізнатись, які інші проблеми.
vanza

1
використовуйте окремі потоки, це буде працювати нормально
Johnydep

Відповіді:


139

По-перше, я б рекомендував замінити рядок

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.


Дякую за вичерпну відповідь. Однак, я думаю, я визначив справжній випадок своїх проблем. До моєї уваги у вашому дописі. stackoverflow.com/questions/3645889/… . Дякую.
James Moore

1
заміна / bin / bash на cmd насправді не вела себе так само, як я створив програму, яка робила те саме, але мала проблеми з bash. У моєму випадку відкриття трьох окремих потоків для кожного вводу / виводу / помилки найкраще працює без проблем для тривалих сеансів інтерактивних команд.
Johnydep


@AlexMills: ваш другий коментар означає, що ви вирішили свою проблему? У вашому першому коментарі ніде не досить детально, щоб сказати, в чому проблема, і чому ви "раптом" отримуєте винятки.
Luke Woodward,

@ Лука, посилання, яке я надав трохи вище вашого коментаря, вирішує мою проблему
Олександр Міллс

4

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

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.