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()