Files
takana/scripts/gnome/gnome-start-qemu.sh
T
Sergio 3d79cdf98b SDD 30 4c: GNOME con los demonios supervisados por arje — y el control arregló colord
Los demonios de sistema pasan de lanzarse con `&` desde un script de 500 líneas
a ser Cards del `genesis`. El bloqueo que este frente daba por corpus se levantó
solo: otro agente selló evolution-data-server y gnome-shell mientras esto se
escribía, y escritorio-gnome quedó 309/309.

SE CORRIÓ UN CONTROL PRIMERO, y es lo único que hace interpretable el resultado:

  colord              control: `MURIÓ al arrancar`   cards: `ya vive (pid 175)`
  ColorManager        control: `NO apareció en 40s`  cards: `OK`
  login1/Accounts/UPower  OK en los dos
  compositor          wayland-0 y shell vivo en los dos

El fallo del control DESAPARECIÓ, y no lo buscaba: colord moría arrancado por el
script y vive arrancado por arje, con el bus ya listo porque la espera está
dentro de su argv. Sin el control, «ColorManager OK» sería un dato suelto en vez
de una diferencia. Los PIDs lo confirman: polkit=119, colord=175, upowerd=178,
accounts=180 — de antes de que el lanzador de sesión existiera.

QUÉ NO SE COMPROBÓ: no hay screendump; QEMU salió por timeout y el control
tampoco lo tuvo. La comparación es serial contra serial y lo que se afirma es
sobre los DEMONIOS, no sobre el pintado.

Las piezas donde corresponde: `takana service-cards` (UNA sola implementación de
receta→Card; el formato es contrato con card_core::Card), `targets.py
--service-paths` (une qué-es con si-arranca), y un inyector en FICHERO APARTE
porque anidar dos heredocs de python falló en vivo — el terminador del interno
cerró el externo y media cosa corrió como shell.

La espera del bus va DENTRO del argv de las 5 recetas de sistema: sin ella un
daemon arranca antes de que dbus escuche y queda en modo idle sin registrar su
nombre — un fallo que no se ve, porque el proceso vive y el bus no lo tiene. Los
5 hashes intactos. Y el guardia de gnome-start es por «¿está corriendo?», no por
una perilla: así es correcto venga de donde venga el proceso y la misma copia
sirve donde no se inyectaron cards.
2026-09-14 01:51:19 +00:00

611 lines
36 KiB
Bash
Executable File

