Archive for the 'Programación' Category

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:

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».

Tamaño constante en GTK

sábado, septiembre 6th, 2014

Hace unos días envié un parche para Slingshot, el lanzador de aplicaciones de Elementary OS. El problema que tenía es que tenía fijados al pixel los tamaños de todos los elementos. Esto se adapta bien si se utiliza el tema de Elementary y, además, en lengua inglesa. Sin embargo, en el momento en que se utiliza otro tema, o bien una lengua en la que alguna categoría de programas sea mucho más larga que en inglés, nos encontraremos con que el conjunto de iconos queda recortado por la derecha:

Se supone que esto no debería ocurrir porque Gtk permite crear interfaces que se redimensionen de manera automática, sin embargo en slingshot esto no ocurría. El motivo es que los iconos no están hechos con un Gtk.IconView, sino con botones dentro de un Gtk.Grid. Pero los autores querían, además, que dichos botones tuviesen un tamaño fijo de 130×130 pixels exactamente, por lo que la única idea que se les ocurrió fue fijar el tamaño exacto de la ventana que contiene todo el lanzador, incluyendo la lista de categorías, el buscador, etc. El motivo de hacerlo así es que dentro de cada botón hay, además del icono, una etiqueta con el nombre de la aplicación, y salvo que el contenedor tenga un tamaño determinado, la etiqueta intentará expandirse todo lo que pueda, y ni siquiera sirve de nada pedir un tamaño deseado, porque si el texto en la etiqueta es más largo, la etiqueta se expandirá todo lo que pueda para contener todo el texto.

La primera idea que se me ocurrió fue engancharme a la señal size_allocate del botón. Esta señal se emite cada vez que un widget cambia de tamaño por el motivo que sea, y recibe como parámetro sus nuevas medidas. Con ella y la propiedad max_width_chars intenté limitar el tamaño de la etiqueta para que el botón tuviese el tamaño máximo deseado; por desgracia era un proceso relativamente lento y que, además, provocaba un parpadeo en la ventana, pues ésta primero tomaba el valor máximo y se reducía progresivamente hasta el tamaño correcto, a medida que las distintas etiquetas iban reduciendo su número de caracteres.

La segunda idea, sugerida por la gente de la lista de desarrollo de Gtk, fue encapsular la etiqueta dentro de un Gtk.Fixed, pues permite ajustar manualmente el tamaño de cualquier widget situado en su interior. Esta solución casi funcionó, pues aunque la etiqueta se mantenía en el ancho deseado, el Gtk.Fixed contenedor se redimensionaba hasta ocupar el mismo espacio que ocuparía la etiqueta si estaba sola.

Ante esto me di cuenta de que la solución iba a necesitar de algo más de investigación y, sobre todo, de meterme en los entresijos del renderizado de Gtk, así que empecé a investigar, y descubrí que todo widget tiene dos métodos que participan de manera especial en la asignación de tamaño: get_preferred_width y get_preferred_height. Estos dos métodos devuelven dos valores: el primero es el tamaño mínimo que necesita dicho widget en el eje correspondiente para mostrar su contenido, y el segundo el tamaño deseado. En el caso de una etiqueta, el tamaño deseado será el ancho que ocupe la totalidad del texto puesto en una única línea, mientras que el tamaño mínimo dependerá de varias opciones: si no hay elipsis de texto coincidirá con el tamaño deseado, pero si no, será menor, de lo necesario para pintar el borde y unos puntos suspensivos.

Cuando un contenedor tiene en su interior otro widget, llama a estos dos métodos para saber cuanto espacio debe reservar. Lo interesante es que dichos valores son, realmente, orientativos, pues el contenedor puede asignar otros tamaños si así lo considera oportuno. Así, si una etiqueta pide 100 pixels pero está en una ventana que mide 200 y tiene activo el flag de expandirse, el tamaño final de la etiqueta será de 200. De igual forma, si una etiqueta pide un tamaño deseado de 100 pixels de ancho pero la ventana mide 50, recibirá 50 y el texto tendrá que ocupar varias líneas.

Ante esto se me ocurrió probar qué ocurriría si creo una nueva clase que herede de Gtk.Label, en la que redefino (override) el método get_preferred_width para que devuelva 120 como ancho mínimo y deseado (porque cada botón tiene un margen de 5 pixels por cada lado). El resultado es justo el deseado: la etiqueta mide exactamente el ancho que se le indica, y aplica las reglas para pasar a nuevas líneas o para añadir elipsis.

Por supuesto esto es fácil de hacer en Vala; en C es posible hacerlo también, pero obliga a meterse en el interior de GObject, una tarea que, sinceramente, no me llama demasiado en este momento.

Cagándola con un bug grave

domingo, agosto 10th, 2014

A veces a uno se le escapan pequeños bug que, en principio, no tienen la menor importancia, hasta que resulta que sí la tienen. Eso me ha ocurrido con la nueva versión en desarrollo de DevedeNG.

