Archivo de la categoría: Nueva versión

Nuevas versiones de programas

A ritmo de conga (13)

Una funcionalidad que echaba en falta es el control manual: poder controlar el robot para mandarlo de un lado a otro con el teléfono, en lugar de tener que cogerlo físicamente.

El problema es que, tras analizar el protocolo de control manual a partir de las capturas que había hecho, me encontré con que era ligeramente diferente del que ya estaba utilizando. Para empezar, los comandos no iban a través del servidor, sino que se enviaban directamente desde el móvil a la aspiradora (por fin tenía sentido el puerto 8888). Por otro lado, los «valores misteriosos» no seguían el mismo patrón. Para empezar, es la tablet quien envía los PINGs, y siempre con el mismo número de secuencia: 1a 27 00 00. Por otro lado, estas son varias cabeceras de comandos enviados desde la tablet a la aspiradora:

d1 00 00 00 | fa 00 c8 00 | 00 00 29 27 | 28 27 00 00 | 00 00 00 00
d1 00 00 00 | fa 00 c8 00 | 00 00 2c 27 | 2b 27 00 00 | 00 00 00 00
d1 00 00 00 | fa 00 c8 00 | 00 00 2f 27 | 2e 27 00 00 | 00 00 00 00
d1 00 00 00 | fa 00 c8 00 | 00 00 32 27 | 31 27 00 00 | 00 00 00 00
db 00 00 00 | fa 00 c8 00 | 00 00 35 27 | 34 27 00 00 | 00 00 00 00

Vemos que el primer campo sigue siendo el tamaño, el cuarto sigue siendo un número de secuencia, y el segundo y el cuarto tienen los mismos valores que en el protocolo con el servidor; pero el tercer campo es diferente; de hecho es el valor del número de secuencia pero con los bytes invertidos y sumándole uno al tercer byte.

Esto ya plantea algunas dudas; por ejemplo ¿qué pasa si el número de secuencia es mayor de 0xFFFF? ¿Está prohibido tal vez? Si está permitido ¿el campo tercero será el cuarto más 256 y con los bytes en orden inverso? De hecho ¿realmente el número de secuencia es de cuatro bytes también en el protocolo original, o puede que sean sólo dos bytes?

De hecho, para probar esto último decidí ver hasta qué valor devolvía la aspiradora, y tras varias pruebas me encontré con que 10.000 es el número de secuencia más grande que envía en el protocolo original, tras el cual vuelve al 1. Pero el servidor sí envía números más grandes de 10.000.

Aparte de este problema, está la cuestión de que en el código tendría que añadir un nuevo socket y gestionarlo… no es difícil, pero sí un rollo. Sin embargo… ¿Qué pasaría si se pudiese controlar desde la conexión original? ¿Puede el servidor enviar comandos de control manual?

La pregunta es legítima, pues cabe suponer que el control manual utiliza la conexión directa para reducir la latencia (a fin de cuentas, es un control interactivo), y además, si estamos en el bar no tiene sentido querer controlar manualmente una aspiradora que no podemos ver. Pero aún así hay casos en los que puede ser necesario, por ejemplo que tengamos varias WiFis en nuestra casa y que el teléfono esté conectada a una y la aspiradora a otra. Así que decidí probar justo eso y… ¡¡¡Funcionó!!! ¡¡¡Es posible enviar comandos de control manual a través del socket del servidor!!! Eso simplifica la tarea enormemente.

Ahora toca analizar el formato. Veamos un primer caso: ordenamos al aspirador ponerse a girar alrededor de sí mismo hacia la derecha durante tres segundos (las respuestas de la aspiradora son, simplemente, el estado actual, así que he borrado casi todo el contenido y lo he reemplazado por unos puntos suspensivos para no alargar demasiado el bloque):

1315.019054889679
s->a e1 00 00 00 | fa 00 c8 00 | 00 00 f2 01 | 2d 27 00 00 | 00 00 00 00
{
  "cmd":0,
  "control":{
    "authCode":"xxxxxx",
    "deviceIp":"192.168.3.56",
    "devicePort":"8888",
    "targetId":"yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy",
    "targetType":"3"
  },
  "seq":0,
  "value":{
    "direction":"4",
    "transitCmd":"108"
  }
}\n

1315.3824479579926
a->s f6 01 00 00 | fa 00 00 00 | 01 00 00 00 | 2d 27 00 00 | 00 00 00 00
[...]



1317.020054889679
s->a e1 00 00 00 | fa 00 c8 00 | 00 00 f2 03 | 2d 27 00 00 | 00 00 00 00
{
  "cmd":0,
  "control":{
    "authCode":"xxxxxx",
    "deviceIp":"192.168.3.56",
    "devicePort":"8888",
    "targetId":"yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy",
    "targetType":"3"
  },
  "seq":0,
  "value":{
    "direction":"4",
    "transitCmd":"108"
  }
}\n

1317.3924479579926
a->s f6 01 00 00 | fa 00 00 00 | 01 00 00 00 | 2d 27 00 00 | 00 00 00 00
[...]



1318.9394080638885
s->a eb 00 00 00 | fa 00 c8 00 | 00 00 f2 01 | 2f 27 00 00 | 00 00 00 00
{
  "cmd":0,
  "control":{
    "authCode":"xxxxxx",
    "deviceIp":"192.168.3.56",
    "devicePort":"8888",
    "targetId":"yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy",
    "targetType":"3"
  },
  "seq":0,
  "value":{
    "direction":"5",
    "tag":"4",
    "transitCmd":"108"
  }
}\n

