Archive for the 'Programación' Category

A ritmo de conga (y 6)

lunes, julio 13th, 2020

Ahora que ya tengo un servidor en marcha capaz de enviar comandos, llegó el momento de analizar todas las posibles opciones. Para ello puse otra vez el tcpdump a capturar y, desde la aplicación oficial, fui enviando todos los posibles comandos, anotando a la vez la hora a la que lo hacía para poder luego identificarlos. También modifiqué mi analizador de paquetes para que mostrase el instante de tiempo en que se emitía o recibía cada paquete.

La primera conclusión es que todos los comandos (a excepción, de momento, de los de mover la aspiradora manualmente, que en principio hay que enviarlos a través del puerto 8888 de la aspiradora; aún no probé qué pasa si lo envío por la conexión normal) se pueden ejecutar con este paquete (con un par de excepciones):

d1 00 00 00 | fa 00 c8 00 | 00 00 09 01 | 1a 27 00 00 | 00 00 00 00
{
  "cmd":0,
  "control":{
    "authCode":"240727",
    "deviceIp":"192.168.18.3",
    "devicePort":"8888",
    "targetId":"zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz",
    "targetType":"3"
  },
  "seq":0,
  "value":{"transitCmd":"ID_COMANDO"}
}\n

Siendo ID_COMANDO un número que identifica la operación. En casi todos los casos, la aspiradora devolverá una respuesta, con la excepción de un par de ellos.

Los posibles comandos que ya tengo identificados son:

  • 98: pide una actualización del estado de la aspiradora. El aspirador no envía asentimiento. Sólo funciona mientras la aspiradora está en marcha.
  • 100: comienza a limpiar
  • 102: deja de limpiar
  • 104: vuelve a la base
  • 106: establece el modo de limpieza. Incluye el parámetro mode antes de transitCmd con uno de los siguientes valores:
    • 11: modo auto
    • 1: modo giro
    • 3: modo random
    • 4: modo edges
    • 6: modo area
    • 8: modo deep cleaning
    • 10: modo scrubbing
  • 110: establece la potencia del aspirador. Incluye el parámetro fan antes de transitCmd con uno de los siguientes valores:
    • 1: detenido
    • 2: normal
    • 3: turbo
    • 4: eco
  • 123: activa los sonidos de la aspiradora. Emitirá un pitido al ponerse en marcha, terminar de cargar, etc.
  • 125: desactiva los sonidos de la aspiradora.
  • 131: pide una actualización del mapa generado. Sólo funciona mientras la aspiradora está en marcha.
  • 145: establece la cantidad de agua en modo fregado. Incluye el parámetro waterTank depués de transitCmd con uno de los siguientes valores:
    • 255: no emitir agua
    • 60: flujo de agua bajo
    • 40: flujo de agua normal
    • 20: flujo de agua alto
  • 400: notifica que la aplicación se acaba de conectar. El aspirador no envía asentimiento.

Cada vez que enviemos un comando, la aspiradora nos devolverá un asentimiento (con la excepción de los comandos 98 y 400). Éste será un paquete como éste:

db 01 00 00 | fa 00 00 00 | 01 00 00 00 | 1a 27 00 00 | 00 00 00 00
{
"version":"1.0",
"control":{
"targetId":"zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz",
"targetType":"3",
"broadcast":"0"
},"value":{
"noteCmd":"102",
"workState":"5",
"workMode":"0",
"fan":"2",
"direction":"0",
"brush":"2",
"battery":"100",
"voice":"2",
"error":"0",
"standbyMode":"1",
"waterTank":"40",
"clearComponent":"0",
"waterMark":"0",
"version":"3.11.416(513)",
"attract":"0",
"deviceIp":"192.168.18.3",
"devicePort":"8888",
"cleanGoon":"2",
"extParam":"{\"cleanModule\":\"3\"}"
}
}

pero con el importante detalle de que el estado que devuelve es el anterior a la ejecución del comando. Esto es: si tenemos el sonido activado (que en el JSON con el estado de la aspiradora viene indicado con voice valiendo 2), y enviamos el comando 125, el estado que nos llegue en el ACK del comando tendrá todavía voice=2. Será justo después cuando la aspiradora nos enviará un paquete status, con el siguiente formato:

bc 01 00 00 | 18 00 00 00 | 01 00 00 00 | 1a 00 00 00 | 00 00 00 00
{
  "version":"1.0",
  "control":{
    "targetId":"0",
    "targetType":"6",
    "broadcast":"0"
  },"value":{
    "noteCmd":"102",
    "workState":"5",
    "workMode":"0",
    "fan":"2",
    "direction":"0",
    "brush":"2",
    "battery":"100",
    "voice":"1",
    "error":"0",
    "standbyMode":"1",
    "waterTank":"40",
    "clearComponent":"0",
    "waterMark":"0",
    "version":"3.11.416(513)",
    "attract":"0",
    "deviceIp":"192.168.18.3",
    "devicePort":"8888",
    "cleanGoon":"2",
    "extParam":"{\"cleanModule\":\"3\"}"
  }
}

donde esta vez sí vemos que voice vale 1. Este paquete debemos asentirlo con un ACK como éste, con el mismo número de secuencia (el cuarto entero de los cinco que forman la cabecera):

3c 00 00 00 | 19 00 c8 00 | 01 00 00 00 | 1a 00 00 00 | 01 00 00 00
{
  "msg":"OK",
  "result":0,
  "version":"1.0"
}\n

En el JSON del estado de la aspiradora tenemos los siguientes campos interesantes:

  • workstate: indica el modo actual en el que está la aspiradora, que puede ser:
    • 1: limpiando
    • 2: parada
    • 4: regresando a la base
    • 5: cargando la batería
    • 6: batería cargada
  • battery: un valor de 0 a 100 indicando el nivel de carga de la batería
  • voice: si vale 1, el pitido de aviso está desactivado; si vale 2, está activado

A ritmo de conga (y 5)

lunes, junio 29th, 2020

¡Victoria, victoria! ¡Mi conga ya me hace caso!

Lo que hice fue escribir un nuevo servidor, esta vez desde cero, que combina HTTP y el protocolo específico de la conga. El código he decidido no hacerlo público hasta que termine el análisis completo, no vaya a ser que meta la pata en algo…

El motivo de no utilizar el código de http.server que ya tenía ha sido, fundamentalmente, porque debido a todos los trucos que tuve que utilizar para que funcionase, al final no usaba casi nada, con lo que me compensaba escribir mi propio servidor. Además, al hacerlo yo todo con select y sockets podía integrar mucho mejor en un único hilo la gestión del puerto 80 y la del 20008.

