Ejecutar comandos Linux desde Java (II)

La primera parte se encuentra aquí.

En la entrada anterior vimos un ejemplo sencillo sobre cómo podíamos ejecutar un comando externo desde el código Java. Voy a intentar construir algo más complejo que esto, concretamente me interesa hacer una clase que pueda lanzar un comando Linux sin bloquear el hilo que lo origina. Es decir, lanzar un comando como si fuera un Thread más de la máquina virtual.

La forma más sencilla que conozco para hacerlo sin ningún tipo de bloqueos es mediante un Listener. A mi me interesa ver en tiempo real la STDOUT y STDERR del comando externo. También quiero saber cuando acaba y cuál es el código de salida. Construyo una interfaz:



public interface ProcessListener {
  // se llama cuando se actualiza la salida estándar
  public void updateStdOut(byte[] b);
  
  // se llama cuando se actualiza StdErr
  public void updateStdErr(byte[] b);
  
  // se llama cuando el proceso termina
  public void processFinish(int exitCode, byte[] stdOut, byte[] stdErr);
}

Esta será la interfaz que implementaremos en una clase que será la responsable de realizar ciertas operaciones en función de las distintas salidas del comando exterior. Ahora necesitamos una clase que será la encargada de lanzar el proceso propiamente dicho:



public class ProcessLauncher implements Runnable {
  private String Command;
  
  private int ExitCode = Integer.MIN_VALUE;
  
  private Thread MyThread;
  
  private Vector StdOut;
  private Vector StdErr;
  
  private ProcessListener MyProcessListener;
  
  private Object Lock = new Object();
  
  private Process MyProcess;
  
  public ProcessLauncher(String command, ProcessListener pl) {
    Command = command;
    MyProcessListener = pl;
    
    StdOut = new Vector();
    StdErr = new Vector();
    
    MyThread = new Thread(this);
    MyThread.start();
  }
  
  public boolean isFinished() {
    return ExitCode != Integer.MIN_VALUE;
  }
  
  public void run() {
    try {
      // arranco el proceso
      MyProcess = startProcess();
      // creo un controlador del proceso
      ProcessController pc = new ProcessController(MyProcess, this);
      
      // ahora hasta que no finalice el método voy leyendo la salida
      while(ExitCode == Integer.MIN_VALUE) {
        synchronized(Lock) {
          // de esta forma me aseguro que nadie más está leyendo la salida
          readStreams();
        }
        
        Thread.sleep(500)// duermo un poco para liberar los recursos
      }
    }catch(Exception e){
      e.printStackTrace();
    }
  }
  
  private void readStreams() throws Exception {
    InputStream stdout = MyProcess.getInputStream();
    InputStream stderr = MyProcess.getErrorStream();
    
    byte[] b;
    int size;
    
    size = stdout.available();
    if(size != 0) {
      b = new byte[size];
      stdout.read(b);
      
      MyProcessListener.updateStdOut(b);
      for(int i = 0; i < b.length; i++) {
        StdOut.add(b[i]);
      }
    }
    
    size = stderr.available();
    if(size != 0) {
      b = new byte[size];
      stderr.read(b);
      
      MyProcessListener.updateStdErr(b);
      for(int i = 0; i < b.length; i++) {
        StdErr.add(b[i]);
      }
    }
  }
  
  private Process startProcess() throws Exception {
    Process proc = Runtime.getRuntime().exec(Command);
    return proc;
  }
  
  public void notifyExit(int exitCodethrows Exception {
    // me notifican que se el proceso se ha terminado
    ExitCode = exitCode;
    
    // leo por última vez las salidas, por si se ha quedado algo
    synchronized (Lock) {
      readStreams();
    }
    
    // notifico al listener
    byte[] stdOut = new byte[StdOut.size()];
    for(int i = 0; i < stdOut.length; i++) {
      stdOut[i((Byte)StdOut.get(i)).byteValue();
    }
    
    byte[] stdErr = new byte[StdErr.size()];
    for(int i = 0; i < stdErr.length; i++) {
      stdErr[i((Byte)StdErr.get(i)).byteValue();
    }
    
    MyProcessListener.processFinish(exitCode, stdOut, stdErr);
  }
}

Esta clase se encargará de leer periodicamente los flujos de salida del proceso e ir tanto guardándolo en memoria como pasándolo al Listener en tiempo real. Pero claro, aquí nos encontramos con otra dificultad y es que resulta que queremos saber a ciencia cierta y en el momento exacto cuando termina nuestro proceso, para ello definimos un controlador de proceso (ProcessController):



public class ProcessController implements Runnable {
  private Process MyProcess;
  
  private Thread MyThread;
  
  private ProcessLauncher MyProcessLauncher;
  
  public ProcessController(Process proc, ProcessLauncher pl) {
    MyProcess = proc;
    MyProcessLauncher = pl;
    
    MyThread = new Thread(this);
    MyThread.start();
  }
  
  public void run() {
    try {
      int code = MyProcess.waitFor();
      
      // notificar al padre
      MyProcessLauncher.notifyExit(code);
    }catch(Exception e){
      e.printStackTrace();
    }
  }
}

El objeto de esta clase se ejecutará en un hilo aparte que se quedará bloqueado hasta que el proceso termine. En cuanto termina el proceso el código de salida es comunicado a ProcessLauncher para que este pueda tomar las acciones oportunas. Para la próxima vez pondré ejemplos de utilización y si me da tiempo prepararé un JAR para que se pueda bajar y usar tranquilamente como si fuera una librería.

3 comentarios a “Ejecutar comandos Linux desde Java (II)”

  1. [...] Ejecutar comandos Linux desde Java (II)    Copias de seguridad de MySQL con mysqldump [...]

  2. jcesar dice:

    podrias poner los ejemplos de utilizacion?

    muy buen blog.

    saludos

  3. danny dice:

    puedes poner un ejemplo de como utilizar tu interfaz para ejecutar procesos.

    lo que a mi me sucede esque tengo varios archivos .bat que necesito ejecutar, pero sólo me ejecuta el primero, al intentar ejecutar el segundo me lanza una excepcion que indica que el proceso esta ocupado

    he tratado de manejarlo con un hilo, y también he tratado con el process.waitFor() pero éste último se queda colgado (al parecer bloqueado),

    Básicamente:

    1.- creo el archivo .bat el cual contiene instrucciones a ejecutar
    2.- ejecuto el .bat
    3.- elmino el .bat
    4.- vuelvo al paso 1 y continuo hasta terminar todas las instrucciones que quiero ejecutar (para ello es necesario ejecutarlas en diferentes bat) pero al intentar ejecutar el siguiente bat no lo hace porque me dice que el proceso esta ocupado.

    espero puedas ayudarme

Deja un comentario