1319.4073688983917
a->s f6 01 00 00 | fa 00 00 00 | 01 00 00 00 | 2f 27 00 00 | 00 00 00 00
[...]

Y ya vemos cómo va: el comando es el 108 , y luego hay un campo direction que vale 4. ¿Será el comando 108 para girar a la derecha, y habrá otros para el resto de direcciones, o será un único comando para todo y el campo direction indica cual de los cuatro posibles movimientos se desea? Además, al cabo de dos segundos vemos que se vuelve a repetir el comando. ¿Por qué?

Finalmente, al cabo de tres segundos se emite el comando con la dirección 5, por lo que cabe suponer que eso significa detente. ¿Pero qué significa el campo tag?

Para resolver todas estas preguntas, veamos todos los comandos emitidos para los cuatro movimientos posibles (adelante, atrás, izquierda y derecha):

adelante:
  "value":{
    "direction":"1",
    "transitCmd":"108"
  }

  "value":{
    "direction":"5",
    "tag": "1",
    "transitCmd":"108"
  }

------------------------
atrás:
  "value":{
    "direction":"2",
    "transitCmd":"108"
  }

  "value":{
    "direction":"5",
    "tag": "2",
    "transitCmd":"108"
  }

------------------------
izquierda:
  "value":{
    "direction":"3",
    "transitCmd":"108"
  }

  "value":{
    "direction":"5",
    "tag": "3",
    "transitCmd":"108"
  }

------------------------
derecha:
  "value":{
    "direction":"4",
    "transitCmd":"108"
  }

  "value":{
    "direction":"5",
    "tag": "4",
    "transitCmd":"108"
  }

¡Ajá! Ahora tiene sentido: se utiliza un único comando, el 108, para los movimientos manuales, con 1, 2, 3 y 4 para moverse adelante, atrás, izquierda y derecha respectivamente. Cuando se quiere parar se emite el mismo comando pero con la dirección 5, y el campo tag especifica qué movimiento es el que se cancela (probablemente por si los comandos llegan fuera de orden).

Además, si probamos con movimientos que duren más tiempo se ve que el servidor vuelve a enviar el comando de movimiento cada dos segundos. Todo apunta a que es una manera de asegurar que el servidor «sigue ahí», y que si pasa mucho tiempo sin recibir un comando de refresco, la aspiradora dejará de ejecutar el último comando. Esto tiene sentido: si se pierde la conexión es importante que el robot no se quede ejecutando una orden de movimiento manual…

Y con esto ya tenemos todo lo necesario para implementar el control manual, y así de bonito se ve en la app:

Como se ve, hay un nuevo icono que permite alternar entre modo mapa y modo control manual, y pulsando las flechas el robot se moverá. Eso sí, por desgracia la aspiradora no puede estar en la base para que este modo funcione, parece una limitación del propio firmware del robot, no de la app original.

Parte 14

A ritmo de conga (12)

Hasta ahora he estado utilizando la web app para controlar mi aspiradora robot tanto desde el ordenador como desde el móvil. El problema es que es bastante pesado, en el móvil, tener que:

  • Abrir el navegador
  • Abrir una pestaña en blanco
  • Escribir la IP del servidor número a número

Así que decidí que tenía que hacer una app para Android. Obviamente no me iba a matar repitiendo todo el trabajo que ya había hecho en JavaScript, así que la solución obvia era hacer una aplicación que simplemente tuviese un WebView (que no es más que un widget con un navegador web completo) y que cargase automáticamente la web app de OpenDoñita automáticamente cada vez que se abriese. Así cualquier cambio que hiciese a la web app aparecería automáticamente también en la app de Android.

Sin embargo, antes que eso tenía que resolver un problema nada trivial: ¿cómo saber la IP del servidor de OpenDoñita? Sí, en mi casa se cual es, pero obviamente si quiero que otras personas puedan usarlo, no es plan de tener que poner la IP «a mano». Además ¿y si el DHCP hace de las suyas en un reinicio y cambia la IP?

La solución vino de la mano de uPnP. Se trata de un estándar para que dispositivos multimedia puedan anunciarse en una red doméstica, y que otros dispositivos puedan identificarlos y comunicarse con ellos de manera estandarizada. También sirve (y será lo que les suene a muchos) para poder abrir puertos externos en el router cuando usamos NAT.

El protocolo uPnP es, en esencia, relativamente sencillo: se utiliza la dirección multicast 239.255.255.250 y el puerto 1900 para enviar y recibir paquetes UDP. Así, si un dispositivo quiere anunciar que cumple con el estándar uPnP, emitirá una serie de paquetes NOTIFY a dicha dirección y puerto, y aquellos dispositivos interesados estarán suscritos a ella para detectar dichos mensajes. Otra manera es que un dispositivo envíe a dicha dirección y puerto un paquete M-SEARCH, y los dispositivos responderán cada uno indicando sus capacidades y demás.

Por supuesto, cuando entramos en detalles nos encontramos con que el protocolo es mucho más rico y complejo de lo que parece. Pero afortunadamente existe el módulo de python iot-upnp, que permite de manera sencilla configurar un dispositivo como servidor uPnP. Precisamente ella ha sido el motivo de que convirtiese el código a asyncio. Básicamente basta con asignar un UUID y un par de cosas más a un diccionario, y nuestro programa ya es un servidor uPnP y responde a los anuncios. Este código está añadido en la última versión del servidor OpenDoñita.

