Runtime.exec() pour les nuls et ProcessBuilder

Il vous est déjà sûrement arrivé, un jour, de vouloir exécuter un application externe à partir de votre programme Java. Et si c’est le cas, vous avez très probablement eu du mal à faire fonctionner votre programme correctement, celui-ci semblant se bloquer et ne plus rien afficher.

Dans ce petit article, je vais vous expliquer rapidement comment éviter ce genre de problème avec le classic Runtime.exec(), et je vous décrirai ensuite son substitut, le ProcessBuilder.

Runtime.exec()

Erreur n°1 : où est le waitFor() ?
Supposons que vous ayez un petit script batch (ou shell) qui va afficher « Hello World! » à l’écran, que vous souhaitez lancer à partir de votre programme. Vous ne savez pas comment faire, alors premier réflexe, vous faites une recherche sur Google et vous apprenez que c’est possible par la méthode statique Runtime.exec(). Vous vous empressez de coder cela, sans bien sûr prendre le temps de lire la documentation :

package com.excilys.labs;

import java.io.IOException;

public class Main {

    public static final String CHEMIN = "C:\\workspace\\";

    public static void main(String[] args) {
        System.out.println("Début du programme");
        try {
            String[] commande = {"cmd.exe", "/C", CHEMIN + "HelloWorld.bat"};
            Runtime.getRuntime().exec(commande);
        } catch (IOException e) {
            e.printStackTrace();
        }
        System.out.println("Fin du programme");
    }
}

Et là, sur votre écran, vous obtenez un magnifique :

Début du programme
Fin du programme

Vous ne comprenez pas pourquoi rien ne s’affiche, mais vous ne paniquez pas. Vous vous dites quand même qu’il serait bon de lire un peu la javadoc avant (mais pas en détail, bien sûr). Vous tombez sur une méthode appelée waitFor() dans la classe Process et vous vous dites qu’elle a l’air de convenir à votre besoin.
En effet, le thread courant (le main) s’est terminé avant que soit lancé votre Process.

Vous modifiez donc un peu votre code :

package com.excilys.labs;

import java.io.IOException;

public class Main {

    public static final String CHEMIN = "C:\\workspace\\";