Básicamente creé una primera clase, BaseServer, que tiene un socket y que gestiona las funcionalidades básicas de éste, incluyendo un método vacío para cada vez que se reciben datos nuevos, y otros métodos para gestionar cuando se cierra. Sobre esta clase he construido absolutamente todo.

Luego creé la clase HTTPConnection, la cual es la que gestiona una conexión HTTP concreta. Cada vez que se hace una petición HTTP, se crea un objeto de este tipo, y es él quien analiza lo que llega. Su función es la misma que el código que hice antes con http.server. Para gestionar esto está la clase HTTPServer, que es la que abre el puerto 80 y crea los objetos anteriores tras cada conexión.

De la misma manera están las clases RobotConnection y RobotServer, pero esta vez para gestionar las conexiones al puerto 20008. El tenerlo dividido así permite controlar varios robots a la vez (aunque no creo que lo implemente de momento, no está de más que la arquitectura subyacente lo esté preparada).

La clase RobotConnection tiene un método llamado send_command, que lo que hace es, básicamente, emitir exactamente las mismas secuencias que registré anteriormente, copiadas byte a byte. Esto, al final, es bastante sencillo porque todos los comandos se emiten con el mismo tipo de paquete, y sólo cambia el número de comando y, en un par de casos, un parámetro extra.

Por otro lado, en base al contenido del segundo, tercero y quinto enteros de los paquetes enviados por la aspiradora puedo saber si es un paquete que espera una respuesta por mi parte, o un ACK de un comando enviado por mí, por lo que en el método de recepción de datos me limito a hacer una lista de comparaciones para ver qué tengo que responder; y si recibo una combinación desconocida, la imprimo.

Todas estas clases están gestionadas por una clase maestra llamada Multiplexer, que lanza primero una instancia de HTTPServer y otra de RobotServer, y va gestionando las nuevas instancias creadas por estos dos objetos mediante select.

Por último, hay una clase llamada Robot que está asociada permanentemente con un robot concreto, aunque éste no esté conectado. De momento se crea bajo demanda, pero en el futuro podría incluir algún tipo de persistencia, por ejemplo para que recuerde la configuración deseada por el usuario o los mapas, aunque se apague la aspiradora. Cada vez que un robot se conecta, se utiliza su identificador único para encontrar el objeto correspondiente y enlazarlo, y cada vez que se pierde la conexión, se informa de ello al objeto Robot para que borre la referencia al objeto RobotConnection.

Para la primera prueba decidí aprovechar el mismo servidor web que ya tengo para los robots e implementar un control sencillo por REST, mediante una sencilla página web. Es justo lo que se ve en el vídeo.

El siguiente paso es analizar y añadir los comandos para configurar el método de trabajo, la potencia del ventilador y el uso de la fregona, y por supuesto el control manual (aunque ya adelanto que parece que es en este caso donde entra en juego el puerto 8888 y la conexión directa con la tablet… a ver si hay suerte y me la puedo saltar).

También quiero analizar cómo se transmite la información de los mapas, pero eso será al final.

Parte 6

A ritmo de conga (y 4)

sábado, junio 27th, 2020

Hay que reconocer que cuando las cosas se complican, se complican de verdad. No se si lo que ha pasado demuestra que el programador que lo hizo es un genio y metió una sutil protección antihacking, o un chapuzas que hizo un código que funciona de milagro.

Me explico: decidí hacer una prueba rápida de la conexión de la aspiradora con un servidor mío, así que escribí un miniservidor con Python3 que escuchaba en el puerto 80, y que cuando recibía una petición a alguno de los documentos a los que responde el de bona robots (/baole-web/common/sumbitClearTime.do y /baole-web/common/getToken.do), anotaba los valores que pasaban y respondía lo que la aspiradora esperaba.

Pero claro, la aspiradora se empeña en conectarse a bl-app-eu.robotbona.com o a bl-im-eu.robotbona.com, así que tenía que engañarla para que se conectase a mi ordenador y no al servidor chino. La solución consistió en dos pasos:

  • primero, editar el fichero /etc/hosts de la Raspberry Pi que usaba como Access Point para conectar la aspiradora, y definir ahí esos dos dominios (además de robotbona.com, por si acaso), de manera que apuntasen a mi ordenador. Si la IP del ordenador fuese 192.168.0.89, habría que añadir:
192.168.0.89 bl-app-eu.robotbona.com
192.168.0.89 bl-im-eu.robotbona.com
192.168.0.89 robotbona.com
  • relanzar dnsmasq en dicha Raspberry Pi con sudo systemctl restart dnsmasq, para que lea los cambios.

Y con esto, cuando la aspiradora se conecte a dicha red y pida la dirección IP de esos dominios, éste le devolverá la de mi equipo, con lo que lo usará como servidor en lugar del real. Por supuesto, cuando termine todo esto habrá que montar el servidor en la Raspberry y que todo se conecte a la WiFi normal, pero eso ya lo haré en el último capítulo.

Ahora bien, como utilicé el módulo http.server, mi programa no implementaba la parte del puerto 20008 (donde realmente se reciben todos los comandos), y como para ello tenía que hacer más cosas, decidí, de momento, no complicarme la vida para una simple prueba, y limitarme lanzar un netcat en dicho puerto en un terminal. Así, cuando la aspiradora se conectase, vería por la pantalla lo que ésta enviase.

Así pues, apagué y encendí la aspiradora, la puse en modo emparejamiento, la emparejé desde mi ordenador, ésta apagó su Access Point, se conectó a la WiFi, hizo la petición sumbitClearTime.do, mi servidor respondió, hizo luego la petición getToken.do, mi servidor respondió también… y volvió a pedir getToken.do… Y otra vez lo volvió a pedir… Y el netcat sin inmutarse.

Algo raro ocurría, porque después de pedir getToken.do, netcat debería mostrar una conexión, pero ahí no ocurría nada. Decidí probar a quitar la redirección en el DNS por si acaso le había pasado algo a la aspiradora, de manera que se conectase al servidor real, y entonces funcionó de nuevo.

Esto era muy raro, así que volví a configurar mi DNS y lancé el tcpdump en la Raspberry para capturar el tráfico que se intercambiaba entre la aspiradora y mi servidor, y ver qué podía estar pasando. Tenía que ser algo en la segunda orden, porque la primera sí funcionaba bien.

Comparé las respuestas de mi servidor simulado con las del servidor real y eran idénticas: los mismos campos, el mismo orden… ¡Espera, había una diferencia sutil entre los JSON que yo enviaba y los que enviaba el servidor! En mi caso, en getToken.do utilizaba las funciones de python para convertir de un diccionario a JSON, y éste lo estaba embelleciendo añadiendo espacios detrás de las comas, de los dos puntos, etc. Sin embargo, en sumbitClearTime.do generaba la cadena «tal cual», por lo que no tenía espacios, igual que la que enviaba el servidor. Aunque un JSON debería funcionar igual con ellos que sin ellos, probablemente quien escribió el código que lo analizaba en la aspiradora podría haber hecho la chapuza de esperarlo de una determinada manera (o bien exigirlo así para detectar hackeos), así que eliminé ese trozo de código y generé la salida «a pelo», juntando cachos de código. Ahora sí funcionaría.