La segunda parte es conseguir que la app de Android pida a los dispositivos uPnP que se identifiquen. En este caso no me compliqué y me limité a crear un socket UDP y enviar directamente una petición uPnP de tipo M-SEARCH, que es un paquete con estos datos:

M-SEARCH * HTTP/1.1\r\n
HOST: 239.255.255.250:1900\r\n
MAN: \"ssdp:discover\"\r\n
MX: 2\r\n
ST: upnp:rootdevice\r\n\r\n

Con eso recibiré un paquete UDP por cada dispositivo raíz uPnP directamente al mismo socket desde el que envié el paquete. Y como sólo quiero conocer la IP y nada más, lo único que necesito hacer es esperar a que aparezca uno que contenga el UUID que envía mi web app, y ese será.

Por supuesto, las cosas no son tan sencillas, pues la clase de sockets, DatagramSocket, es síncrona, lo que significa que no la podemos utilizar desde el bucle principal de Android sino que necesitamos crear un thread. Para ello utilicé una clase que extiende AsyncTask. Aunque es un método obsoleto, lo es a partir de la API 30, la cual fue lanzada ayer como quien dice (pertenece a Android 11), por lo que prefiero utilizarla y garantizar que mi código va a funcionar en móviles antiguos. Ahora simplemente implemento el método doInBackground() y dentro hago un bucle en el que envío el paquete anterior, me pongo a esperar respuestas con un timeout de 2 segundos (que coincide con el valor de MX de mi petición), y cuando salte éste, si no he conseguido la IP, repito el proceso. Pero si en alguno de los paquetes venía el UUID correcto, salgo del bucle y retorno del método. Es entonces cuando se ejecutará el método onPostExecute() recibiendo como parámetro el valor que devolví en doInBackground(). Lo interesante es que mientras que ésta última se ejecutaba en otro thread, onPostExecute() se ejecuta en el mismo thread desde el que se creó el objeto, o sea, desde el bucle principal en mi caso, con lo que ahí podré llamar al método loadUrl() del WebView para que cargue la página.

La otra cuestión importante es poder capturar el botón de Atras de Android para poder ocultar la pantalla de configuración si se pulsa, pero salir de la app si se pulsa desde la pantalla principal. Para ello sobreescribo en la actividad principal el método onBackPressed(), que es el que se llama cuando el usuario pulsa el botón, y dentro de él utilizo evaluateJavascript() para llamar a la función back_android() de la web app. Esta función hará lo que tenga que hacer y devolverá el valor 0 si no se debe hacer nada, o 1 si se debe salir de la aplicación.

Y el resto no es más que el típico código para gestionar el ciclo de vida de una aplicación de Android.

El código está disponible en mi repositorio gitlab de OpenDoñita para Android.

Parte 13

Multipackager, Fedora y Debian

Desde hace tiempo puedo generar paquetes fácilmente para varias distribuciones gracias a un pequeño programa que me hice, multipackager, que a partir de las fuentes, me hace paquetes para Debian estable y SID, Ubuntu LTS y actual, Fedora y Arch. Para ello genera un contenedor con la distribución concreta, añade los paquetes necesarios y ejecuta el programa que genera el paquete.

Por desgracia, ya llevaba una temporada con problemas con Fedora: no tenía manera de generar una imagen para Fedora 31. Afortunadamente, echando un vistazo a cómo lo hacía LXC pude encontrar una manera, algo chapucera, para qué negarlo.

Por desgracia, hace unos días terminó de romper del todo: hasta ahora, para generar las imágenes de contenedores utilizaba yum, la herramienta de paquetería de Fedora/Red Hat, que, afortunadamente, estaba disponible en Debian SID. Pero por desgracia, hace unos días fue eliminada de los repositorios. Algunos dirán que esa herramienta está obsoleta, y que hoy en día se utiliza dnf, y es verdad, pero por desgracia esa nunca estuvo disponible en Debian, y no fui capaz de instalarla a partir de las fuentes.

Ante esto, necesitaba una solución. Decidí echar un vistazo de nuevo a ver qué hacía LXC, y me encontré con que partía de la imagen Live ISO de Fedora, montándola, extrayendo el fichero con la imagen squashfs, extrayendo de ella el sistema de archivos, y configurando un sistema nuevo con ellos. Lo veía bastante lioso, pero no veía otra opción, así que empecé a ver cómo automatizar la descarga de la ISO… y de pronto encontré un interesante directorio en el servidor web llamado Containers… y efectivamente, contenía imágenes del sistema de archivos básicas, listas para ser utilizadas. Lo único que tenía que hacer era procesar la página web que listaba el contenido de la carpeta para obtener el nombre del fichero correcto, y a correr.

Y gracias a eso, multipackager ya vuelve a funcionar a pleno rendimiento, y he podido sacar un nuevo paquete de Cronopete que funciona en Fedora 32.

Nueva versión de ActivityAppLauncher

Acabo de subir una nueva versión de ActivityAppLauncher, la extensión de Gnome Shell que muestra un menú de categorías en el modo Overview. El motivo de actualizarla, después de años sin tener que tocarla prácticamente, es que en Gnome Shell 3.36.0 han hecho varios cambios internos que rompen las cosas que tuve que hacer para que funcionase.

Sin embargo, aproveché para echar un vistazo más en detalle al viejo código (a fin de cuentas, fue una de las primeras extensiones que escribí), y encontré una manera más elegante y, sobre todo, fiable, de añadir la vista con las aplicaciones: ahora es una página «con todas las de la ley» dentro del modo Overview, y no un pegote metido con calzador. Esto tiene la ventaja de que hay una animación al pasar de la lista de ventanas al de aplicaciones y viceversa, pero a cambio no me ha sido posible conservar la lista de escritorios cuando se muestra la lista de aplicaciones.

