Quand Runtime.exec () ne le fera pas

Dans le cadre du langage Java, le java.langpackage est implicitement importé dans chaque programme Java. Les pièges de ce paquet surgissent souvent, affectant la plupart des programmeurs. Ce mois-ci, je vais discuter des pièges cachés dans la Runtime.exec()méthode.

Piège 4: quand Runtime.exec () ne le fera pas

La classe java.lang.Runtimecomporte une méthode statique appelée getRuntime(), qui récupère l'environnement d'exécution Java actuel. C'est la seule manière d'obtenir une référence à l' Runtimeobjet. Avec cette référence, vous pouvez exécuter des programmes externes en appelant la méthode de la Runtimeclasse exec(). Les développeurs appellent souvent cette méthode pour lancer un navigateur pour afficher une page d'aide en HTML.

Il existe quatre versions surchargées de la exec()commande:

  • public Process exec(String command);
  • public Process exec(String [] cmdArray);
  • public Process exec(String command, String [] envp);
  • public Process exec(String [] cmdArray, String [] envp);

Pour chacune de ces méthodes, une commande - et éventuellement un ensemble d'arguments - est transmise à un appel de fonction spécifique au système d'exploitation. Cela crée ensuite un processus spécifique au système d'exploitation (un programme en cours d'exécution) avec une référence à une Processclasse renvoyée à la machine virtuelle Java. La Processclasse est une classe abstraite, car une sous-classe spécifique de Processexiste pour chaque système d'exploitation.

Vous pouvez passer trois paramètres d'entrée possibles dans ces méthodes:

  1. Une chaîne unique qui représente à la fois le programme à exécuter et tous les arguments de ce programme
  2. Un tableau de chaînes qui séparent le programme de ses arguments
  3. Un tableau de variables d'environnement

Passez les variables d'environnement dans le formulaire name=value. Si vous utilisez la version de exec()avec une seule chaîne pour le programme et ses arguments, notez que la chaîne est analysée en utilisant un espace blanc comme délimiteur via la StringTokenizerclasse.

Trébucher dans une exception IllegalThreadStateException

Le premier écueil concernant Runtime.exec()est le IllegalThreadStateException. Le premier test le plus répandu d'une API est de coder ses méthodes les plus évidentes. Par exemple, pour exécuter un processus externe à la machine virtuelle Java, nous utilisons la exec()méthode. Pour voir la valeur renvoyée par le processus externe, nous utilisons la exitValue()méthode sur la Processclasse. Dans notre premier exemple, nous tenterons d'exécuter le compilateur Java ( javac.exe):

Listing 4.1 BadExecJavac.java

import java.util. *; import java.io. *; Public class BadExecJavac {public static void main (String args []) {try {Runtime rt = Runtime.getRuntime (); Process proc = rt.exec ("javac"); int exitVal = proc.exitValue (); System.out.println ("Process exitValue:" + exitVal); } catch (Throwable t) {t.printStackTrace (); }}}

Une série de BadExecJavacproduits:

E: \ classes \ com \ javaworld \ jpitfalls \ article2> java BadExecJavac java.lang.IllegalThreadStateException: le processus ne s'est pas arrêté à java.lang.Win32Process.exitValue (méthode native) à BadExecJavac.main (BadExecJavac.java:13) 

Si un processus externe n'est pas encore terminé, la exitValue()méthode lèvera un IllegalThreadStateException; c'est pourquoi ce programme a échoué. Alors que la documentation indique ce fait, pourquoi cette méthode ne peut-elle pas attendre qu'elle puisse donner une réponse valide?

Un examen plus approfondi des méthodes disponibles dans la Processclasse révèle une waitFor()méthode qui fait précisément cela. En fait, waitFor()renvoie également la valeur de sortie, ce qui signifie que vous n'utiliseriez pas exitValue()et waitFor()en conjonction les uns avec les autres, mais choisiriez plutôt l'un ou l'autre. Le seul moment possible que vous utiliseriez à la exitValue()place waitFor()serait lorsque vous ne voulez pas que votre programme bloque l'attente d'un processus externe qui pourrait ne jamais se terminer. Au lieu d'utiliser la waitFor()méthode, je préférerais passer un paramètre booléen appelé waitFordans la exitValue()méthode pour déterminer si le thread actuel doit attendre ou non. Un booléen serait plus avantageux carexitValue()est un nom plus approprié pour cette méthode, et il n'est pas nécessaire que deux méthodes exécutent la même fonction dans des conditions différentes. Une telle discrimination de condition simple est le domaine d'un paramètre d'entrée.

