Files
hammer/docs/runbooks/gnome-qemu-desktop.md
T
sergioandClaude Opus 5 723f3ead33 gnome: 🏔🏔 EL ESCRITORIO PINTA — Overview de GNOME desde fuente, con captura
docs/evidencia/gnome-shell-qemu-2026-07-29.png: el conmutador de espacios de trabajo,
el reloj en el panel, la miniatura del escritorio, el dash, y la notificación estándar
de GNOME por sesión de root. Todo construido desde fuente sobre el kernel de hammer,
arje-zero como PID1 y musl.

**EL MURO NO ERA DE KMS.** El diagnóstico anterior culpaba al plano primario de
virtio-gpu («no advertised formats», sólo `Queue mode set`, ningún page flip). Era
correlación. Se probó quitando MUTTER_DEBUG_FORCE_KMS_MODE=simple —el log pasó a decir
`using atomic mode setting`, o sea que el cambio SÍ tomó efecto— y la pantalla siguió
con exactamente 2 colores. Refutada. Mutter no tenía un frame que presentar; no es que
no supiera presentarlo.

La causa estaba en el stack trace, en el log, desde el principio: la excepción por el
gschema `org.gnome.settings-daemon.peripherals.touchscreen` ocurre DENTRO de
`Main.start()`, construyendo los quick settings ⇒ el panel nunca se arma. Proceso vivo,
compositor con DRM master, nada que pintar.

Receta `gsd-schemas`: los gschemas de gnome-settings-daemon SIN g-s-d, que sigue
aparcada por GTK3/X11. **Tercera vez que aparece la misma lección** (tras la política
D-Bus de login1 y el `org.gnome.login-screen` de libgdm): un gschema no es un fichero de
datos del demonio, es una interfaz publicada que consume otro programa.

Dos trampas de diagnóstico, las dos ahora blindadas:
- **`glib-compile-schemas` sale con 0 aunque RECHACE un esquema.** Instalé los once XML,
  el log dijo `esquemas OK` y el shell seguía diciendo `not found`: faltaba
  `org.gnome.settings-daemon.enums.xml`, que meson genera con glib-mkenums y no es uno
  de los `.in`. Sin los `<enum>`, glib descarta el esquema entero, avisa por stderr y
  sigue. `gnome-start` ahora vuelca ese stderr SIEMPRE, no sólo al fallar.
- El log serial persiste entre arranques y me hizo leer dos veces un `STATUS +30s` de una
  VM ya muerta. Truncarlo antes de arrancar; `Failed to get "write" lock` avisa de que
  hay otro QEMU con el disco tomado.

Y queda escrito cómo validar CON PANTALLA sin humano delante: screendump por el monitor
de QEMU, con una métrica barata previa a mirar — contar colores distintos. Negro = 2;
pintando = 582, con el azul GNOME (2,60,136) al frente.

Lo que falta son detalles, ninguno impide el escritorio: lanzar upowerd desde gnome-start,
un tema de cursor, el setuid de colord y gnome-control-center.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 18:05:20 -04:00

14 KiB

Runbook — GNOME en QEMU: el escritorio PINTA

Estado al 2026-07-29: cerrado. gnome-shell arranca desde fuente sobre el kernel de hammer, toma el DRM master y los tres dispositivos de input por logind, expone wayland-0, y dibuja el Overview de GNOME. Cinco muros caídos, cada uno por medición y no por conjetura:

muro causa real cómo se midió
SIGSEGV en get_seat_proxy object path de la Session mal escapado (_1 en vez de _31) el cliente calcula el path solo y no pregunta
cuelgue de la hebra de input el fd de TakeDevice sin O_NONBLOCK wchan=evdev_read en /proc, no el core
Requiring <X> en bucle 7 typelibs de runtime + 2 capas más abajo js/misc/dependencies.js + cierre de <include>
pantalla negra Main.start() abortaba por un gschema de g-s-d el stack trace, que estaba en el log desde el principio
(descartado) KMS legacy NO era la causa atomic tomó efecto y la pantalla siguió con 2 colores

Hermano de kde-qemu-desktop.md, del que reusa el andamiaje: la base work/metal-rootfs, la inyección de mesa-llvmpipe/LLVM/musl, el truco del console-getty y el propio scripts/kde/run-qemu-desktop.sh.

Ciclo completo

cd ~/hammer
bash scripts/gnome/hydrate-gnome.sh          # cierre de gnome-shell + las raíces de RUNTIME → work/gnome-rootfs
bash scripts/gnome/qemu-desktop-image.sh     # funde base+gnome, parchea, → work/hammer-gnome-qemu.img
IMG=work/hammer-gnome-qemu.img SERIAL=work/gnome-qemu-serial.log MON=work/gnome-monitor.sock \
  TIMEOUT=150 DISP=none bash scripts/kde/run-qemu-desktop.sh