También aproveché para cambiar el sistema de instalación por uno basado en meson, vilmente fusilado del utilizado en desktop-icons.

En breve estará disponible en la página de extensiones de Gnome Shell, pero mientras tanto, se puede bajar del repositorio GIT.

Un pasito p’alante María, y uno p’atrás

Hace cosa de un mes se publicó la versión 3.34 de Gnome Shell, la cual incorporaba muchas mejoras internas de rendimiento. Por desgracia, también incorporaba tantos cambios internos que un amplio porcentaje de extensiones no funcionan con ella. Esto era algo de lo que ya nos habíamos olvidado, pues desde hace unas cuantas versiones no había cambios de arquitectura internos tan grandes que afectasen de esta manera a las extensiones. Aunque tuve suerte con ActivityAppLauncher y con Desktop Icons NG (pues ambas funcionan perfectamente sin ningún cambio), no la hubo con la extensión Desktop Icons original, principalmente por culpa del nuevo sistema de herencia en clases Javascript, que, al permitir heredar desde clases de GObject, ya no obliga a meter un actor St en una propiedad de una clase, sino que la clase en sí puede heredar directamente de ella.

Afortunadamente, Marco Trevisan se encargó de subir un parche que corregía este problema; sin embargo, para poder garantizar que funcionaba correctamente tuve que esperar a que Debian Sid actualizase Gnome Shell desde 3.30 a 3.34, cosa que ocurrió hace casi dos semanas. Tras unas cuantas pruebas del código procedí a empaquetarlo y subirlo. Alegría, alboroto, labor cumplida, bla bla bla…

Hasta que empiezan a llegar los avisos de bugs. En concreto, había uno muy grave, que rompía completamente el escritorio cada vez que se intentaba renombrar un icono. Ante semejante problema, tuve que poner en cuarentena rápidamente la nueva versión de la extensión y empezar a trabajar en el parche. Desgraciadamente, no conseguía avanzar: todo lo que intentaba seguía fallando de manera misteriosa. Afortunadamente, una vez más Marco Trevisan se puso manos a la obra y subió otro parche que corregía este problema y, de paso, actualizaba la forma del cuadro de diálogo para renombrar ficheros, haciéndola más consistente con Nautilus (nunca le podré estar lo suficientemente agradecido).

Ya puestos, aproveché para pedirle que revisase un parche mío que llevaba tiempo esperando ser aplicado: uno que corregía el tamaño de los thumbnails. Finalmente se pudo aplicar a tiempo, por lo que es posible que los usuarios de Ubuntu 19.10 puedan disfrutarla «de serie».

Pero los problemas no se acabaron ahí, pues surgieron dos bugs extra que afectaban, esta vez sí, a Desktop Icons NG. El primero es que, por un error en Mutter, el gestor de ventanas de Gnome Shell, el drag and drop desde el escritorio hacia una ventana de Nautilus deja de funcionar, cuando en Gnome Shell 3.30 y 3.32 siempre funcionó perfectamente. Tras mucho esfuerzo conseguí encontrar el parche concreto en el que se introdujo el error, y lo reporté. EDITO: ya lo han corregido. ¡Genial!

El segundo problema era más grave, pues por un bug en Gnome Shell en sí, crear una ventana de tipo DESKTOP con Gnome Shell trabajando en X.org (no se en Wayland, pues actualmente no se puede), rompía el modo de Actividades: era entrar en él y el escritorio se quedaba tonto. Afortunadamente lo resolvieron a tiempo para la versión 3.34.1, que salió hace cuatro días, por lo que ya no es un problema

Por lo tanto, si estás utilizando Gnome Desktop y Gnome Shell 3.34.0, te recomiendo que actualices de inmediato a Gnome Shell 3.34.1, para asegurarse de que mis extensiones trabajen lo mejor posible.

Nueva versión de Cronopete y Terminus

Acabo de lanzar una nueva versión de Términus y de Cronopete.

En el caso de Términus era necesario por un problemilla al usar el modo «Guake» desde Wayland: dado que marca la ventana para que esté encima de todas las demás, los menús contextuales del botón derecho no se pueden ver. Esto es porque un menú va en una ventana independiente, y dado que la ventana principal está situada encima de absolutamente todas, también lo estará encima de la ventana del menú.

La solución consistió en utilizar un Gtk.Popover. Este widget es similar a un menú, pero en lugar de utilizar una ventana independiente se pinta directamente en la misma ventana que el widget padre. El inconveniente es que no se puede hacer aparecer en el punto donde esté el cursor del ratón (o, al menos, yo no he sido capaz de ello).

Por otro lado se supone que es posible construir un menú «de verdad» con un Gtk.Popover, pero yo no he sido capaz, así que no me quedó otra que construirlo con Gtk.Button directamente. El resultado, pese a todo, funciona bien.

A mayores hice algunos cambios pequeños en el código para eliminar algunos warnings por utilizar funciones obsoletas.

Respecto a Cronopete, aproveché para lanzar una nueva versión con un cambio que, aunque pequeño, hace más cómodo su uso: ahora, si se produce un aviso de que un fichero se ha «desvanecido», no aparecerá un popup de aviso. Este mensaje se produce porque Rsync hace primero una lista con los ficheros que va a copiar, y una vez que la tiene es cuando procede a copiarlos de uno en uno, por lo que si, después de haber hecho la lista, uno de los ficheros desaparece (se «desvanece»), emite un aviso. Este aviso no es crítico, por lo que no tiene sentido molestar al usuario.