Volví a lanzar todo, encendí la aspiradora, me senté a ver en pantalla las conexiones al servidor… y volvió a fallar otra vez. Había alguna otra diferencia. ¿Pero cual?

Volví a revisar todo, y me di cuenta de que mi código estaba enviando una línea con el nombre del servidor. En concreto, el campo Server valía BaseHTTP/0.6 Python/3.8.3, mientras que el servidor real no enviaba dicho campo.

El problema es que el módulo http.server no permite eliminar ese campo… ¿Como resolverlo? La solución consitió en sobrescribir el método send_header del objeto, de manera que ignorase dicho campo. Esto funciona porque el módulo la usa para añadir sus campos propios. Así pues, añadí esto al servidor:

def send_header(self, token, data):
    if token != 'Server':
        super().send_header(token, data)

¡Y con esto por fin funcio…! No, seguía fallando. No era eso tampoco.

Hmm… En el código de error estoy enviando, después del 200, el texto OK, mientras que su servidor no envía nada, sólo hay un espacio detrás del 200 y ya luego el retorno de carro. ¡Seguro que es eso!

¡Agh, tampoco! No lo entiendo, estoy enviando exactamente lo mismo que su servidor: tanto las cabeceras como el contenido es idéntico. ¿Por qué no funciona? Si abro el wireshark y comparo las capturas reales con las mías a nivel de byte… son iguales. ¿Qué demonios está pasando? Es exactamente lo mismo, con la única excepción de…

No…

No habrán sido capaces…

No pueden haberlo hecho…

Lo fueron. Resulta que su servidor envía en un solo paquete la cabecera HTTP y el primer bloque de los datos en formato chunk, y en un paquete diferente el segundo y último bloque de los datos chunk; o sea, esto va en un paquete IP:

HTTP/1.1 200 \r\n
Date: Sat, 27 Jun 2020 16:38:08 GMT\r\n
Content-Type: application/json;charset=UTF-8\r\n
Transfer-Encoding: chunked\r\n
Connection: close\r\n
Set-Cookie: SERVERID=abcdefghijklmnopqrstuvwxyz12345|1234567890|1234567890;Path=/\r\n
\r\n
2b\r\n
{"msg":"ok","result":"0","version":"1.0.0"}\r\n

Y esto va en el siguiente paquete IP:

0\r\n
\r\n

Sin embargo, mi servidor enviaba en un paquete la cabecera HTTP, y el resto en un paquete diferente. O sea, esto iba en un paquete:

HTTP/1.1 200 \r\n
Date: Sat, 27 Jun 2020 16:38:08 GMT\r\n
Content-Type: application/json;charset=UTF-8\r\n
Transfer-Encoding: chunked\r\n
Connection: close\r\n
Set-Cookie: SERVERID=abcdefghijklmnopqrstuvwxyz12345|1234567890|1234567890;Path=/\r\n
\r\n

Y esto en el siguiente:

2b\r\n
{"msg":"ok","result":"0","version":"1.0.0"}\r\n
0\r\n
\r\n

Obviamente, en condiciones normales eso daría igual, pues TCP es un protocolo orientado a byte y se supone que da igual que un trozo viaje en un paquete IP y otro en el siguiente, pero es que esa era la única diferencia que quedaba, y efectivamente, en cuanto cambié el código para que lo emitiese de esa manera, por fin funcionó y se conectó al puerto 20008. ¿Será algún tipo de protección antihacking, o simplemente que, a nivel de programación del microcontrolador hicieron alguna chapuza? Sin analizar el firmware no puedo saberlo, y la verdad, es una tarea que, en este momento, no me llama nada. Pero da igual, porque lo importante es que…

Parte 5

Desktop icons, Gnome shell y Wayland

viernes, septiembre 20th, 2019

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.

Ventana transparente en Gtk 3 y Javascript

sábado, agosto 17th, 2019

Hacer una ventana con el fondo transparente utilizando Gtk es un clásico; de hecho hay ejemplos de como hacerlo en python, en C y en Vala. Pero falta como hacerlo con Javascript, y dado que es el lenguaje de moda para Gnome (y también por una serie de circunstancias extra) me he decidido a hacer el ejemplo. Este es el código:

#!/usr/bin/env gjs

imports.gi.versions.Gtk = '3.0';
const Gtk = imports.gi.Gtk;
const Gdk = imports.gi.Gdk;

Gtk.init(null);

let window = new Gtk.Window();
window.set_app_paintable(true);
let screen = window.get_screen();
let visual = screen.get_rgba_visual();
if (visual && screen.is_composited()) {
    window.set_visual(visual);
    window.connect('draw', (widget, cr) => {
        Gdk.cairo_set_source_rgba(cr, new Gdk.RGBA({red: 0.0,
                                                    green: 0.0,
                                                    blue: 0.0,
                                                    alpha: 0.0}));
        cr.paint();
        return false;
    });
    window.connect('delete-event', () => {
        Gtk.main_quit();
    });
    window.show_all();
    Gtk.main();
} else {
    print("El entorno de ventanas no admite transparencia");
}

Aunque el código no tiene nada de especial comparado con las versiones en otros lenguajes, sí es cierto que tuve problemas con Gdk.cairo_set_source_rgba, pues, como se ve, no sigue la estructura «normal» de llamadas como si fuera un objeto, sino que sigue el formato de C. Lo lógico sería hacer cr.cairo_set_source_rgba(…), pero no funciona. Y lo mismo con algunas otras.

1984: mi pequeño homenaje a un paper icónico del CGI

domingo, junio 16th, 2019

En 1984, hace ya la friolera de 35 años, un grupo de investigadores de la división de gráficos por ordenador de Lucasfilm (lo que un par de años después pasaría a llamarse Píxar) publicó un paper donde presentó la solución definitiva a muchos de los problemas del momento de los gráficos por ordenador orientados al cine y al entretenimiento. Me refiero al Distributed Ray Tracing. Esta técnica permitía añadir a los gráficos por ordenador hechos mediante Ray Tracing elementos fotorrealistas tales como motion blur, profundidad de campo, sombras con penumbras, antialiasing, y más, y todo ello sin añadir prácticamente carga computacional extra. Esa era su gran novedad.