Par conséquent, pour éviter ce piège, attrapez le IllegalThreadStateExceptionou attendez que le processus se termine.

Maintenant, corrigeons le problème dans le Listing 4.1 et attendons que le processus se termine. Dans le Listing 4.2, le programme tente à nouveau de s'exécuter javac.exepuis attend la fin du processus externe:

Listing 4.2 BadExecJavac2.java

import java.util. *; import java.io. *; Public class BadExecJavac2 {public static void main (String args []) {try {Runtime rt = Runtime.getRuntime (); Process proc = rt.exec ("javac"); int exitVal = proc.waitFor (); System.out.println ("Process exitValue:" + exitVal); } catch (Throwable t) {t.printStackTrace (); }}}

Malheureusement, une exécution de BadExecJavac2ne produit aucune sortie. Le programme se bloque et ne se termine jamais. Pourquoi le javacprocessus ne se termine-t-il jamais?

Pourquoi Runtime.exec () se bloque

La documentation Javadoc du JDK fournit la réponse à cette question:

Étant donné que certaines plates-formes natives ne fournissent qu'une taille de tampon limitée pour les flux d'entrée et de sortie standard, le fait de ne pas écrire rapidement le flux d'entrée ou de lire le flux de sortie du sous-processus peut entraîner le blocage du sous-processus, voire un blocage.

S'agit-il simplement d'un cas où les programmeurs ne lisent pas la documentation, comme l'indique le conseil souvent cité: lisez le manuel fin (RTFM)? La réponse est partiellement oui. Dans ce cas, la lecture du Javadoc vous amènerait à mi-chemin; il explique que vous devez gérer les flux vers votre processus externe, mais il ne vous dit pas comment.

