Implementar un Singleton en Python

domingo, enero 29, 2012

Singleton es un patrón de diseño cuya función es evitar que un objeto pueda ser instanciado más de una vez. En este post traigo una receta simple para implementar el patrón singleton en Python.

En esta implementación utilizaremos un archivo de bloqueo (lock file) para indicar si la aplicación está en ejecución o no. Python cuenta con el módulo fcntl que nos proporciona una interfaz bastante cómoda para el control de archivos pero está disponible solo para Linux/Unix, eso implica que debemos usar medidas alternativas para Windows.

Detectar SO e importar módulos

Lo primero que debemos hacer es detectar el sistema operativo y ejecutar los import correspondientes para cada caso. Usaremos además el módulo tempfile para generar el lock file como un archivo temporal del sistema.
#!/usr/bin/python2
# -*- coding: utf-8 -*-

import os
import sys
import tempfile

OS = None
if sys.platform.startswith('linux'):
    OS = 'linux'
    import fcntl
elif sys.platform.startswith('win32'):
    OS = 'windows'


Definir la clase Singleton

Después de detectar el sistema operativo definimos la clase Singleton. Básicamente, esta clase será la encargada de crear el lock file al inicio o generar una advertencia y termina la ejecución en caso de que el archivo ya exista (es decir, que ya existe una instancia de la aplicación en ejecución).
class Singleton:
    def __init__(self):
        # Variable para almacenar el file descriptor
        self.fd = None
        # Ruta para el lock file en la carpeta temporal del sistema
        self.filepath = os.path.abspath(os.path.join(tempfile.gettempdir(), 
            'myapp.pid'))
        
        if OS == 'linux':
            # Para el caso de linux usamos el módulo fcntl para crear el archivo
            # y bloquearlo automáticamente. Si la operación falla es porque el
            # archivo ya existe y está bloqueado.
            self.fd = open(self.filepath, 'w')
            try:
                fcntl.lockf(self.fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
            except IOError:
                self.__exit()
        elif OS == 'windows':
            try:
                # Para el caso windows simplemente creamos el archivo "a mano",
                # pero verificamos primero si el archivo existe e intentamos 
                # removerlo (para casos en que la ejecución previa haya sido 
                # interrumpida)
                if os.path.exists(self.filepath):
                    os.unlink(self.filepath)
                self.fd = os.open(self.filepath, os.O_CREAT|os.O_EXCL|os.O_RDWR)
            except OSError, err:
                if err.errno == 13:
                    self.__exit()
    
    def __del__(self):
        # Para el caso de windows también debemos destruir el archivo "a mano" 
        # al finalizar la ejecución del programa.
        if OS == 'windows':
            if self.fd:
                os.close(self.fd)
                os.unlink(self.filepath)
    
    def __exit(self):
        print 'Ya hay una instancia en ejecución. Saliendo'
        sys.exit(-1)
En el __init__ se observa que creamos una variable para almacenar el file descriptor (fd), luego generamos una ruta para el lock file. Posteriormente creamos un lock file con el módulo fcntl (en el caso linux) o creamos un archivo regular (para el caso windows). Adicionalmente, en el caso windows necesitamos hacernos cargo del archivo al finalizar la ejecución, para eso sobreescribimos el método __del__ y colocamos nuestro código. Adicionalmente tenemos la función __exit(), que es la encargada de detener la ejecución del programa de forma elegante.

Clase de pruebas

Con los pasos anteriores tenemos lista nuestra implementación simple del patrón singleton. Ahora, ¿Cómo la usamos? Creamos una clase (por ejemplo MyApp) que herede de singleton y ponemos un bucle infinito para que se mantenga haciendo "algo".
class MyApp(Singleton):
    def __init__(self):
        Singleton.__init__(self)
        print 'Ejecutando MyApp'
        # Creamos un bucle infinito solo para mantener la aplicación en
        # ejecución
        while 1:
            continue


¿Cómo se vería nuestro script?

El código completo de nuestro script se vería así:
#!/usr/bin/python2
# -*- coding: utf-8 -*-

import os
import sys
import tempfile

OS = None
if sys.platform.startswith('linux'):
    OS = 'linux'
    import fcntl
elif sys.platform.startswith('win32'):
    OS = 'windows'

class Singleton:
    def __init__(self):
        # Variable para almacenar el file descriptor
        self.fd = None
        # Ruta para el lock file en la carpeta temporal del sistema
        self.filepath = os.path.abspath(os.path.join(tempfile.gettempdir(), 
            'myapp.pid'))
        
        if OS == 'linux':
            # Para el caso de linux usamos el módulo fcntl para crear el archivo
            # y bloquearlo automáticamente. Si la operación falla es porque el
            # archivo ya existe y está bloqueado.
            self.fd = open(self.filepath, 'w')
            try:
                fcntl.lockf(self.fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
            except IOError:
                self.__exit()
        elif OS == 'windows':
            try:
                # Para el caso windows simplemente creamos el archivo "a mano",
                # pero verificamos primero si el archivo existe e intentamos 
                # removerlo (para casos en que la ejecución previa haya sido 
                # interrumpida)
                if os.path.exists(self.filepath):
                    os.unlink(self.filepath)
                self.fd = os.open(self.filepath, os.O_CREAT|os.O_EXCL|os.O_RDWR)
            except OSError, err:
                if err.errno == 13:
                    self.__exit()
    
    def __del__(self):
        # Para el caso de windows también debemos destruir el archivo "a mano" 
        # al finalizar la ejecución del programa.
        if OS == 'windows':
            if self.fd:
                os.close(self.fd)
                os.unlink(self.filepath)
    
    def __exit(self):
        print 'Ya hay una instancia en ejecución. Saliendo'
        sys.exit(-1)

class MyApp(Singleton):
    def __init__(self):
        Singleton.__init__(self)
        print 'Ejecutando MyApp'
        # Creamos un bucle infinito solo para mantener la aplicación en
        # ejecución
        while 1:
            continue
    
if __name__ == '__main__':
    app = MyApp()


Probando el singleton

Para probarlo abrimos un terminal, nos colocamos en la carpeta donde esté ubicado nuestro script y lo ejecutamos por primera vez. Eso nos dará como resultado algo como:
$ python myapp.py 
Ejecutando MyApp


Abrimos una terminal nueva (sin cerrar la terminal anterior) e intentamos ejecutar la aplicación por segunda vez. Eso nos devolverá:
$ python myapp.py 
Ya hay una instancia en ejecución. Saliendo
¡Y voilá! Logramos que un script de Python pueda ser ejecutado una sola vez.

Hay implementaciones más complejas que almacenan el ID del proceso dentro del lock file y cada vez que se intenta ejecutar una nueva instancia se lee el ID y se verifica que realmente exista un proceso en ejecución con ese identificador. Pero como dije al principio, esta es una receta simple, así que as implementaciones más complejas las dejamos como tareas para el lector ;)

Mi propuesta para Canaima-Instalador

sábado, enero 28, 2012

Actualmente estoy colaborando con el desarrollo de canaima-instalador, una aplicación para Canaima GNU/Linux que permitirá al usuario instalar/probar Canaima de una manera fácil e intuitiva, y he decidido hacer una propuesta sobre cómo debe lucir y funcionar el nuevo instalador. La propuesta a continuación.

Paso 1: LiveCD

Lo primero que hace falta cambiar es el menú de inicio del liveCD. Actualmente tenemos una pantalla llena de opciones que, más allá de facilitar el uso, pueden confundir a los usuarios menos experimentados. Mi sugerencia es que el liveCD arranque automáticamente y presente un menú inferior con las posibles opciones para usuarios más experimentados.


Paso 2: Bienvenida

Aquí es donde comienza a ejecutarse canaima-instalador. La idea es que sea tipo OEM, es decir, que no cargue el escritorio sino lo mínimo necesario para ejecutarse. Se le preguntará al usuario si desea probar la distribución o instalar. Para el primer caso se cierra canaima-instalador y se continúa con la carga del escritorio, para el segundo caso se sigue el flujo normal de la aplicación.


Paso 3: Requisitos

Se le muestra al usuario cuáles son los requisitos mínimos necesarios para obtener mejores resultados al instalar Canaima. En caso de que no se cumpla con un requisito crítico (por ejemplo, el espacio en disco) la instalación no debería continuar.


Paso 4: Modo de instalación

Acá (luego de haber analizado el hardware) se le presentan al usuario las distintas opciones que tiene para instalar. En teoría todas deberían ser automáticas excepto la de "Particionamiento Avanzado" que ejecutará una instancia de gparted y bloqueará canaima-instalador hasta que el usuario termine de definir sus particiones. Una vez que el usuario haga clic en "Siguiente" no podrá volver atrás. Acá termina la primera fase de instalación y comienza el copiado de los archivos en el disco duro en segundo plano. La idea es aprovechar el tiempo, mientras se copian los archivos solicitamos al usuario el resto de la información y una vez terminada la copia se ejecutan las tareas correspondientes. De esta forma logramos minimizar considerablemente el tiempo de instalación (tal como lo hace Ubuntu).


Paso 5: Configuración de teclado

Como expliqué en el punto anterior, mientras se copian los archivos en segundo plano solicitamos el resto de la información al usuario. Acá se le pedirá que seleccione la distribución del teclado. Es importante observar que el botón de "Anterior" no estará disponible en este paso.


Paso 6: Configuración de usuarios

En esta fase se le pedirán los datos de las cuentas (root y usuario regular). Es importante habilitar un link (o botón) de ayuda donde se pueda explicar qué significa root y por qué es importante definir esa contraseña. Es importante recalcar que si el usuario no ha terminado de introducir la información complementaria y la copia de archivos finaliza entonces las tareas en segundo plano se detendrán hasta que se culmine esta fase.


Paso 7: Imágenes aleatorias sobre las bondades de Canaima

Al igual que Ubuntu y otros sistemas operativos, podemos mostrar una serie de imágenes aleatorias que informen al usuario sobre las bondades de Canaima GNU/Linux y del Software Libre mientras termina la instalación.


Paso 8: Fin de la instalación

Al finalizar todo exitosamente se le mostrará un pequeño diálogo al usuario para que reinicie el equipo y comience a disfrutar de las bondandes del SL.



Bueno, esta es mi idea de lo que debería ser un instalador fácil e intuitivo para Canaima GNU/Linux. Se escuchan comentarios Update para los haters: Sí, mi propuesta está basada casi enteramente en el instalador de Ubuntu porque me parece un excelente instalador ¿Cuál es el problema con eso?