Es cierto que el Ray Tracing es una técnica tan costosa a nivel de procesador y, sobre todo, de memoria, que, durante décadas, Renderman, el render desarrollado por Píxar y utilizado en la inmensa mayoría de escenas de películas hechas por ordenador, no la utilizaba, sino que se basaba en la técnica REYES (Renders Everything You Ever Saw). Era la única manera de poder hacer gráficos fotorrealistas en los equipos de los 80, los 90 y buena parte de los 2000. Sin embargo, algunas de las técnicas desarrolladas en el paper se pudieron adaptar a dicho algoritmo, específicamente el motion blur y la profundidad de campo. Hoy en día, en cambio, sí que se utiliza Ray Tracing de manera rutinaria, basado en mejoras de las técnicas presentadas en este paper. Así trabajan hoy en día Ray Tracers comerciales como Arnold, o el propio Renderman a partir de su versión 19.

Sin embargo, además de todo esto, al final de dicho paper aparece una imagen que muestra las capacidades de los algoritmos descritos en él, y que ya se puede considerar icónica. Dicha imagen se titula 1984 y muestra una mesa de billar con cinco bolas, las cuales exhiben los efectos de motion blur, sombras suaves, reflectividad (lo que permite ver la ventana de la sala y a una persona sosteniendo un taco)…

¿Y a qué viene toda esta chapa? Pues a que he querido hacer un pequeño homenaje a dicha publicación y a los genios detrás de ella programando un pequeño Ray Tracer propio con el que reproducir dicha imagen. Y éste es el resultado:

Mi versión/homenaje. Pulsa sobre ella para ampliarla.

Al igual que en la imagen original, todas las bolas son reflectantes (lo que permite apreciar el entorno de la sala), y cuatro de ellas tienen motion blur: algunas (como la blanca y la bola 9) tienen un motion blur sencillo, y las otras dos, la 8 y la 4, uno más complejo, al estar detenidas durante parte del tiempo de apertura del obturador y moverse sólo durante parte del tiempo de exposición. Por supuesto, no cabe esperar una reproducción fidedigna (¡Soy programador, Jim, no artista!), pero lo importante para mí no es tanto la imagen en sí, sino el hecho de que he sido capaz de programar un Ray Tracer desde cero. Desde que leí el paper original quedé prendado de la elegancia del algoritmo, y tenía ganas de intentar implementarlo.

Sin embargo, lo divertido (relativamente) es que acabé escribiendo no uno, sino dos Ray Tracers: cuando empecé con este proyecto, hace un par de semanas, aún no tenía muy claro el nivel de complejidad que me iba a encontrar, así que decidí no complicarme la vida más de lo estrictamente necesario y opté por utilizar Python para escribirlo, junto con numpy para toda la parte de vectores y matrices que necesitaba. Primero construí un Ray Caster capaz de mostrar esferas y planos. Luego le añadí soporte de mapeado de texturas. A continuación antialiasing y soporte de luces. Llegado a este punto añadí soporte de motion blur y empecé a preparar ya la escena de la sala de billar en lugar de las imágenes de demostración con una Cornell Box.

El resultado es el de ahí arriba. Sin embargo, tenía un problema serio: es desesperadamente lento. Es cierto que, como dije antes, la técnica de Ray Tracing es muy costosa computacionalmente, pero es que hablamos de ¡18 horas para esa imagen a 1920×1080! ¡Y eso que ya había hecho unas cuantas optimizaciones que me permitieron reducir los tiempos a, aproximadamente, dos tercios de los originales, además de utilizar todos los núcleos de mi ordenador! Es más o menos lo que habría tardado a principios de los 90 con PovRay y un 8086.

Fue por esto que, aunque el objetivo original ya estaba cumplido, tomé la decisión de portar el código a C++ para ver cuanto podía arañar. Quería saber si el problema estaba en Python o en mi código. Aunque dicho así puede sonar a locura, lo cierto es que el código de Python era bastante sencillo (actualmente son un total de 1415 líneas de código, que, tras quitar líneas en blanco y comentarios, se quedan en 899), con lo que era factible coger cada uno de los ficheros y traducirlo de un lenguaje a otro. Sólo tuve que escribir un par de clases para trabajar con vectores y matrices y alguna cosa más, lo que no fue especialmente difícil. Es cierto que probablemente podría haber usado cualquiera de las alternativas que seguro que hay por ahí ya escritas, pero dado que sólo tengo que trabajar con matrices y vectores de tamaño fijo, y sólo necesito un puñado de operaciones muy concretas, era más trabajo aprender a usarlas que escribirlo yo. Y encima es probable que me diese más rendimiento (repito: tamaño fijo).

El resultado fue, en una sola palabra: pasmoso. De las más de 18 horas pasé a 398 segundos. Repito: menos de siete minutos para la misma imagen: 1920×1080 y supersampling de 8×8.

Sí es cierto que me encontré con un problema algo raro: en Python utilicé fork() para poder aprovechar todos mis núcleos porque, como es de sobra conocido, el GIL no permite que varios threads se ejecuten de manera concurrente. Esto complicó bastante el código, pues tuve que usar pipes para que el programa principal fuese repartiendo nuevas líneas a cada proceso hijo a medida que iban terminando y quedando libres, y también para que éstos enviasen las líneas ya renderizadas al padre para que las uniese en una sola imagen. Para la versión en C++ no quería complicarme tanto, así que decidí utilizar pthreads desde el principio, de manera que todos los hijos accediesen a la misma memoria para ir depositando sus líneas en el punto correcto sin tener que enviarlas mediante IPC, y autoasignándose la siguiente línea mediante un semáforo. Sin embargo, algo raro ocurría: tardaba más que cuando utilizaba un único proceso. Si no fuese porque top indicaba una carga de procesador del 400%, y que el procesador indicaba que había seis threads de mi programa, habría asegurado que estaba utilizando una biblioteca de threads en espacio de usuario, sin threads reales.

Después de muchas pruebas, de quemarme las pestañas viendo el código, y buceando en Stack Overflow y otros sitios, no conseguí descubrir qué ocurría, así que decidí utilizar también fork() en C++, pero añadiendo memoria compartida para simplificar la comunicación y la complejidad al máximo, y ahí sí que conseguí la ganancia de rendimiento esperada; esto es, multiplicar la velocidad por (prácticamente) el número de núcleos del ordenador.

Y esto es más o menos todo. Aquellos que quieran curiosear un poco en mi código, lo tienen disponible en mi repositorio git:

https://gitlab.com/rastersoft/1984

Múltiples archivos en aplicaciones Gtk con Javascript

viernes, mayo 24th, 2019

Desde hace unos meses estoy colaborando como desarrollador con Carlos Soriano, echándole una mano con Desktop Icons, la extensión para Gnome Shell que escribió para devolver los iconos al escritorio tras retirar dicho soporte de Nautilus.

