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.
Desktop icons, Gnome shell y Wayland por A cuadros está licenciado bajo una Licencia Creative Commons Atribución-CompartirIgual 4.0 Internacional.
He leído la entrada de tu blog (la he traducido en DeepL ya que no hablo español), y estoy impresionado por tu habilidad e ingenio. Muchas gracias por todo el esfuerzo que has puesto en el desarrollo de esta extensión, tu trabajo es muy apreciado.
I’m happy that you find it useful. Enjoy it!!!