Archivo por días: 23 agosto, 2011

DBus en Vala

Actualizado Estoy trabajando en un nuevo proyecto, escrito íntegramente en Vala, y la verdad es que cuantas más cosas aprendo sobre él, más me gusta.

Lo último que descubrí fue como trabajar con DBus en Vala, y resulta que es extremadamente sencillo y elegante, aunque tuve que investigar bastante hasta encontrar como hacer algunas cosas, pues los ejemplos que vienen en la página tratan de como trabajar con un servidor hecho por uno mismo, cuando yo necesitaba acceder a un servicio del sistema.

Así pues, y sin más dilación, veamos como se puede usar DBus para obtener una lista de los discos duros conectados por USB al ordenador, y como formatearlos.

Lo primero que vemos es que toda esa información la gestiona el demonio UDisk, el cual exporta una serie de funciones y objetos a través de DBus para que cualquier programa pueda acceder a ellos. Si echamos un vistazo a la documentación de la API, vemos que se exporta el método EnumerateDevices, el cual nos devuelve un array de objetos.

¿Y como accedemos a este método del objeto UDisk desde Vala? Pues con este código:

[DBus (name = "org.freedesktop.UDisks")]
interface UDisk_if : GLib.Object {
    public abstract ObjectPath[] EnumerateDevices() throws IOError;
}

UDisk_if udisk = Bus.get_proxy_sync<UDisk_if> (BusType.SYSTEM, "org.freedesktop.UDisks","/org/freedesktop/UDisks");
var retval = udisk.EnumerateDevices();

Aquí vemos que primero definimos una interfaz con todos los métodos a los que vamos a querer acceder (en este caso sólo uno), sacando de la documentación los parámetros y demás, y le añadimos antes una cabecera con el nombre del objeto. Luego, creamos un proxy síncrono, conectándonos a través del bus del sistema (BusType.SYSTEM), al servidor UDisk (org.freedesktop.UDisks) y pedimos acceso al objeto deseado (/org/freedesktop/UDisks). Finalmente, llamamos al método deseado, almacenando el resultado en una nueva variable.

Es importante recalcar que cualquier método que se defina puede emitir, siempre, una excepción de tipo IOError, por lo que siempre se debe añadir al final de la definición la coletilla throws IOError. De no hacerlo se producirá un error de compilación. También comentar que dado que esta función sólo devuelve un parámetro, la he escrito en formato resultado metodo(parametros); sin embargo, también es perfectamente válida la sintaxis void metodo (parametros,…, out resultado). Esta segunda forma es obligatoria cuando un método devuelve más de un resultado. Con esa sintaxis, la interfaz tendría esta forma:

[DBus (name = "org.freedesktop.UDisks")]
interface UDisk_if : GLib.Object {
    public abstract void EnumerateDevices(out ObjectPath[] path) throws IOError;
}

En retval tenemos ahora un array de objetos ObjectPath, cada uno de tipo Device, representando una unidad extraíble. ¿Y ahora, qué? Pues ahora vamos a imprimir el punto de montaje de cada uno de ellos. Si miramos la documentación del objeto Device, vemos que dicha información se almacena como una propiedad, así que definimos nuestra interfaz así:

[DBus (name = "org.freedesktop.UDisks.Device")]
interface Device_if : GLib.Object {
    public abstract string IdLabel { owned get; }
    public abstract string[] DeviceMountPaths { owned get; }

    public abstract void FilesystemUnmount(string[] options) throws IOError;
    public abstract void FilesystemCreate(string type, string[] options) throws IOError;
    public abstract void PartitionModify (string type, string label, string[] options) throws IOError;
    public abstract void FilesystemMount(string type, string[] options, out string mount_path) throws IOError;
}

Ponemos { owned get; } en lugar de {owned get; set; } porque los campos son de sólo lectura. También he añadido los cuatro métodos necesarios para poder formatear una partición, aunque en el ejemplo no los voy a utilizar.

Ahora llega el momento del código que accede a los datos, que es éste:

Device_if device2;
foreach (ObjectPath o in retval) {
    device2 = Bus.get_proxy_sync<Device_if> (BusType.SYSTEM, "org.freedesktop.UDisks",o);
    GLib.stdout.printf("Disco %s montado en:n",device2.IdLabel);
    foreach (string s in device2.DeviceMountPaths) {
        GLib.stdout.printf("    %sn",s);
    }
}

Así, primero recorremos la lista de objetos que obtuvimos con la anterior llamada (en retval), y creamos un proxy síncrono para cada uno de ellos, contra el servidor de UDisk (él es quien nos dio la lista de objetos, por lo que estos tienen que estar en él). Luego imprimimos la etiqueta simplemente leyendo la propiedad del objeto, y por último imprimimos todos los puntos de montaje (una misma partición puede estar montada en varios sitios a la vez).

Actualización: Uno de los problemas que tuve fue que la llamada a FilesystemCreate (que formatea la unidad especificada) tarda varios segundos en ejecutarse, lo que hacía que la GUI se quedase colgada. La primera solución que encontré fue ejecutarla en un thread aparte, y usar espera activa para detectar cuando terminó, pero era muy poco elegante. La solución definitiva consiste en realizar llamadas asíncronas a DBus, explicadas en el tutorial de Vala. La idea consiste en añadir la palabra reservada async en la definición de la función en la clase, con lo que la llamada a DBus será no-bloqueante, y definir un callback anónimo.

[DBus (name = "org.freedesktop.UDisks.Device")]
interface Device_if : GLib.Object {
  [...]
  public abstract async void FilesystemCreate(string type, string[] options) throws IOError;
  [...]
}

  // Llamamos a la función, añadiéndo como último parámetro
  // una función anónima que se ejecutará al terminar la
  // ejecución
  device2.FilesystemCreate.begin(format,options, (obj,res) => {
    try {
      // La llamada a .end recoge los valores devueltos (de haberlos)
      // y dispara cualquier excepción que se haya producido
      device2.FilesystemCreate.end(res);
      return;
    } catch (IOError e) {
      // Captura de excepciones (en este caso, fallo durante el formateo)
    }
  });

Una vez que hemos hecho la llamada, podemos volver al bucle principal de GTK o llamar a la función run() de un cuadro de diálogo, y éste seguirá funcionando en paralelo a la ejecución de nuestra llamada a DBus.