Como muchos sabrán ya, las extensiones, al igual que el resto de Gnome Shell, están escritas en Javascript, y además no utilizan Gtk sino Clutter/St. Sin embargo, Javascript también se puede utilizar para escribir aplicaciones normales, de escritorio, con Gtk de toda la vida. Para ello basta con utilizar el shebang #!/usr/bin/gjs al principio del archivo. Personalmente sigo prefiriendo Python, pero obviamente para gustos hay colores.

Y precisamente aquí me encontré con el primer gran problema: estoy intentando portar parte de una extensión a espacio de usuario, y dado que el código de la extensión está repartido en varios archivos, quería mantenerlo así también en la aplicación. Por desgracia, por más que buscaba no encontraba como resolver el problema, pues aunque existen algunas aplicaciones Gtk en Javascript que ocupaban varios archivos, no tenía muy claro cómo hacían para que, al llamar a imports, la máquina virtual encontrase el fichero. Pero por fin, después de buscar y rebuscar, encontré la clave:

imports.searchPath.unshift(ruta_donde_buscar_ficheros);

Esa llamada recibe como parámetro un string donde podemos añadir una ruta extra donde debe buscar ficheros. Basta con que pongamos la carpeta donde se encuentran el resto de ficheros .js de nuestro proyecto, y el comando imports los encontrará igual que encuentra los ficheros del sistema. Por supuesto es necesario añadirla antes de hacer ningún imports.

Usando DBus desde lenguaje C y JavaScript

viernes, diciembre 11th, 2015

Trabajar con DBus desde Python o Vala es muy sencillo: esconden la complejidad del protocolo de manera que uno tiene la ilusión de estar haciendo una llamada local. Sin embargo, históricamente, trabajar con DBus desde C ha sido considerado un peñazo de dimensiones colosales. Sin embargo, desde la llegada de la biblioteca GDBus, que integra DBus dentro de GIO la cosa se ha simplificado bastante, sobre todo en aquellos casos en los que simplemente queremos llamar de manera síncrona a un método remoto.

Debido a algunos cambios que estuve haciendo en Panther Launcher, necesité poder hacer precisamente eso: llamar a un método remoto mediante DBus desde C, para implementarlo dentro del applet para Gnome Flashback. Dado que no encontré documentación sencilla para este detalle concreto (esto es, sin usar entre medias un bucle de eventos), voy a comentar como lo hice, por si le es útil a alguien más.

Lo primero que necesitamos para poder llamar a un método remoto mediante DBus es la interfaz correspondiente donde se define dicho método. Esta interfaz se puede obtener de manera muy sencilla mediante dbus-send y las capacidades de introspección de DBus:

dbus-send --session --type=method_call --print-reply --dest=com.rastersoft.panther.remotecontrol /com/rastersoft/panther/remotecontrol org.freedesktop.DBus.Introspectable.Introspect

En este ejemplo obtenemos las interfaces disponibles en el objeto  /com/rastersoft/panther/remotecontrol, del servicio  com.rastersoft.panther.remotecontrol. El resultado es éste:

freedesktop.DBus.Introspectable.Introspect
method return time=1449871353.404951 sender=:1.1222 -> destination=:1.1246 serial=104 reply_serial=2
 string "<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
 "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<!-- GDBus 2.46.2 -->
<node>
 <interface name="org.freedesktop.DBus.Properties">
   <method name="Get">
     <arg type="s" name="interface_name" direction="in"/>
     <arg type="s" name="property_name" direction="in"/>
     <arg type="v" name="value" direction="out"/>
   </method>
   <method name="GetAll">
     <arg type="s" name="interface_name" direction="in"/>
     <arg type="a{sv}" name="properties" direction="out"/>
   </method>
   <method name="Set">
     <arg type="s" name="interface_name" direction="in"/>
     <arg type="s" name="property_name" direction="in"/>
     <arg type="v" name="value" direction="in"/>
   </method>
   <signal name="PropertiesChanged">
     <arg type="s" name="interface_name"/>
     <arg type="a{sv}" name="changed_properties"/>
     <arg type="as" name="invalidated_properties"/>
   </signal>
 </interface>
 <interface name="org.freedesktop.DBus.Introspectable">
   <method name="Introspect">
     <arg type="s" name="xml_data" direction="out"/>
   </method>
 </interface>
 <interface name="org.freedesktop.DBus.Peer">
   <method name="Ping"/>
   <method name="GetMachineId">
     <arg type="s" name="machine_uuid" direction="out"/>
   </method>
 </interface>
 <interface name="com.rastersoft.panther.remotecontrol">
   <method name="DoPing">
     <arg type="i" name="v" direction="in"/>
     <arg type="i" name="result" direction="out"/>
   </method>
   <method name="DoShow">
   </method>
 </interface>
</node>

Vemos que aparecen varias interfaces, dentro de cada una varios métodos, y dentro de cada método puede haber cero o más parámetros, y cero o más valores devueltos. Para nuestros propósitos no vamos a necesitar todo, sino sólo la interfaz com.rastersoft.panther.remotecontrol, así que eliminaremos el resto de entradas (y las líneas superiores descriptivas, sólo queremos el XML), y nos quedará esto:

<node>
 <interface name="com.rastersoft.panther.remotecontrol">
   <method name="DoPing">
     <arg type="i" name="v" direction="in"/>
     <arg type="i" name="result" direction="out"/>
   </method>
   <method name="DoShow">
   </method>
 </interface>
</node>

Este fichero XML es el que describe las llamadas que vamos a implementar.

Si queremos llamar a alguno de estos métodos desde JavaScript (por ejemplo, para hacer una llamada desde una extensión de Gnome Shell), sólo necesitaremos hacer lo siguiente:

const Gio = imports.gi.Gio;

const MyIface = '<node>\
 <interface name="com.rastersoft.panther.remotecontrol">\
  <method name="DoShow" />\
  <method name="DoPing" >\
   <arg name="n" direction="in" type="i"/>\
   <arg name="response" direction="out" type="i"/>\
  </method>\
 </interface>\
</node>';

const MyProxy = Gio.DBusProxy.makeProxyWrapper(MyIface);

let instance = new MyProxy(Gio.DBus.session, 'com.rastersoft.panther.remotecontrol','/com/rastersoft/panther/remotecontrol');
instance.DoShowSync();
instance.DoPingSync(0);

Primero importamos Gio para tener acceso a GDBus. Después insertamos el XML con la interfaz en una variable, y creamos un Proxy DBus con ella. Finalmente, cada vez que queramos acceder a un objeto (en este caso /com/rastersoft/panther/remotecontrol) de un servicio DBus (en este caso com.rastersoft.panther.remotecontrol), sólo tenemos que crear una instancia del proxy anterior, el cual nos permitirá llamar a los métodos definidos en la interfaz.

