diff --git a/docs/26-atuq-envoltorio-gecko.md b/docs/26-atuq-envoltorio-gecko.md
index 13adc0a7..4da0c379 100644
--- a/docs/26-atuq-envoltorio-gecko.md
+++ b/docs/26-atuq-envoltorio-gecko.md
@@ -1632,6 +1632,45 @@ cosas distintas de «arranca y se ve».
Artefactos de la corrida, para quien siga: `/mnt/cosecha/takana-cosmic-qemu.img` (12 G) y las
capturas en `work/cosmic-{1..6}.png`.
+#### 6.10.quater El COMPOSITOR no es la causa: con `cosmic-comp` nesteado el navegador SÍ pinta (2026-09-14)
+
+El §6.10.ter dejó dos variables cambiando a la vez —el compositor (cosmic-comp+llvmpipe contra
+sway+pixman) y el arranque real contra `bwrap`— y ninguna medición que las separe. `scripts/cosmic/
+atuq-en-cosmic.sh` saca la primera de encima en ~2 min por vuelta, sin QEMU y sin imagen:
+
+ sway (headless, pixman) → cosmic-comp (winit, cliente de sway) → atuq
+
+sway es andamio: no se prueba nada de él, sólo le da a cosmic-comp una pantalla donde vivir, y `grim`
+captura de SU lado, o sea que fotografía la ventana de cosmic-comp con lo que haya dentro. El
+`--control` corre **ese mismo binario de `atuq`** —el del cierre `escritorio-cosmic` hidratado para la
+imagen, no otro— directo sobre sway, sin tocar nada más.
+
+**El resultado, en píxeles:**
+
+ cosmic-comp magenta de la página: 586331 px bbox (205,169)-(1277,717)
+ control sway magenta de la página: 586331 px bbox (205,169)-(1277,717)
+
+Las dos capturas coinciden **al píxel** en la región de la página, y no por casualidad ni porque el
+navegador se haya escapado a sway: en la captura previa al lanzamiento, la ventana de cosmic-comp
+ocupa exactamente `(2,81)-(1277,717)` = 1276×637 de su color de fondo, que es el mismo rectángulo que
+sway le da a un cliente maximizado bajo su barra de título. Las dos corridas enteras difieren en 1130
+píxeles, todos en la franja de las barras de título. Y el `MOZ_LOG` del widget lo confirma desde el
+otro lado: en la corrida con cosmic el motor ve `mode output size 1276 x 637` (la ventana del
+compositor nesteado) y en el control `1280 x 720` (la pantalla de sway).
+
+⇒ **`cosmic-comp` no es el que impide la ventana.** Con GL por software, sin GPU y hasta con el
+navegador entero pintando dentro, la cadena funciona.
+
+**Y de paso, qué camino de buffers usa cuando funciona:** `WaylandBufferSHM::CreateWlBuffer()` en cada
+cuadro — memoria compartida, no dmabuf. Es la línea de base contra la que comparar lo que haga en la
+imagen: si allá el motor eligiera otro camino, se vería en la misma traza.
+
+Lo que queda como variable —y es donde sigue la caza— es el arranque de verdad: `cosmic-comp` sobre
+**kms/DRM de virtio-gpu** en vez de winit, el PID1 de arje, el seat. Para eso está
+`scripts/cosmic/atuq-en-imagen.py`, que arranca la imagen, maneja el serial, lanza el navegador con el
+mismo `MOZ_LOG` y pide capturas por QMP — la corrida del §6.10.ter fue a mano y no dejó un solo log
+que se pueda releer, que es exactamente lo que hace falta para comparar.
+
### 6.11 Y ahora la pregunta que faltaba: ¿REPRODUCE? (2026-09-07)
El §6.10 preguntó «¿qué NO puede hacer?» y contestó leyendo cadenas. Media pregunta. La otra mitad
diff --git a/scripts/cosmic/atuq-en-cosmic.sh b/scripts/cosmic/atuq-en-cosmic.sh
new file mode 100755
index 00000000..353f3ca3
--- /dev/null
+++ b/scripts/cosmic/atuq-en-cosmic.sh
@@ -0,0 +1,172 @@
+#!/bin/sh
+# atuq-en-cosmic.sh — ¿el navegador PINTA SU VENTANA bajo `cosmic-comp`? (SDD 26 §6.10.ter)
+#
+# scripts/cosmic/atuq-en-cosmic.sh # atuq dentro de cosmic-comp
+# scripts/cosmic/atuq-en-cosmic.sh --control # el MISMO atuq, pero directo sobre sway
+#
+# ── POR QUÉ EXISTE ────────────────────────────────────────────────────────────────────────────
+# El 2026-09-14 se arrancó por primera vez una IMAGEN DE DISCO con COSMIC y `atuq` adentro: el
+# navegador arranca, sus extensiones corren, WebRender inicializa… y la ventana no aparece nunca.
+# Bajo `sway` headless el mismo artefacto pinta perfecto. Entre los dos casos cambian DOS cosas a la
+# vez —el compositor (cosmic-comp+llvmpipe contra sway+pixman) y el arranque real contra `bwrap`—,
+# así que ninguna medición de las que había separa una de la otra.
+#
+# Este arnés cambia UNA sola: el compositor. Todo lo demás —el mismo cierre `escritorio-cosmic`
+# hidratado, el mismo binario de `atuq`, la misma jaula— queda fijo, y `--control` corre ese mismo
+# `atuq` sobre sway sin tocar nada más. Cuesta ~2 min por vuelta en vez de armar una imagen de 12 G
+# y arrancarla en QEMU sin KVM.
+#
+# ── LA CADENA ─────────────────────────────────────────────────────────────────────────────────
+# sway (headless, pixman) → cosmic-comp (winit, cliente de sway) → atuq
+# `grim` captura del lado de SWAY, o sea que fotografía la ventana de cosmic-comp con lo que haya
+# dentro. sway es andamio: no lo prueba nada, sólo le da a cosmic-comp una pantalla donde vivir.
+#
+# ── LA EVIDENCIA ES UN DIFF DE PÍXELES, Y ADEMÁS SE MIRA EL ÁRBOL ─────────────────────────────
+# Se captura ANTES de lanzar el navegador y DESPUÉS. Un log en verde es compatible con una pantalla
+# vacía —es exactamente lo que pasó en la imagen— así que el veredicto es cuántos píxeles cambiaron.
+# Y se guarda el `MOZ_LOG` del widget: si la ventana se crea pero nunca recibe su `configure`, eso
+# sólo se ve ahí.
+#
+# env: RFS_COSMIC (def /mnt/cosecha/cosmic-rootfs) cierre de escritorio-cosmic
+# RFS_SWAY (def /mnt/cosecha/escritorios/sway-rootfs) el andamio
+# MESA (def el artefacto vigente de mesa-llvmpipe) GL por software para cosmic-comp
+# ESPERA (def 60) segundos para que el navegador pinte
+# OUT (def un temporal) dónde quedan capturas y logs
+set -eu
+ROOT="$(cd "$(dirname "$0")/../.." && pwd)"; cd "$ROOT"
+RFS_COSMIC="${RFS_COSMIC:-/mnt/cosecha/cosmic-rootfs}"
+RFS_SWAY="${RFS_SWAY:-/mnt/cosecha/escritorios/sway-rootfs}"
+ALPINE="$ROOT/.dev-fs/alpine"
+ESPERA="${ESPERA:-60}"
+CONTROL=0
+for a in "$@"; do
+ case "$a" in
+ --control) CONTROL=1 ;;
+ *) echo "✗ opción desconocida: $a" >&2; exit 1 ;;
+ esac
+done
+
+# mesa-llvmpipe por HASH VIGENTE, no por el más reciente del store (con dos artefactos del mismo
+# paquete conviviendo, la fecha miente). Es el mismo criterio de cosmic-nested.sh.
+if [ -z "${MESA+x}" ]; then
+ MH=$(./target/release/takana --store /mnt/cosecha/store hash recipes/mesa-llvmpipe.toml 2>/dev/null | tr -d '\n' | sed 's/b3://')
+ MESA=$(ls -d /mnt/cosecha/store/"$MH"-mesa-llvmpipe 2>/dev/null || true)
+fi
+[ -d "$RFS_COSMIC" ] || { echo "✗ falta el cierre de cosmic en $RFS_COSMIC" >&2
+ echo " scripts/hydrate-profile.py escritorio-cosmic --store /mnt/cosecha/store --into $RFS_COSMIC" >&2; exit 1; }
+[ -d "$RFS_SWAY" ] || { echo "✗ falta el rootfs de sway en $RFS_SWAY" >&2; exit 1; }
+[ -x "$RFS_COSMIC/usr/bin/atuq" ] || { echo "✗ el cierre no trae /usr/bin/atuq" >&2; exit 1; }
+[ -n "${MESA:-}" ] || echo " ⚠ sin mesa-llvmpipe sellado: cosmic-comp no va a poder iniciar EGL" >&2
+
+OUT="${OUT:-$(mktemp -d)}"; mkdir -p "$OUT"
+echo "== salida: $OUT"
+
+# Una página de UN SOLO COLOR imposible de confundir con nada del escritorio: si aparece, apareció
+# la ventana del navegador y no un repintado de cosmic.
+cat > "$OUT/pagina.html" <<'HTML'
+
atuq en cosmic
+
+ATUQ PINTA
+
+HTML
+mkdir -p "$OUT/perfil"
+cat > "$OUT/perfil/user.js" <<'JS'
+user_pref("browser.dom.window.dump.enabled", true);
+user_pref("browser.shell.checkDefaultBrowser", false);
+JS
+
+cat > "$OUT/dentro.sh" <<'ADENTRO'
+#!/bin/sh
+export XDG_RUNTIME_DIR=/run/user/0 HOME=/root LANG=C XDG_DATA_DIRS=/usr/share
+export XDG_CURRENT_DESKTOP=COSMIC XDG_SESSION_TYPE=wayland RUST_BACKTRACE=1
+export LIBGL_ALWAYS_SOFTWARE=1 LIBGL_DRIVERS_PATH=/usr/lib/dri
+mkdir -p "$XDG_RUNTIME_DIR" /root /dev/shm /etc; chmod 700 "$XDG_RUNTIME_DIR"
+[ -s /etc/passwd ] || echo "root:x:0:0:root:/root:/bin/sh" > /etc/passwd
+[ -s /etc/machine-id ] || tr -d - < /proc/sys/kernel/random/uuid > /etc/machine-id
+
+lanzar_atuq() { # $1 = WAYLAND_DISPLAY donde tiene que pintar
+ WAYLAND_DISPLAY="$1" \
+ MOZ_ENABLE_WAYLAND=1 GDK_BACKEND=wayland \
+ MOZ_DISABLE_CONTENT_SANDBOX=1 MOZ_DISABLE_RDD_SANDBOX=1 MOZ_DISABLE_UTILITY_SANDBOX=1 \
+ MOZ_DISABLE_GMP_SANDBOX=1 MOZ_DISABLE_SOCKET_PROCESS_SANDBOX=1 \
+ MOZ_LOG='Widget:5,WidgetWayland:5,WaylandBackend:5,timestamp' \
+ MOZ_LOG_FILE=/salida/moz \
+ /usr/bin/atuq --no-remote --profile /salida/perfil file:///salida/pagina.html \
+ > /salida/atuq.log 2>&1 &
+ echo "@@ atuq lanzado (pid $!) contra $1"
+}
+
+(
+ i=0
+ while [ ! -S /run/user/0/wayland-1 ] && [ $i -lt 60 ]; do sleep 1; i=$((i+1)); done
+ [ -S /run/user/0/wayland-1 ] || { echo "@@ sin socket de sway"; exit 1; }
+ export WAYLAND_DISPLAY=wayland-1
+ /usr/bin/swaybg -c '#1a4b8c' >/dev/null 2>&1 &
+ sleep 2
+ export DBUS_SESSION_BUS_ADDRESS=$(/usr/bin/dbus-daemon --session --fork --print-address 2>/dev/null)
+ echo "@@ dbus=${DBUS_SESSION_BUS_ADDRESS:-VACIO}"
+
+ if [ "${CONTROL:-0}" = "1" ]; then
+ echo "@@ CONTROL: el navegador va directo sobre sway (sin cosmic-comp)"
+ DEST=wayland-1
+ else
+ echo "@@ arrancando cosmic-comp nesteado sobre sway…"
+ cosmic-comp >/salida/cosmic.log 2>&1 &
+ CC=$!
+ i=0; WD=""
+ while [ $i -lt 60 ]; do
+ WD=$(ls /run/user/0/wayland-* 2>/dev/null | grep -v lock | grep -v 'wayland-1$' | head -1)
+ [ -n "$WD" ] && break
+ kill -0 $CC 2>/dev/null || { echo "@@ cosmic-comp murió antes del socket:"; tail -25 /salida/cosmic.log; exit 1; }
+ i=$((i+1)); sleep 1
+ done
+ [ -n "$WD" ] || { echo "@@ cosmic-comp sin socket en 60 s:"; tail -25 /salida/cosmic.log; exit 1; }
+ DEST=$(basename "$WD")
+ echo "@@ cosmic-comp OK ($DEST)"
+ sleep 5
+ fi
+
+ /usr/bin/grim /salida/antes.png && echo "@@ captura ANTES"
+ lanzar_atuq "$DEST"
+ sleep "${ESPERA:-60}"
+ /usr/bin/grim /salida/despues.png && echo "@@ captura DESPUES"
+ echo "@@ procesos atuq: $(ps -o comm= 2>/dev/null | grep -c atuq)"
+ sleep 1
+ /usr/bin/swaymsg exit 2>/dev/null
+) &
+
+export WLR_BACKENDS=headless WLR_HEADLESS_OUTPUTS=1 WLR_RENDERER=pixman
+exec /usr/bin/sway -d 2>&1
+ADENTRO
+
+# El ORDEN de los overlays manda: alpine trae la libc, sway el andamio, el cierre de cosmic pisa lo
+# que comparta con él, y mesa-llvmpipe va ÚLTIMO porque el cierre trae la mesa de Intel real
+# (iris_dri) y en una máquina sin GPU eso no inicia EGL.
+TIEMPO=$((ESPERA + 180))
+timeout "$TIEMPO" bwrap \
+ --overlay-src "$ALPINE" --overlay-src "$RFS_SWAY" --overlay-src "$RFS_COSMIC" \
+ ${MESA:+--overlay-src "$MESA"} --tmp-overlay / \
+ --bind "$OUT" /salida --dev /dev --proc /proc --tmpfs /run --tmpfs /tmp \
+ --dev-bind-try /dev/dri /dev/dri \
+ --unshare-pid --unshare-uts --hostname atuq-en-cosmic \
+ --uid 0 --gid 0 --die-with-parent \
+ --setenv PATH /usr/bin:/bin:/usr/sbin:/sbin \
+ --setenv CONTROL "$CONTROL" --setenv ESPERA "$ESPERA" \
+ /bin/sh /salida/dentro.sh > "$OUT/log.txt" 2>&1 || true
+
+grep -a '@@' "$OUT/log.txt" || true
+[ -f "$OUT/antes.png" ] && [ -f "$OUT/despues.png" ] || {
+ echo "✗ no hubo capturas — mirá $OUT/log.txt" >&2; tail -20 "$OUT/log.txt" >&2; exit 1; }
+
+python3 - "$OUT/antes.png" "$OUT/despues.png" <<'PY'
+import sys
+from PIL import Image, ImageChops
+a, b = (Image.open(p).convert("RGB") for p in sys.argv[1:3])
+d = ImageChops.difference(a, b).convert("L")
+cambiados = d.width * d.height - d.histogram()[0]
+magenta = sum(n for n, c in b.getcolors(maxcolors=1 << 24) if c == (255, 0, 255))
+print(f" píxeles cambiados: {cambiados} magenta de la página: {magenta}")
+PY
+echo " capturas: $OUT/antes.png $OUT/despues.png"
+echo " logs: $OUT/atuq.log $OUT/cosmic.log $OUT/moz.*"
diff --git a/scripts/cosmic/atuq-en-imagen.py b/scripts/cosmic/atuq-en-imagen.py
new file mode 100755
index 00000000..2c6e2eb1
--- /dev/null
+++ b/scripts/cosmic/atuq-en-imagen.py
@@ -0,0 +1,210 @@
+#!/usr/bin/env python3
+"""atuq-en-imagen.py — arranca la IMAGEN DE DISCO con COSMIC y le pide al navegador que pinte.
+
+ scripts/cosmic/atuq-en-imagen.py # lanza atuq y fotografía la pantalla
+ scripts/cosmic/atuq-en-imagen.py --solo-cosmic # control: NO lanza atuq (línea de base)
+
+── POR QUÉ ────────────────────────────────────────────────────────────────────────────────────
+El §6.10.ter del SDD 26 dejó esto medido y sin explicar: en la imagen arrancada `atuq` corre —sus
+extensiones sondean, WebRender inicializa— y la ventana NO APARECE, mientras que bajo `sway` headless
+el mismo artefacto pinta. `scripts/cosmic/atuq-en-cosmic.sh` ya sacó una variable de encima:
+**con cosmic-comp NESTEADO (backend winit, GL por software) el navegador pinta**, o sea que no es el
+compositor por sí mismo. Lo que queda es el arranque de verdad: kms/DRM sobre virtio-gpu, el PID1 de
+arje, el seat. Eso sólo se mide acá adentro.
+
+La corrida anterior fue A MANO por el serial y no dejó ni un log que se pueda releer. Este guion
+hace lo mismo pero REPETIBLE y con la evidencia guardada: maneja el serial, lanza el navegador con
+`MOZ_LOG` del widget de Wayland, pide capturas por QMP y saca los logs por el serial.
+
+── CÓMO HABLA CON LA VM ───────────────────────────────────────────────────────────────────────
+`console-getty` corre `cosmic-start`, que tras su ventana de observación (3 min) hace `exec /bin/sh`
+CON EL ENTORNO DE LA SESIÓN puesto (WAYLAND_DISPLAY, DBUS_SESSION_BUS_ADDRESS). O sea que el shell
+del serial es exactamente desde donde un usuario lanzaría una app — y por eso no hace falta
+reconstruir el entorno a mano, que es como se inventan diferencias que después se confunden con la
+causa.
+"""
+import argparse, json, os, shutil, socket, subprocess, sys, time
+
+RAIZ = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+IMG = os.environ.get("IMG", "/mnt/cosecha/takana-cosmic-qemu.img")
+OVMF_CODE = os.environ.get("OVMF_CODE", "/usr/share/edk2/x64/OVMF_CODE.4m.fd")
+OVMF_VARS = os.environ.get("OVMF_VARS", "/usr/share/edk2/x64/OVMF_VARS.4m.fd")
+MEM = os.environ.get("MEM", "2560")
+SMP = os.environ.get("SMP", "2")
+
+
+class Serial:
+ """El serial de la VM. Todo lo que entra queda en el .log crudo, pase lo que pase."""
+
+ def __init__(self, ruta, log):
+ self.log = open(log, "ab", buffering=0)
+ for _ in range(120):
+ try:
+ self.s = socket.socket(socket.AF_UNIX); self.s.connect(ruta); break
+ except OSError:
+ time.sleep(0.5)
+ else:
+ raise SystemExit(f"no pude conectarme al serial {ruta}")
+ self.s.settimeout(1.0)
+ self.buf = b""
+
+ def leer(self, segundos):
+ fin = time.time() + segundos
+ while time.time() < fin:
+ try:
+ d = self.s.recv(65536)
+ except socket.timeout:
+ continue
+ if not d:
+ break
+ self.buf += d; self.log.write(d)
+ return self.buf
+
+ def esperar(self, marca, segundos, nombre=""):
+ """Lee hasta ver `marca`. Devuelve lo leído desde la llamada anterior."""
+ vistos = len(self.buf)
+ fin = time.time() + segundos
+ while time.time() < fin:
+ try:
+ d = self.s.recv(65536)
+ except socket.timeout:
+ continue
+ if d:
+ self.buf += d; self.log.write(d)
+ if marca.encode() in self.buf[vistos:]:
+ return self.buf[vistos:].decode("utf-8", "replace")
+ print(f" ⚠ nunca llegó «{marca}» {nombre} en {segundos}s", flush=True)
+ return self.buf[vistos:].decode("utf-8", "replace")
+
+ def cmd(self, orden, segundos=30):
+ """Manda una orden y devuelve su salida (delimitada por una marca propia)."""
+ marca = f"__FIN{int(time.time()*1000) % 100000}__"
+ self.s.sendall(f"{orden}; echo {marca}\n".encode())
+ sal = self.esperar(marca, segundos, nombre=f"de «{orden[:40]}»")
+ return sal
+
+
+def qmp(ruta, ordenes):
+ s = socket.socket(socket.AF_UNIX); s.settimeout(30); s.connect(ruta)
+ f = s.makefile("rwb", buffering=0)
+ f.readline() # greeting
+ f.write(b'{"execute":"qmp_capabilities"}\n'); f.readline()
+ salidas = []
+ for o in ordenes:
+ f.write(json.dumps(o).encode() + b"\n"); salidas.append(f.readline())
+ s.close()
+ return salidas
+
+
+def main():
+ ap = argparse.ArgumentParser()
+ ap.add_argument("--solo-cosmic", action="store_true", help="control: no lanza el navegador")
+ ap.add_argument("--espera", type=int, default=180, help="segundos para que el navegador pinte")
+ ap.add_argument("--out", default=os.path.join(RAIZ, "work/atuq-imagen"))
+ a = ap.parse_args()
+
+ out = a.out; os.makedirs(out, exist_ok=True)
+ vars_fd = os.path.join(out, "OVMF_VARS.fd")
+ shutil.copy(OVMF_VARS, vars_fd)
+ ser_sock, qmp_sock = os.path.join(out, "serial.sock"), os.path.join(out, "qmp.sock")
+ for p in (ser_sock, qmp_sock):
+ if os.path.exists(p):
+ os.unlink(p)
+
+ qemu = [
+ "nice", "-n", "10", "qemu-system-x86_64",
+ "-machine", "q35", "-accel", "tcg", "-cpu", "max", "-m", MEM, "-smp", SMP,
+ "-vga", "none", "-device", "virtio-gpu-pci", "-display", "none", "-nic", "none",
+ "-drive", f"if=pflash,format=raw,unit=0,file={OVMF_CODE},readonly=on",
+ "-drive", f"if=pflash,format=raw,unit=1,file={vars_fd}",
+ "-drive", f"file={IMG},format=raw,if=virtio",
+ "-serial", f"unix:{ser_sock},server,nowait",
+ "-qmp", f"unix:{qmp_sock},server,nowait",
+ "-no-reboot",
+ ]
+ print("== arrancando la imagen (TCG, sin KVM — paciencia)")
+
+ def primero_en_la_fila():
+ # El repo es COMPARTIDO y casi siempre hay una tanda de la granja compilando en esta misma
+ # máquina (4 cores, 7,6 G). Si la RAM se acaba, el que tiene que morir es ESTA VM y no el
+ # build de otro agente: `oom_score_adj` alto es la única forma de decirlo por adelantado.
+ with open("/proc/self/oom_score_adj", "w") as f:
+ f.write("900")
+
+ vm = subprocess.Popen(qemu, stdout=open(os.path.join(out, "qemu.log"), "wb"),
+ stderr=subprocess.STDOUT, preexec_fn=primero_en_la_fila)
+ try:
+ ser = Serial(ser_sock, os.path.join(out, "serial.log"))
+ print("== esperando a COSMIC…", flush=True)
+ ser.esperar("compositor OK", 600, nombre="(arranque de cosmic-comp)")
+ print(" compositor arriba; esperando el shell del serial (ventana de observación)", flush=True)
+ ser.esperar("fin de la ventana de observación", 420, nombre="(fin de observación)")
+ time.sleep(3)
+ ser.s.sendall(b"\n")
+ print(ser.cmd("echo ENTORNO: WD=$WAYLAND_DISPLAY XDG=$XDG_RUNTIME_DIR DBUS=$DBUS_SESSION_BUS_ADDRESS").strip())
+ print(ser.cmd("ls -l $XDG_RUNTIME_DIR; ls /usr/lib/dri").strip())
+
+ qmp(qmp_sock, [{"execute": "screendump", "arguments": {"filename": os.path.join(out, "antes.ppm")}}])
+ print("== captura ANTES", flush=True)
+
+ if a.solo_cosmic:
+ print("== CONTROL: no se lanza el navegador")
+ else:
+ # Una página de un color imposible de confundir con el escritorio.
+ ser.cmd("mkdir -p /tmp/perfil /var/log/cosmic")
+ ser.cmd("printf '%s' '"
+ "ATUQ PINTA
' > /tmp/pagina.html")
+ ser.cmd("printf '%s\\n' 'user_pref(\"browser.dom.window.dump.enabled\", true);' > /tmp/perfil/user.js")
+ ser.cmd("printf '%s\\n' 'user_pref(\"browser.shell.checkDefaultBrowser\", false);' >> /tmp/perfil/user.js")
+ lanz = ("MOZ_ENABLE_WAYLAND=1 GDK_BACKEND=wayland "
+ "MOZ_LOG='Widget:5,WidgetWayland:5,WaylandBackend:5,timestamp' "
+ "MOZ_LOG_FILE=/var/log/cosmic/moz "
+ "/usr/bin/atuq --no-remote --profile /tmp/perfil file:///tmp/pagina.html "
+ "> /var/log/cosmic/atuq.log 2>&1 &")
+ print(ser.cmd(lanz).strip())
+ print(f"== navegador lanzado; observando {a.espera}s", flush=True)
+ t0 = time.time(); n = 0
+ while time.time() - t0 < a.espera:
+ time.sleep(min(60, max(5, a.espera / 3))); n += 1
+ qmp(qmp_sock, [{"execute": "screendump",
+ "arguments": {"filename": os.path.join(out, f"durante-{n}.ppm")}}])
+ vivos = ser.cmd("for c in /proc/[0-9]*/comm; do cat $c 2>/dev/null; done | grep -c atuq")
+ print(f" +{int(time.time()-t0)}s procesos atuq: {vivos.splitlines()[1].strip() if len(vivos.splitlines())>1 else '?'}", flush=True)
+
+ qmp(qmp_sock, [{"execute": "screendump", "arguments": {"filename": os.path.join(out, "despues.ppm")}}])
+ print("== captura DESPUES", flush=True)
+
+ # La evidencia que no se puede leer de una captura: qué hizo el widget de Wayland.
+ print("---- atuq.log (cola) ----")
+ print(ser.cmd("tail -25 /var/log/cosmic/atuq.log", 60))
+ print("---- MOZ_LOG: creación de la ventana ----")
+ print(ser.cmd("grep -a -m 40 -E 'nsWindow::Create|CreateNative|Map|configure|Show|frame callback|Buffer' "
+ "/var/log/cosmic/moz.moz_log | head -40", 120))
+ print("---- MOZ_LOG: últimas líneas ----")
+ print(ser.cmd("tail -25 /var/log/cosmic/moz.moz_log", 60))
+ print("---- tamaño de los logs ----")
+ print(ser.cmd("wc -l /var/log/cosmic/moz.moz_log /var/log/cosmic/atuq.log", 30))
+ finally:
+ try:
+ qmp(qmp_sock, [{"execute": "quit"}])
+ except Exception:
+ pass
+ time.sleep(2); vm.terminate()
+ try:
+ vm.wait(20)
+ except Exception:
+ vm.kill()
+
+ # Las capturas salen en PPM (lo único que sabe QMP); a PNG para poder mirarlas.
+ try:
+ from PIL import Image
+ for f in sorted(os.listdir(out)):
+ if f.endswith(".ppm"):
+ Image.open(os.path.join(out, f)).save(os.path.join(out, f[:-4] + ".png"))
+ print("== capturas convertidas a PNG en", out)
+ except Exception as e:
+ print(" ⚠ sin conversión a PNG:", e)
+
+
+if __name__ == "__main__":
+ main()