Enviar mensajes a través de D-Bus usando Python

martes, febrero 14, 2012

D-Bus es un sistema que permite la comunicación entre diferentes procesos. Es desarrollado como parte del proyecto freedesktop.org buscando ofrecer una solución simple y común para los distintos entornos de escritorio.

Conceptos básicos


Tipos de Bus

Hay dos tipos de buses que se pueden usar con D-Bus. El bus de sesión, que se crea con cada sesión de usuario y es local a esa sesión, y el bus de sistema. Este último es global, se inicia cuando arranca el equipo y generalmente se utiliza para comunicarse con procesos como udev, NetworkManager o HAL.

Rutas a Objetos

Cada lenguaje de programación tiene sus objetos nativos (usualmente representados por clases). La ruta a un objeto es la forma en que D-Bus permite hacer referencia a un objeto nativo y que las aplicaciones remotas puedan usarlo. Un ruta a un objeto luce como la ruta de un archivo (Unix-like) y es común generarlas como un nombre de dominio en reversa, por ejemplo: /org/gnome/myapp/MyObject. Sin embargo, cada desarrollador puede usar la ruta que mejor le parezca, siempre y cuando sea única.

Métodos y Señales

Los métodos son operaciones (con o sin parámetros) que pueden invocarse en un objeto y que eventualmente pueden devolver un resultado. Las señales son notificaciones que se envían al bus y son recibidas por los observadores (objetos que escuchan o se conectan a esas señales). Estas señales también pueden enviar datos de interés para el receptor.

Objetos Proxy

Un objeto proxy no es más que un objeto Python que viene a representar a un objeto remoto en otro proceso. Esto nos permite emplear los métodos del objeto remoto como si fueran métodos nativos. Para instanciar un objeto proxy necesitamos la "ruta del objeto".

Estructura Básica

Para usar D-Bus es importante entender la estructura e interacciones básicas. En palabras simples, lo que tenemos son dos aplicaciones que se "hablan" entre sí a través de un canal común. Generalmente una de ellas actúa como "servidor", ofreciendo métodos y señales que podrán ser usados por una aplicación "cliente". En la imagen a continuación se ilustra claramente el concepto.



Un ejemplo bastante común es el funcionamiento de un reproductor de música (como el servidor) que ofrece métodos para informar sobre su estado actual y una aplicación de notificaciones (como cliente) que usa estos métodos para mostrar diálogos en el escritorio cada vez que cambia una canción.

Aplicación "servidor"


Lo primero que debemos hacer para empezar a trabajar con D-Bus es conectarnos a un bus. En nuestro caso será al bus de sesión porque no nos interesa interactuar con procesos del sistema operativo.

Adicionalmente, para ejecutar llamadas asíncronas a los métodos (y que la aplicación no se bloquee mientras espera) debemos configurar un bucle principal. Para el momento de escribir este post, python-dbus solo soporta el bucle principal de GLib, así que usaremos esas librerías para crear nuestro loop.

Los import que necesitamos para esto serían:
import dbus
import dbus.service
import gobject

from dbus.mainloop.glib import DBusGMainLoop

Luego, definimos (en variable globales) el nombre del bus y la ruta de nuestro objeto:
DBUS_BUSNAME = 'org.example.ExampleDBus'
DBUS_MYOBJECT_PATH = '/org/example/ExampleDBus/MyObject'

Ahora definimos la clase de nuestro server, con la inicialización mínima para que funcione D-Bus:
class DBusService(dbus.service.Object):
    
    def __init__(self):
        # Le indicamos a D-Bus que usaremos el loop de GLib como bucle
        # predeterminado
        DBusGMainLoop(set_as_default=True)
        
        # Establecemos la conexión al bus de sesión
        self.session_bus = dbus.SessionBus()
        name = dbus.service.BusName(DBUS_BUSNAME, self.session_bus)
        
        # Inicializamos el objeto D-Bus
        dbus.service.Object.__init__(self, self.session_bus, DBUS_MYOBJECT_PATH)
        
        # Arrancamos el bucle principal
        loop = gobject.MainLoop()
        
        # Colocamos el loop dentro de un try/except para detectar cuando el 
        # usuario presione Ctrl + C y finalizar la aplicación limpiamente
        try:
            print "Servicio DBus iniciado"
            loop.run()
        except KeyboardInterrupt:
            loop.quit()
            print "Servicio DBus finalizado"