Cabe recalcar que aquí estoy llamando a los métodos con su nombre terminado en Sync. Eso significa que la llamada será bloqueante, y no retornará hasta que se reciba la respuesta del otro extremo. Es posible hacer llamadas asíncronas, pero no lo he investigado y no voy a entrar ahí.

Hasta aquí JavaScript, que es la parte más sencilla. Ahora llega el turno de como hacer esto mismo desde C.

GDbus tiene un generador de código que nos simplifica el trabajo. Para usarlo basta con grabar en un fichero el XML anterior (por ejemplo, remotecontrol.xml) y llamar a gdbus-codegen para que construya el código necesario para poder llamar a dichos métodos:

gdbus-codegen --c-generate-object-manager --generate-c-code dbus remotecontrol.xml

Este comando creará dos ficheros: dbus.c y dbus.h (se utiliza el nombre indicado en –generate-c-code) a partir del fichero remotecontrol.xml. Existen algunos parámetros extra, como –c-namespace, que permite especificar un prefijo para todas las funciones que se generen, y así evitar choques de nombres.

En dichos ficheros tendremos un montón de código ya escrito, y es precisamente este código el que nos simplifica el proceso, pues no tendremos que escribirlo nosotros. Una vez que lo tenemos, sólo hemos de llamarlo así:

#include "dbus.h"

GError *error = NULL;

ComRastersoftPantherRemotecontrol *proxy;

proxy = com_rastersoft_panther_remotecontrol_proxy_new_for_bus_sync (G_BUS_TYPE_SESSION,
                                                  G_DBUS_PROXY_FLAGS_NONE,
                                                  "com.rastersoft.panther.remotecontrol",
                                                  "/com/rastersoft/panther/remotecontrol",
                                                  NULL, /* GCancellable */
                                                  &error);
if (proxy != NULL) {
    error = NULL;
    retval = com_rastersoft_panther_remotecontrol_call_do_show_sync(proxy,NULL,&error);

    ...

    error = NULL;
    gint value;
    retval = com_rastersoft_panther_remotecontrol_call_do_ping_sync(proxy,0,&value,NULL,&error);
}

Tras incluir el fichero dbus.h para disponer de las llamadas, lo primero que hacemos es crear un puntero de tipo GError para recibir las excepciones que se produzcan.

A continuación creamos de manera síncrona un proxy de la interfaz concreta que queremos utilizar. Vemos que lo estamos haciendo para el bus de sesión, para el objeto /com/rastersoft/panther/remotecontrol del servicio com.rastersoft.panther.remotecontrol. Es fundamental que el puntero GError esté inicializado a NULL, pues de no hacerlo la llamada fallará.

Y una vez que tenemos dicho proxy ya podemos llamar a los métodos remotos. Vemos que cada llamada está formada por el nombre de la interfaz, más _call_, más el nombre del método, y en este caso, como la llamada queremos que se bloquee hasta que llegue la respuesta, termina en _sync. El primer parámetro es el proxy, y los dos últimos un puntero a la función para cancelar la llamada (que ponemos a NULL para no complicarnos la vida) y un puntero a GError, que también debe estar inicializado a NULL antes de llamar a la función.

Entre medias se introducen, en el mismo orden en que están definidos en el XML, los parámetros de entrada (que se pasan por valor) y punteros para los parámetros de salida.

Para compilarlo hay que utilizar pkg-config gio-2.0 gio-unix-2.0 para que se añadan las cabeceras y bibliotecas necesarias.

Y ya está, con esto podemos por fin llamar de manera sencilla un método DBus desde lenguaje C.

Peleandome con Python 3.4

jueves, julio 30th, 2015

Dado que quiero tener soporte para gnutls, tuve que cambiar el USE de mi distribución a:

USE="${ARCH} -pam -fortran -sanitize -iptables -static -systemd -mdev gnutls internal-glib -caps -filecaps -X -gtk -qt -tk"

Las siete últimas adiciones fueron para no añadir nada de entorno gráfico (pues no tiene sentido en el WebTV) y para asegurar de que las nuevas bibliotecas necesarias para incluir gnutls se puedan compilar. Hubo varios problemillas, pero uno a uno los fui resolviendo.

Por desgracia, el último escollo estaba en python. Cuando compilé el sistema la versión estable era la 3.3, pero ahora ya salió la 3.4. El problema es que se negaba a compilar, dando un error raro:

Python build finished successfully!
The necessary bits to build these optional modules were not found:
    _tkinter
To find the necessary bits, look in setup.py in detect_modules() for the module's name.
Failed to build these modules:
    _socket               _ssl

Decía que se había compilado correctamente, pero emerge devolvía un error. Al principio pensaba que el problema estaba en tkinter, el módulo gráfico de python, pero no tenía sentido porque había especificado que no quería ni tk, ni X ni nada relacionado con un entorno gráfico. Entonces, revisando el log, vi que en medio de la compilación había este error:

/tmp/portage/dev-lang/python-3.4.3/work/Python-3.4.3/Modules/socketmodule.o
/tmp/portage/dev-lang/python-3.4.3/work/Python-3.4.3/Modules/socketmodule.c: In function 'makesockaddr':
/tmp/portage/dev-lang/python-3.4.3/work/Python-3.4.3/Modules/socketmodule.c:1175:14: error: dereferencing pointer to incomplete type
         if (a->can_ifindex) {
              ^
/tmp/portage/dev-lang/python-3.4.3/work/Python-3.4.3/Modules/socketmodule.c:1176:32: error: dereferencing pointer to incomplete type
             ifr.ifr_ifindex = a->can_ifindex;
                                ^
/tmp/portage/dev-lang/python-3.4.3/work/Python-3.4.3/Modules/socketmodule.c:1183:38: error: dereferencing pointer to incomplete type
                                     a->can_family);
                                      ^
/tmp/portage/dev-lang/python-3.4.3/work/Python-3.4.3/Modules/socketmodule.c: In function 'getsockaddrlen':
/tmp/portage/dev-lang/python-3.4.3/work/Python-3.4.3/Modules/socketmodule.c:1802:28: error: invalid application of 'sizeof' to incomplete type 'struct sockaddr_can'
         *len_ret = sizeof (struct sockaddr_can);
                            ^
building '_ssl' extension

¿CAN? ¿En el módulo de sockets? Bastante raro. Rebuscando encontré que, efectivamente, desde el núcleo 2.6.25 hay soporte para el bus CAN; sin embargo, mi núcleo (y sus cabeceras) es el 2.6.22. ¿Por qué se empeñaba en incluir soporte? Por otro lado, python 3.3 también trae de serie soporte para bus CAN, pero esa versión sí compilaba bien. ¿Qué estaba pasando?

Al final descubrí que el núcleo 2.6.22 tiene algo de soporte del bus CAN, pero parece que no el suficiente, y eso lía a python 3.4.