El bug ni siquiera era tal, sino más bien una combinación de valores por defecto poco adecuados, mensajes poco claros, y código que hacía demasiadas cosas. Básicamente, DevedeNG necesita una carpeta vacía donde crear el proyecto, así que le pide al usuario que le indique una. Si dicha carpeta ya existe le indica que va a proceder a borrarla, dejándole escoger si quiere continuar o cancelar antes de hacerlo.

Hasta aquí todo parece normal. El problema es que la carpeta por defecto era la carpeta personal del usuario, y el mensaje de aviso decía simplemente: «La carpeta seleccionada ya existe. Si continúa, será borrada. ¿Continuar?». Este mensaje no es lo suficientemente claro porque en ninguna parte aparece qué carpeta se va a borrar. Ante esto, un usuario que se limite a pulsar Siguiente->Siguiente->Siguiente se encontrará con su carpeta personal borrada.

El código ya está corregido, para lo que he aplicado las siguientes lecciones aprendidas:

  • Debe evitarse utilizar la carpeta de usuario ($HOME) como destino por defecto cuando haya que realizar alguna acción que borre una carpeta. En el caso de DevedeNG, el primer cambio fue que la carpeta destino se componga de la carpeta escogida por el usuario más el nombre del proyecto, por lo que, por defecto, nunca será la carpeta de usuario sola la escogida para ser borrada.
  • Siempre que se le pida al usuario confirmación para borrar una carpeta debe mostrarse claramente en el texto qué carpeta se va a borrar. Nunca se debe dar por supuesto que el usuario lo sabe.
  • En la medida de lo posible no debe borrarse la carpeta al completo, sino buscar qué ficheros y directorios son los que pueden interferir con el proceso que se quiere realizar, borrando exclusivamente esos y ninguno más.

Afortunadamente, el usuario que se tropezó con este problema tenía copia de seguridad, por lo que todo quedó en un susto.

XCB y Cairo

sábado, septiembre 28th, 2013

Actualizado: Por fin he terminado la primera versión usable de TabletWM y TabletLauncher. Con ellos y el driver para la pantalla táctil he conseguido el gran objetivo de poder utilizar aplicaciones GNU/Linux nativas en una tablet sin teclado ni ratón.

Crear TabletWM fue, sin duda, la parte más compleja, porque supuso trabajar con un API completamente nuevo, como es XCB. Tenía claro que no quería utilizar XLib, así que me lié la manta a la cabeza, pero partiendo de un gestor de ventanas muy, pero que muy básico hecho por Cinolt a finales de 2011.

Lo primero que aprendí es que el protocolo X es lento, por lo que en un dispositivo como una tablet, con capacidades limitadas, no es recomendable pedir al servidor X las propiedades de una ventana cada vez que se quiere mostrar en pantalla o cambiar sus dimensiones o posición. Sin embargo no parece existir una manera de que el servidor avise al gestor de ventanas de que una propiedad ha cambiado, y dado que estas propiedades contienen información importante necesaria a la hora de mapear una ventana, la mejor solución que encontré fue hacer una caché de datos que relleno en el momento de hacer visible una ventana (en ese momento las propiedades ya tienen que estar definidas), aunque antes puedo haber almacenado otras operaciones que se hayan hecho en la ventana, como un cambio de tamaño o de posición.

Otra cosa que aprendí es que hay dos grupos de eventos de interés para un gestor de ventanas: request y notification. El primero se emite cuando una aplicación pide realizar alguna operación sobre una ventana suya, y la segunda si el resultado de dicha operación cambia algo realmente en la ventana. Así, si una aplicación pide cambiar el tamaño de una ventana, ejecutará un comando xcb_configure_window(). Si no existe un gestor de ventanas, dicho comando se ejecuta normalmente; pero si lo hay, se genera un evento xcb_configure_request en el gestor. Este puede decidir si ignorarlo, ejecutarlo tal cual o modificarlo. Si decide ejecutarlo y el tamaño de la ventana cambia realmente, se emite un evento xcb_configure_notification, que llegará a la aplicación.

Y aquí encontré otro de los problemas que tuve: mi intención es que las ventanas estén maximizadas siempre, por lo que cada vez que recibía un evento xcb_configure_request, emitía un comando xcb_configure_window() con el tamaño máximo de la pantalla. El problema ocurría con algunas ventanas que pedían ser más grandes que la pantalla, por ejemplo la ventana de configuración de Firefox. En este caso, se mostraba la ventana en la primera pestaña, y la pantalla es lo suficientemente grande como para contener todo; al recibir el evento, TabletWM modifica los valores pedidos por el navegador, ajustándolos al tamaño de la pantalla, y cambia las dimensiones de la ventana. Se emite el evento xcb_configure_notification y todo sigue perfectamente.

Pero cuando se escoge la pestaña de Seguridad, el nuevo contenido no entra en la pantalla, por lo que Firefox pide un tamaño más grande. El gestor de ventanas recibe el evento y cambia los parámetros por las dimensiones de la pantalla (que es el mismo tamaño que ya tiene la ventana), y da la orden de cambiar el tamaño. Pero como es el mismo, nunca se genera un evento xcb_configure_notification, pero Firefox espera que le llegue. El resultado es que la ventana queda sin refrescar.