Otro cambio que incluye es haber eliminado el fork del proceso principal: antes, por seguridad (y por una serie de problemas en las versiones anteriores) había dos procesos de cronopete: el principal, que es el que maneja todo, y un proceso watchdog que comprueba si el principal ha fallado y lo lanza de nuevo. Sin embargo, desde que cambié el sistema de copia para que utilice Rsync, esta precaución no es necesaria, y de hecho da algún que otro problemilla al apagar los equipos, cuando uno de los procesos se queda lanzado y no quiere morirse.

Como de costumbre, se pueden bajar ambos programas en distintos formatos desde mi página web.

Terminus 1.6.0 funciona igual en Wayland y en X11

Los que utilicéis mi terminal Terminus en Wayland habréis notado que no responde exactamente igual que en X11. Esto es debido a las limitaciones que impone este servidor gráfico, que impide que un programa mantenga su ventana siempre encima de las demás, o que la fije en todos los escritorios.

Sin embargo, después de los excelentes resultados que conseguí con Desktop Icons NG, y que expliqué en una entrada anterior, se me ocurrió que podía hacer algo similar con Terminus (pues, a fin de cuentas, ya usaba una extensión en Gnome Shell para permitir que funcionase en Wayland).

Y dicho y hecho: he reutilizado la clase que diseñé para lanzar un proceso desde una extensión y detectar si una ventana pertenece a él, y ahora Terminus ya responde exactamente igual en X11 y en Wayland: la ventana siempre se mantiene arriba de todo, y aparece en todos los escritorios. A mayores, ahora se garantiza que siempre aparecerá en el monitor principal.

Como de costumbre se puede bajar desde la sección de Terminus de mi web.

Nueva versión de Cronopete y Devede

Con bastante retraso publico esta entrada. Hace dos meses lancé la versión 4.11.0 de Cronopete, que añade una funcionalidad fun-da-men-tal: avisar si no hay espacio para hacer al menos una copia. Esto puede sonar raro, pero en realidad se trata de un bug bastante sutil: resulta que si no hay espacio suficiente para hacer ni siquiera la primera copia, sencillamente… ¡nunca avisaba! Se quedaba intentándolo una y otra vez. Esto lo descubrí de casualidad gracias a un usuario que intentaba utilizar (sin éxito, claro) un pincho USB de poca capacidad para las copias.

En cuanto a Devede, por fin incorpora algo que mucha gente pidió: cambiar la etiqueta del disco, para que no sea DVDVIDEO.

Como de costumbre, se pueden descargar en mi web.

Desktop icons, Gnome shell y Wayland

Hace cosa de un año y medio la gente de Gnome anunció que eliminaba de Nautilus todo el código que pintaba los iconos de escritorio. Para los que no se quieran leer el tocho, resumiré rápidamente que el motivo era, básicamente, que dicho código venía de muchos años atrás, era extremadamente complejo debido a las limitaciones de las versiones antiguas de GTK, y cada vez era más difícil de mantener a la vez que se añadían nuevas características al resto de Nautilus. Además, hacía ya seis años que Gnome 3 no utilizaba iconos de escritorio. Por si fuera poco, tenía muchos bugs de difícil solución (por ejemplo, el soporte multimonitor no funcionaba demasiado bien). Y a todo esto había que sumar el hecho de que en Wayland no funcionaría correctamente debido a las limitaciones que impone en aras de la seguridad: en efecto, en Wayland una aplicación no puede decidir donde colocar una ventana ni mantenerla fija en el fondo, entre otras cosas. El motivo de esta limitación es evitar que una aplicación maliciosa pueda hacerse pasar, por ejemplo, por el escritorio, o por una barra de tareas, etc, poniendo en riesgo la seguridad del sistema. Por desgracia, esto también significa que, en Wayland, cosas como las barras del escritorio, un dock o los iconos de escritorio no se pueden delegar en una aplicación, sino que tienen que ser manejadas desde dentro del gestor de ventanas. Es por esto que se creó la extensión de Gnome Shell llamada Desktop Icons: para seguir ofreciendo iconos en el escritorio en Gnome Shell para aquellos que lo deseasen (como yo).

La versión disponible en aquel momento ya implementaba la funcionalidad más básica, pero tenía un defecto que, para mí, era muy grave: no permitía utilizar una única pulsación para lanzar un icono, sino que obligaba a utilizar doble click. Como yo estoy acostumbrado a la primera manera, me lié la manta a la cabeza y envié un parche para implementarlo. Tras muchos cambios para adecuar el estilo de código al que se utiliza en el proyecto Gnome y más cosas (nunca podré agradecer lo suficiente a Carlos Soriano su infinita paciencia enseñándome a manejar Git en condiciones), lo aprobaron. Y le cogí el gustillo, con lo que detrás de él vino otro, y otro, y otro más… Hasta que, recientemente, me ofrecieron ser el mantenedor del código (lo que para mí es un honor, todo sea dicho).

Un año después, la extensión ya incorpora todo lo que se pretendía para la versión 1.0 y más, y se incluye por defecto en la versión de Gnome Shell de Ubuntu, y no puedo menos que agradecer a toda la gente que ha colaborado con parches e informes de bugs.

