Processus Java avec flux d'entrée / sortie

90

J'ai l'exemple de code suivant ci-dessous. echo testGrâce à quoi vous pouvez entrer une commande dans le shell bash ie et avoir le résultat en écho. Cependant, après la première lecture. Les autres flux de sortie ne fonctionnent pas?

Pourquoi est-ce ou est-ce que je fais quelque chose de mal? Mon objectif final est de créer une tâche programmée Threaded qui exécute une commande périodiquement vers / bash afin que OutputStreamet InputStreamdoive travailler en tandem et ne pas cesser de fonctionner. J'ai également rencontré l'erreur java.io.IOException: Broken pipedes idées?

Merci.

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
la source
"Pipe cassée" signifie probablement que le processus fils s'est arrêté. N'avez pas complètement examiné le reste de votre code pour voir quels sont les autres problèmes.
vanza
1
utilisez des threads séparés, cela fonctionnera très bien
Johnydep

Réponses:

139

Tout d'abord, je recommanderais de remplacer la ligne

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

avec les lignes

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

ProcessBuilder est nouveau dans Java 5 et facilite l'exécution des processus externes. À mon avis, son amélioration la plus significative Runtime.getRuntime().exec()est qu'il vous permet de rediriger l'erreur standard du processus enfant dans sa sortie standard. Cela signifie que vous n'en avez qu'un seul InputStreamà lire. Avant cela, vous deviez avoir deux threads séparés, une lecture à partir de stdoutet une lecture à partir de stderr, pour éviter que le tampon d'erreur standard ne se remplisse alors que le tampon de sortie standard était vide (provoquant le blocage du processus enfant), ou vice versa.

Ensuite, les boucles (dont vous avez deux)

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

quitte uniquement lorsque le reader, qui lit la sortie standard du processus, retourne la fin du fichier. Cela se produit uniquement lorsque le bashprocessus se termine. Il ne retournera pas de fin de fichier s'il se trouve actuellement qu'il n'y a plus de sortie du processus. Au lieu de cela, il attendra la prochaine ligne de sortie du processus et ne retournera pas avant d'avoir cette ligne suivante.

Puisque vous envoyez deux lignes d'entrée au processus avant d'atteindre cette boucle, la première de ces deux boucles se bloquera si le processus n'est pas sorti après ces deux lignes d'entrée. Il restera là en attendant qu'une autre ligne soit lue, mais il n'y aura jamais d'autre ligne à lire.

J'ai compilé votre code source (je suis sous Windows pour le moment, donc j'ai remplacé /bin/bashpar cmd.exe, mais les principes devraient être les mêmes), et j'ai trouvé que:

  • après avoir tapé sur deux lignes, la sortie des deux premières commandes apparaît, mais le programme se bloque,
  • si je tape, disons, echo testet ensuite exit, le programme sort de la première boucle depuis la fin du cmd.exeprocessus. Le programme demande alors une autre ligne d'entrée (qui est ignorée), saute directement sur la deuxième boucle puisque le processus fils est déjà sorti, puis se ferme.
  • si je tape exitet puis echo test, j'obtiens une IOException se plaignant de la fermeture d'un tuyau. Il faut s'y attendre - la première ligne d'entrée a entraîné la fermeture du processus et il n'y a nulle part où envoyer la deuxième ligne.

J'ai vu une astuce qui fait quelque chose de similaire à ce que vous semblez vouloir, dans un programme sur lequel je travaillais auparavant. Ce programme conservait un certain nombre de shells, y exécutait des commandes et lisait la sortie de ces commandes. L'astuce utilisée était de toujours écrire une ligne «magique» qui marque la fin de la sortie de la commande shell, et de l'utiliser pour déterminer quand la sortie de la commande envoyée au shell était terminée.

J'ai pris votre code et j'ai tout remplacé après la ligne affectée writerpar la boucle suivante:

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

Après avoir fait cela, je pourrais exécuter de manière fiable quelques commandes et que la sortie de chacune me revienne individuellement.

Les deux echo --EOF--commandes dans la ligne envoyée au shell sont là pour garantir que la sortie de la commande se termine avec --EOF--même le résultat d'une erreur de la commande.

Bien entendu, cette approche a ses limites. Ces limitations incluent:

  • si j'entre une commande qui attend l'entrée de l'utilisateur (par exemple un autre shell), le programme semble se bloquer,
  • il suppose que chaque processus exécuté par le shell termine sa sortie par une nouvelle ligne,
  • cela devient un peu confus si la commande exécutée par le shell arrive à écrire une ligne --EOF--.
  • bashsignale une erreur de syntaxe et quitte si vous entrez du texte avec un ).

Ces points peuvent ne pas vous intéresser si tout ce que vous envisagez d'exécuter en tant que tâche planifiée va être limité à une commande ou à un petit ensemble de commandes qui ne se comporteront jamais de manière aussi pathologique.

EDIT : améliorez la gestion des sorties et d'autres modifications mineures suite à son exécution sous Linux.

Luke Woodward
la source
Merci pour la réponse complète Cependant, je pense avoir identifié le vrai cas de mes problèmes. Porté à mon attention dans votre message. stackoverflow.com/questions/3645889/… . Je vous remercie.
James moore
1
remplacer / bin / bash par cmd ne se comportait pas vraiment de la même manière, car j'ai créé un programme qui faisait la même chose mais avait des problèmes avec bash. Dans mycase, l'ouverture de trois threads séparés pour chaque entrée / sortie / err fonctionne mieux sans aucun problème pour les commandes interactives de longue session.
Johnydep
@AlexMills: votre deuxième commentaire signifie-t-il que vous avez résolu votre problème? Il n'y a pas assez de détails dans votre premier commentaire pour dire quel est le problème, ni pourquoi vous obtenez «soudainement» des exceptions.
Luke Woodward
@Luke, le lien que j'ai fourni juste au-dessus de votre commentaire résout mon problème
Alexander Mills
4

Je pense que vous pouvez utiliser un thread comme demon-thread pour lire votre entrée et votre lecteur de sortie sera déjà en boucle while dans le thread principal afin que vous puissiez lire et écrire en même temps.Vous pouvez modifier votre programme comme ceci:

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

et vous pouvez lire sera le même que ci-dessus ie

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

rendre votre rédacteur comme final sinon il ne pourra pas être accessible par classe interne.

Shashank Jain
la source
1

Vous avez writer.close();dans votre code. Alors bash reçoit EOF sur son stdinet sort. Ensuite, vous obtenez Broken pipeen essayant de lire à partir du stdoutbash défunt.

gpeche
la source