    public static void main(String[] args) {
        System.out.println("Début du programme");
        try {
            String[] commande = {"cmd.exe", "/C", CHEMIN + "HelloWorld.bat"};
            Process p = Runtime.getRuntime().exec(commande);
            p.waitFor();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Fin du programme");
    }
}

A l’exécution, vous obtenez simplement :

Début du programme

Vous commencez à paniquer…

Erreur n°2 : la gestion des flux
Votre programme s’est bloqué. Vous avez vite compris que votre waitFor() y est pour quelque chose, mais vous ne comprenez pas vraiment pourquoi le programme externe ne finit pas de s’exécuter pour rendre la main au main().
Généralement, c’est à partir d’ici que la plupart des développeurs se découragent et disent à leur chef de projet/client : « Bon, il va falloir lancer le script à la main. »

En jetant à nouveau un coup d’œil à la documentation de la classe Process, vous avez remarqué un paragraphe sur lequel vous ne vous êtes pas attardé à la première lecture :

By default, the created subprocess does not have its own terminal or console. All its standard I/O (i.e. stdin, stdout, stderr) operations will be redirected to the parent process, where they can be accessed via the streams obtained using the methods getOutputStream(), getInputStream(), and getErrorStream(). The parent process uses these streams to feed input to and get output from the subprocess. Because some native platforms only provide limited buffer size for standard input and output streams, failure to promptly write the input stream or read the output stream of the subprocess may cause the subprocess to block, or even deadlock.

Que signifie ce charabia ?
Contrairement à d’autres langages de programmation (comme le C), en Java, les flux d’entrés et de sorties ne sont pas communs au programme appelant et au programme appelé.
Le cas d’interblocage intervient lorsque le flux est plein et que le programme appelé attend que ce dernier soit vidé, tandis que le programme appelant attend simplement que le programme appelé se termine.

Voici comment résoudre ce problème (à noter que pour le cas de notre HelloWorld, le flux d’erreur n’est pas nécessaire, mais je le mets pour faire un exemple complet) :

package com.excilys.labs;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class Main {

    public static final String CHEMIN = "C:\\workspace\\";

    private static BufferedReader getOutput(Process p) {
        return new BufferedReader(new InputStreamReader(p.getInputStream()));
    }

    private static BufferedReader getError(Process p) {
        return new BufferedReader(new InputStreamReader(p.getErrorStream()));
    }

    public static void main(String[] args) {
        System.out.println("Début du programme");
        try {
            String[] commande = {"cmd.exe", "/C", CHEMIN + "HelloWorld.bat"};
            Process p = Runtime.getRuntime().exec(commande);
            BufferedReader output = getOutput(p);
            BufferedReader error = getError(p);
            String ligne = "";

            while ((ligne = output.readLine()) != null) {
                System.out.println(ligne);
            }
           
            while ((ligne = error.readLine()) != null) {
                System.out.println(ligne);
            }

            p.waitFor();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Fin du programme");
    }
}

La joie vous envahie soudainement. L’exécution du programme vous donne :

Début du programme

C:\workspace\Runtime echo Hello World!
Hello World!
Fin du programme

Mais je vous arrête tout de suite, ce programme marche très bien si, lors de son exécution, le script réussi complètement ou, au contraire, plante complètement. Mais il sera bloqué si il y a un mélange de messages d’exécution et de messages d’erreurs.

Erreur n°3 : traiter les flux dans le même thread
La tâche qui consistait à simplement lancer un programme externe à partir de son code Java semblait si facile au départ, et dans l’idée. Elle est devenue au fur et à mesure plus complexe.
Afin de compléter ce programme et le rendre entièrement fonctionnel, on va devoir lancer plusieurs threads en parallèles pour traiter les flux en même temps :

package com.excilys.labs;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

class AfficheurFlux implements Runnable {

    private final InputStream inputStream;

    AfficheurFlux(InputStream inputStream) {
        this.inputStream = inputStream;
    }

    private BufferedReader getBufferedReader(InputStream is) {
        return new BufferedReader(new InputStreamReader(is));
    }

    @Override
    public void run() {
        BufferedReader br = getBufferedReader(inputStream);
        String ligne = "";
        try {
            while ((ligne = br.readLine()) != null) {
                System.out.println(ligne);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

public class Main {

    public static final String CHEMIN = "C:\\workspace\\";

    public static void main(String[] args) {
        System.out.println("Début du programme");
        try {
            String[] commande = {"cmd.exe", "/C", CHEMIN + "HelloWorld.bat"};
            Process p = Runtime.getRuntime().exec(commande);
            AfficheurFlux fluxSortie = new AfficheurFlux(p.getInputStream());
            AfficheurFlux fluxErreur = new AfficheurFlux(p.getErrorStream());

            new Thread(fluxSortie).start();
            new Thread(fluxErreur).start();

            p.waitFor();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Fin du programme");
    }
}

Le programme fonctionne à présent. ;-)

Erreur (bonus) n°4 : Runtime.exec() ne s’utilise pas comme un interpréteur de commande
Vous ne pouvez pas placer comme unique argument de la méthode exec() une chaîne entière composée du nom du programme et de ses arguments :

Runtime.getRuntime().exec("java MonAppli arg1 arg2");
ou
Runtime.getRuntime().exec("ls -al");

Il faudra utiliser la version avec plusieurs arguments de la méthode exec().

La classe ProcessBuilder
Depuis la version 1.5 de Java est apparue la classe ProcessBuilder. Son utilisation n’est pas révolutionnaire par rapport à Runtime.exec(), mais elle aura au moins le mérite d’être une classe dédiée à cette fonction et propose quelques fonctionnalités en plus qui peuvent s’avérer utiles.

Avec le ProcessBuilder, vous pouvez modifier vos variables d’environnement directement à partir de votre programme Java. La méthode environment() vous récupérera une Map des variables, que vous pourrez ensuite modifier avant de lancer votre processus.
La seconde méthode à connaître est directory(File directory) qui vous permettra de modifier le répertoire d’exécution de la commande.

Voici le code légèrement modifié, afin d’utiliser ProcessBuilder, qui affichera aussi vos variables d’environnement et en ajoutera une avant d’exécuter le processus :

package com.excilys.labs;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Map;
import java.util.Map.Entry;

class AfficheurFlux implements Runnable {

    private final InputStream inputStream;

    AfficheurFlux(InputStream inputStream) {
        this.inputStream = inputStream;
    }

    private BufferedReader getBufferedReader(InputStream is) {
        return new BufferedReader(new InputStreamReader(is));
    }

    @Override
    public void run() {
        BufferedReader br = getBufferedReader(inputStream);
        String ligne = "";
        try {
            while ((ligne = br.readLine()) != null) {
                System.out.println(ligne);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

public class Main {

    public static final String CHEMIN = "C:\\workspace\\";

    public static void main(String[] args) {
        try {
            ProcessBuilder pb = new ProcessBuilder("cmd.exe", "/C",
                    "HelloWorld.bat");
            pb.directory(new File(CHEMIN));
           
            Map env = pb.environment();
            for (Entry entry : env.entrySet()) {
                System.out.println(entry.getKey() + " : " + entry.getValue());
            }
           
            env.put("MonArg", "Valeur");
           
            Process p = pb.start();
            AfficheurFlux fluxSortie = new AfficheurFlux(p.getInputStream());
            AfficheurFlux fluxErreur = new AfficheurFlux(p.getErrorStream());
            new Thread(fluxSortie).start();
            new Thread(fluxErreur).start();
            p.waitFor();

        } catch (IOException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
VN:R_U [1.9.22_1171]
Rating: 0 (from 0 votes)
Share
Ce contenu a été publié dans Java, Trucs & astuces. Vous pouvez le mettre en favoris avec ce permalien.

Une réponse à Runtime.exec() pour les nuls et ProcessBuilder

  1. Ping : Basic Tomcat configuration | An Phong Do

Laisser un commentaire