grep -a "gnome-qemu ::" work/gnome-qemu-serial.log | awk '!seen[$0]++'

DISP=gtk para ver la ventana (necesita DISPLAY=:0 GDK_BACKEND=x11). Regla heredada de KDE: validar escritorios con PANTALLA, nunca sólo por serial — y sin humano delante se cumple con el screendump del monitor de QEMU, ver abajo. Aplicarla es lo que destapó el muro de hoy: el serial decía «sesión viva» y la pantalla estaba negra.

Sacar el core y el backtrace

El gnome-start deja el core en la raíz ext4 y hace sync, así que sobrevive al SIGKILL de QEMU.

sfdisk -J work/hammer-gnome-qemu.img      # localizar hammer-root (part 2)
dd if=work/hammer-gnome-qemu.img of=/tmp/root.img bs=1M skip=129 count=5113 status=none
debugfs -R "dump /core.gnome-shell.<pid> /tmp/core.gnome-shell" /tmp/root.img
gdb -batch -q -ex "set sysroot $PWD/work/gnome-qemu-rootfs" \
    -ex "core-file /tmp/core.gnome-shell" -ex "bt 25" work/gnome-qemu-rootfs/usr/bin/gnome-shell

🏔 GNOME PINTA (2026-07-29)

gnome-shell en QEMU

El Overview de GNOME Shell, construido entero desde fuente sobre el kernel de hammer, arje-zero como PID1 y musl: el conmutador de espacios de trabajo arriba a la izquierda, el reloj en el panel, la miniatura del escritorio, el dash abajo, y la notificación estándar de GNOME por sesión de root («Logged in as a privileged user»).

Cómo se validó, y es la parte reusable: screendump por el monitor de QEMU, sin humano delante.

printf 'screendump /tmp/gnome.ppm\n' | socat - UNIX-CONNECT:work/gnome-monitor.sock
python3 -c "from PIL import Image; Image.open('/tmp/gnome.ppm').save('/tmp/gnome.png')"

Métrica barata para saber si pinta antes de mirar: contar colores distintos. Con la pantalla negra eran 2 (negro + el gris de la consola del kernel); pintando son 582, con el azul GNOME (2,60,136) y los grises del shell (34,34,38) al frente.

El muro que parecía de KMS y era de GSettings

El diagnóstico anterior de este runbook culpaba al KMS: «el plano primario de virtio-gpu no publica formatos, sólo hay Queue mode set y ningún page flip». Era una correlación, no la causa. Se probó quitando MUTTER_DEBUG_FORCE_KMS_MODE=simple —el log pasó a decir using atomic mode setting, o sea que el cambio SÍ tomó efecto— y la pantalla siguió con exactamente 2 colores. Refutada.

La causa estaba en el stack trace, en el log, desde el principio:

GNOME Shell-CRITICAL: Failed to setup quick settings:
  Error: GSettings schema org.gnome.settings-daemon.peripherals.touchscreen not found
Stack trace: systemActions.js:156 → status/system.js:236 → quickSettings.js:29

Esa excepción ocurre dentro de Main.start(), construyendo los quick settings: el panel nunca se termina de armar, así que no hay nada que pintar. El proceso queda vivo y el compositor con DRM master — de ahí la pantalla negra con gnome-shell=2. Mutter no tenía un frame que presentar; no es que no supiera presentarlo.

Lo resolvió la receta gsd-schemas: los gschemas de gnome-settings-daemon sin g-s-d (que sigue aparcada por GTK3/X11). Tercera vez que aparece la misma lección — un gschema no es un fichero de datos del demonio, es una interfaz publicada que consume otro programa.

Dos trampas del camino, las dos de diagnóstico

  1. glib-compile-schemas sale con 0 aunque RECHACE un esquema. Instalé los once XML, el log dijo esquemas OK, y el shell seguía diciendo schema ... not found. Lo que faltaba era org.gnome.settings-daemon.enums.xml —que meson genera con glib-mkenums y no es uno de los .in—: sin los <enum>, glib descarta el esquema entero, avisa por stderr y sigue. gnome-start ahora vuelca ese stderr siempre, no sólo cuando el comando falla.
  2. El log serial persiste entre arranques. Dos veces leí un STATUS +30s que era del arranque anterior y saqué conclusiones de una VM muerta. Truncar (: > serial.log) antes de arrancar, y esperar a que la imagen esté hecha Y no haya otro QEMU con el disco tomado (Failed to get "write" lock es el aviso de que hay uno vivo).