Si se fijan en el código anterior, nuestra clase hereda de dbus.service.Object (porque estamos construyendo un objeto D-Bus que será instanciado por una aplicación remota). En la inicialización le indicamos a D-Bus que usaremos el loop de GLib, establecemos la conexión al bus de sesión y arrancamos el loop.

He decidido colocar la ejecución del loop principal dentro de un try/except para que el usuario pueda usar Ctrl + C para salir elegantemente de la aplicación.

Hecho esto podemos proceder a definir un par de métodos. Hagamos un método que imprima un saludo y otro que nos devuelva un valor, en este caso la hora.
    @dbus.service.method(DBUS_BUSNAME)
    def say_hello(self, name):
        print "Hola, %s" % name
    
    @dbus.service.method(DBUS_BUSNAME)
    def get_time(self):
        return time.strftime("%H:%M")

En los párrafos previos comenté que un objeto proxy no es más que una representación de un objeto D-Bus en un objeto Python, así que para exportar un método nativo como método D-Bus usamos el decorador @dbus.service.method y le pasamos como parámetro el nombre del bus. Luego definimos nuestras funciones como lo haríamos normalmente en cualquier clase de Python y ya con esto tendríamos un servidor muy básico listo para funcionar.

El código completo debería quedar así:
#!/usr/bin/python
# -*- coding: utf-8 -*-

import time
import dbus
import gobject
import dbus.service

from dbus.mainloop.glib import DBusGMainLoop

DBUS_BUSNAME = 'org.example.ExampleDBus'
DBUS_MYOBJECT_PATH = '/org/example/ExampleDBus/MyObject'

class DBusService(dbus.service.Object):
    
    def __init__(self):
        # Le indicamos a D-Bus que usaremos el loop de GLib como bucle
        # predeterminado
        DBusGMainLoop(set_as_default=True)
        
        # Establecemos la conexión al bus de sesión
        self.session_bus = dbus.SessionBus()
        name = dbus.service.BusName(DBUS_BUSNAME, self.session_bus)
        
        # Inicializamos el objeto D-Bus
        dbus.service.Object.__init__(self, self.session_bus, DBUS_MYOBJECT_PATH)
        
        # Arrancamos el bucle principal
        loop = gobject.MainLoop()
        
        # Colocamos el loop dentro de un try/except para detectar cuando el 
        # usuario presione Ctrl + C y finalizar la aplicación limpiamente
        try:
            print "Servicio DBus iniciado"
            loop.run()
        except KeyboardInterrupt:
            loop.quit()
            print "Servicio DBus finalizado"
        
    @dbus.service.method(DBUS_BUSNAME)
    def say_hello(self, name):
        print "Hola, %s" % name
    
    @dbus.service.method(DBUS_BUSNAME)
    def get_time(self):
        return time.strftime("%H:%M")
    
    @dbus.service.signal(DBUS_BUSNAME)
    def kill(self):
        return 'killed'

if __name__ == '__main__':
    service = DBusService()

Aplicación "cliente"

La aplicación cliente es mucho más simple. La única librería que necesitamos es la de D-Bus, el nombre del bus y la ruta del objeto.
import dbus

DBUS_BUSNAME = 'org.example.ExampleDBus'
DBUS_MYOBJECT_PATH = '/org/example/ExampleDBus/MyObject'

Ahora pasemos a crear la clase. Al igual que en el servidor, necesitamos establecer la conexión con el bus y "mapear" el objeto D-Bus en un objeto Python. En este caso la variable self.my_object es la que contiene la representación de dicho objeto.
class DBusClient:
    
    def __init__(self):
        self.session_bus = dbus.SessionBus()
        self.my_object = self.session_bus.get_object(DBUS_BUSNAME, 
            DBUS_MYOBJECT_PATH)