La solución que encontré fue editar el fichero /usr/include/bits/socket.h, y comentar la línea donde se define AF_CAN:

//#define AF_CAN PF_CAN

Y con eso, por fin, pude compilar absolutamente todo, listo para empezar a preparar el sistema que va a llevar definitivamente.

Generando Gentoo para el WebTV

domingo, julio 19th, 2015

Nota: actualizado el parche para BusyBox.

Estos días estoy bastante liado con el trabajo-que-paga-las-facturas, pero por suerte he podido sacar un rato para cacharrear. Me he puesto con el WebTV (que tengo bastante abandonado desde que me compré la Raspberry Pi) y he decidido intentar meter un sistema «decente». ¿A qué me refiero? Pues a que, por defecto, la máxima versión de Debian que puede correr es wheezy, que ya es old-stable. La estable actual (jessie) necesita un núcleo más reciente, y se niega a trabajar con el que trae el WebTV (2.6.22).

Ante esto decidí probar con Gentoo, a ver si conseguía compilarlo todo. A continuación indicaré como lo hice.

NOTA: para los que no quieran leerse este tocho, en mi web está disponible este entorno Gentoo completo para WebTV, ya compilado y listo para usar.

Para empezar, me bajé la stage 3 de Gentoo para X86_64, bajé también la última lista de paquetes de portage, y descomprimí ésta última en /usr de la stage 3. Con ello ya pude lanzar un contenedor de gentoo con

sudo systemd-nspawn -D /directorio/con/la/stage3 /bin/bash

Aquí toca primero preparar el sistema para hacer crossdev. Esto se puede repasar en una entrada anterior: Emergiendo.

Una vez dentro intenté hacer un crossdev para compilar una gentoo para mipsel usando

crossdev --kernel 2.6.22 -t mipsel -v

Por desgracia fallaba: se empeñaba en utilizar las cabeceras de la versión 2.4.36. ¿Qué pasaba? Pues que aunque en los repositorios de Gentoo sí existen las cabeceras del kernel 2.6.22, éstas no están disponibles en la lista de ebuilds de portage.

Ante esto empecé a buscar y probar, y finalmente con la ayuda de la gente de IRC del canal #Gentoo-kernel conseguí el ebuild de las cabeceras para la versión 2.6.22-r2. Sin embargo tuve que grabarlo en /usr/portage/sys-kernel/linux-headers con el nombre linux-headers-2.6.22-r3, pues la R3 es la versión disponible en el repositorio.

Tras ello actualicé el fichero Manifest para que encontrase el nuevo ebuild con

ebuild /usr/portage/sys-kernel/linux-headers/linux-headers-2.6.22-r3.ebuild manifest

Probé de nuevo a generar el crossdev pero seguía intentando usar la versión 2.4.36… porque la versión 2.6.22-r3 es posterior a la 2.6.22 a secas. Cambiando el parámetro en crossdev solucionó el problema.

crossdev --kernel 2.6.22-r3 -t mipsel -v

Por desgracia, ahora me encontraba con otro: ocurría un error durante la instalación de las cabeceras:

HOSTCC  scripts/unifdef
scripts/unifdef.c:209:25: error: conflicting types for 'getline'
 static Linetype         getline(void);
                         ^
