Processo Java com Fluxo de Entrada / Saída

90

Eu tenho o seguinte exemplo de código abaixo. Por meio do qual você pode inserir um comando no shell bash, ou seja, echo teste ter o resultado ecoado de volta. No entanto, após a primeira leitura. Outros fluxos de saída não funcionam?

Por que isso ou estou fazendo algo errado? Meu objetivo final é criar uma tarefa agendada Threaded que executa um comando periodicamente para / bash para que OutputStreame InputStreamtenha que trabalhar em conjunto e não parar de funcionar. Eu também tenho experimentado o erro java.io.IOException: Broken pipealguma idéia?

Obrigado.

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);
}
James Moore
fonte
"Tubo quebrado" provavelmente significa que o processo filho foi encerrado. Não olhei completamente para o resto do seu código para ver quais são os outros problemas.
vanza
1
use tópicos separados, funcionará muito bem
Johnydep

Respostas:

139

Em primeiro lugar, eu recomendaria substituir a linha

Process process = Runtime.getRuntime ().exec ("/bin/bash");

com as linhas

ProcessBuilder builder = new ProcessBuilder("/bin/bash");
builder.redirectErrorStream(true);
Process process = builder.start();

ProcessBuilder é novo no Java 5 e torna a execução de processos externos mais fácil. Em minha opinião, sua melhoria mais significativa Runtime.getRuntime().exec()é que ele permite redirecionar o erro padrão do processo filho para sua saída padrão. Isso significa que você só tem um InputStreampara ler. Antes disso, você precisava ter dois Threads separados, um lendo stdoute outro lendo stderr, para evitar o preenchimento do buffer de erro padrão enquanto o buffer de saída padrão estava vazio (fazendo com que o processo filho travasse) ou vice-versa.

Em seguida, os loops (dos quais você tem dois)

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

só sai quando o reader, que lê a saída padrão do processo, retorna o fim do arquivo. Isso só acontece quando o bashprocesso é encerrado. Ele não retornará o fim do arquivo se no momento não houver mais saída do processo. Em vez disso, ele aguardará a próxima linha de saída do processo e não retornará até que tenha a próxima linha.

Como você está enviando duas linhas de entrada para o processo antes de chegar a este loop, o primeiro desses dois loops irá travar se o processo não tiver saído após essas duas linhas de entrada. Ele ficará parado esperando que outra linha seja lida, mas nunca haverá outra linha para ser lida.

Compilei seu código-fonte (estou no Windows no momento, então substituí /bin/bashpor cmd.exe, mas os princípios devem ser os mesmos) e descobri que:

  • depois de digitar duas linhas, a saída dos dois primeiros comandos aparece, mas então o programa trava,
  • se eu digitar, digamos, echo teste, em seguida exit, o programa sairá do primeiro loop desde que o cmd.exeprocesso foi encerrado. O programa então pede outra linha de entrada (que é ignorada), pula direto sobre o segundo loop, uma vez que o processo filho já saiu, e então sai sozinho.
  • se eu digitar exite echo test, em seguida , obtenho uma IOException reclamando sobre um tubo sendo fechado. Isso era esperado - a primeira linha de entrada fez com que o processo fosse encerrado e não há para onde enviar a segunda linha.

Eu vi um truque que faz algo semelhante ao que você parece querer, em um programa que eu costumava trabalhar. Este programa mantinha vários shells, executava comandos neles e lia a saída desses comandos. O truque usado era sempre escrever uma linha 'mágica' que marca o fim da saída do comando shell e usá-la para determinar quando a saída do comando enviado ao shell havia terminado.

Peguei seu código e substituí tudo após a linha que atribui a writerpelo seguinte loop:

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;
    }
}

Depois de fazer isso, eu poderia executar alguns comandos de forma confiável e ter a saída de cada um voltando para mim individualmente.

Os dois echo --EOF--comandos na linha enviada para o shell estão lá para garantir que a saída do comando seja encerrada --EOF--mesmo em caso de erro do comando.

Claro, essa abordagem tem suas limitações. Essas limitações incluem:

  • se eu inserir um comando que aguarda a entrada do usuário (por exemplo, outro shell), o programa parece travar,
  • ele assume que cada processo executado pelo shell termina sua saída com uma nova linha,
  • fica um pouco confuso se o comando executado pelo shell escrever uma linha --EOF-- .
  • bashrelata um erro de sintaxe e sai se você inserir algum texto com um não correspondente ).

Esses pontos podem não importar para você se tudo o que você está pensando em executar como uma tarefa agendada for restrito a um comando ou um pequeno conjunto de comandos que nunca se comportarão de maneira tão patológica.

EDIT : melhore o tratamento de saída e outras pequenas alterações após a execução no Linux.

Luke Woodward
fonte
Obrigado pela resposta abrangente. No entanto, acho que identifiquei o verdadeiro caso de meus problemas. Chamado a minha atenção em sua postagem. stackoverflow.com/questions/3645889/… . Obrigado.
James Moore,
1
substituir / bin / bash por cmd realmente não se comportou da mesma forma, pois criei um programa que fazia o mesmo, mas tinha problemas com o bash. No meu caso, abrir três threads separados para cada entrada / saída / erro funciona melhor sem nenhum problema para comandos interativos de sessão longa.
Johnydep
@AlexMills: seu segundo comentário significa que você resolveu seu problema? Não há detalhes suficientes em seu primeiro comentário para dizer qual é o problema, nem por que você está recebendo exceções "de repente".
Luke Woodward,
@Luke, o link que forneci logo acima de seu comentário resolve meu problema
Alexander Mills,
4

Eu acho que você pode usar thread como demon-thread para ler sua entrada e seu leitor de saída já estará em loop while no thread principal para que você possa ler e escrever ao mesmo tempo. Você pode modificar seu programa assim:

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

e você pode ler será o mesmo que acima, ou seja

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

faça seu escritor como final, caso contrário, ele não poderá ser acessado pela classe interna.

Shashank Jain
fonte
1

Você tem writer.close();em seu código. Assim, o bash recebe EOF stdine sai. Então você começa Broken pipeao tentar ler do stdoutbash extinto.

gpeche
fonte