Cuando Runtime.exec () no

Como parte del lenguaje Java, el java.langpaquete se importa implícitamente en cada programa Java. Los errores de este paquete surgen a menudo y afectan a la mayoría de los programadores. Este mes, discutiré las trampas que acechan en el Runtime.exec()método.

Error 4: cuando Runtime.exec () no

La clase java.lang.Runtimepresenta un método estático llamado getRuntime(), que recupera el entorno de ejecución de Java actual. Esa es la única forma de obtener una referencia al Runtimeobjeto. Con esa referencia, puede ejecutar programas externos invocando el método de la Runtimeclase exec(). Los desarrolladores suelen llamar a este método para iniciar un navegador para mostrar una página de ayuda en HTML.

Hay cuatro versiones sobrecargadas del exec()comando:

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

Para cada uno de estos métodos, se pasa un comando, y posiblemente un conjunto de argumentos, a una llamada de función específica del sistema operativo. Esto posteriormente crea un proceso específico del sistema operativo (un programa en ejecución) con una referencia a una Processclase devuelta a la máquina virtual Java. La Processclase es una clase abstracta, porque Processexiste una subclase específica de para cada sistema operativo.

Puede pasar tres posibles parámetros de entrada a estos métodos:

  1. Una sola cadena que representa tanto el programa a ejecutar como cualquier argumento para ese programa
  2. Una matriz de cadenas que separan el programa de sus argumentos.
  3. Una matriz de variables de entorno

Pase las variables de entorno en el formulario name=value. Si usa la versión de exec()con una sola cadena tanto para el programa como para sus argumentos, tenga en cuenta que la cadena se analiza utilizando espacios en blanco como delimitador a través de la StringTokenizerclase.

Tropezar con una IllegalThreadStateException

El primer escollo relacionado con Runtime.exec()es el IllegalThreadStateException. La primera prueba predominante de una API es codificar sus métodos más obvios. Por ejemplo, para ejecutar un proceso que es externo a la máquina virtual Java, usamos el exec()método. Para ver el valor que devuelve el proceso externo, usamos el exitValue()método en la Processclase. En nuestro primer ejemplo, intentaremos ejecutar el compilador de Java ( javac.exe):

Listado 4.1 BadExecJavac.java

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

Una racha de BadExecJavacproductos:

E: \ classes \ com \ javaworld \ jpitfalls \ article2> java BadExecJavac java.lang.IllegalThreadStateException: el proceso no ha finalizado en java.lang.Win32Process.exitValue (método nativo) en BadExecJavac.main (BadExecJavac.java:13) 

Si un proceso externo aún no se ha completado, el exitValue()método arrojará un IllegalThreadStateException; es por eso que este programa falló. Si bien la documentación establece este hecho, ¿por qué este método no puede esperar hasta que pueda dar una respuesta válida?

Una mirada más detallada a los métodos disponibles en la Processclase revela un waitFor()método que hace precisamente eso. De hecho, waitFor()también devuelve el valor de salida, lo que significa que no usarías exitValue()y waitFor()en conjunto, sino que elegirías uno o el otro. El único tiempo posible que usaría en exitValue()lugar de waitFor()sería cuando no desea que su programa bloquee la espera en un proceso externo que quizás nunca se complete. En lugar de usar el waitFor()método, preferiría pasar un parámetro booleano llamado waitForal exitValue()método para determinar si el hilo actual debe esperar o no. Un booleano sería más beneficioso porqueexitValue()es un nombre más apropiado para este método y no es necesario que dos métodos realicen la misma función en diferentes condiciones. Esta discriminación de condición simple es el dominio de un parámetro de entrada.

Por lo tanto, para evitar esta trampa, capture IllegalThreadStateExceptiono espere a que se complete el proceso.

Ahora, solucionemos el problema del Listado 4.1 y esperemos a que se complete el proceso. En el Listado 4.2, el programa nuevamente intenta ejecutarse javac.exey luego espera a que se complete el proceso externo:

Listado 4.2 BadExecJavac2.java

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

Desafortunadamente, una ejecución de BadExecJavac2no produce ningún resultado. El programa se cuelga y nunca se completa. ¿Por qué el javacproceso nunca se completa?

Por qué se bloquea Runtime.exec ()

La documentación de Javadoc del JDK proporciona la respuesta a esta pregunta:

Debido a que algunas plataformas nativas solo proporcionan un tamaño de búfer limitado para flujos de entrada y salida estándar, si no se escribe rápidamente el flujo de entrada o se lee el flujo de salida del subproceso, el subproceso se puede bloquear e incluso bloquear.

¿Es este solo un caso de programadores que no leen la documentación, como se implica en el consejo frecuentemente citado: lea el manual fino (RTFM)? La respuesta es parcialmente sí. En este caso, leer el Javadoc le llevaría a la mitad del camino; explica que necesita manejar las transmisiones a su proceso externo, pero no le dice cómo.

Otra variable está en juego aquí, como es evidente por la gran cantidad de preguntas y conceptos erróneos de los programadores sobre esta API en los grupos de noticias: aunque Runtime.exec()y las API de proceso parecen extremadamente simples, esa simplicidad es engañosa debido al uso simple u obvio de la API. es propenso a errores. La lección aquí para el diseñador de API es reservar API simples para operaciones simples. Las operaciones propensas a complejidades y dependencias específicas de la plataforma deben reflejar el dominio con precisión. Es posible que una abstracción se lleve demasiado lejos. La JConfigbiblioteca proporciona un ejemplo de una API más completa para manejar operaciones de archivos y procesos (consulte Recursos a continuación para obtener más información).

Ahora, sigamos la documentación de JDK y manejemos el resultado del javacproceso. Cuando se ejecuta javacsin argumentos, se genera un conjunto de declaraciones de uso que describen cómo ejecutar el programa y el significado de todas las opciones disponibles del programa. Sabiendo que esto va a la stderrsecuencia, puede escribir fácilmente un programa para agotar esa secuencia antes de esperar a que finalice el proceso. El Listado 4.3 completa esa tarea. Si bien este enfoque funcionará, no es una buena solución general. Por lo tanto, el programa del Listado 4.3 se nombra MediocreExecJavac; proporciona sólo una solución mediocre. Una mejor solución vaciaría tanto el flujo de error estándar como el flujo de salida estándar. Y la mejor solución vaciaría estos flujos simultáneamente (lo demostraré más adelante).

Listado 4.3 MediocreExecJavac.java

importar java.util. *; importar java.io. *; public class MediocreExecJavac {public static void main (String args []) {try {Runtime rt = Runtime.getRuntime (); Proceso proc = rt.exec ("javac"); InputStream stderr = proc.getErrorStream (); InputStreamReader isr = nuevo InputStreamReader (stderr); BufferedReader br = new BufferedReader (isr); Línea de cadena = nulo; System.out.println (""); while ((línea = br.readLine ())! = null) System.out.println (línea); System.out.println (""); int exitVal = proc.waitFor (); System.out.println ("Process exitValue:" + exitVal); } captura (Throwable t) {t.printStackTrace (); }}}

Una serie de MediocreExecJavacgenera:

E: \ classes \ com \ javaworld \ jpitfalls \ article2> java MediocreExecJavac Uso: javac donde incluye: -g Genera toda la información de depuración -g: none No genera información de depuración -g: {lines, vars, source} Genera solo información de depuración -O Optimizar; puede dificultar la depuración o ampliar los archivos de clase -nowarn No generar advertencias -verosado Mensajes de salida sobre lo que está haciendo el compilador -deprecación Ubicaciones de origen de salida donde se utilizan API obsoletas -classpath Especificar dónde encontrar archivos de clase de usuario -sourcepath Especificar dónde encontrar archivos de origen de entrada -bootclasspath Anular la ubicación de los archivos de clase de arranque -extdirs Anular la ubicación de las extensiones instaladas -d Especificar dónde colocar los archivos de clase generados -encoding Especificar la codificación de caracteres utilizada por los archivos de origen -target Generar archivos de clase para una versión específica de VM Salida del proceso Valor: 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) 

Como se indicó anteriormente, el valor de error de 2significa "archivo no encontrado", lo que, en este caso, significa que dir.exeno se pudo encontrar el ejecutable nombrado . Eso es porque el comando de directorio es parte del intérprete de comandos de Windows y no un ejecutable separado. Para ejecutar el intérprete de comandos de Windows, ejecute command.como cmd.exe, según el sistema operativo Windows que utilice. El Listado 4.5 ejecuta una copia del intérprete de comandos de Windows y luego ejecuta el comando proporcionado por el usuario (por ejemplo, dir).

Listado 4.5 GoodWindowsExec.java