In file included from scripts/unifdef.c:70:0:
/usr/include/stdio.h:678:20: note: previous declaration of 'getline' was here
 extern _IO_ssize_t getline (char **__restrict __lineptr,
                    ^
scripts/Makefile.host:118: recipe for target 'scripts/unifdef' failed
make[1]: *** [scripts/unifdef] Error 1
Makefile:927: recipe for target 'headers_install' failed
make: *** [headers_install] Error 2
emake failed

Tocaba buscar más soluciones. Afortunadamente esta era sencilla: bastaba con editar el fichero unifdef.c y sustituir todas las ocurrencias de getline por otra cosa, como por ejemplo get_line. Preparé el siguiente parche para ello:

--- a/scripts/unifdef.c
+++ b/scripts/unifdef.c
@@ -206,7 +206,7 @@ static void             done(void);
 static void             error(const char *);
 static int              findsym(const char *);
 static void             flushline(bool);
-static Linetype         getline(void);
+static Linetype         get_line(void);
 static Linetype         ifeval(const char **);
 static void             ignoreoff(void);
 static void             ignoreon(void);
@@ -512,7 +512,7 @@ process(void)
 
 	for (;;) {
 		linenum++;
-		lineval = getline();
+		lineval = get_line();
 		trans_table[ifstate[depth]][lineval]();
 		debug("process %s -> %s depth %d",
 		    linetype_name[lineval],
@@ -526,7 +526,7 @@ process(void)
  * help from skipcomment().
  */
 static Linetype
-getline(void)
+get_line(void)
 {
 	const char *cp;
 	int cursym;

Y entonces me encontré con el problema de como aplicarlo durante la generación del crossdev. La cosa no era sencilla, porque se empeña en comprobar los valores de sha256, sha512 y whirlpool de todo lo que baje. En teoría se pueden añadir parches manualmente en /etc/portage/patches, pero tras probar de todo no conseguí que funcionase, así que al final fui a la solución cazurra y metí el comando de parcheado directamente en el ebuild. Para ello edité el fichero /usr/portage/sys-kernel/linux-headers/linux-headers-2.6.22-r3.ebuild y lo dejé como sigue:

# Copyright 1999-2007 Gentoo Foundation
# Distributed under the terms of the GNU General Public License v2
# $Header: /var/cvsroot/gentoo-x86/sys-kernel/linux-headers/Attic/linux-headers-2.6.22-r2.ebuild,v 1.11 2008/04/12 22:24:36 vapier dead $

ETYPE="headers"
H_SUPPORTEDARCH="alpha amd64 arm cris hppa m68k mips ia64 ppc ppc64 s390 sh sparc x86"
inherit kernel-2
detect_version

echo ${PV}
echo ${PATCH_VER}

PATCH_VER="3"
SRC_URI="mirror://gentoo/gentoo-headers-base-${PV}.tar.bz2"
[[ -n ${PATCH_VER} ]] && SRC_URI="${SRC_URI} mirror://gentoo/gentoo-headers-${PV}-${PATCH_VER}.tar.bz2"

KEYWORDS="-* alpha amd64 arm hppa ia64 m68k mips ppc ppc64 s390 sh sparc x86"

DEPEND="dev-util/unifdef"
RDEPEND=""

S=${WORKDIR}/gentoo-headers-base-${PV}

src_unpack() {
        unpack ${A}
        cd "${S}"
        [[ -n ${PATCH_VER} ]] && EPATCH_SUFFIX="patch" epatch "${WORKDIR}"/${PV}
        patch -p1 < /getline.patch
}

src_install() {
        kernel-2_src_install
        cd "${D}"
        egrep -r '[[:space:]](asm|volatile|inline)[[:space:](]' .
        headers___fix $(find -type f)
}

src_test() {
        make ARCH=$(tc-arch-kernel) headers_check || die
}

Luego copié el texto del parche en el fichero /getline.patch y actualicé de nuevo el manifest. Ahora, siempre que se intente instalar el paquete, se parcheará correctamente.

A intentarlo otra vez… y otra vez falla, esta vez porque la versión 2.20 de glibc necesita, al menos, un kernel 2.6.32. Como el núcleo disponible es el que es, toca probar con una versión anterior. La 2.19r1 fue suficiente:

crossdev --kernel 2.6.22-r3 --l 2.19-r1 -t mipsel -v

Finalmente, con esto ya es capaz de compilar glibc. Ahora vienen las curvas, porque no es capaz de compilar el soporte de Fortran para el GCC, ni las pruebas sanity ni algunas cosas más, así que toca armarse de paciencia e ir probando opciones de USE hasta que todo compile. El resultado final es que hay que usar la siguiente línea (añadí el -X para reducir las dependencias, pues en el WebTV no es necesario):

USE="-fortran -sanitize -X" CFLAGS="-O2 -pipe" crossdev --kernel 2.6.22-r3 --l 2.19-r1 -s4 -t mipsel -v

Inicializamos los wrappers de compilación cruzada…

emerge-wrapper --target mipsel-unknown-linux-gnu --init

Y ya tenemos el sistema de compilación cruzada para MIPSel. Ahora toca configurar el entorno y preparar el sistema. Para ello lo primero es borrar el enlace /usr/mipsel-unknown-linux-gnu/etc/portage/make.profile (que, por defecto, apunta a /usr/portage/profiles/embedded) y sustituirlo por uno que apunte a /usr/portage/profiles/ default/linux/mips/13.0/mipsel.

Una vez hecho esto editamos /usr/mipsel-unknown-linux-gnu/etc/portage/make.conf, y ahí modificamos la línea donde se define el USE para añadir, al menos, -fortran -sanitize -X -iptables. También podemos añadir, opcionalmente, un MAKEOPTS=»-jX» (siendo X el número de núcleos de nuestro procesador más uno), para que la compilación sea más rápida.

También tenemos que editar el fichero /usr/mipsel-unknown-linux-gnu/etc/portage/package.mask, y añadir estas líneas:

>sys-libs/glibc-2.19-r1
>sys-kernel/linux-headers-2.6.22-r3

Con ellas evitamos que instale versiones posteriores de ambos paquetes, que harían que el sistema dejase de funcionar en nuestro WebTV.

Con esto ya podemos intentar generar nuestro sistema base con

emerge-mipsel-unknown-linux-gnu system

Si falla al compilar Busybox, es probable que haya alguna opción que no le gusta. En ese caso hay que editar su fichero .ebuild en /usr/portage/sys-apps/busybox/busybox-X.Y.Z.ebuild, añadir las opciones de configuración que se quieren activar o desactivar, y luego ejecutar ebuild /usr/portage/sys-apps/busybox/busybox-X.Y.Z.ebuild manifest para actualizar el manifest. En mi caso, el problema es que se activan por defecto el soporte de UBIFS y de I2C, cosa que no parece gustarle, así que para eliminarlo tuve que añadir las siguientes líneas en el sitio adecuado del ebuild:

busybox_config_option n I2CGET
busybox_config_option n I2CSET
busybox_config_option n I2CDUMP
busybox_config_option n I2CDETECT
busybox_config_option n UBIATTACH
busybox_config_option n UBIDETACH
busybox_config_option n UBIMKVOL
busybox_config_option n UBIRMVOL
busybox_config_option n UBIRSVOL
busybox_config_option n UBIUPDATEVOL

Con suerte, en un par de días este parche ya estará incluido en los repositorios oficiales.

Otro problema, esta vez más grave, es con Perl: se trata de un paquete al que no le gusta que le hagan compilación cruzada. El resultado es que, simplemente, no podemos instalarlo así. La solución consiste en, de momento, hacer creer al sistema que sí está instalado, e instalarlo manualmente desde el sistema final una vez que ya estamos en el equipo. Para hacer esto basta con editar el fichero /usr/mipsel-unknown-linux-gnu/etc/portage/profile/package.provided y poner, en cada línea, los paquetes que queremos marcar como instalados. Hice lo mismo con los paquetes de UDev, que tampoco los necesito. En mi caso su contenido fue:

dev-lang/perl-5.22
virtual/perl-Data-Dumper-2.158.0
perl-core/File-Temp-0.230.400-r1
virtual/perl-File-Temp-0.230.400-r3
dev-perl/Text-Unidecode-1.230.0
dev-perl/libintl-perl-1.240.0
virtual/perl-File-Spec-3.560.0
dev-perl/Unicode-EastAsianWidth-1.330.0-r1
sys-fs/udev-222
virtual/udev-217
sys-fs/udev-init-scripts-30
virtual/dev-manager-0

Pero, obviamente, depende de la versión de portage y de los paquetes disponibles.

Tras instalar todo esto, si el equipo es de 64 bits nos encontraremos con que nos ha metido varios elementos de python en /usr/lib64, cuando todo debería ir en /usr/lib. Es por esto que debemos mover todos los ficheros del primero al segundo.

Ahora ya podemos copiar el contenido de /usr/mipsel-unknown-linux-gnu/ a un disco duro externo (dentro de una carpeta llamada bg_apps), añadir un fichero init y otro vacío llamado no_base_system, y ya podemos arrancar nuestro sistema Gentoo en el WebTV.

Pero aún no hemos acabado. Para empezar, es necesario hacer el siguiente enlace cada vez que se encienda el equipo:

ln -s /proc/self/fd /dev/fd

para que emerge funcione correctamente. También es recomendable editar el fichero /etc/portage/make.conf y eliminar la opción de compilación -pipe, pues consume más memoria, y en un equipo relativamente limitado como el WebTV nos puede dar problemas con compilaciones muy tochas.

Por otro lado, tenemos que comentar las entradas de Perl que pusimos en el fichero /usr/mipsel-unknown-linux-gnu/etc/portage/profile/package.provided, y procer a instalarlos todos con emerge.

No hay que olvidar que, debido a la gran cantidad de ficheros que tiene el directorio /usr/portage, el arranque de la sesión en segundo plano del WebTV tardará bastante tiempo (en torno a un minuto), pues antes de lanzar la sesión, el sistema revisa todos y cada uno de los ficheros para asegurarse de que no hay «cosas raras».


Utilizamos cookies para garantizar que tenga la mejor experiencia en nuestro sitio web.