Para llamar a un método del objeto remoto debemos importar el método correspondiente, y como estaremos usando varios métodos vamos a crear una función interna que nos ayude con esta tarea.
    def __get_dbus_method(self, name):
        return self.my_object.get_dbus_method(name)

Procedemos a crear nuestros métodos nativos que obtendrán el método remoto del objeto D-Bus y lo llamarán con los parámetros correspondientes según sea el caso.
    def say_hello(self, name):
        # Almacenamos el método en una variable y luego lo llamamos con los
        # parámetros correspondientes
        method = self.__get_dbus_method('say_hello')
        method(name)
    
    def print_time(self):
        method = self.__get_dbus_method('get_time')
        # Obtenemos el valor de retorno del método y la imprimimos en pantalla
        current_time = method()
        print "Son las %s" % current_time

Con eso ya deberíamos tener un cliente funcional. El código completo del cliente debería quedar algo como:
#!/usr/bin/python
# -*- coding: utf-8 -*-

import dbus

DBUS_BUSNAME = 'org.example.ExampleDBus'
DBUS_MYOBJECT_PATH = '/org/example/ExampleDBus/MyObject'
        
class DBusClient:
    
    def __init__(self):
        self.session_bus = dbus.SessionBus()
        self.my_object = self.session_bus.get_object(DBUS_BUSNAME, 
            DBUS_MYOBJECT_PATH)
    
    def __get_dbus_method(self, name):
        return self.my_object.get_dbus_method(name)
        
    def say_hello(self, name):
        # Almacenamos el método en una variable y luego lo llamamos con los
        # parámetros correspondientes
        method = self.__get_dbus_method('say_hello')
        method(name)
    
    def print_time(self):
        method = self.__get_dbus_method('get_time')
        # Obtenemos el valor de retorno del método y la imprimimos en pantalla
        current_time = method()
        print "Son las %s" % current_time

if __name__ == '__main__':
    client = DBusClient()
    client.say_hello('Pedro')
    client.say_hello('Maria')
    client.print_time()
Es importante que observe que al ejecutar el script del cliente se llamará al método say_hello dos veces (con los parámetros 'Pedro' y 'Maria' respectivamente) y luego al método print_time.

Pruebas

Para probar abrimos dos terminales. En la primera ejecutamos el script del servidor (yo lo llamé dbus_service.py) y veremos algo como:
$ python dbus_service.py 
Servicio DBus iniciado
Con un mensaje indicándonos que el servicio está corriendo y esperando ser utilizado.

En la segunda terminal ejecutamos el script del cliente (lo llamé dbus_client.py) y veremos algo como:
$ python dbus_client.py 
Son las 12:55
Muy bien, imprimió la hora pero... ¿No se suponía que también habíamos llamado al método say_hello dos veces? ¿Qué pasó?

Bueno, volvamos a ver la terminal del servicio y veremos nuestro ansiado resultado:
$ python dbus_service.py 
Servicio DBus iniciado
Hola, Pedro
Hola, Maria

Tal como lo esperabamos, el método say_hello imprime en la instancia del servidor el nombre envíado desde el cliente y por otro lado, el cliente imprime la hora que le devuelve el método get_time desde el servidor. Hemos enviado mensajes en ambas direcciones usando D-Bus, ¡todo un éxito!

Espero que esta receta simple les haya ayudado a comprender como funciona D-Bus y como usarlo desde Python. Más adelante estaré escribiendo otros posts sobre cómo emitir/recibir señales y cómo enviar grandes cantidades de datos "al vuelo" a través de D-Bus.

Fuentes:

1 comentarios:

Anónimo dijo...

Wil, me parece maravilloso este post, me está sirviendo de mucho para mi trabajo de diploma, te lo agradezco. Me interesaría saber si puedes publicar algo para usar python, dbus y policykit, te lo agradecería inmensamente.