Une autre variable est en jeu ici, comme le montre le grand nombre de questions des programmeurs et d'idées fausses concernant cette API dans les groupes de discussion: bien Runtime.exec()que les API Process semblent extrêmement simples, cette simplicité est trompeuse car l'utilisation simple, ou évidente, de l'API est sujet aux erreurs. La leçon ici pour le concepteur d'API est de réserver des API simples pour des opérations simples. Les opérations sujettes à des complexités et des dépendances spécifiques à la plate-forme doivent refléter le domaine avec précision. Il est possible qu'une abstraction soit poussée trop loin. La JConfigbibliothèque fournit un exemple d'une API plus complète pour gérer les opérations de fichiers et de processus (voir Ressources ci-dessous pour plus d'informations).

Maintenant, suivons la documentation JDK et gérons la sortie du javacprocessus. Lorsque vous exécutez javacsans aucun argument, il produit un ensemble d'instructions d'utilisation qui décrivent comment exécuter le programme et la signification de toutes les options de programme disponibles. Sachant que cela va vers le stderrflux, vous pouvez facilement écrire un programme pour épuiser ce flux avant d'attendre que le processus se termine. Le listing 4.3 complète cette tâche. Bien que cette approche fonctionne, ce n'est pas une bonne solution générale. Ainsi, le programme du Listing 4.3 est nommé MediocreExecJavac; il n'apporte qu'une solution médiocre. Une meilleure solution viderait à la fois le flux d'erreur standard et le flux de sortie standard. Et la meilleure solution serait de vider ces flux simultanément (je le démontrerai plus tard).

Listing 4.3 MediocreExecJavac.java

import java.util. *; import java.io. *; classe publique MediocreExecJavac {public static void main (String args []) {try {Runtime rt = Runtime.getRuntime (); Process proc = rt.exec ("javac"); InputStream stderr = proc.getErrorStream (); InputStreamReader isr = new InputStreamReader (stderr); BufferedReader br = nouveau BufferedReader (isr); Ligne de chaîne = null; System.out.println (""); while ((ligne = br.readLine ())! = null) System.out.println (ligne); System.out.println (""); int exitVal = proc.waitFor (); System.out.println ("Process exitValue:" + exitVal); } catch (Throwable t) {t.printStackTrace (); }}}

Une série de MediocreExecJavacgénère:

E: \ classes \ com \ javaworld \ jpitfalls \ article2> java MediocreExecJavac Utilisation: javac où inclut: -g Générer toutes les informations de débogage -g: aucun Générer aucune information de débogage -g: {lines, vars, source} Générer seulement quelques informations de débogage -O Optimiser; peut gêner le débogage ou agrandir les fichiers de classe -nowarn Ne générer aucun avertissement -verbose Messages de sortie sur ce que fait le compilateur -deprecation Emplacements de la source de sortie où les API obsolètes sont utilisées -classpath Spécifiez où trouver les fichiers de classe utilisateur -sourcepath Spécifiez où trouver les fichiers source d'entrée -bootclasspath Remplacer l'emplacement des fichiers de classe d'amorçage -extdirs Remplacer l'emplacement des extensions installées -d Spécifier où placer les fichiers de classe générés -encoding Spécifier l'encodage de caractères utilisé par les fichiers source -target Générer des fichiers de classe pour une version de VM spécifique Process exitValue: 2

So, MediocreExecJavac works and produces an exit value of 2. Normally, an exit value of 0 indicates success; any nonzero value indicates an error. The meaning of these exit values depends on the particular operating system. A Win32 error with a value of 2 is a "file not found" error. That makes sense, since javac expects us to follow the program with the source code file to compile.

Thus, to circumvent the second pitfall -- hanging forever in Runtime.exec() -- if the program you launch produces output or expects input, ensure that you process the input and output streams.

Assuming a command is an executable program

Under the Windows operating system, many new programmers stumble upon Runtime.exec() when trying to use it for nonexecutable commands like dir and copy. Subsequently, they run into Runtime.exec()'s third pitfall. Listing 4.4 demonstrates exactly that:

Listing 4.4 BadExecWinDir.java

import java.util.*; import java.io.*; public class BadExecWinDir { public static void main(String args[]) { try { Runtime rt = Runtime.getRuntime(); Process proc = rt.exec("dir"); InputStream stdin = proc.getInputStream(); InputStreamReader isr = new InputStreamReader(stdin); BufferedReader br = new BufferedReader(isr); String line = null; System.out.println(""); while ( (line = br.readLine()) != null) System.out.println(line); System.out.println(""); int exitVal = proc.waitFor(); System.out.println("Process exitValue: " + exitVal); } catch (Throwable t) { t.printStackTrace(); } } } 

A run of BadExecWinDir produces:

E:\classes\com\javaworld\jpitfalls\article2>java BadExecWinDir java.io.IOException: CreateProcess: dir error=2 at java.lang.Win32Process.create(Native Method) at java.lang.Win32Process.(Unknown Source) at java.lang.Runtime.execInternal(Native Method) at java.lang.Runtime.exec(Unknown Source) at java.lang.Runtime.exec(Unknown Source) at java.lang.Runtime.exec(Unknown Source) at java.lang.Runtime.exec(Unknown Source) at BadExecWinDir.main(BadExecWinDir.java:12) 

Comme indiqué précédemment, la valeur d'erreur de 2signifie «fichier introuvable», ce qui, dans ce cas, signifie que l'exécutable nommé dir.exen'a pas pu être trouvé. En effet, la commande de répertoire fait partie de l'interpréteur de commandes Windows et non d'un exécutable distinct. Pour exécuter l'interpréteur de commandes Windows, exécutez soit command.comou cmd.exe, selon le système d'exploitation Windows que vous utilisez. Le listing 4.5 exécute une copie de l'interpréteur de commandes Windows puis exécute la commande fournie par l'utilisateur (par exemple, dir).

Listing 4.5 GoodWindowsExec.java