From 756ed9c6a633b53055892ba4c6bcd1522b783cc5 Mon Sep 17 00:00:00 2001 From: Sergio Date: Mon, 14 Sep 2026 14:11:49 +0000 Subject: [PATCH] =?UTF-8?q?atuq:=20el=20compositor=20NO=20era=20la=20causa?= =?UTF-8?q?=20=E2=80=94=20con=20cosmic-comp=20nesteado=20el=20navegador=20?= =?UTF-8?q?pinta?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit El §6.10.ter dejó dos variables cambiando a la vez (compositor y arranque real) 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 de andamio, cosmic-comp nesteado encima por winit, atuq adentro, y `grim` capturando del lado de sway. El `--control` corre EL MISMO binario de atuq —el del cierre hidratado para la imagen— directo sobre sway. Las dos capturas coinciden al píxel en la región de la página (586331 px magenta, mismo bbox), y no es que el navegador se escape a sway: antes de lanzarlo, la ventana de cosmic-comp ocupa 1276x637 en ese mismo rectángulo, y el MOZ_LOG del widget ve `mode output size 1276 x 637` con cosmic contra `1280 x 720` en el control. Las dos corridas enteras difieren en 1130 píxeles, todos en las barras de título. ⇒ cosmic-comp no es el que impide la ventana. Queda como variable el arranque de verdad: kms/DRM sobre virtio-gpu en vez de winit, el PID1 de arje, el seat. De paso queda anotado el camino de buffers cuando SÍ funciona —`WaylandBufferSHM`, memoria compartida y no dmabuf—, que es la línea de base contra la que comparar la traza de la imagen. Y `scripts/cosmic/atuq-en-imagen.py` para medir allá: 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. --- docs/26-atuq-envoltorio-gecko.md | 39 ++++++ scripts/cosmic/atuq-en-cosmic.sh | 172 +++++++++++++++++++++++++ scripts/cosmic/atuq-en-imagen.py | 210 +++++++++++++++++++++++++++++++ 3 files changed, 421 insertions(+) create mode 100755 scripts/cosmic/atuq-en-cosmic.sh create mode 100755 scripts/cosmic/atuq-en-imagen.py 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()