jueves, 3 de noviembre de 2011

Alarmas y subprocesos en PERL

Poniendo el despertador...

En determinadas ocasiones se da la situación de necesitar ejecutar un comando de sistema desde PERL, bien para capturar su salida y usarla para algo o bien descartarla. Si no tenemos cuidado y controlamos lo que estamos ejecutando podemos caer en el error de disparar procesos que queden colgados e interrumpir el flujo principal de nuestro programa, o peor aún podemos no advertir que el comando que pedimos ejecutar falló por algun error y el programa principal no recibió alerta alguna continuando como si todo fuese normal. Para salvar estos inconvenientes vamos a usar la instrucción eval y la señal ALRM.

Veamos ejemplo.pl.
 
#! /usr/bin/perl

use strict;

my $log; 
my $status;
eval { 
        local $SIG{ALRM} = sub {die "timeout\n"};
        alarm 15;
        $log        = `micomando.sh` || die;
        $status     = $?>>8;
        alarm 0;
};
if ( $@ ) 
{
    if ( /timeout/ )
    {
        print "Salida por timeout\n";
    }
      else
    {
        print   "Comando fallido\n".
                "Valor en '\$\!': $!\n".
                "Valor en '\$\@': $@\n";
    }
}
  else
{
    print "Ningun error\nLog: $log\n";
}

Comandos a utilizar.

eval:
Ejecutara el bloque de codigo contenido entre las llaves y si se produce un error podremos realizar la evaluacion necesaria usando la variable especial $@, esto se conoce como "Excepciones". Cuando se produce la "excepción" tendremos la posibilidad de ejecutar algun codigo destinado a brindar mas informacion al usuario para indicarle que fue lo que salio mal.

alarm:
El comando alarm toma como argumento el nro de segundos en el que se producira la llamada. Es decir en este caso 15 segundos. Pasado ese tiempo se dispara la llama a la señal ALRM y por consiguiente se ejecuta el codigo indicado entre llaves die "timeout\n" .

Variables especiales:

%SIG
Hash interno de PERL que contiene "signal handlers". Es decir podemos asignar una funcion o bloque de codigo a una señal en particular. Que es exactamente lo que se hace en esta linea local $SIG{ALRM} = sub {die "timeout\n"}; concretamente esta linea indica que cuando se produzca la señal ALRM se ejecutara el codigo que esta entre llaves. Al ejecutarla con local nos aseguramos de que la modificación no se propague por el resto del programa y permanezca dentro del bloque eval.

$@
Otra variable interna de PERL que captura el mensaje de error del ultimo bloque eval, aqui se almacenara el mensaje de error que arroje el bloque eval en el caso de producirse una falla.

$!
Variable interna de PERL, esta variable toma un valor cuando una llamada al sistema falla. El valor solo es valido inmediatamente despues de la llamada que falló.Esta variable tiene una forma particular de comportarse, si se la invoca como un string, devolvera el mensaje en formato "human readable" pero si se la invoca en forma numerica devolvera el valor de la constante definida en errno.h. Un experimento interesante para realizar es printf("%d: %s\n", $!, $! ); y podremos ver ambos valores.

$?
Variable inerna de PERL, esta variable es bastante especial en el sentido de que en realidad es una palabra de 16 bits (o 2 bytes) que almacena diferente información dependiendo del caso.
La información que devolverá es la siguiente:

  1. El valor de salida del subproceso.
  2. La señal, si existiese una, que recibio el subproceso.
  3. Si se generó core dump o no (boolean) .
Hay que tener en cuenta que los valores que contiene esta variable son bits, por lo tanto deberemos utilizar los operadores de bit sobre la misma.En la man page se explica con detalles.

Ejecutando comandos externos.

Cuando comienza a correr el script ejecuta el codigo que se encuentra en el bloque eval, carga la funcion anonima en el handler ALRM y setea la alarma para dentro de 5 segundos. Luego procede a lanzar el programa "micomando.sh" seguido del operador "or", tambien indicado como ||, y la instrucción die. Seguidamente se captura la salida del comando en la variable $log y el status de la ejecucion en $status y se procede a poner la alarma nuevamente en 0.

En el caso de que el programa "micomando.sh" quede en espera por mas de 5 segundos, el programa principal toma el control y ejecuta el comando die con el mensaje "timeout". Una aclaración aqui, el comando lanzado de fondo sigue corriendo, el die no lo matará sino que simplemente devolverá el control al flujo principal con la excepción. Una buena modificación seria obtener el PID del proceso lanzado y enviarle una señal SIGKILL para matarlo si hemos detectado que no responde.

En el caso de que la ejecución falle, por ejemplo por falta de permisos o porque el comando no existe, se ejecutará die y saldrá tambien con una excepción pero el error ya no será "timeout", si examinamos la variable $@ veremos que tiene el mensaje "Died at ./child_die.pl line 11". Continuando con la ejecución se puede ver que se declara la variable $status pero no se usa en ninguna parte del script. En realidad en la segunda entrega voy a agregar mas codigo para hacer un poco mas inteligente el script y que pueda tomar alguna decisión con respecto al status que devolvio el comando ejecutado. Por ahora solo quedara definida la variable y almacenará el status de la ejecución del ultimo comando si este fue exitoso ( que será siempre 0 ); caso contrario nunca se llenará ya que por ahora sale en la linea anterior con el die.

Esto es solamente una introducción al tema, PERL tiene documentación muy interesante y muy variada al respecto y hay capítulos enteros dedicados a esto, pero para implementar algo rapido y sencillo sirve.

Mas info en paginas del manual de PERL a traves de perldoc:
  • perlipc
  • perlvar
  • perlfunc
Libros de OREILLY:
  • Mastering PERL
  • PERL CookBook

No hay comentarios.:

Publicar un comentario