#!/bin/sh
# gnome-start-qemu.sh — arranca gnome-shell (Wayland) en una VM QEMU con render por SOFTWARE
# (kms_swrast/llvmpipe) sobre el DRM de virtio-gpu. Análogo de plasma-start-qemu.sh, y hereda sus
# lecciones caras: el diagnóstico va POR SERIAL porque la ventana gtk se congela si mirada reinicia
# su Xwayland (QEMU sigue vivo por dentro). Ver [[kde-metal-qemu-desktop]].
#
# Se instala como /usr/bin/gnome-start y lo ejecuta DIRECTO el console-getty (no vía login shell).
# Salir: matar gnome-shell. Logs: /tmp/gnome-shell.log + tee al serial.
#
# DIFERENCIA CLAVE CON PLASMA: acá NO hay dos procesos (compositor + shell). gnome-shell ES el
# compositor — embebe mutter como librería (libmutter-16.so) y `--wayland` lo hace display server.
# Por eso no hay un equivalente de "kwin arriba pero plasmashell murió": si el shell cae, cae todo.
set -u
# getty ejecuta este script DIRECTO ⇒ PATH viene vacío. Primero eso, antes de invocar nada.
export PATH=/usr/bin:/bin:/usr/sbin:/sbin
# El modo lo hornea la imagen (GNOME_MODE=... en scripts/gnome/qemu-desktop-image.sh).
[ -r /etc/gnome-mode ] && . /etc/gnome-mode
say() { echo "== gnome-qemu :: $*" | tee -a /dev/console /dev/ttyS0 2>/dev/null; }
dump() { echo "---- $1 ----" > /dev/ttyS0 2>/dev/null; cat "$1" > /dev/ttyS0 2>/dev/null; }
# OJO con el test de vivo: `kill -0` TIENE ÉXITO sobre un ZOMBI (el hijo existe como PID hasta que
# el padre lo cosecha, y acá el padre es este script, que sólo hace wait al final). La primera
# versión usaba kill -0 y por eso reportó "sin wayland-0 tras 45s" cuando en realidad gnome-shell ya
# había muerto en el primer segundo: el diagnóstico decía "0 hilos" porque no quedaba proceso, sólo
# la entrada zombi. Se mira el State de /proc en su lugar.
alive() { s=$(sed -n 's/^State:[[:space:]]*//p' /proc/$1/status 2>/dev/null); case "$s" in ""|Z*) return 1;; *) return 0;; esac; }
export LD_LIBRARY_PATH=/usr/lib:/lib:/usr/lib/gnome-shell:/usr/lib/mutter-16
# GI_TYPELIB_PATH es EL env crítico de esta sesión, y el que no tiene análogo en KDE: gnome-shell es
# JavaScript sobre gjs y carga TODO por typelib. Los del sistema van en /usr/lib/girepository-1.0,
# pero St/Shell/Gvc/Shew se instalan APARTE en /usr/lib/gnome-shell (son privados del shell). Sin
# esta ruta, gjs no encuentra imports.gi.St y el shell muere en el primer require.
export GI_TYPELIB_PATH=/usr/lib/girepository-1.0:/usr/lib/gnome-shell:/usr/lib/gnome-shell/girepository-1.0
export XDG_DATA_DIRS=/usr/share
export XDG_RUNTIME_DIR=/run/user/0
export XDG_CONFIG_HOME=/root/.config
export XDG_CACHE_HOME=/root/.cache
export XDG_SESSION_TYPE=wayland
export HOME=/root
export GSETTINGS_SCHEMA_DIR=/usr/share/glib-2.0/schemas
export LANG=C.UTF-8
# XCURSOR_PATH/XCURSOR_THEME: sin tema de cursor mutter avisa «No cursor theme available» y —con el
# cursor por software, que en virtio-gpu es obligatorio— el puntero se mueve INVISIBLE. Lo provee
# `adwaita-icon-theme`, y el nombre del tema tiene que coincidir con el directorio bajo
# /usr/share/icons. El `org.gnome.desktop.interface cursor-theme` de GSettings dice lo mismo, pero
# estas variables las lee también libXcursor/wayland-cursor sin pasar por GSettings.
export XCURSOR_PATH=/usr/share/icons
export XCURSOR_THEME=Adwaita
export XCURSOR_SIZE=24
# --- render por software sobre virtio-gpu (los mismos fixes que costaron la campaña KDE) ---
export LIBGL_ALWAYS_SOFTWARE=1
export GALLIUM_DRIVER=llvmpipe
# GBM_ALWAYS_SOFTWARE=1 — virtio-gpu-pci SIN 3D expone un nodo KMS-only. Sin esto gbm carga kms_swrast
# pero deja gbm_dri->software=FALSE y dri2_initialize_drm entra al bloque `if(!software)` llamando
# queryCompatibleRenderOnlyDeviceFd(), NULL en el driver software ⇒ SIGSEGV. Fue el MURO 2 de KDE.
export GBM_ALWAYS_SOFTWARE=1
# kms_swrast y NO swrast: el compositor abre card0 como KMS y crea un gbm device encima ⇒ necesita el
# driver software PARA KMS (dumb buffers). Con 'swrast' pelado gbm_create_device falla.
export MESA_LOADER_DRIVER_OVERRIDE=kms_swrast
export LIBGL_DRIVERS_PATH=/usr/lib/dri
# llvmpipe SINCRÓNICO: su JIT multi-thread segfaultea al componer la 1ra superficie en esta VM.
export LP_NUM_THREADS=1
# Análogo mutter de un fix de kwin: cursor por software (el plano de cursor por HW no se presenta en
# virtio-gpu con render software ⇒ cursor invisible).
export MUTTER_DEBUG_DISABLE_HW_CURSORS=1
# MUTTER_DEBUG_FORCE_KMS_MODE — perilla EXPERIMENTAL, apagada por defecto desde 2026-07-29.
#
# Estuvo en `simple` (modeset legacy en vez de atomic), copiado de la campaña KDE. Pero ahí el que
# fallaba con atomic era KWIN, no mutter: se heredó por analogía, no por medición. Y el síntoma que
# dejó es exactamente el que el modo legacy explica — la sesión sube entera, `gnome-shell` corre
# cinco minutos sin un solo error, y la PANTALLA SIGUE NEGRA con la consola del kernel encima. En el
# log de KMS: `Plane has no advertised formats` para el plano primario de virtio-gpu (sin 3D no
# publica IN_FORMATS) y sólo `Queue mode set` — ningún page flip, ningún scanout.
#
# Con atomic, mutter negocia formatos y planos por la ruta moderna, que es la que sabe qué hacer
# cuando el driver no publica IN_FORMATS. Se deja la variable como perilla para volver a probar el
# camino legacy sin editar el script: `KMS_MODE=simple bash scripts/gnome/qemu-desktop-image.sh`,
# que lo hornea en /etc/gnome-mode (el getty no hereda ambiente, igual que GNOME_MODE).
[ -n "${KMS_MODE:-}" ] && export MUTTER_DEBUG_FORCE_KMS_MODE="$KMS_MODE"
# MUTTER_DEBUG_KMS_THREAD_TYPE=user: mutter corre su hebra KMS con prioridad de TIEMPO REAL por
# defecto. En esta VM no hay privilegios de RT scheduling, y el síntoma es exactamente el que dio el
# diagnóstico — hilo principal dormido en `futex_do_wait` con la "KMS thread" viva pero sin avanzar,
# el shell con /dev/dri/card0 ya abierto y ningún socket wayland creado nunca.
export MUTTER_DEBUG_KMS_THREAD_TYPE=user
# MUTTER_DEBUG_SEND_KMS_MODIFIERS=0: el equivalente en mutter del KWIN_DRM_USE_MODIFIERS=0 que costó
# la campaña KDE. virtio-gpu ANUNCIA soportar drmModeAddFB2WithModifiers pero RECHAZA el modifier
# explícito (aun LINEAR=0) con EINVAL ⇒ no se puede crear el framebuffer del compositor. Los nombres
# de estas variables están verificados contra los strings de libmutter-16.so.0, no de memoria.
export MUTTER_DEBUG_SEND_KMS_MODIFIERS=0
if [ "${DIAG:-1}" = 1 ]; then
export G_MESSAGES_DEBUG=all
# MUTTER_DEBUG es una LISTA DE TÓPICOS (g_parse_debug_string sobre meta_debug_keys, core/util.c:42),
# no un booleano: con `1` no matcheaba ninguna clave y no encendía nada. `backend` es el que
# imprime «Opening and taking control of device file '<path>'» por cada dispositivo que pasa por
# MetaDevicePool ⇒ dice EXACTAMENTE en cuál se queda parada la hebra de input.
export MUTTER_DEBUG=backend,input,kms
export EGL_LOG_LEVEL=debug
export LIBGL_DEBUG=verbose
fi
mkdir -p "$XDG_RUNTIME_DIR" "$XDG_CONFIG_HOME" "$XDG_CACHE_HOME" /run/dbus /var/lib/dbus /root
chmod 700 "$XDG_RUNTIME_DIR"
[ -s /etc/machine-id ] || cat /proc/sys/kernel/random/uuid 2>/dev/null | tr -d - > /etc/machine-id
cp -f /etc/machine-id /var/lib/dbus/machine-id 2>/dev/null || true
# ── ACTIVAR LAS EXTENSIONES QUE LA IMAGEN TRAE ────────────────────────────────────────────────
# Una extensión INSTALADA no hace nada: el shell sólo carga las que estén en
# `org.gnome.shell enabled-extensions`. Sin esto, empaquetarlas sella artefactos que la imagen
# lleva y nadie ejecuta — «sellado ≠ instalado», un escalón más abajo.
#
# ⚠ POR QUÉ LA LISTA SE ARMA ACÁ Y NO EN CADA RECETA. `enabled-extensions` es un ARRAY: un override
# lo escribe ENTERO. Si cada receta trajera el suyo, no se sumarían — ganaría el fichero que
# compile último (orden alfabético) y las demás quedarían instaladas y MUERTAS, sin un solo error.
# La lista tiene que existir en un único sitio, y una receta no puede ser ese sitio: hammer exige
# `[source]` en toda receta (comprobado: «missing field `source`»), así que no hay forma de escribir
# una receta de pura política sin inventarle una fuente. Acá, donde ya se compilan los esquemas, sí.
#
# La política es «lo que la imagen instala, se activa»: el catálogo sólo empaqueta extensiones que
# queremos encendidas. Quien no quiera una la apaga desde su dconf, que gana sobre el default.
EXT_DIR=/usr/share/gnome-shell/extensions
OVR="$GSETTINGS_SCHEMA_DIR/60_hammer-extensiones.gschema.override"
if [ -d "$EXT_DIR" ]; then
uuids=""
for d in "$EXT_DIR"/*/; do
[ -s "$d/metadata.json" ] || continue
u="$(basename "$d")"
uuids="$uuids${uuids:+, }'$u'"
done
if [ -n "$uuids" ]; then
nuevo_ovr="[org.gnome.shell]
enabled-extensions=[$uuids]"
# Si la lista cambió respecto de la compilada, hay que RECOMPILAR: el bloque de abajo sólo
# compila cuando falta `gschemas.compiled`, así que un override nuevo sobre una imagen ya
# arrancada quedaría ignorado en silencio.
if [ ! -f "$OVR" ] || [ "$(cat "$OVR")" != "$nuevo_ovr" ]; then
printf '%s\n' "$nuevo_ovr" > "$OVR"
rm -f "$GSETTINGS_SCHEMA_DIR/gschemas.compiled"
fi
say "extensiones a activar: $uuids"
else
say "no hay extensiones instaladas en $EXT_DIR"
fi
fi
# gschemas.compiled: meson NO lo genera cuando DESTDIR está seteado (lo dice en el log del build:
# "Skipping custom install script because DESTDIR is set"). Sin él, GSettings aborta al primer
# g_settings_new() y el shell no llega ni a abrir el DRM. Se compila acá, en el primer arranque.
if [ ! -f "$GSETTINGS_SCHEMA_DIR/gschemas.compiled" ]; then
say "compilando $(ls $GSETTINGS_SCHEMA_DIR/*.xml 2>/dev/null | wc -l) esquemas GSettings..."
glib-compile-schemas "$GSETTINGS_SCHEMA_DIR" 2>/tmp/schemas.log \
&& say "esquemas OK" || { say "!! glib-compile-schemas FALLÓ:"; dump /tmp/schemas.log; }
# SIEMPRE volcar el log, no sólo al fallar. `glib-compile-schemas` **sale con 0 aunque RECHACE un
# esquema**: imprime el motivo por stderr y sigue con los demás. Con el volcado condicionado al
# código de salida, un esquema descartado quedaba invisible detrás de un «esquemas OK» — y eso fue
# exactamente lo que escondió que los gschemas de g-s-d les faltaban los `<enum>`, mientras el
# shell moría con «schema ... not found» y el fichero estaba instalado. Perdí un ciclo por creerle
# al código de salida en vez de leer stderr.
if [ -s /tmp/schemas.log ]; then
say "!! glib-compile-schemas se QUEJÓ (aunque no falle, puede haber descartado esquemas):"
dump /tmp/schemas.log
fi
fi
say "GPU DRM: $(ls /dev/dri/ 2>/dev/null | tr '\n' ' ')"
[ -e /dev/dri/card0 ] || say "!! no hay /dev/dri/card0 — ¿virtio-gpu no cargó?"
# D-Bus de SISTEMA: mutter habla con logind por el bus de sistema. En esta distro login1 lo provee
# arje-logind-compat; si no está, mutter cae a su ruta sin sesión (que es justo lo que libelogind
# vino a cubrir del lado de la C-ABI). El bus tiene que existir igual.
if [ ! -S /run/dbus/system_bus_socket ]; then
if dbus-daemon --system --fork 2>/tmp/dbus-system.log; then
say "system bus ON"
else
say "!! system bus NO arrancó:"; dump /tmp/dbus-system.log
fi
fi
# D-Bus de sesión: gnome-shell EXIGE uno (registra org.gnome.Shell y org.gnome.Mutter.*).
if [ -z "${DBUS_SESSION_BUS_ADDRESS:-}" ]; then
DBUS_SESSION_BUS_ADDRESS=$(dbus-daemon --session --fork --print-address 2>/dev/null)
export DBUS_SESSION_BUS_ADDRESS
fi
say "dbus sesión: ${DBUS_SESSION_BUS_ADDRESS:-NO ARRANCO}"
# arje-logind-compat: el login1 del fractal. Va DESPUÉS del bus de sistema (registra
# org.freedesktop.login1 ahí) y ANTES del shell. Mutter no sólo lo consulta por D-Bus: pregunta por
# SU sesión vía la C-ABI sd-login, que libelogind resuelve LEYENDO /run/systemd/sessions. Sin este
# daemon esos ficheros no existen y mutter muere en "Failed to find any matching session".
if supervisado arje-logind-compat "logind-compat"; then :
elif [ -x /usr/bin/arje-logind-compat ]; then
# ARJE_LOGIN_STATE=1: la perilla que YA traía el daemon y que nadie prendía. Su comentario en
# arje-compat lo dice explícito — "en un host CON systemd real pisaría su estado; en arje (sin
# systemd) el launcher de sesión lo prende. Default off" — y el launcher de sesión es ESTE script.
# Con ella escribe /run/systemd/{sessions,seats,users}, que es lo que libelogind lee.
export ARJE_LOGIN_STATE=1
/usr/bin/arje-logind-compat >/tmp/logind-compat.log 2>&1 &
i=0
while [ $i -lt 40 ]; do
[ -d /run/systemd/sessions ] && [ -n "$(ls /run/systemd/sessions 2>/dev/null)" ] && break
i=$((i+1)); sleep 0.25
done
say "logind-compat: sesiones=$(ls /run/systemd/sessions 2>/dev/null | tr '\n' ' ') seats=$(ls /run/systemd/seats 2>/dev/null | tr '\n' ' ')"
[ -n "$(ls /run/systemd/sessions 2>/dev/null)" ] || dump /tmp/logind-compat.log
else
say "!! sin /usr/bin/arje-logind-compat — mutter no va a hallar sesión"
fi
# ── PUENTE EXPLÍCITO: el estado de sesión que arje-logind-compat todavía no escribe ─────────────
# ESTO ES ANDAMIO, NO LA SOLUCIÓN. arje-logind-compat adquiere org.freedesktop.login1 y sirve el
# Manager por D-Bus, pero NO deja nada en /run/systemd/{sessions,seats,users} — su handshake con el
# bus de arje falla ("Announce respuesta inesperada: identity mismatch"), así que nunca hay una
# sesión registrada por el fractal. Y mutter no pregunta por D-Bus: usa la C-ABI sd-login, que
# libelogind (arje-sdlogin-compat) resuelve LEYENDO esos ficheros. De ahí "Failed to find any
# matching session" aun con login1 arriba.
#
# Escribimos el estado a mano, en el formato que libelogind lee (claves sacadas del propio .so:
# LEADER/SEAT/CLASS/TYPE/ACTIVE/STATE/VTNR y CAN_GRAPHICAL del seat). Sirve para PROBAR que el resto
# de la pila funciona; el arreglo de verdad es que arje-logind-compat registre la sesión del fractal
# —o que el Announce deje de fallar— y escriba esto solo. Queda marcado para no confundir andamio
# con producto.
if [ -z "$(ls /run/systemd/sessions 2>/dev/null)" ]; then
say "puente: escribiendo estado de sesión a mano (arje-logind-compat no lo hizo)"
mkdir -p /run/systemd/sessions /run/systemd/seats /run/systemd/users
cat > /run/systemd/sessions/1 <<EOF
UID=0
NAME=root
LEADER=$$
SEAT=seat0
TYPE=wayland
CLASS=user
ACTIVE=1
STATE=active
VTNR=1
REMOTE=0
EOF
cat > /run/systemd/seats/seat0 <<EOF
CAN_GRAPHICAL=1
CAN_MULTI_SESSION=1
ACTIVE=1
ACTIVE_SESSIONS=1
SESSIONS=1
EOF
cat > /run/systemd/users/0 <<EOF
NAME=root
STATE=active
SESSIONS=1
ACTIVE_SESSIONS=1
EOF
fi
# XDG_SESSION_ID/SEAT/VTNR: libelogind los menciona entre sus símbolos, y son además el contrato
# estándar por el que un proceso sabe cuál es SU sesión sin consultar por PID.
export XDG_SESSION_ID=1
export XDG_SEAT=seat0
export XDG_VTNR=1
# ── ESPERAR EL NOMBRE, NO EL PROCESO ────────────────────────────────────────────────────────────
# El paso que faltaba, y es la otra mitad de «un pid no es un servicio»: comprobar que el daemon
# ADQUIRIÓ su nombre en el bus. Se le da tiempo y, si no aparece, se vuelca su log y la lista de
# nombres del bus — que es el dato que dice si el problema es «no arrancó», «arrancó y no pidió el
# nombre» o «lo pidió y se lo negaron».
# $3 = décimas de segundo a esperar (default 20 = 10 s). colord necesita más: en el PRIMER arranque
# crea sus tres bases de datos en /var/lib/colord y escanea los directorios de perfiles ICC antes de
# pedir el nombre, y con 10 s daba «NO apareció» sin estar colgado — o sea un falso negativo.
#
# Se define ACÁ ARRIBA —antes del primer daemon— y no al final junto a las llamadas, porque el bloque
# de polkit de abajo la necesita para poder ESPERAR antes de lanzar a sus clientes.
esperar_nombre() {
n="$1"; log="$2"; lim="${3:-20}"; i=0
while [ $i -lt "$lim" ]; do
if dbus-send --system --dest=org.freedesktop.DBus --print-reply \
/org/freedesktop/DBus org.freedesktop.DBus.NameHasOwner "string:$n" 2>/dev/null \
| grep -q "boolean true"; then
say "bus: $n OK"
return 0
fi
i=$((i+1)); sleep 0.5
done
say "!! bus: $n NO apareció en $((lim/2))s"
[ -n "$log" ] && [ -s "$log" ] && dump "$log"
say "nombres en el bus de sistema:"
dbus-send --system --dest=org.freedesktop.DBus --print-reply \
/org/freedesktop/DBus org.freedesktop.DBus.ListNames 2>/dev/null \
| tr -d '" ' | grep -v '^\s*$' > /tmp/busnames.log
dump /tmp/busnames.log
return 1
}
# ── ¿YA LO SUPERVISA ARJE? (SDD 30 §4c) ─────────────────────────────────────────────────────────
# Desde que los demonios de sistema son Cards del `genesis`, PID 1 ya los encarnó antes de que este
# script corra. Relanzarlos daría DOS procesos peleando por el mismo nombre del bus, y el segundo
# fallaría con un error que no nombra la causa.
#
# El guardia se hace por «¿está corriendo?» y NO por una perilla de la imagen, a propósito: así es
# correcto venga de donde venga el proceso —arje, otro script, una corrida anterior— y esta misma
# copia sigue sirviendo en una imagen donde las Cards no se inyectaron. Una condición que consulta
# el estado real no puede desincronizarse de él; un flag sí.
ya_corre() { # $1 = nombre del ejecutable tal como lo ve el kernel
pidof "$1" >/dev/null 2>&1
}
supervisado() { # $1 = ejecutable, $2 = etiqueta para el log
if ya_corre "$1"; then say "$2 ya vive (pid $(pidof "$1" | cut -d' ' -f1)) — lo supervisa arje, no lo relanzo"; return 0; fi
return 1
}
# arje-polkit-compat: el PolicyKit1 del fractal. **VA ANTES que accounts-daemon y upowerd**, y el
# orden no es estético: los dos se bloquean en `polkit_authority_get_sync()` al arrancar, así que si
# `org.freedesktop.PolicyKit1` no está en el bus se quedan conectados pero SIN adquirir su nombre —y
# gnome-shell después espera 25 s por cada uno—. Nuestra receta `polkit` es libs-only a propósito
# (el demonio lo pone arje). Ojo: el shim autoriza TODO; ver recipes/arje-polkit-compat.toml.
#
# ⚠ Y «antes» tiene que ser POR EL NOMBRE, no por el orden de las líneas. Lanzarlo primero no alcanza:
# se lanza en background y tarda ~2 s en adquirir `org.freedesktop.PolicyKit1`, mientras accounts-daemon
# y upowerd arrancan de inmediato y llaman a `polkit_authority_get_sync()` en ese hueco. Entonces D-Bus
# intenta ACTIVAR el servicio (nadie tiene el nombre todavía) y falla:
# Error calling StartServiceByName for org.freedesktop.PolicyKit1:
# Failed to execute program org.freedesktop.PolicyKit1: Permission denied
# y los dos daemons se MUEREN («Failed to initialize daemon»). Después el shim adquiere el nombre
# tranquilo y el chequeo del final dice `PolicyKit1 OK` — o sea que el resumen final da verde y los
# clientes ya están muertos. La carrera es real y la ganaba cualquiera: es la misma lección de «un pid
# no es un servicio», aplicada al productor en vez de al consumidor.
if supervisado arje-polkit-compat "polkit-compat"; then :
elif [ -x /usr/bin/arje-polkit-compat ]; then
/usr/bin/arje-polkit-compat >/tmp/polkit-compat.log 2>&1 &
say "arje-polkit-compat lanzado (pid $!)"
esperar_nombre org.freedesktop.PolicyKit1 /tmp/polkit-compat.log \
|| say "!! sigo igual: accounts-daemon y upowerd van a fallar al pedir la autoridad"
else
say "!! sin arje-polkit-compat — accounts-daemon y upowerd se van a colgar pidiendo autorización"
fi
# accounts-daemon: el servicio D-Bus de cuentas (org.freedesktop.Accounts). El shell lo consulta
# desde JavaScript vía `imports.gi.AccountsService` para el nombre y el avatar del usuario del menú.
#
# Se lanza EXPLÍCITO aunque el artefacto instale su `.service` de activación: la activación por bus
# de sistema pasa por `dbus-daemon-launch-helper`, que en una distro normal es setuid root y acá no
# lo es. Corriendo todo como root la activación probablemente funcionaría igual, pero depender de eso
# es depender de un accidente; lanzarlo a mano es una línea y no deja ambigüedad. Si además llegara a
# activarse por bus, el segundo simplemente no adquiere el nombre y se va: no hay daño.
if supervisado accounts-daemon "accounts-daemon"; then :
elif [ -x /usr/libexec/accounts-daemon ]; then
/usr/libexec/accounts-daemon >/tmp/accounts-daemon.log 2>&1 &
say "accounts-daemon lanzado (pid $!)"
else
say "!! sin /usr/libexec/accounts-daemon — el menú de usuario del shell va a fallar"
fi
# upowerd: el servicio de energía. Mismo caso que accounts-daemon —el shell lo consulta por D-Bus
# para el indicador de batería— y el mismo motivo para lanzarlo a mano: sin esto la activación por bus
# se queda esperando y el log lo dice sin ambigüedad,
# Error calling StartServiceByName for org.freedesktop.UPower:
# Failed to activate service ... timed out (service_start_timeout=25000ms)
# o sea VEINTICINCO SEGUNDOS de arranque tirados esperando algo que nunca iba a venir.
# Los directorios de estado los deriva upower de --prefix (historydir/statedir vacíos ⇒
# $prefix/var/lib/upower); si no existen, arranca y se muere sin ruido.
mkdir -p /usr/var/lib/upower /var/lib/upower
if supervisado upowerd "upowerd"; then :
elif [ -x /usr/libexec/upowerd ]; then
/usr/libexec/upowerd >/tmp/upowerd.log 2>&1 &
UPOWERD_PID=$!
say "upowerd lanzado (pid $UPOWERD_PID)"
# Chequeo real, no «lo lancé y confío»: la primera versión reportaba «lanzado» y el shell seguía
# esperando 25s por org.freedesktop.UPower. Un pid no es un servicio.
sleep 1
if alive $UPOWERD_PID; then
say "upowerd sigue vivo"
else
say "!! upowerd MURIÓ al arrancar:"; dump /tmp/upowerd.log
fi
else
say "!! sin upowerd — el shell va a esperar 25s a su activación D-Bus y seguir sin batería"
fi
# ── AUDIO: PipeWire (decisión del usuario, 2026-07-29) ──────────────────────────────────────────
# Van TRES procesos y el orden importa, porque cada uno es cliente del anterior:
# 1. `pipewire` — el servidor. Crea $XDG_RUNTIME_DIR/pipewire-0.
# 2. `pipewire-pulse` — el puente que habla el protocolo de PulseAudio contra él. Es lo que
# gnome-shell necesita (gvc usa libpulse), y sin (1) no tiene con quién hablar.
# 3. `wireplumber` — el gestor de sesión: enumera dispositivos y aplica la política. Sin él (1) y
# (2) funcionan pero el único sink es `auto_null`, el nodo de descarte.
# Los tres son servicios de USUARIO: sus sockets viven en $XDG_RUNTIME_DIR, creado más arriba.
#
# ⚠ NO duplicar el bloque de wireplumber. Estuvo dos veces por un merge descuidado de este script y el
# resultado fue DOS wireplumber vivos peleándose el grafo — visible en `wpctl status` como dos clientes
# «WirePlumber» con pids distintos. El síntoma no es un error: es una lista de clientes que se lee
# normal si no se cuenta.
# ANTES de arrancar nada: ¿hay hardware que enumerar? Es la pregunta que separa «el stack de audio
# falla» de «no hay tarjeta», y las dos se ven igual en un `wpctl status` con `Devices:` vacío. El
# cmdline del kernel lleva `quiet loglevel=3`, así que los mensajes de ALSA NO salen por el serial —
# mirar /dev y /sys es la única forma directa.
say "audio: /dev/snd = $(ls /dev/snd 2>/dev/null | tr "\n" " ")"
say "audio: /sys/class/sound = $(ls /sys/class/sound 2>/dev/null | tr '\n' ' ')"
say "audio: HDA en el bus PCI = $(grep -ci 'audio' /proc/bus/pci/devices 2>/dev/null || echo '?')"
if [ -x /usr/bin/pipewire ]; then
/usr/bin/pipewire >/tmp/pipewire.log 2>&1 &
say "pipewire lanzado (pid $!)"
i=0; while [ $i -lt 20 ] && [ ! -S "$XDG_RUNTIME_DIR/pipewire-0" ]; do i=$((i+1)); sleep 0.25; done
if [ -S "$XDG_RUNTIME_DIR/pipewire-0" ]; then
say "socket pipewire-0 OK"
/usr/bin/pipewire-pulse >/tmp/pipewire-pulse.log 2>&1 &
say "pipewire-pulse lanzado (pid $!)"
i=0; while [ $i -lt 20 ] && [ ! -S "$XDG_RUNTIME_DIR/pulse/native" ]; do i=$((i+1)); sleep 0.25; done
if [ -S "$XDG_RUNTIME_DIR/pulse/native" ]; then
say "socket pulse/native OK — gvc va a poder conectar"
else
say "!! pipewire-pulse no creó pulse/native:"; dump /tmp/pipewire-pulse.log
fi
# wireplumber: el GESTOR DE SESIÓN. Va tercero porque necesita que el servidor ya esté; es el que
# convierte «PipeWire acepta clientes» en «PipeWire tiene dispositivos»: enumera por udev/alsa y
# aplica la política (qué es entrada, qué es salida, qué se enlaza con qué), y esa política está
# escrita en Lua. Sin él el único sink es `auto_null`, el nodo de descarte.
if [ -x /usr/bin/wireplumber ]; then
# WIREPLUMBER_DEBUG=3: sin esto su log no dice POR QUÉ descarta una tarjeta, y «Devices: vacío»
# es indistinguible de «no hay hardware». Con el nivel de debug puesto, el monitor de ALSA
# imprime cada device que ve y el motivo del descarte.
WIREPLUMBER_DEBUG=3 /usr/bin/wireplumber >/tmp/wireplumber.log 2>&1 &
WP_PID=$!
say "wireplumber lanzado (pid $WP_PID)"
sleep 2
if alive $WP_PID; then
say "wireplumber sigue vivo"
# El dato que importa no es el pid sino QUÉ VE. `wpctl status` lista los nodos que la política
# creó: si sale vacío, wireplumber corre pero no enumeró nada.
wpctl status >/tmp/wpctl.log 2>&1 || true
dump /tmp/wpctl.log
# Y su propio log, SIEMPRE — no sólo si muere. Un wireplumber vivo que no enumera nada es el
# caso interesante, y es justo el que quedaba sin evidencia. Se filtran las líneas de alsa/udev,
# que son las que dicen qué vio y qué descartó.
# Filtro ESTRECHO: `alsa` y nada más. La primera versión filtraba
# `alsa|udev|sound|card|monitor` y el `tail -40` se llenó de bluez/v4l2/libcamera —tres
# monitores que avisan que su plugin no está, ruido garantizado— dejando fuera justo las líneas
# de ALSA. Un filtro ancho con cola corta es peor que ninguno.
say "wireplumber :: ALSA"
grep -iE "alsa" /tmp/wireplumber.log 2>/dev/null | head -40 > /tmp/wp-alsa.log
dump /tmp/wp-alsa.log
# Y el log COMPLETO a la raíz ext4, que sí se sincroniza: /tmp es tmpfs y muere con la VM.
# Se saca con `debugfs -R "dump /wireplumber.log ..."`, igual que el core.
cp /tmp/wireplumber.log /wireplumber.log 2>/dev/null || true
sync
else
say "!! wireplumber MURIÓ:"; dump /tmp/wireplumber.log
fi
else
say "!! sin wireplumber — PipeWire acepta clientes pero no habrá dispositivos"
fi
else
say "!! pipewire no creó su socket:"; dump /tmp/pipewire.log
fi
else
say "!! sin /usr/bin/pipewire — el shell va a avisar 'Failed to connect context'"
fi
# colord: gestión de color (curva ICC por monitor). Mutter lo pide al arrancar y, si no está, se come
# 25 s de timeout de activación D-Bus. Se lanza explícito por el mismo motivo que accounts-daemon y
# upowerd: la activación por bus funciona (el launch-helper ya es setuid) pero deja el arranque
# esperando, y lanzarlo a mano además deja un log donde mirar si no adquiere el nombre.
if supervisado colord "colord"; then :
elif [ -x /usr/libexec/colord ]; then
# --verbose: su log normal se corta después de abrir las tres bases de datos y no dice nada más, así
# que sin esto un colord que no adquiere el nombre es indistinguible de uno que se cuelga.
CD_VERBOSE=1 /usr/libexec/colord --verbose >/tmp/colord.log 2>&1 &
CD_PID=$!
say "colord lanzado (pid $CD_PID)"
# ⚠ ESTA COMPROBACIÓN FALTABA, y es la misma que yo mismo escribí para upowerd: **un pid no es un
# servicio**. Sin ella, «colord lanzado (pid N)» + «bus: ColorManager NO apareció» no distingue
# MURIÓ de SE COLGÓ, que son dos investigaciones distintas. Es el hueco que dejó el diagnóstico de
# colord a medias.
sleep 2
if alive $CD_PID; then say "colord sigue vivo"; else say "!! colord MURIÓ al arrancar"; fi
else
say "!! sin /usr/libexec/colord — mutter va a esperar 25s y seguir sin gestión de color"
fi
esperar_nombre org.freedesktop.login1 /tmp/logind-compat.log
# PolicyKit1 NO se chequea acá: ya se esperó arriba, antes de lanzar a sus clientes. Chequearlo sólo
# al final era precisamente lo que dejaba pasar la carrera con verde.
esperar_nombre org.freedesktop.Accounts /tmp/accounts-daemon.log
esperar_nombre org.freedesktop.UPower /tmp/upowerd.log
esperar_nombre org.freedesktop.ColorManager /tmp/colord.log 80 # 40 s: ver el comentario
# El log de colord SIEMPRE, no sólo si falla el nombre: un daemon vivo que no adquiere su nombre es el
# caso interesante y es justo el que se quedaba sin evidencia.
say "colord :: log completo"
dump /tmp/colord.log
if [ "${DIAG:-1}" = 1 ]; then
ulimit -c unlimited 2>/dev/null || true
echo '/core.%e.%p' > /proc/sys/kernel/core_pattern 2>/dev/null || true
fi
say "typelibs visibles: $(ls /usr/lib/girepository-1.0/*.typelib 2>/dev/null | wc -l) sistema + $(ls /usr/lib/gnome-shell/*.typelib 2>/dev/null | wc -l) del shell"
# ── SONDA DE INPUT — barata, y separa las dos hipótesis del muro ────────────────────────────────
# El muro es la hebra de input de mutter: arranca y nunca señala `input_thread_initialized`.
# `libinput list-devices` hace EXACTAMENTE lo que hace `init_libinput()` —udev_new +
# libinput_udev_create_context + libinput_udev_assign_seat("seat0")— pero con `open()/close()`
# pelados en vez del `TakeDevice` de logind. O sea que discrimina:
# · la sonda cuelga o no lista nada ⇒ el muro es libudev-zero/libinput (enumeración por /sys)
# · la sonda lista los dispositivos ⇒ libinput anda; el cuelgue está del lado de mutter
# (MetaDevicePool → TakeDevice, o algo anterior en el hilo: input settings, keymap)
# Ojo: `libinput` es un multiplexor de subcomandos y `list-devices` NO es interactivo, pero igual se
# le pone reloj — si el problema es un cuelgue, la sonda cuelga también, y hay que matarla.
say "sonda: /sys/class/input = $(ls /sys/class/input 2>/dev/null | tr '\n' ' ')"
say "sonda: /dev/input = $(ls /dev/input 2>/dev/null | tr '\n' ' ')"
if [ -x /usr/bin/libinput ]; then
( libinput list-devices >/tmp/libinput-devices.log 2>&1; echo "rc=$?" >>/tmp/libinput-devices.log ) &
LI_PID=$!
i=0
while [ $i -lt 24 ] && alive $LI_PID; do i=$((i+1)); sleep 0.5; done
if alive $LI_PID; then
say "!! SONDA COLGADA: 'libinput list-devices' no volvió en 12s ⇒ el muro ES libinput/libudev-zero"
say " wchan=$(cat /proc/$LI_PID/wchan 2>/dev/null) syscall=$(cat /proc/$LI_PID/syscall 2>/dev/null)"
kill -9 $LI_PID 2>/dev/null
else
say "sonda 'libinput list-devices' VOLVIÓ (no cuelga) — dispositivos:"
dump /tmp/libinput-devices.log
fi
else
say "sonda: no hay /usr/bin/libinput en la imagen"
fi
# GNOME_MODE=headless — EXPERIMENTO DECISIVO, no un modo de producción. El backend headless de
# mutter crea el seat con META_SEAT_NATIVE_FLAG_NO_LIBINPUT, o sea que SALTEA `init_libinput()`,
# que es justo el último paso del hilo de input antes de señalar `input_thread_initialized`
# (meta-seat-impl.c:3098). Si en headless aparece wayland-0, el bloqueo está en libinput/udev y no
# en el resto del arranque; si tampoco aparece, el problema es anterior (keymap, input settings).
case "${GNOME_MODE:-drm}" in
headless) SHELL_ARGS="--headless --virtual-monitor 1280x800" ;;
*) SHELL_ARGS="--wayland" ;;
esac
say "lanzando gnome-shell $SHELL_ARGS (modo=${GNOME_MODE:-drm})..."
/usr/bin/gnome-shell $SHELL_ARGS >/tmp/gnome-shell.log 2>&1 &
SHELL_PID=$!
# stream EN VIVO al serial (busybox sed no soporta -u ⇒ tail -f directo, sin pipes).
( echo "==== gnome-shell.log (stream vivo) ===="; tail -f /tmp/gnome-shell.log ) > /dev/ttyS0 2>/dev/null &
i=0
while [ $i -lt 240 ]; do
[ -S "$XDG_RUNTIME_DIR/wayland-0" ] && break
alive $SHELL_PID || { wait $SHELL_PID; RC=$?; say "!! gnome-shell murió (código $RC) antes de exponer wayland-0:"; dump /tmp/gnome-shell.log; say "cores: $(ls /core.* 2>/dev/null | tr '\n' ' ' || echo NINGUNO)"; exec /bin/sh; }
i=$((i+1)); sleep 0.5
done
if [ -S "$XDG_RUNTIME_DIR/wayland-0" ]; then
say "compositor OK (wayland-0) — el shell ES el display server"
else
say "!! sin wayland-0 tras 120s:"; dump /tmp/gnome-shell.log
# El shell NO murió: quedó COLGADO. Sin gdb en la imagen, el diagnóstico barato es dónde está
# bloqueado el hilo principal (wchan) y contra qué socket, que es lo que distingue "esperando una
# respuesta D-Bus que no llega" de "spinning en el renderer".
say "--- diagnóstico del cuelgue (pid $SHELL_PID) ---"
say "XDG_RUNTIME_DIR ($XDG_RUNTIME_DIR): $(ls -a $XDG_RUNTIME_DIR 2>/dev/null | tr '\n' ' ')"
say "sockets wayland en el sistema: $(find / -name 'wayland-*' -maxdepth 6 2>/dev/null | tr '\n' ' ')"
say "wchan: $(cat /proc/$SHELL_PID/wchan 2>/dev/null)"
say "state: $(grep -a ^State /proc/$SHELL_PID/status 2>/dev/null)"
say "stack principal:"; cat /proc/$SHELL_PID/stack > /dev/ttyS0 2>/dev/null
say "syscall: $(cat /proc/$SHELL_PID/syscall 2>/dev/null)"
say "fds abiertos:"; ls -l /proc/$SHELL_PID/fd 2>/dev/null > /dev/ttyS0
say "hilos: $(ls /proc/$SHELL_PID/task 2>/dev/null | wc -l)"
# POR HILO, y ANTES del SIGABRT. La primera versión hacía este bucle DESPUÉS de abortar, o sea
# sobre un /proc/<pid>/task que ya no existía: salía vacío y no dijo nada. Es el dato más barato
# que hay para ubicar la «Mutter Input Thread», porque el core no sirve: gdb no puede desenrollar
# a través de musl (sin CFI) y devuelve `?? ()` para los 11 hilos que no son el principal.
say "--- por hilo (comm / wchan / syscall) ---"
for t in $(ls /proc/$SHELL_PID/task 2>/dev/null); do
say " tid $t [$(cat /proc/$SHELL_PID/task/$t/comm 2>/dev/null)] wchan=$(cat /proc/$SHELL_PID/task/$t/wchan 2>/dev/null) syscall=$(cut -d' ' -f1 /proc/$SHELL_PID/task/$t/syscall 2>/dev/null)"
done
# ¿Sigue vivo y respondiendo el login1? Si el shell está esperando una respuesta D-Bus que no
# llega, esto lo separa en dos: daemon muerto/colgado vs daemon vivo que ya contestó.
say "arje-logind-compat: pids=$(pgrep -f arje-logind-compat 2>/dev/null | tr '\n' ' ')"
for p in $(pgrep -f arje-logind-compat 2>/dev/null); do
say " logind pid $p state=$(sed -n 's/^State:[[:space:]]*//p' /proc/$p/status 2>/dev/null) wchan=$(cat /proc/$p/wchan 2>/dev/null)"
done
( dbus-send --system --print-reply --dest=org.freedesktop.login1 \
/org/freedesktop/login1 org.freedesktop.DBus.Properties.Get \
string:org.freedesktop.login1.Manager string:NAutoVTs >/tmp/login1-ping.log 2>&1
echo "rc=$?" >>/tmp/login1-ping.log ) &
PING_PID=$!
i=0; while [ $i -lt 16 ] && alive $PING_PID; do i=$((i+1)); sleep 0.5; done
if alive $PING_PID; then
say "!! login1 NO responde (ping D-Bus colgado 8s) ⇒ el daemon se quedó tildado"
kill -9 $PING_PID 2>/dev/null
else
say "login1 responde al ping D-Bus:"; dump /tmp/login1-ping.log
fi
# /tmp es tmpfs ⇒ el log del daemon se pierde con la VM. Copiarlo a la raíz ext4, que sí se
# sincroniza y se puede sacar con debugfs junto al core.
cp /tmp/logind-compat.log /logind-compat.log 2>/dev/null || true
cp /tmp/gnome-shell.log /gnome-shell.log 2>/dev/null || true
# Sin gdb en la imagen, la única forma de ver DÓNDE está bloqueado es forzarle un core y leerlo
# con el gdb del host (`thread apply all bt`). SIGABRT lo produce y no lo maneja nadie.
say "forzando core del proceso colgado (SIGABRT)..."
kill -ABRT $SHELL_PID 2>/dev/null
sleep 12; sync; sync
say "cores: $(ls -la /core.* 2>/dev/null | tr '\n' ' ' || echo NINGUNO)"
exec /bin/sh
fi
export WAYLAND_DISPLAY=wayland-0
( n=0; while sleep 15; do
n=$((n+1))
say "STATUS +$((n*15))s: gnome-shell=$(pgrep gnome-shell 2>/dev/null | wc -l)"
done ) &
wait $SHELL_PID
RC=$?
say "gnome-shell salió (código $RC) — volcando log:"
dump /tmp/gnome-shell.log
cp /tmp/gnome-shell.log /gnome-shell.log 2>/dev/null || true
say "cores en /: $(ls -la /core.* 2>/dev/null | tr '\n' ' ' || echo NINGUNO)"
sync; sync
say "== disco SYNCED. serial CONGELADO. matá QEMU desde el host. =="
sleep 3600