gioser

sergio/takana

> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…

TOML 188Rust 55Shell 30Markdown 281211 Sutichaykuna

⎇ 534014c3ec69daea789a1d0523e1d00a69c6ed65 scripts/gnome/gnome-start-qemu.sh Shell · 610 siq'ikuna · 35.9 KB Ñawpa willay ch'uya

SDD 30 4c: GNOME con los demonios supervisados por arje — y el control arregló colord 3d79cdf98 · Sergio ·

1#!/bin/sh
2# gnome-start-qemu.sh — arranca gnome-shell (Wayland) en una VM QEMU con render por SOFTWARE
3# (kms_swrast/llvmpipe) sobre el DRM de virtio-gpu. Análogo de plasma-start-qemu.sh, y hereda sus
4# lecciones caras: el diagnóstico va POR SERIAL porque la ventana gtk se congela si mirada reinicia
5# su Xwayland (QEMU sigue vivo por dentro). Ver [[kde-metal-qemu-desktop]].
6#
7# Se instala como /usr/bin/gnome-start y lo ejecuta DIRECTO el console-getty (no vía login shell).
8# Salir: matar gnome-shell. Logs: /tmp/gnome-shell.log + tee al serial.
9#
10# DIFERENCIA CLAVE CON PLASMA: acá NO hay dos procesos (compositor + shell). gnome-shell ES el
11# compositor — embebe mutter como librería (libmutter-16.so) y `--wayland` lo hace display server.
12# Por eso no hay un equivalente de "kwin arriba pero plasmashell murió": si el shell cae, cae todo.
13set -u
14# getty ejecuta este script DIRECTO ⇒ PATH viene vacío. Primero eso, antes de invocar nada.
15export PATH=/usr/bin:/bin:/usr/sbin:/sbin
16# El modo lo hornea la imagen (GNOME_MODE=... en scripts/gnome/qemu-desktop-image.sh).
17[ -r /etc/gnome-mode ] && . /etc/gnome-mode
18say() { echo "== gnome-qemu :: $*" | tee -a /dev/console /dev/ttyS0 2>/dev/null; }
19dump() { echo "---- $1 ----" > /dev/ttyS0 2>/dev/null; cat "$1" > /dev/ttyS0 2>/dev/null; }
20# OJO con el test de vivo: `kill -0` TIENE ÉXITO sobre un ZOMBI (el hijo existe como PID hasta que
21# el padre lo cosecha, y acá el padre es este script, que sólo hace wait al final). La primera
22# versión usaba kill -0 y por eso reportó "sin wayland-0 tras 45s" cuando en realidad gnome-shell ya
23# había muerto en el primer segundo: el diagnóstico decía "0 hilos" porque no quedaba proceso, sólo
24# la entrada zombi. Se mira el State de /proc en su lugar.
25alive() { s=$(sed -n 's/^State:[[:space:]]*//p' /proc/$1/status 2>/dev/null); case "$s" in ""|Z*) return 1;; *) return 0;; esac; }
26
27export LD_LIBRARY_PATH=/usr/lib:/lib:/usr/lib/gnome-shell:/usr/lib/mutter-16
28# GI_TYPELIB_PATH es EL env crítico de esta sesión, y el que no tiene análogo en KDE: gnome-shell es
29# JavaScript sobre gjs y carga TODO por typelib. Los del sistema van en /usr/lib/girepository-1.0,
30# pero St/Shell/Gvc/Shew se instalan APARTE en /usr/lib/gnome-shell (son privados del shell). Sin
31# esta ruta, gjs no encuentra imports.gi.St y el shell muere en el primer require.
32export GI_TYPELIB_PATH=/usr/lib/girepository-1.0:/usr/lib/gnome-shell:/usr/lib/gnome-shell/girepository-1.0
33export XDG_DATA_DIRS=/usr/share
34export XDG_RUNTIME_DIR=/run/user/0
35export XDG_CONFIG_HOME=/root/.config
36export XDG_CACHE_HOME=/root/.cache
37export XDG_SESSION_TYPE=wayland
38export HOME=/root
39export GSETTINGS_SCHEMA_DIR=/usr/share/glib-2.0/schemas
40export LANG=C.UTF-8
41# XCURSOR_PATH/XCURSOR_THEME: sin tema de cursor mutter avisa «No cursor theme available» y —con el
42# cursor por software, que en virtio-gpu es obligatorio— el puntero se mueve INVISIBLE. Lo provee
43# `adwaita-icon-theme`, y el nombre del tema tiene que coincidir con el directorio bajo
44# /usr/share/icons. El `org.gnome.desktop.interface cursor-theme` de GSettings dice lo mismo, pero
45# estas variables las lee también libXcursor/wayland-cursor sin pasar por GSettings.
46export XCURSOR_PATH=/usr/share/icons
47export XCURSOR_THEME=Adwaita
48export XCURSOR_SIZE=24
49
50# --- render por software sobre virtio-gpu (los mismos fixes que costaron la campaña KDE) ---
51export LIBGL_ALWAYS_SOFTWARE=1
52export GALLIUM_DRIVER=llvmpipe
53# GBM_ALWAYS_SOFTWARE=1 — virtio-gpu-pci SIN 3D expone un nodo KMS-only. Sin esto gbm carga kms_swrast
54# pero deja gbm_dri->software=FALSE y dri2_initialize_drm entra al bloque `if(!software)` llamando
55# queryCompatibleRenderOnlyDeviceFd(), NULL en el driver software ⇒ SIGSEGV. Fue el MURO 2 de KDE.
56export GBM_ALWAYS_SOFTWARE=1
57# kms_swrast y NO swrast: el compositor abre card0 como KMS y crea un gbm device encima ⇒ necesita el
58# driver software PARA KMS (dumb buffers). Con 'swrast' pelado gbm_create_device falla.
59export MESA_LOADER_DRIVER_OVERRIDE=kms_swrast
60export LIBGL_DRIVERS_PATH=/usr/lib/dri
61# llvmpipe SINCRÓNICO: su JIT multi-thread segfaultea al componer la 1ra superficie en esta VM.
62export LP_NUM_THREADS=1
63# Análogo mutter de un fix de kwin: cursor por software (el plano de cursor por HW no se presenta en
64# virtio-gpu con render software ⇒ cursor invisible).
65export MUTTER_DEBUG_DISABLE_HW_CURSORS=1
66# MUTTER_DEBUG_FORCE_KMS_MODE — perilla EXPERIMENTAL, apagada por defecto desde 2026-07-29.
67#
68# Estuvo en `simple` (modeset legacy en vez de atomic), copiado de la campaña KDE. Pero ahí el que
69# fallaba con atomic era KWIN, no mutter: se heredó por analogía, no por medición. Y el síntoma que
70# dejó es exactamente el que el modo legacy explica — la sesión sube entera, `gnome-shell` corre
71# cinco minutos sin un solo error, y la PANTALLA SIGUE NEGRA con la consola del kernel encima. En el
72# log de KMS: `Plane has no advertised formats` para el plano primario de virtio-gpu (sin 3D no
73# publica IN_FORMATS) y sólo `Queue mode set` — ningún page flip, ningún scanout.
74#
75# Con atomic, mutter negocia formatos y planos por la ruta moderna, que es la que sabe qué hacer
76# cuando el driver no publica IN_FORMATS. Se deja la variable como perilla para volver a probar el
77# camino legacy sin editar el script: `KMS_MODE=simple bash scripts/gnome/qemu-desktop-image.sh`,
78# que lo hornea en /etc/gnome-mode (el getty no hereda ambiente, igual que GNOME_MODE).
79[ -n "${KMS_MODE:-}" ] && export MUTTER_DEBUG_FORCE_KMS_MODE="$KMS_MODE"
80# MUTTER_DEBUG_KMS_THREAD_TYPE=user: mutter corre su hebra KMS con prioridad de TIEMPO REAL por
81# defecto. En esta VM no hay privilegios de RT scheduling, y el síntoma es exactamente el que dio el
82# diagnóstico — hilo principal dormido en `futex_do_wait` con la "KMS thread" viva pero sin avanzar,
83# el shell con /dev/dri/card0 ya abierto y ningún socket wayland creado nunca.
84export MUTTER_DEBUG_KMS_THREAD_TYPE=user
85# MUTTER_DEBUG_SEND_KMS_MODIFIERS=0: el equivalente en mutter del KWIN_DRM_USE_MODIFIERS=0 que costó
86# la campaña KDE. virtio-gpu ANUNCIA soportar drmModeAddFB2WithModifiers pero RECHAZA el modifier
87# explícito (aun LINEAR=0) con EINVAL ⇒ no se puede crear el framebuffer del compositor. Los nombres
88# de estas variables están verificados contra los strings de libmutter-16.so.0, no de memoria.
89export MUTTER_DEBUG_SEND_KMS_MODIFIERS=0
90
91if [ "${DIAG:-1}" = 1 ]; then
92  export G_MESSAGES_DEBUG=all
93  # MUTTER_DEBUG es una LISTA DE TÓPICOS (g_parse_debug_string sobre meta_debug_keys, core/util.c:42),
94  # no un booleano: con `1` no matcheaba ninguna clave y no encendía nada. `backend` es el que
95  # imprime «Opening and taking control of device file '<path>'» por cada dispositivo que pasa por
96  # MetaDevicePool ⇒ dice EXACTAMENTE en cuál se queda parada la hebra de input.
97  export MUTTER_DEBUG=backend,input,kms
98  export EGL_LOG_LEVEL=debug
99  export LIBGL_DEBUG=verbose
100fi
101
102mkdir -p "$XDG_RUNTIME_DIR" "$XDG_CONFIG_HOME" "$XDG_CACHE_HOME" /run/dbus /var/lib/dbus /root
103chmod 700 "$XDG_RUNTIME_DIR"
104[ -s /etc/machine-id ] || cat /proc/sys/kernel/random/uuid 2>/dev/null | tr -d - > /etc/machine-id
105cp -f /etc/machine-id /var/lib/dbus/machine-id 2>/dev/null || true
106
107# ── ACTIVAR LAS EXTENSIONES QUE LA IMAGEN TRAE ────────────────────────────────────────────────
108# Una extensión INSTALADA no hace nada: el shell sólo carga las que estén en
109# `org.gnome.shell enabled-extensions`. Sin esto, empaquetarlas sella artefactos que la imagen
110# lleva y nadie ejecuta — «sellado ≠ instalado», un escalón más abajo.
111#
112# ⚠ POR QUÉ LA LISTA SE ARMA ACÁ Y NO EN CADA RECETA. `enabled-extensions` es un ARRAY: un override
113# lo escribe ENTERO. Si cada receta trajera el suyo, no se sumarían — ganaría el fichero que
114# compile último (orden alfabético) y las demás quedarían instaladas y MUERTAS, sin un solo error.
115# La lista tiene que existir en un único sitio, y una receta no puede ser ese sitio: hammer exige
116# `[source]` en toda receta (comprobado: «missing field `source`»), así que no hay forma de escribir
117# una receta de pura política sin inventarle una fuente. Acá, donde ya se compilan los esquemas, sí.
118#
119# La política es «lo que la imagen instala, se activa»: el catálogo sólo empaqueta extensiones que
120# queremos encendidas. Quien no quiera una la apaga desde su dconf, que gana sobre el default.
121EXT_DIR=/usr/share/gnome-shell/extensions
122OVR="$GSETTINGS_SCHEMA_DIR/60_hammer-extensiones.gschema.override"
123if [ -d "$EXT_DIR" ]; then
124  uuids=""
125  for d in "$EXT_DIR"/*/; do
126    [ -s "$d/metadata.json" ] || continue
127    u="$(basename "$d")"
128    uuids="$uuids${uuids:+, }'$u'"
129  done
130  if [ -n "$uuids" ]; then
131    nuevo_ovr="[org.gnome.shell]
132enabled-extensions=[$uuids]"
133    # Si la lista cambió respecto de la compilada, hay que RECOMPILAR: el bloque de abajo sólo
134    # compila cuando falta `gschemas.compiled`, así que un override nuevo sobre una imagen ya
135    # arrancada quedaría ignorado en silencio.
136    if [ ! -f "$OVR" ] || [ "$(cat "$OVR")" != "$nuevo_ovr" ]; then
137      printf '%s\n' "$nuevo_ovr" > "$OVR"
138      rm -f "$GSETTINGS_SCHEMA_DIR/gschemas.compiled"
139    fi
140    say "extensiones a activar: $uuids"
141  else
142    say "no hay extensiones instaladas en $EXT_DIR"
143  fi
144fi
145
146# gschemas.compiled: meson NO lo genera cuando DESTDIR está seteado (lo dice en el log del build:
147# "Skipping custom install script because DESTDIR is set"). Sin él, GSettings aborta al primer
148# g_settings_new() y el shell no llega ni a abrir el DRM. Se compila acá, en el primer arranque.
149if [ ! -f "$GSETTINGS_SCHEMA_DIR/gschemas.compiled" ]; then
150  say "compilando $(ls $GSETTINGS_SCHEMA_DIR/*.xml 2>/dev/null | wc -l) esquemas GSettings..."
151  glib-compile-schemas "$GSETTINGS_SCHEMA_DIR" 2>/tmp/schemas.log \
152    && say "esquemas OK" || { say "!! glib-compile-schemas FALLÓ:"; dump /tmp/schemas.log; }
153  # SIEMPRE volcar el log, no sólo al fallar. `glib-compile-schemas` **sale con 0 aunque RECHACE un
154  # esquema**: imprime el motivo por stderr y sigue con los demás. Con el volcado condicionado al
155  # código de salida, un esquema descartado quedaba invisible detrás de un «esquemas OK» — y eso fue
156  # exactamente lo que escondió que los gschemas de g-s-d les faltaban los `<enum>`, mientras el
157  # shell moría con «schema ... not found» y el fichero estaba instalado. Perdí un ciclo por creerle
158  # al código de salida en vez de leer stderr.
159  if [ -s /tmp/schemas.log ]; then
160    say "!! glib-compile-schemas se QUEJÓ (aunque no falle, puede haber descartado esquemas):"
161    dump /tmp/schemas.log
162  fi
163fi
164
165say "GPU DRM: $(ls /dev/dri/ 2>/dev/null | tr '\n' ' ')"
166[ -e /dev/dri/card0 ] || say "!! no hay /dev/dri/card0 — ¿virtio-gpu no cargó?"
167
168# D-Bus de SISTEMA: mutter habla con logind por el bus de sistema. En esta distro login1 lo provee
169# arje-logind-compat; si no está, mutter cae a su ruta sin sesión (que es justo lo que libelogind
170# vino a cubrir del lado de la C-ABI). El bus tiene que existir igual.
171if [ ! -S /run/dbus/system_bus_socket ]; then
172  if dbus-daemon --system --fork 2>/tmp/dbus-system.log; then
173    say "system bus ON"
174  else
175    say "!! system bus NO arrancó:"; dump /tmp/dbus-system.log
176  fi
177fi
178
179# D-Bus de sesión: gnome-shell EXIGE uno (registra org.gnome.Shell y org.gnome.Mutter.*).
180if [ -z "${DBUS_SESSION_BUS_ADDRESS:-}" ]; then
181  DBUS_SESSION_BUS_ADDRESS=$(dbus-daemon --session --fork --print-address 2>/dev/null)
182  export DBUS_SESSION_BUS_ADDRESS
183fi
184say "dbus sesión: ${DBUS_SESSION_BUS_ADDRESS:-NO ARRANCO}"
185
186# arje-logind-compat: el login1 del fractal. Va DESPUÉS del bus de sistema (registra
187# org.freedesktop.login1 ahí) y ANTES del shell. Mutter no sólo lo consulta por D-Bus: pregunta por
188# SU sesión vía la C-ABI sd-login, que libelogind resuelve LEYENDO /run/systemd/sessions. Sin este
189# daemon esos ficheros no existen y mutter muere en "Failed to find any matching session".
190if supervisado arje-logind-compat "logind-compat"; then :
191elif [ -x /usr/bin/arje-logind-compat ]; then
192  # ARJE_LOGIN_STATE=1: la perilla que YA traía el daemon y que nadie prendía. Su comentario en
193  # arje-compat lo dice explícito — "en un host CON systemd real pisaría su estado; en arje (sin
194  # systemd) el launcher de sesión lo prende. Default off" — y el launcher de sesión es ESTE script.
195  # Con ella escribe /run/systemd/{sessions,seats,users}, que es lo que libelogind lee.
196  export ARJE_LOGIN_STATE=1
197  /usr/bin/arje-logind-compat >/tmp/logind-compat.log 2>&1 &
198  i=0
199  while [ $i -lt 40 ]; do
200    [ -d /run/systemd/sessions ] && [ -n "$(ls /run/systemd/sessions 2>/dev/null)" ] && break
201    i=$((i+1)); sleep 0.25
202  done
203  say "logind-compat: sesiones=$(ls /run/systemd/sessions 2>/dev/null | tr '\n' ' ') seats=$(ls /run/systemd/seats 2>/dev/null | tr '\n' ' ')"
204  [ -n "$(ls /run/systemd/sessions 2>/dev/null)" ] || dump /tmp/logind-compat.log
205else
206  say "!! sin /usr/bin/arje-logind-compat — mutter no va a hallar sesión"
207fi
208
209# ── PUENTE EXPLÍCITO: el estado de sesión que arje-logind-compat todavía no escribe ─────────────
210# ESTO ES ANDAMIO, NO LA SOLUCIÓN. arje-logind-compat adquiere org.freedesktop.login1 y sirve el
211# Manager por D-Bus, pero NO deja nada en /run/systemd/{sessions,seats,users} — su handshake con el
212# bus de arje falla ("Announce respuesta inesperada: identity mismatch"), así que nunca hay una
213# sesión registrada por el fractal. Y mutter no pregunta por D-Bus: usa la C-ABI sd-login, que
214# libelogind (arje-sdlogin-compat) resuelve LEYENDO esos ficheros. De ahí "Failed to find any
215# matching session" aun con login1 arriba.
216#
217# Escribimos el estado a mano, en el formato que libelogind lee (claves sacadas del propio .so:
218# LEADER/SEAT/CLASS/TYPE/ACTIVE/STATE/VTNR y CAN_GRAPHICAL del seat). Sirve para PROBAR que el resto
219# de la pila funciona; el arreglo de verdad es que arje-logind-compat registre la sesión del fractal
220# —o que el Announce deje de fallar— y escriba esto solo. Queda marcado para no confundir andamio
221# con producto.
222if [ -z "$(ls /run/systemd/sessions 2>/dev/null)" ]; then
223  say "puente: escribiendo estado de sesión a mano (arje-logind-compat no lo hizo)"
224  mkdir -p /run/systemd/sessions /run/systemd/seats /run/systemd/users
225  cat > /run/systemd/sessions/1 <<EOF
226UID=0
227NAME=root
228LEADER=$$
229SEAT=seat0
230TYPE=wayland
231CLASS=user
232ACTIVE=1
233STATE=active
234VTNR=1
235REMOTE=0
236EOF
237  cat > /run/systemd/seats/seat0 <<EOF
238CAN_GRAPHICAL=1
239CAN_MULTI_SESSION=1
240ACTIVE=1
241ACTIVE_SESSIONS=1
242SESSIONS=1
243EOF
244  cat > /run/systemd/users/0 <<EOF
245NAME=root
246STATE=active
247SESSIONS=1
248ACTIVE_SESSIONS=1
249EOF
250fi
251# XDG_SESSION_ID/SEAT/VTNR: libelogind los menciona entre sus símbolos, y son además el contrato
252# estándar por el que un proceso sabe cuál es SU sesión sin consultar por PID.
253export XDG_SESSION_ID=1
254export XDG_SEAT=seat0
255export XDG_VTNR=1
256
257# ── ESPERAR EL NOMBRE, NO EL PROCESO ────────────────────────────────────────────────────────────
258# El paso que faltaba, y es la otra mitad de «un pid no es un servicio»: comprobar que el daemon
259# ADQUIRIÓ su nombre en el bus. Se le da tiempo y, si no aparece, se vuelca su log y la lista de
260# nombres del bus — que es el dato que dice si el problema es «no arrancó», «arrancó y no pidió el
261# nombre» o «lo pidió y se lo negaron».
262# $3 = décimas de segundo a esperar (default 20 = 10 s). colord necesita más: en el PRIMER arranque
263# crea sus tres bases de datos en /var/lib/colord y escanea los directorios de perfiles ICC antes de
264# pedir el nombre, y con 10 s daba «NO apareció» sin estar colgado — o sea un falso negativo.
265#
266# Se define ACÁ ARRIBA —antes del primer daemon— y no al final junto a las llamadas, porque el bloque
267# de polkit de abajo la necesita para poder ESPERAR antes de lanzar a sus clientes.
268esperar_nombre() {
269  n="$1"; log="$2"; lim="${3:-20}"; i=0
270  while [ $i -lt "$lim" ]; do
271    if dbus-send --system --dest=org.freedesktop.DBus --print-reply \
272         /org/freedesktop/DBus org.freedesktop.DBus.NameHasOwner "string:$n" 2>/dev/null \
273       | grep -q "boolean true"; then
274      say "bus: $n OK"
275      return 0
276    fi
277    i=$((i+1)); sleep 0.5
278  done
279  say "!! bus: $n NO apareció en $((lim/2))s"
280  [ -n "$log" ] && [ -s "$log" ] && dump "$log"
281  say "nombres en el bus de sistema:"
282  dbus-send --system --dest=org.freedesktop.DBus --print-reply \
283    /org/freedesktop/DBus org.freedesktop.DBus.ListNames 2>/dev/null \
284    | tr -d '" ' | grep -v '^\s*$' > /tmp/busnames.log
285  dump /tmp/busnames.log
286  return 1
287}
288
289# ── ¿YA LO SUPERVISA ARJE? (SDD 30 §4c) ─────────────────────────────────────────────────────────
290# Desde que los demonios de sistema son Cards del `genesis`, PID 1 ya los encarnó antes de que este
291# script corra. Relanzarlos daría DOS procesos peleando por el mismo nombre del bus, y el segundo
292# fallaría con un error que no nombra la causa.
293#
294# El guardia se hace por «¿está corriendo?» y NO por una perilla de la imagen, a propósito: así es
295# correcto venga de donde venga el proceso —arje, otro script, una corrida anterior— y esta misma
296# copia sigue sirviendo en una imagen donde las Cards no se inyectaron. Una condición que consulta
297# el estado real no puede desincronizarse de él; un flag sí.
298ya_corre() {  # $1 = nombre del ejecutable tal como lo ve el kernel
299  pidof "$1" >/dev/null 2>&1
300}
301supervisado() {  # $1 = ejecutable, $2 = etiqueta para el log
302  if ya_corre "$1"; then say "$2 ya vive (pid $(pidof "$1" | cut -d' ' -f1)) — lo supervisa arje, no lo relanzo"; return 0; fi
303  return 1
304}
305
306# arje-polkit-compat: el PolicyKit1 del fractal. **VA ANTES que accounts-daemon y upowerd**, y el
307# orden no es estético: los dos se bloquean en `polkit_authority_get_sync()` al arrancar, así que si
308# `org.freedesktop.PolicyKit1` no está en el bus se quedan conectados pero SIN adquirir su nombre —y
309# gnome-shell después espera 25 s por cada uno—. Nuestra receta `polkit` es libs-only a propósito
310# (el demonio lo pone arje). Ojo: el shim autoriza TODO; ver recipes/arje-polkit-compat.toml.
311#
312# ⚠ Y «antes» tiene que ser POR EL NOMBRE, no por el orden de las líneas. Lanzarlo primero no alcanza:
313# se lanza en background y tarda ~2 s en adquirir `org.freedesktop.PolicyKit1`, mientras accounts-daemon
314# y upowerd arrancan de inmediato y llaman a `polkit_authority_get_sync()` en ese hueco. Entonces D-Bus
315# intenta ACTIVAR el servicio (nadie tiene el nombre todavía) y falla:
316#   Error calling StartServiceByName for org.freedesktop.PolicyKit1:
317#     Failed to execute program org.freedesktop.PolicyKit1: Permission denied
318# y los dos daemons se MUEREN («Failed to initialize daemon»). Después el shim adquiere el nombre
319# tranquilo y el chequeo del final dice `PolicyKit1 OK` — o sea que el resumen final da verde y los
320# clientes ya están muertos. La carrera es real y la ganaba cualquiera: es la misma lección de «un pid
321# no es un servicio», aplicada al productor en vez de al consumidor.
322if supervisado arje-polkit-compat "polkit-compat"; then :
323elif [ -x /usr/bin/arje-polkit-compat ]; then
324  /usr/bin/arje-polkit-compat >/tmp/polkit-compat.log 2>&1 &
325  say "arje-polkit-compat lanzado (pid $!)"
326  esperar_nombre org.freedesktop.PolicyKit1 /tmp/polkit-compat.log \
327    || say "!! sigo igual: accounts-daemon y upowerd van a fallar al pedir la autoridad"
328else
329  say "!! sin arje-polkit-compat — accounts-daemon y upowerd se van a colgar pidiendo autorización"
330fi
331
332# accounts-daemon: el servicio D-Bus de cuentas (org.freedesktop.Accounts). El shell lo consulta
333# desde JavaScript vía `imports.gi.AccountsService` para el nombre y el avatar del usuario del menú.
334#
335# Se lanza EXPLÍCITO aunque el artefacto instale su `.service` de activación: la activación por bus
336# de sistema pasa por `dbus-daemon-launch-helper`, que en una distro normal es setuid root y acá no
337# lo es. Corriendo todo como root la activación probablemente funcionaría igual, pero depender de eso
338# es depender de un accidente; lanzarlo a mano es una línea y no deja ambigüedad. Si además llegara a
339# activarse por bus, el segundo simplemente no adquiere el nombre y se va: no hay daño.
340if supervisado accounts-daemon "accounts-daemon"; then :
341elif [ -x /usr/libexec/accounts-daemon ]; then
342  /usr/libexec/accounts-daemon >/tmp/accounts-daemon.log 2>&1 &
343  say "accounts-daemon lanzado (pid $!)"
344else
345  say "!! sin /usr/libexec/accounts-daemon — el menú de usuario del shell va a fallar"
346fi
347
348# upowerd: el servicio de energía. Mismo caso que accounts-daemon —el shell lo consulta por D-Bus
349# para el indicador de batería— y el mismo motivo para lanzarlo a mano: sin esto la activación por bus
350# se queda esperando y el log lo dice sin ambigüedad,
351#   Error calling StartServiceByName for org.freedesktop.UPower:
352#     Failed to activate service ... timed out (service_start_timeout=25000ms)
353# o sea VEINTICINCO SEGUNDOS de arranque tirados esperando algo que nunca iba a venir.
354# Los directorios de estado los deriva upower de --prefix (historydir/statedir vacíos ⇒
355# $prefix/var/lib/upower); si no existen, arranca y se muere sin ruido.
356mkdir -p /usr/var/lib/upower /var/lib/upower
357if supervisado upowerd "upowerd"; then :
358elif [ -x /usr/libexec/upowerd ]; then
359  /usr/libexec/upowerd >/tmp/upowerd.log 2>&1 &
360  UPOWERD_PID=$!
361  say "upowerd lanzado (pid $UPOWERD_PID)"
362  # Chequeo real, no «lo lancé y confío»: la primera versión reportaba «lanzado» y el shell seguía
363  # esperando 25s por org.freedesktop.UPower. Un pid no es un servicio.
364  sleep 1
365  if alive $UPOWERD_PID; then
366    say "upowerd sigue vivo"
367  else
368    say "!! upowerd MURIÓ al arrancar:"; dump /tmp/upowerd.log
369  fi
370else
371  say "!! sin upowerd — el shell va a esperar 25s a su activación D-Bus y seguir sin batería"
372fi
373
374# ── AUDIO: PipeWire (decisión del usuario, 2026-07-29) ──────────────────────────────────────────
375# Van TRES procesos y el orden importa, porque cada uno es cliente del anterior:
376#   1. `pipewire`        — el servidor. Crea $XDG_RUNTIME_DIR/pipewire-0.
377#   2. `pipewire-pulse`  — el puente que habla el protocolo de PulseAudio contra él. Es lo que
378#                          gnome-shell necesita (gvc usa libpulse), y sin (1) no tiene con quién hablar.
379#   3. `wireplumber`     — el gestor de sesión: enumera dispositivos y aplica la política. Sin él (1) y
380#                          (2) funcionan pero el único sink es `auto_null`, el nodo de descarte.
381# Los tres son servicios de USUARIO: sus sockets viven en $XDG_RUNTIME_DIR, creado más arriba.
382#
383# ⚠ NO duplicar el bloque de wireplumber. Estuvo dos veces por un merge descuidado de este script y el
384# resultado fue DOS wireplumber vivos peleándose el grafo — visible en `wpctl status` como dos clientes
385# «WirePlumber» con pids distintos. El síntoma no es un error: es una lista de clientes que se lee
386# normal si no se cuenta.
387# ANTES de arrancar nada: ¿hay hardware que enumerar? Es la pregunta que separa «el stack de audio
388# falla» de «no hay tarjeta», y las dos se ven igual en un `wpctl status` con `Devices:` vacío. El
389# cmdline del kernel lleva `quiet loglevel=3`, así que los mensajes de ALSA NO salen por el serial —
390# mirar /dev y /sys es la única forma directa.
391say "audio: /dev/snd = $(ls /dev/snd 2>/dev/null | tr "\n" " ")"
392say "audio: /sys/class/sound = $(ls /sys/class/sound 2>/dev/null | tr '\n' ' ')"
393say "audio: HDA en el bus PCI = $(grep -ci 'audio' /proc/bus/pci/devices 2>/dev/null || echo '?')"
394if [ -x /usr/bin/pipewire ]; then
395  /usr/bin/pipewire >/tmp/pipewire.log 2>&1 &
396  say "pipewire lanzado (pid $!)"
397  i=0; while [ $i -lt 20 ] && [ ! -S "$XDG_RUNTIME_DIR/pipewire-0" ]; do i=$((i+1)); sleep 0.25; done
398  if [ -S "$XDG_RUNTIME_DIR/pipewire-0" ]; then
399    say "socket pipewire-0 OK"
400    /usr/bin/pipewire-pulse >/tmp/pipewire-pulse.log 2>&1 &
401    say "pipewire-pulse lanzado (pid $!)"
402    i=0; while [ $i -lt 20 ] && [ ! -S "$XDG_RUNTIME_DIR/pulse/native" ]; do i=$((i+1)); sleep 0.25; done
403    if [ -S "$XDG_RUNTIME_DIR/pulse/native" ]; then
404      say "socket pulse/native OK — gvc va a poder conectar"
405    else
406      say "!! pipewire-pulse no creó pulse/native:"; dump /tmp/pipewire-pulse.log
407    fi
408    # wireplumber: el GESTOR DE SESIÓN. Va tercero porque necesita que el servidor ya esté; es el que
409    # convierte «PipeWire acepta clientes» en «PipeWire tiene dispositivos»: enumera por udev/alsa y
410    # aplica la política (qué es entrada, qué es salida, qué se enlaza con qué), y esa política está
411    # escrita en Lua. Sin él el único sink es `auto_null`, el nodo de descarte.
412    if [ -x /usr/bin/wireplumber ]; then
413      # WIREPLUMBER_DEBUG=3: sin esto su log no dice POR QUÉ descarta una tarjeta, y «Devices: vacío»
414      # es indistinguible de «no hay hardware». Con el nivel de debug puesto, el monitor de ALSA
415      # imprime cada device que ve y el motivo del descarte.
416      WIREPLUMBER_DEBUG=3 /usr/bin/wireplumber >/tmp/wireplumber.log 2>&1 &
417      WP_PID=$!
418      say "wireplumber lanzado (pid $WP_PID)"
419      sleep 2
420      if alive $WP_PID; then
421        say "wireplumber sigue vivo"
422        # El dato que importa no es el pid sino QUÉ VE. `wpctl status` lista los nodos que la política
423        # creó: si sale vacío, wireplumber corre pero no enumeró nada.
424        wpctl status >/tmp/wpctl.log 2>&1 || true
425        dump /tmp/wpctl.log
426        # Y su propio log, SIEMPRE — no sólo si muere. Un wireplumber vivo que no enumera nada es el
427        # caso interesante, y es justo el que quedaba sin evidencia. Se filtran las líneas de alsa/udev,
428        # que son las que dicen qué vio y qué descartó.
429        # Filtro ESTRECHO: `alsa` y nada más. La primera versión filtraba
430        # `alsa|udev|sound|card|monitor` y el `tail -40` se llenó de bluez/v4l2/libcamera —tres
431        # monitores que avisan que su plugin no está, ruido garantizado— dejando fuera justo las líneas
432        # de ALSA. Un filtro ancho con cola corta es peor que ninguno.
433        say "wireplumber :: ALSA"
434        grep -iE "alsa" /tmp/wireplumber.log 2>/dev/null | head -40 > /tmp/wp-alsa.log
435        dump /tmp/wp-alsa.log
436        # Y el log COMPLETO a la raíz ext4, que sí se sincroniza: /tmp es tmpfs y muere con la VM.
437        # Se saca con `debugfs -R "dump /wireplumber.log ..."`, igual que el core.
438        cp /tmp/wireplumber.log /wireplumber.log 2>/dev/null || true
439        sync
440      else
441        say "!! wireplumber MURIÓ:"; dump /tmp/wireplumber.log
442      fi
443    else
444      say "!! sin wireplumber — PipeWire acepta clientes pero no habrá dispositivos"
445    fi
446  else
447    say "!! pipewire no creó su socket:"; dump /tmp/pipewire.log
448  fi
449else
450  say "!! sin /usr/bin/pipewire — el shell va a avisar 'Failed to connect context'"
451fi
452
453# colord: gestión de color (curva ICC por monitor). Mutter lo pide al arrancar y, si no está, se come
454# 25 s de timeout de activación D-Bus. Se lanza explícito por el mismo motivo que accounts-daemon y
455# upowerd: la activación por bus funciona (el launch-helper ya es setuid) pero deja el arranque
456# esperando, y lanzarlo a mano además deja un log donde mirar si no adquiere el nombre.
457if supervisado colord "colord"; then :
458elif [ -x /usr/libexec/colord ]; then
459  # --verbose: su log normal se corta después de abrir las tres bases de datos y no dice nada más, así
460  # que sin esto un colord que no adquiere el nombre es indistinguible de uno que se cuelga.
461  CD_VERBOSE=1 /usr/libexec/colord --verbose >/tmp/colord.log 2>&1 &
462  CD_PID=$!
463  say "colord lanzado (pid $CD_PID)"
464  # ⚠ ESTA COMPROBACIÓN FALTABA, y es la misma que yo mismo escribí para upowerd: **un pid no es un
465  # servicio**. Sin ella, «colord lanzado (pid N)» + «bus: ColorManager NO apareció» no distingue
466  # MURIÓ de SE COLGÓ, que son dos investigaciones distintas. Es el hueco que dejó el diagnóstico de
467  # colord a medias.
468  sleep 2
469  if alive $CD_PID; then say "colord sigue vivo"; else say "!! colord MURIÓ al arrancar"; fi
470else
471  say "!! sin /usr/libexec/colord — mutter va a esperar 25s y seguir sin gestión de color"
472fi
473
474esperar_nombre org.freedesktop.login1 /tmp/logind-compat.log
475# PolicyKit1 NO se chequea acá: ya se esperó arriba, antes de lanzar a sus clientes. Chequearlo sólo
476# al final era precisamente lo que dejaba pasar la carrera con verde.
477esperar_nombre org.freedesktop.Accounts /tmp/accounts-daemon.log
478esperar_nombre org.freedesktop.UPower /tmp/upowerd.log
479esperar_nombre org.freedesktop.ColorManager /tmp/colord.log 80   # 40 s: ver el comentario
480# El log de colord SIEMPRE, no sólo si falla el nombre: un daemon vivo que no adquiere su nombre es el
481# caso interesante y es justo el que se quedaba sin evidencia.
482say "colord :: log completo"
483dump /tmp/colord.log
484
485if [ "${DIAG:-1}" = 1 ]; then
486  ulimit -c unlimited 2>/dev/null || true
487  echo '/core.%e.%p' > /proc/sys/kernel/core_pattern 2>/dev/null || true
488fi
489
490say "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"
491
492# ── SONDA DE INPUT — barata, y separa las dos hipótesis del muro ────────────────────────────────
493# El muro es la hebra de input de mutter: arranca y nunca señala `input_thread_initialized`.
494# `libinput list-devices` hace EXACTAMENTE lo que hace `init_libinput()` —udev_new +
495# libinput_udev_create_context + libinput_udev_assign_seat("seat0")— pero con `open()/close()`
496# pelados en vez del `TakeDevice` de logind. O sea que discrimina:
497#   · la sonda cuelga o no lista nada ⇒ el muro es libudev-zero/libinput (enumeración por /sys)
498#   · la sonda lista los dispositivos ⇒ libinput anda; el cuelgue está del lado de mutter
499#     (MetaDevicePool → TakeDevice, o algo anterior en el hilo: input settings, keymap)
500# Ojo: `libinput` es un multiplexor de subcomandos y `list-devices` NO es interactivo, pero igual se
501# le pone reloj — si el problema es un cuelgue, la sonda cuelga también, y hay que matarla.
502say "sonda: /sys/class/input = $(ls /sys/class/input 2>/dev/null | tr '\n' ' ')"
503say "sonda: /dev/input = $(ls /dev/input 2>/dev/null | tr '\n' ' ')"
504if [ -x /usr/bin/libinput ]; then
505  ( libinput list-devices >/tmp/libinput-devices.log 2>&1; echo "rc=$?" >>/tmp/libinput-devices.log ) &
506  LI_PID=$!
507  i=0
508  while [ $i -lt 24 ] && alive $LI_PID; do i=$((i+1)); sleep 0.5; done
509  if alive $LI_PID; then
510    say "!! SONDA COLGADA: 'libinput list-devices' no volvió en 12s ⇒ el muro ES libinput/libudev-zero"
511    say "   wchan=$(cat /proc/$LI_PID/wchan 2>/dev/null) syscall=$(cat /proc/$LI_PID/syscall 2>/dev/null)"
512    kill -9 $LI_PID 2>/dev/null
513  else
514    say "sonda 'libinput list-devices' VOLVIÓ (no cuelga) — dispositivos:"
515    dump /tmp/libinput-devices.log
516  fi
517else
518  say "sonda: no hay /usr/bin/libinput en la imagen"
519fi
520# GNOME_MODE=headless — EXPERIMENTO DECISIVO, no un modo de producción. El backend headless de
521# mutter crea el seat con META_SEAT_NATIVE_FLAG_NO_LIBINPUT, o sea que SALTEA `init_libinput()`,
522# que es justo el último paso del hilo de input antes de señalar `input_thread_initialized`
523# (meta-seat-impl.c:3098). Si en headless aparece wayland-0, el bloqueo está en libinput/udev y no
524# en el resto del arranque; si tampoco aparece, el problema es anterior (keymap, input settings).
525case "${GNOME_MODE:-drm}" in
526  headless) SHELL_ARGS="--headless --virtual-monitor 1280x800" ;;
527  *)        SHELL_ARGS="--wayland" ;;
528esac
529say "lanzando gnome-shell $SHELL_ARGS (modo=${GNOME_MODE:-drm})..."
530/usr/bin/gnome-shell $SHELL_ARGS >/tmp/gnome-shell.log 2>&1 &
531SHELL_PID=$!
532# stream EN VIVO al serial (busybox sed no soporta -u ⇒ tail -f directo, sin pipes).
533( echo "==== gnome-shell.log (stream vivo) ===="; tail -f /tmp/gnome-shell.log ) > /dev/ttyS0 2>/dev/null &
534
535i=0
536while [ $i -lt 240 ]; do
537  [ -S "$XDG_RUNTIME_DIR/wayland-0" ] && break
538  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; }
539  i=$((i+1)); sleep 0.5
540done
541if [ -S "$XDG_RUNTIME_DIR/wayland-0" ]; then
542  say "compositor OK (wayland-0) — el shell ES el display server"
543else
544  say "!! sin wayland-0 tras 120s:"; dump /tmp/gnome-shell.log
545  # El shell NO murió: quedó COLGADO. Sin gdb en la imagen, el diagnóstico barato es dónde está
546  # bloqueado el hilo principal (wchan) y contra qué socket, que es lo que distingue "esperando una
547  # respuesta D-Bus que no llega" de "spinning en el renderer".
548  say "--- diagnóstico del cuelgue (pid $SHELL_PID) ---"
549  say "XDG_RUNTIME_DIR ($XDG_RUNTIME_DIR): $(ls -a $XDG_RUNTIME_DIR 2>/dev/null | tr '\n' ' ')"
550  say "sockets wayland en el sistema: $(find / -name 'wayland-*' -maxdepth 6 2>/dev/null | tr '\n' ' ')"
551  say "wchan: $(cat /proc/$SHELL_PID/wchan 2>/dev/null)"
552  say "state: $(grep -a ^State /proc/$SHELL_PID/status 2>/dev/null)"
553  say "stack principal:"; cat /proc/$SHELL_PID/stack > /dev/ttyS0 2>/dev/null
554  say "syscall: $(cat /proc/$SHELL_PID/syscall 2>/dev/null)"
555  say "fds abiertos:"; ls -l /proc/$SHELL_PID/fd 2>/dev/null > /dev/ttyS0
556  say "hilos: $(ls /proc/$SHELL_PID/task 2>/dev/null | wc -l)"
557  # POR HILO, y ANTES del SIGABRT. La primera versión hacía este bucle DESPUÉS de abortar, o sea
558  # sobre un /proc/<pid>/task que ya no existía: salía vacío y no dijo nada. Es el dato más barato
559  # que hay para ubicar la «Mutter Input Thread», porque el core no sirve: gdb no puede desenrollar
560  # a través de musl (sin CFI) y devuelve `?? ()` para los 11 hilos que no son el principal.
561  say "--- por hilo (comm / wchan / syscall) ---"
562  for t in $(ls /proc/$SHELL_PID/task 2>/dev/null); do
563    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)"
564  done
565  # ¿Sigue vivo y respondiendo el login1? Si el shell está esperando una respuesta D-Bus que no
566  # llega, esto lo separa en dos: daemon muerto/colgado vs daemon vivo que ya contestó.
567  say "arje-logind-compat: pids=$(pgrep -f arje-logind-compat 2>/dev/null | tr '\n' ' ')"
568  for p in $(pgrep -f arje-logind-compat 2>/dev/null); do
569    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)"
570  done
571  ( dbus-send --system --print-reply --dest=org.freedesktop.login1 \
572      /org/freedesktop/login1 org.freedesktop.DBus.Properties.Get \
573      string:org.freedesktop.login1.Manager string:NAutoVTs >/tmp/login1-ping.log 2>&1
574    echo "rc=$?" >>/tmp/login1-ping.log ) &
575  PING_PID=$!
576  i=0; while [ $i -lt 16 ] && alive $PING_PID; do i=$((i+1)); sleep 0.5; done
577  if alive $PING_PID; then
578    say "!! login1 NO responde (ping D-Bus colgado 8s) ⇒ el daemon se quedó tildado"
579    kill -9 $PING_PID 2>/dev/null
580  else
581    say "login1 responde al ping D-Bus:"; dump /tmp/login1-ping.log
582  fi
583  # /tmp es tmpfs ⇒ el log del daemon se pierde con la VM. Copiarlo a la raíz ext4, que sí se
584  # sincroniza y se puede sacar con debugfs junto al core.
585  cp /tmp/logind-compat.log /logind-compat.log 2>/dev/null || true
586  cp /tmp/gnome-shell.log /gnome-shell.log 2>/dev/null || true
587  # Sin gdb en la imagen, la única forma de ver DÓNDE está bloqueado es forzarle un core y leerlo
588  # con el gdb del host (`thread apply all bt`). SIGABRT lo produce y no lo maneja nadie.
589  say "forzando core del proceso colgado (SIGABRT)..."
590  kill -ABRT $SHELL_PID 2>/dev/null
591  sleep 12; sync; sync
592  say "cores: $(ls -la /core.* 2>/dev/null | tr '\n' ' ' || echo NINGUNO)"
593  exec /bin/sh
594fi
595export WAYLAND_DISPLAY=wayland-0
596
597( n=0; while sleep 15; do
598    n=$((n+1))
599    say "STATUS +$((n*15))s: gnome-shell=$(pgrep gnome-shell 2>/dev/null | wc -l)"
600  done ) &
601
602wait $SHELL_PID
603RC=$?
604say "gnome-shell salió (código $RC) — volcando log:"
605dump /tmp/gnome-shell.log
606cp /tmp/gnome-shell.log /gnome-shell.log 2>/dev/null || true
607say "cores en /: $(ls -la /core.* 2>/dev/null | tr '\n' ' ' || echo NINGUNO)"
608sync; sync
609say "== disco SYNCED. serial CONGELADO. matá QEMU desde el host. =="
610sleep 3600