Por desgracia, a medida que más y más usuarios la han ido instalando, han ido surgiendo algunos problemas inherentes al hecho de que sea una extensión, problemas que no eran nada evidentes al principio y que sólo se han ido haciendo visibles a medida que la cantidad de usuarios crecía.

El primer gran problema es que todas las extensiones se ejecutan en el mismo bucle principal que el compositor de ventanas. Esto significa que una extensión que necesite mucho tiempo para ejecutar una operación puede, literalmente, congelar todo el escritorio, incluyendo el mismísimo repintado de todas las ventanas. En el caso de extensiones «normales», que se limiten a mostrar un icono en la barra de tareas o así, esto no es un problema, porque el trabajo que realizan es mínimo; sin embargo, en el caso de Desktop Icons sí lo es, pues cada vez que tiene que refrescar el escritorio (por ejemplo porque se añade o borra un fichero), tarda en torno a medio segundo en realizar todas las operaciones (leer la lista de ficheros en el directorio, obtener sus metadatos, generar los pixmaps, eliminar las cuadrículas e iconos previos, generar una nueva cuadrícula, crear los nuevos iconos, y pintarlos en su sitio), y durante ese tiempo todo el escritorio se congela. Obviamente, por que ocurra una vez cada mucho no es muy problemático, pero sin duda es molesto para el usuario.

Solucionar esto, aunque no es totalmente imposible, no resulta sencillo: actualmente ya utilizamos funciones asíncronas en todos los sitios posibles para evitar bloquear la cola de eventos, pero no es suficiente. Además sería necesario evitar repintar todo el escritorio y sus elementos, y sólo añadir o quitar los iconos de los ficheros que se hayan añadido o eliminado. Pese a todo, esto sólo reduciría un poco más el problema, pero no lo resolvería del todo, pues si un programa añade y borra constantemente un fichero al escritorio, por ejemplo, puede aún bloquear la cola, en buena medida porque algunas operaciones de repintado se realizan sólo cuando ya no quedan operaciones pendientes en la cola de eventos. Además, implementar esto implicaría un importante rediseño interno, y teniendo en cuenta que algunas distribuciones cuentan con Desktop Icons para sus versiones de soporte a largo plazo, no es algo que se pueda hacer de cualquier manera, sino que debe implementarse de manera muy progresiva y con una buena revisión por pares de todos y cada uno de los parches, para garantizar que no hay errores ni regresiones.

Otro problema es la imposibilidad (al menos actualmente) de integrar Drag’n’Drop al completo: aunque dentro de Gnome Shell existe soporte de Drag’n’Drop, no es compatible con operaciones desde el «espacio de usuario»; esto es: una aplicación no puede ni enviar al compositor, ni recibir de él, eventos de Drag’n’Drop . Aunque en principio sería posible implementar dicha comunicación, no es una tarea trivial, y de hecho, tras tantear a algún programador de Mutter, la conclusión es que es lo suficientemente complicado como para que sólo valga la pena pasar el trabajo de implementarlo en Wayland, pero no en X11.

Y de aquí llegamos al tercer problema: las extensiones se escriben con Clutter + St, los cuales no funcionan exactamente igual que Gtk. Un ejemplo es la forma en que se procesan los eventos de enter_notify y exit_notify, que obligó a añadir una serie de trucos en el código que permite seleccionar un grupo de iconos mediante «goma elástica», para gestionar correctamente aquellos casos en los que el cursor entraba en una ventana en mitad de una selección, o cuando pasaba por encima de la barra superior de Gnome. Esto es algo que en Gtk está resuelto desde hace años, gracias al mayor número de usuarios y programadores que trabajan con él, y que permite detectar más bugs.

El cuarto problema radica en los cambios entre versiones del escritorio. Dado que Gnome Shell no ofrece una API estable para las extensiones, éstas tienen que interactuar directamente con el código interno, por lo que cualquier cambio les puede afectar. En el caso de una extensión tan compleja como Desktop Icons este problema es aún mayor, hasta el punto de que la actual versión 19.01 será, probablemente, la última compatible con Gnome Shell 3.30 y 3.32, y las nuevas versiones necesitarán como mínimo Gnome Shell 3.34.

A la vista de todos estos problemas, hace un par de meses empecé a sopesar la posibilidad de hacer un cambio radical en el diseño y mover toda la lógica a un proceso independiente del compositor, de manera que toda la gestión de los iconos del escritorio se realice sin interferir con la composición y demás, además de utilizar Gtk directamente en lugar de St y Clutter. De hecho, cuando se decidió crear la extensión, los autores originales (Carlos Soriano y Ernestas Kulik) sopesaron seriamente esta misma posibilidad, pero lo descartaron precisamente por las limitaciones impuestas por el modelo de seguridad de Wayland, y porque hacerlo dentro de una extensión tenía más sentido en aquel momento, pues era muchísimo más fácil.

Por supuesto, esto es más sencillo de decir que de hacer, pues, como ya dije antes, aunque en X11 no hay ningún problema en que una aplicación mantenga una ventana en el fondo, o que la elimine de la lista de ventanas para que no aparezca al hacer Alt + Tab, en Wayland eso es totalmente imposible por diseño. Además, dado que las extensiones están escritas en Javascript, no es posible lanzar código en un nuevo thread, y aunque se pudiese, no sería posible que dicho código llamase a funciones de Clutter o St, por lo que hay que descartar la idea de descargar el trabajo en un thread paralelo. Lanzar un proceso independiente sí es posible, pero éste trabajaría desde fuera del compositor, por lo que, en principio, seguiríamos con el mismo problema.

