Processo Java con flusso di input / output


90

Ho il seguente esempio di codice di seguito. Per cui puoi inserire un comando nella bash shell ie echo teste ottenere l'eco del risultato. Tuttavia, dopo la prima lettura. Altri flussi di output non funzionano?

Perché questo o sto facendo qualcosa di sbagliato? Il mio obiettivo finale è creare un'attività pianificata con thread che esegue periodicamente un comando su / bash in modo che OutputStreame InputStreamdebba lavorare in tandem e non smettere di funzionare. Ho anche riscontrato l'errore java.io.IOException: Broken pipequalche idea?

Grazie.

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

"Tubo rotto" probabilmente significa che il processo figlio è terminato. Non ho esaminato completamente il resto del codice per vedere quali sono gli altri problemi.
vanza

1
usa thread separati, funzionerà perfettamente
Johnydep

Risposte:


139

In primo luogo, consiglierei di sostituire la linea

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

con le linee

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

ProcessBuilder è una novità di Java 5 e semplifica l'esecuzione di processi esterni. A mio parere, il suo miglioramento più significativo Runtime.getRuntime().exec()è che consente di reindirizzare l'errore standard del processo figlio nel suo output standard. Ciò significa che ne hai solo uno InputStreamda cui leggere. Prima di questo, era necessario disporre di due Thread separati, uno da lettura stdoute uno da lettura stderr, per evitare il riempimento del buffer di errore standard mentre il buffer di output standard era vuoto (causando il blocco del processo figlio) o viceversa.

Successivamente, i loop (di cui hai due)

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

esce solo quando reader, che legge dallo standard output del processo, restituisce end-of-file. Questo accade solo quando il bashprocesso termina. Non restituirà la fine del file se al momento non ci sono più output dal processo. Invece, attenderà la prossima riga di output dal processo e non tornerà finché non avrà questa riga successiva.

Poiché stai inviando due linee di input al processo prima di raggiungere questo ciclo, il primo di questi due cicli si bloccherà se il processo non è terminato dopo queste due linee di input. Rimarrà lì in attesa che venga letta un'altra riga, ma non ci sarà mai un'altra riga da leggere.

Ho compilato il tuo codice sorgente (sono su Windows al momento, quindi l'ho sostituito /bin/bashcon cmd.exe, ma i principi dovrebbero essere gli stessi), e ho scoperto che:

  • dopo aver digitato due righe, appare l'output dei primi due comandi, ma poi il programma si blocca,
  • se digito, ad esempio, echo teste poi exit, il programma esce dal primo ciclo da quando il cmd.exeprocesso è terminato. Il programma quindi richiede un'altra riga di input (che viene ignorata), salta direttamente il secondo ciclo poiché il processo figlio è già terminato e quindi esce da solo.
  • se digito exite poi echo test, ottengo una IOException che si lamenta della chiusura di una pipe. Questo è prevedibile: la prima riga di input ha causato l'uscita del processo e non c'è nessun posto dove inviare la seconda riga.

Ho visto un trucco che fa qualcosa di simile a quello che sembri desiderare, in un programma su cui lavoravo. Questo programma ha mantenuto un certo numero di shell, ha eseguito comandi in esse e ha letto l'output di questi comandi. Il trucco utilizzato è stato quello di scrivere sempre una riga "magica" che segna la fine dell'output del comando della shell e utilizzarla per determinare quando l'output del comando inviato alla shell era terminato.

Ho preso il tuo codice e ho sostituito tutto dopo la riga che assegna a writercon il seguente ciclo:

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

Dopo aver fatto ciò, potrei eseguire in modo affidabile alcuni comandi e ricevere l'output di ciascuno di essi individualmente.

I due echo --EOF--comandi nella riga inviata alla shell servono a garantire che l'output del comando venga terminato --EOF--anche in caso di errore del comando.

Ovviamente questo approccio ha i suoi limiti. Queste limitazioni includono:

  • se inserisco un comando che attende l'input dell'utente (ad esempio un'altra shell), il programma sembra bloccarsi,
  • assume che ogni processo eseguito dalla shell termini il suo output con una nuova riga,
  • diventa un po 'confuso se il comando eseguito dalla shell scrive una riga --EOF--.
  • bashsegnala un errore di sintassi ed esce se si immette del testo con un'estensione senza corrispondenza ).

Questi punti potrebbero non essere importanti per te se qualunque cosa tu stia pensando di eseguire come attività pianificata sarà limitata a un comando o un piccolo insieme di comandi che non si comporteranno mai in modi così patologici.

MODIFICA : migliora la gestione delle uscite e altre modifiche minori dopo l'esecuzione su Linux.


Grazie per la risposta esauriente Tuttavia, penso di aver identificato il vero motivo dei miei problemi. Portato alla mia attenzione nel tuo post. stackoverflow.com/questions/3645889/… . Grazie.
James Moore,

1
sostituire / bin / bash con cmd non si è comportato allo stesso modo, poiché ho creato un programma che faceva lo stesso ma aveva problemi con bash. Nel mio caso, l'apertura di tre thread separati per ogni input / output / err funziona meglio senza alcun problema per i comandi interattivi di sessione lunga.
Johnydep


@AlexMills: il tuo secondo commento significa che hai risolto il tuo problema? Non ci sono abbastanza dettagli nel tuo primo commento per dire qual è il problema, né perché stai ricevendo eccezioni "improvvisamente".
Luke Woodward

@Luke, il link che ho fornito appena sopra il tuo commento risolve il mio problema
Alexander Mills

4

Penso che tu possa usare thread come demon-thread per leggere il tuo input e il tuo lettore di output sarà già nel ciclo while nel thread principale in modo da poter leggere e scrivere allo stesso tempo.Puoi modificare il tuo programma in questo modo:

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 puoi leggere sarà lo stesso di sopra, ad es

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

rendi il tuo scrittore definitivo altrimenti non sarà accessibile dalla classe interna.


1

Hai writer.close();nel tuo codice. Quindi bash riceve EOF sul suo stdined esce. Poi si ottiene Broken pipequando si cerca di leggere dal stdoutdefunto bash.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.