Lo que queda, y ninguno impide el escritorio

  • Failed to activate service 'org.freedesktop.UPower': timed out — falta lanzar upowerd en gnome-start, como se hace con accounts-daemon.
  • No cursor theme available — falta un tema de cursor (adwaita-icon-theme).
  • colord no arranca: su helper setuid no tiene los permisos correctos en la imagen.
  • Missing required core component Settings — es gnome-control-center, que no está en el corpus.

CÓMO SE CERRÓ LA CAPA JS (2026-07-29)

Era el muro anterior, y quedó cerrado. Vale la pena dejar cómo, porque el método es reusable.

El síntoma era: el compositor sube, expone wayland-0, y el shell sale con código 1 porque a imports.gi.* le falta un typelib. Cada vez que se cerraba uno, aparecía el siguiente.

Y ESA ITERACIÓN ERA INNECESARIA — la lista está declarada en el propio shell. js/misc/dependencies.js enumera EXACTAMENTE lo que el shell exige al arrancar. Cruzada contra el rootfs hidratado, la frontera completa es de siete typelibs, de cinco recetas nuevas y una a rehacer (medido, no estimado):

typelib lo provee estado
GnomeDesktop-4.0, GnomeBG-4.0 gnome-desktop ya sellada, pero con -Dintrospection=false y estática ⇒ hace falta la variante de isla dinámica
Geoclue-2.0 geoclue sin receta
GWeather-4.0 libgweather sin receta
IBus-1.0 ibus sin receta
Rsvg-2.0 librsvg sin receta (es Rust)
UPowerGlib-1.0 upower sin receta

Lo demás de esa lista ya está: AccountsService, Atk, Atspi, Gcr, Gdk, Gdm, Gio, GioUnix, GDesktopEnums, GdkPixbuf, Graphene, Pango, Polkit, PolkitAgent, Soup, y los cinco de mutter/shell (Meta, Clutter, Cogl, Shell, St). GnomeBluetooth, NM/NMA4 y Malcontent son condicionales de compilación o de runtime y no bloquean.

La regla que sale de acá, y es el precio de no haberla aplicado antes: cuando el muro es Requiring X, no cierres X y vuelvas a arrancar — leé el fichero donde el programa DECLARA sus dependencias de runtime y cerralas todas de una. Cada ronda de este bucle cuesta un rebuild de imagen y un arranque completo.

Cerrados por este camino hasta hoy, en orden: AccountsServiceDBus-1.0 (que incluye Atspi) → cairo-1.0 (que incluyen Gdk y Pango) → Gdm-1.0.

CÓMO CAYÓ EL MURO ANTERIOR — la hebra de input (cerrado 2026-07-29)

Vale la pena dejarlo escrito porque el diagnóstico que había acá era falso y la forma de refutarlo es reusable.

Decía: «el hilo principal espera en un condvar a que la Mutter Input Thread avise que terminó de inicializarse; nunca avisa» —eso era correcto— «hipótesis principal: libudev-zero, que libinput_udev_assign_seat() enumera por udev y el rootfs no trae udev de verdad». Eso era la conjetura, apoyada en un episodio parecido de otro frente (mirada-usb-nvidia).

La sonda que la refutó: libinput list-devices, que hace exactamente lo que hace init_libinput() de mutter —udev_new + libinput_udev_create_context + libinput_udev_assign_seat("seat0")—. Volvió con rc=0 y los tres dispositivos listados. La enumeración por /sys con libudev-zero funciona perfecto. Está horneada en gnome-start y corre en cada arranque, antes del shell.

El dato que dio la causa: tid 201 [Mutter Input Th] wchan=evdev_read syscall=0. El hilo estaba dormido en un read() de evdev DENTRO DEL KERNEL. Ese volcado por hilo ya estaba en el script pero corría después del kill -ABRT, sobre un /proc/<pid>/task que ya no existía: salía vacío y no decía nada. El core tampoco servía — gdb no desenrolla a través de musl y devuelve ?? () para los 11 hilos que no son el principal. Acá /proc le gana al core.

La causa: arje-logind-compat abría el device de TakeDevice sin O_NONBLOCK. El compositor no elige los flags —recibe el fd ya abierto por el bus—, y libinput asume no-bloqueante para poder leer en bucle hasta EAGAIN; lo dice donde abre (libinput/src/evdev.c:2401) y le pasa O_RDWR|O_NONBLOCK|O_CLOEXEC a open_restricted(), pero mutter, cuando hay logind, ignora esos flags y delega la apertura. systemd-logind abre con O_NONBLOCK, así que la dependencia no se nota hasta que la implementa otro. Arreglado en tawasuyu 3dd88f582, con test.