Sin embargo, existe una alternativa extra, que es justo la que decidí investigar, que consiste en repartir el trabajo: por una parte tenemos un programa normal, que trabaja desde fuera del compositor y que utiliza GTK para gestionar absolutamente todo lo relacionado con el escritorio y sus iconos mediante una o varias ventanas normales, exactamente igual a como hacía el viejo Nautilus en el modo «iconos de escritorio»; y, por otro lado, una pequeña extensión cuyo trabajo consiste en lanzar el programa anterior (y relanzarlo cada vez que se muera), detectar la ventana que abre, y asegurarse de que ésta se mantenga donde debe. De esta manera, el código dentro de la extensión es mínimo y se limita exclusivamente a las operaciones que son imposibles de realizar desde el exterior del código del compositor.

Por supuesto, es fundamental no romper el modelo de seguridad de Wayland, lo que significa que no se puede permitir bajo ningún concepto que una aplicación extraña se pueda aprovechar de este mecanismo para colocar su propia ventana como el fondo del escritorio (o como cualquier otro elemento). Para ello, la única solución es que sea la extensión quien lance el proceso, y que cada vez que aparezca una ventana, compruebe si ésta pertenece al proceso que ella misma ha lanzado, otorgándole esos privilegios exclusivamente en el caso de que así sea. Dado que el código ha sido lanzado específicamente por la extensión, se puede considerar que es código tan confiable como el de dicha extensión (y más si el programa a lanzar forma parte de la propia extensión): si un programa malicioso puede reemplazar el código de la aplicación de escritorio, también podría hacerlo directamente con el código de la extensión, por lo que el nivel de seguridad es exactamente el mismo.

Ahora llega la cuestión de cómo detectar que una ventana pertenece al proceso lanzado desde la extensión. Mi primera idea fue utilizar metawindow_get_pid() en cada ventana nueva que apareciese, para obtener el PID del proceso que creó la ventana y compararlo con el PID del proceso que hemos lanzado desde la extensión; por desgracia, dicha llamada sólo funciona en X11 pero no en Wayland, porque utiliza un dato específico de X11. Sin embargo, existe otra llamada, metawindow_get_client_pid(), que sí funciona desde ambos sistemas; por desgracia es privada, lo que significa que sólo se puede llamar desde C y desde dentro de mutter, nunca desde una extensión escrita en Javascript.

Propuse entonces en el canal IRC de Gnome Shell que dicha llamada se cambiase a pública, pero mi idea no convenció porque existen varios ataques que involucran PIDs de procesos, por lo que, aunque yo hiciese las cosas bien, hacer pública dicha llamada podría suponer abrir la caja de Pandora. Sin embargo, sí me redirigieron al código de XWayland para que viese ahí como lo hacen de manera segura: básicamente, al lanzar el proceso crean manualmente un socket y asignan un extremo a Wayland, pasando el otro extremo al proceso hijo para que se comunique a través de él; luego basta con engancharnos a la señal map() y, por cada ventana que aparezca, comparar si su socket coincide con el que creamos nosotros para nuestro proceso hijo, en cuyo caso podemos estar seguros de que esa ventana pertenece a él y no a otro.

La idea era buena, pero por desgracia no se puede implementar directamente en Javascript porque precisa de varias llamadas privadas de mutter, además de que tampoco se pueden crear sockets desde Javascript. Ante esto me sugirieron escribir una clase GObject que lo implementase y exportase una interfaz para ello, y así lo hice: mi primer parche para mutter y mi primera clase GObject. Esta clase sólo funciona con Wayland (para X11 no tiene sentido, pues la propia aplicación puede detectarlo y hacer ella misma el trabajo), y permite lanzar un proceso y detectar si una ventana concreta pertenece o no a dicho proceso. Si se utiliza desde X11, la parte de lanzar el proceso también funciona, pero el método de detectar si una ventana pertenece o no al proceso genera una excepción (que, obviamente, se puede capturar). Esto permite simplificar el código en la extensión a la hora de hacer que funcione en ambos entornos de ventanas.

Aunque dicho parche funciona bien, aún sigue pendiente de aprobación; pero yo quería poder usar YA la nueva versión de Desktop Icons, así que, como a cabezón no me gana nadie, decidí ver qué podía hacer para detectar de manera segura la ventana de un proceso utilizando sólo lo que ya tenía disponible en Javascript. Para ello se me ocurrió que la aplicación de escritorio podría poner como título de la ventana una cadena concreta que la extensión pudiese identificar (tiene que ser en el título, pues no parece haber absolutamente nada más en una ventana que se pueda asignar de manera libre por el programa y que una extensión pueda leer). El problema es que dicha cadena no puede ser predecible, pues entonces otras aplicaciones podrían hacerse pasar por la legítima. Ante esto, decidí que la extensión generaría un UUID aleatorio de 128 bits justo antes de lanzar el proceso, y se lo pasaría a éste para que lo ponga en el título de la ventana (y, por supuesto, calculando uno nuevo cada vez que la aplicación se muera). Pero claro, pasarlo como parámetro por la línea de comandos sería completamente inseguro porque cualquier programa puede leer los parámetros de cualquier otro proceso (basta hacer un ps ax o leer /proc), por lo que tenía que ser algo más seguro. Al final la solución consistió en pasarlo a través de stdin, de manera que nadie más pueda leerlo (habría sido más elegante utilizar un pipe específico, pero por desgracia desde Javascript no es posible crear nuevos pipes).