La solución que apliqué fue ejecutar primero el comando de cambio de tamaño tal cual llega, y ejecutar luego un segundo comando con el tamaño que el gestor de ventanas desea. De esa manera la aplicación siempre recibe el evento que espera, y todos felices.

Otra curiosidad fue a la hora de leer ciertas cadenas de texto en las propiedades. En algunos casos, una propiedad (como por ejemplo _XKB_RULES_NAMES) contiene varias cadenas separadas por NUL (o sea, un byte a cero). Si pedimos el tamaño de ésta en la cookie (cookie->length) nos devolverá la longitud de la primera cadena exclusivamente. Si queremos obtener todas tenemos que utilizar xcb_get_property_value_length(), que sí nos dará el tamaño total. Por si fuera poco, no se garantiza que al final de la cadena haya un NUL, por lo que debemos tener en cuenta el tamaño para no pasarnos, en lugar de usar strcpy().

El teclado fue otro de los problemas serios que tuve: el método normal de entrada por teclado de X es relativamente rudimentario, por lo que hoy en día se utiliza la extensión XKB. Por desgracia esta extensión no está portada tal cual a XCB, sino que existe un proyecto separado, XCB-COMMON, que contiene aquellas partes no dependientes de un servidor X. Por si fuera poco, no puedo emular la pulsación de un carácter concreto, sino sólo la pulsación de una tecla en sí. Esto significa que los códigos que tengo que enviar a las X dependerán del idioma del teclado escogido por el usuario, además de tener que emular pulsaciones múltiples como la tecla mayúsculas, AltGR, etc.

Encima, esta biblioteca está orientada a obtener un carácter a partir de una pulsación, cuando yo necesitaba el proceso inverso. Aunque parte del trabajo lo hace (dado un carácter devuelve el código de la tecla), no devuelve el código de los modificadores necesarios. Así, si pido la arroba, me devuelve el código de la tecla ‘2’, pero no me dice si necesito pulsar también las mayúsculas (caso del teclado norteamericano) o AtlGR (caso del teclado español). Esto me obligó a realizar una pequeña chapuza, consistente en probar todas las posibles combinaciones de teclas normales y teclas modificadoras (shift, control, etc) para ver qué caracteres produce cada una.

Y para rizar el rizo, no es posible siempre obtener la combinación correcta, por lo que en algunos casos no queda más remedio que redefinir un código de tecla que no se utilice con un carácter determinado (por ejemplo, la letra Ñ). Esto hay que hacerlo con las funciones de entrada clásicas, no con las de XKB, por lo que el resultado final es algo caótico, pero funciona, y permite definir un teclado cualquiera en pantalla a partir de los caracteres que se quieren mostrar en lugar de las teclas físicas que se deben pulsar.

Pasando a Cairo, decidí utilizar esta biblioteca para los elementos gráficos (como el teclado o la ventana de apagado) en lugar de las funciones de X por varios motivos:

  • Cairo ofrece antialiasing, tanto en fuentes como en primitivas gráficas.
  • Cairo es una biblioteca moderna con un API sencillo y potente.
  • Ya conozco Cairo, pero no las funciones gráficas de X.
  • No supone una carga extra porque el lanzador de aplicaciones utiliza GTK, que por debajo trabaja también con Cairo.

Trabajar con Cairo directamente desde XCB no es muy complicado. Para ello, primero se debe crear una ventana, y a continuación una superficie Cairo con cairo_xcb_surface_create(). La principal complicación es conocer el visual_type de la ventana, para poder pasárselo a la función. Esto lo podemos saber mediante el siguiente código (la variable conn es el handler de la conexión con el servidor X):

    xcb_screen_t *scr=xcb_setup_roots_iterator(xcb_get_setup(conn)).data;
    xcb_visualtype_t *visual_type = NULL;
    xcb_depth_iterator_t depth_iter;

    for (depth_iter = xcb_screen_allowed_depths_iterator (scr); depth_iter.rem; xcb_depth_next (&depth_iter)) {
        xcb_visualtype_iterator_t visual_iter;

        visual_iter = xcb_depth_visuals_iterator (depth_iter.data);
        for (; visual_iter.rem; xcb_visualtype_next (&visual_iter)) {
            if (scr->root_visual == visual_iter.data->visual_id) {
                visual_type = visual_iter.data;
                break;
            }
        }
    }

Luego no tenemos más que engancharnos al evento expose de la ventana (XCB_EVENT_MASK_EXPOSURE) y, cada vez que se reciba, generar un contexto Cairo y repintar la ventana. Un detalle extra a tener en cuenta es que cada vez que se redimensione la ventana es necesario llamar a cairo_xcb_surface_set_size(), para cambiar el tamaño de la superficie Cairo.

Ah, y no olvidar hacer un xcb_flush() al terminar de pintar algo, para que efectivamente lo pinte en la ventana.


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