Y por qué la sonda no lo veía: libinput list-devices abre el devnode él mismo, con O_NONBLOCK. Sólo el camino por logind pasaba por el fd malo. Esa asimetría es justamente lo que la convirtió en un experimento y no en una repetición.

Tres perillas de diagnóstico que quedaron y valen para el próximo muro: MUTTER_DEBUG es una lista de tópicos (g_parse_debug_string sobre meta_debug_keys, src/core/util.c:42), no un booleano — con 1 no encendía nada; con backend imprime «Opening and taking control of device file ''» por cada device y dice en cuál se para. El ping D-Bus a login1 separa «daemon tildado» de «daemon vivo que ya contestó». Y los logs de /tmp (tmpfs) se copian a la raíz ext4 antes de abortar, para sacarlos con debugfs junto al core.

LO QUE YA SE ARREGLÓ ANTES (dos bugs reales en arje-logind-compat)

Ambos encontrados arrancando esta imagen, ambos con test, ambos ya en tawasuyu y sellados:

  1. La sesión se creaba PEREZOSAMENTE. ensure_user sólo corría dentro de un método del Manager, y write_login_state vive adentro. Pero mutter no empieza por D-Bus: lo primero que hace es sd_pid_get_session(), que arje-sdlogin-compat resuelve LEYENDO /run/systemd/sessions. Huevo y gallina — el estado en disco sólo se escribía después de un pedido que sólo ocurre si el estado ya existe. Ahora la sesión se crea al arrancar (eager_session, apagable con ARJE_LOGIN_EAGER=0, uid por ARJE_LOGIN_UID).
  2. El object path de la Session estaba mal escapado. Era /session/_1; la convención de systemd (bus_label_escape) codifica como _<hex> todo lo que no sea [A-Za-z0-9] y también el primer carácter si es dígito, así que el id "1" da _31. Importa porque el cliente calcula el path por su cuenta y no pregunta: mutter reimplementa la misma regla en meta-dbus-utils.c:escape_dbus_component. Con _1 no había nadie sirviendo ahí, la propiedad Seat volvía NULL y mutter —que no chequea— moría de SIGSEGV en get_seat_proxy. Los objetos User NO usan este escapado (systemd hardcodea _<uid>), así que user_path() queda igual.

De paso, un arreglo de portabilidad musl preexistente: libc::ioctl toma c_ulong en glibc y c_int en musl, así que DRM_IOCTL_SET_MASTER se guarda pelado y se convierte en el sitio de uso.

Y ARJE_LOGIN_STATE=1 ya existía: el comentario del daemon dice literalmente «en arje (sin systemd) el launcher de sesión lo prende. Default off» — y el launcher es gnome-start. El puente que escribía /run/systemd/ a mano fue reinventar esa perilla; queda como fallback inerte.

El muro gráfico que este runbook listaba como «el que viene después» nunca apareció: los fixes que KDE dejó pagos y precargados en gnome-start-qemu.sh (GBM_ALWAYS_SOFTWARE, kms_swrast, LP_NUM_THREADS=1, MUTTER_DEBUG_SEND_KMS_MODIFIERS=0) alcanzaron: mutter crea el renderer gbm y elige /dev/dri/card0 como primaria sin quejarse. Es el rédito directo de la campaña anterior.

Lo que falta, en orden

  1. Los seis proveedores de typelib de la tabla de arriba, en una sola tanda. gnome-desktop dinámica+introspectada primero (ya está sellada, sólo cambia el modo), después geoclue, libgweather, upower, librsvg e ibus. Ojo al costo de libelogind: re-sellarla re-hashea todo lo que la declara (mutter, gnome-shell…) ⇒ pasar yupana radio libelogind ANTES de tocarla — ya pasó dos veces en esta campaña.
  2. Empaquetar la política D-Bus de login1 dentro del artefacto de arje-logind-compat (hoy la escribe el script de imagen).
  3. Arreglar el identity mismatch del Announce al bus del fractal (hoy sólo un WARN).
  4. Con la capa JS viva: validar CON PANTALLA (DISP=gtk), nunca sólo por serial — regla heredada de KDE (metal-dual-desktop-en-curso).

Recetas nuevas de esta etapa

accountsservice (b3:0328b0d0, + parche fgetspent_r para musl) · gi-foreign-typelibs (b3:c8a4d2f8 — un .gir NO es un typelib: gjs resuelve contra el compilado) · libgdm (b3:46fac484 — sólo la librería cliente; el demonio sigue aparcado por linux-pam). libelogind pasó de 14 a 25 símbolos en tres tandas.