Para simplificar aún más el código escribí una pequeña clase Javascript que es compatible a nivel de métodos con la clase GObject de mi parche, de manera que, si se aprueba, sólo tendré que reemplazar una clase por otra en el código (o incluso utilizar una u otra en función de la versión de Gnome Shell).

Con esto resolví el primer problema, pero ahora quedaba el segundo: aunque ya puedo identificar qué ventana es la del escritorio ¿como hago para mantenerla donde debe estar?

La solución elegante sería cambiar el tipo de ventana a uno de los tipos estándar (en concreto, a META_WINDOW_DESKTOP). Por desgracia, desde Javascript no es posible cambiarlo, pues la llamada es privada. Obviamente preparé un parche para cambiarlo, donde, además de hacerla pública, también la renombro para que sea consistente. Este cambio convenció mucho más en el canal IRC, por lo que espero que sea finalmente aceptado junto con el otro, pues la ventaja de estos dos parches es que no son específicos para este proyecto, sino que permiten, en general, «externalizar» el trabajo que actualmente se realiza dentro del compositor, lo que permitiría que más elementos, como por ejemplo la barra superior o un dock, puedan ser gestionados desde un proceso externo.

Sin embargo, seguía queriendo poder utilizar YA el programa, así que lo que hice fue engancharme a varias señales para forzar la ventana a permanecer en su sitio:

  • raised: esta señal se emite cada vez que una ventana pasa a primer plano. En su callback llamo al método lower(), que se encarga de mandarla debajo de todas las demás ventanas, manteniéndola así al fondo.
  • position-changed: como su nombre indica, esta señal se emite cuando el usuario cambia la posición de una ventana. En el callback la devuelvo siempre a donde le corresponde. Y es que, aunque la ventana del escritorio no está decorada (y, por tanto, en principio el usuario no tendría donde pinchar para moverla), sigue siendo posible utilizar combinaciones de teclas para cambiarla de sitio, cosa que no se puede permitir.

A mayores llamo a stick() para que la ventana aparezca en todos los workspaces.

Con esto ya se puede conseguir que la ventana permanezca siempre en su sitio, pero aún queda por evitar que aparezca en la lista de ventanas. De no hacerlo, aparecerá en el modo Actividades de Gnome Shell y en el cambiador de ventanas (el de Alt + Tab). Para solucionar esto hay que reemplazar tres métodos:

  • Meta.Display.get_tab_list()
  • Shell.Global.get_window_actors()
  • Meta.Workspace.list_windows()

Con estos tres métodos, la ventana desaparece «lo suficiente» como para que sea usable (por ejemplo, en Dash to Dock no desaparece, pero es un mal menor). Por supuesto no es muy elegante, y el resultado será perfecto si se acepta el parche para cambiar el tipo de ventana.

Y de esta manera tan horrorosa (aunque sólo hasta que aprueben mis parches… si es que los aprueban, claro) conseguí mover toda la funcionalidad del escritorio fuera del compositor, lo que resuelve de un plumazo todos los problemas anteriores. El resto del trabajo fue, básicamente, convertir los widgets de St y Clutter en los equivalentes de Gtk, quitar mucho código asíncrono que ya no era necesario y sólo complicaba terriblemente la lógica, y algunos detalles a mayores como añadir código extra en la extensión para que, durante el arranque, le comunique cuantos monitores hay, así como sus coordenadas y tamaños.

La extensión ya está disponible en la página de extensiones de Gnome Shell, y ofrece, además de todo lo que ya tiene la extensión original, Drag’n’Drop, no congelar la composición del entorno gráfico cuando se refresca el escritorio, más velocidad, mostrar los nombres de ficheros demasiado largos cuando se pase por encima el ratón, y más.

Nueva versión de Cronopete

Recién salida del horno está Cronopete 4.10. Los cambios son relativamente pequeños, pero necesarios.

Para empezar, ahora ya no copia las carpetas .gvfs, .dbus, .cache ni .var/app/*/cache si se ha marcado la casilla de copiar también las carpetas ocultas del directorio raíz. Esto era importante porque dichas carpetas no contienen datos del usuario, sino otras cosas que no tiene sentido preservar. En el caso de .gvfs (aunque desde hace un tiempo ya no se utiliza este destino sino otro en /run/user), es una carpeta donde se montan las unidades remotas a las que se accede mediante Gio, el framework de Gtk para acceder a ficheros, por lo que lo que hay ahí son datos remotos que deberían ser preservados desde la otra máquina. En el caso de .dbus, contiene el socket para acceder al bus de sesión de D-Bus, por lo que no hay nada que preservar (de hecho, intentar sacar una copia de dicha carpeta da error). En el caso de .cache, como su nombre indica, lo que contiene son ficheros temporales, como por ejemplo la caché del navegador. No tiene ningún sentido desperdiciar disco duro para copiar eso. Y lo mismo para las carpetas .var/app/*/cache, las cuales contienen ficheros temporales de programas en formato flatpack, y que no tiene sentido preservar por el mismo motivo que la carpeta .cache.

El segundo cambio es que ahora permite salir del programa, una opción que me pidieron expresamente. Por supuesto está relativamente escondida para evitar activarla por accidente (un programa de copia de seguridad, por definición, no debe ser trivial de desactivar).

Por último, remodelé un poquito la interfaz de configuración por petición de varios usuarios, pues aunque era una copia perfecta de la de Time Machine de Apple, en la práctica los textos no eran demasiado claros, y había que usar prueba y error para encontrar algunas cosas. Ahora están más claros.

¡A disfrutarlo!