#!/bin/sh # dunst-headless.sh — ¿la imagen de sway ATIENDE una notificación, y se VE? # # scripts/wlr/dunst-headless.sh # sale 0 si la notificación se dibuja # scripts/wlr/dunst-headless.sh --negative-control # borra el .service y exige que NO se vea # scripts/wlr/dunst-headless.sh --via-atuq # el emisor es una PÁGINA WEB en el navegador # # ── QUÉ PRUEBA, QUE NO ES LO MISMO QUE «ESTÁ INSTALADO» ─────────────────────────────────────── # Tener `libnotify` en la imagen es media función: el que la completa es un daemon escuchando # `org.freedesktop.Notifications`. KDE lo tiene por plasma-workspace, GNOME por gnome-shell, COSMIC # por cosmic-notifications; sway no tenía a nadie, y por eso entró `dunst`. Que la receta selle no # prueba nada de eso: hay que ver el bus ACTIVAR el daemon y el daemon DIBUJAR. # # El camino que se ejercita es el de verdad, no un atajo: # # notify-send → bus de sesión → activación por el .service → dunst arranca → pinta en sway # # **Nadie lanza dunst a mano acá**, a propósito: si el `.service` no estuviera, o el `Exec` apuntara # a otro lado, o dunst no supiera hablar wayland, este guion no vería nada. Es el mismo camino que # recorre una página web pidiendo notificar en `atuq`. # # ── `--via-atuq`: LA MISMA CADENA, PERO EMPEZANDO EN UNA PÁGINA WEB ─────────────────────────── # El modo por defecto prueba la cadena del SISTEMA. Éste prueba la que le importa al usuario y la # que motivó todo el hilo del SDD 26 §6.10: una página llama a `new Notification(...)`, `libxul` # hace `dlopen("libnotify.so.4")` —que no es NEEDED de ningún ELF, o sea invisible para todo # auditor—, eso habla D-Bus, el bus activa dunst y dunst dibuja. Cinco piezas, y la única forma de # saber que están las cinco es verlo. # El veredicto acá NO puede ser sólo el diff de píxeles —el navegador ocupa la pantalla y cambia # solo—, así que la captura «antes» se toma con atuq YA ABIERTO y la página espera 25 s antes de # notificar. Y se exige además que el `onshow` del objeto Notification haya disparado, que es el # motor diciendo que el sistema aceptó la notificación. # # ── POR QUÉ notify-send Y NO dunstify ───────────────────────────────────────────────────────── # Porque `dunstify` no se shipea: mezcla la GLib ESTÁTICA del corpus con la COMPARTIDA que arrastra # `libnotify.so.4` y se cae con SIGSEGV hasta en `--help` (dos copias de GObject en un proceso — el # cuadro ya conocido de este repo). El emisor de la casa es `notify-send`, que viene dentro del # artefacto de `libnotify` y enlaza toda la cadena compartida. El porqué largo está en la receta. # # ── LA EVIDENCIA ES UN DIFF DE PÍXELES, NO UN LOG ───────────────────────────────────────────── # Se captura ANTES y DESPUÉS de mandar la notificación y se cuenta cuántos píxeles cambiaron en el # cuadrante donde dunst dibuja. Un log lleno de líneas verdes es compatible con una pantalla vacía — # ya pasó en este mismo andamio— y el color solo tampoco alcanza: swaynag pinta su barra de error en # el MISMO rojo que dunst usa para `critical`, así que «hay rojo» no distingue nada. Lo que no se # puede falsear es que aparezca algo donde antes no había nada. set -eu ROOT="$(cd "$(dirname "$0")/../.." && pwd)"; cd "$ROOT" RFS="${RFS:-/mnt/cosecha/escritorios/sway-rootfs}" ALPINE="$ROOT/.dev-fs/alpine" CONTROL=0 ATUQ=0 for a in "$@"; do case "$a" in --negative-control) CONTROL=1 ;; --via-atuq) ATUQ=1 ;; *) echo "✗ opción desconocida: $a" >&2; exit 1 ;; esac done [ -d "$RFS" ] || { echo "✗ no está el rootfs en $RFS — corré antes:" >&2 echo " scripts/hydrate-profile.py escritorio-sway --into $RFS" >&2; exit 1; } command -v bwrap >/dev/null || { echo "✗ falta bwrap" >&2; exit 1; } OUT="$(mktemp -d)" trap 'rm -rf "$OUT"' EXIT cat > "$OUT/dentro.sh" <<'ADENTRO' #!/bin/sh export XDG_RUNTIME_DIR=/run/user/0 HOME=/root LANG=C XDG_DATA_DIRS=/usr/share export WLR_BACKENDS=headless WLR_HEADLESS_OUTPUTS=1 WLR_RENDERER=pixman mkdir -p "$XDG_RUNTIME_DIR" /root; chmod 700 "$XDG_RUNTIME_DIR" # El control negativo saca el fichero que hace posible la activación. La jaula corre sobre un # overlay temporal, así que borrar acá no toca el rootfs de verdad. [ "${CONTROL:-0}" = "1" ] && rm -f /usr/share/dbus-1/services/org.knopwob.dunst.service ( i=0 while [ ! -S /run/user/0/wayland-1 ] && [ $i -lt 40 ]; do sleep 1; i=$((i+1)); done [ -S /run/user/0/wayland-1 ] || { echo "@@ sin socket wayland"; exit 1; } export WAYLAND_DISPLAY=wayland-1 /usr/bin/swaybg -c '#1a4b8c' & sleep 3 # El bus se levanta CON el entorno wayland puesto: lo que él active lo hereda, igual que en una # sesión de verdad. Al revés, dunst arrancaría sin display y saldría con status 1. eval "$(/usr/bin/dbus-daemon --session --fork --print-address=1 | { read a; echo "DBUS_SESSION_BUS_ADDRESS=$a"; })" export DBUS_SESSION_BUS_ADDRESS [ "${ATUQ:-0}" = "1" ] || /usr/bin/grim /salida/antes.png echo "@@ dueño ANTES: $(timeout 10 /usr/bin/dbus-send --session --dest=org.freedesktop.DBus \ --print-reply=literal /org/freedesktop/DBus org.freedesktop.DBus.NameHasOwner \ string:org.freedesktop.Notifications 2>&1 | tr -d ' \n')" if [ "${ATUQ:-0}" = "1" ]; then export MOZ_ENABLE_WAYLAND=1 GDK_BACKEND=wayland LIBGL_DRIVERS_PATH=/usr/lib/dri # Los tres sandboxes de Gecko se apagan porque esto YA corre dentro de una jaula sin userns # anidado: con ellos puestos los procesos hijos mueren en bucle. Es entorno de prueba, no una # preferencia del artefacto. export MOZ_DISABLE_CONTENT_SANDBOX=1 MOZ_DISABLE_RDD_SANDBOX=1 MOZ_DISABLE_UTILITY_SANDBOX=1 /usr/bin/atuq --no-remote --profile /salida/perfil file:///salida/notificar.html \ > /salida/atuq.log 2>&1 & sleep 22 # que el navegador ya esté pintado ANTES de la captura «antes» /usr/bin/grim /salida/antes.png sleep 12 # la página notifica a los 25 s y repite cada 5 else timeout 25 /usr/bin/notify-send -u critical "hammer" "dunst lo activó el bus" 2>&1 echo "@@ notify-send rc=$?" sleep 5 fi echo "@@ servidor: $(timeout 10 /usr/bin/dbus-send --session --dest=org.freedesktop.Notifications \ --print-reply=literal /org/freedesktop/Notifications \ org.freedesktop.Notifications.GetServerInformation 2>&1 | tr -s ' ')" /usr/bin/grim /salida/despues.png && echo "@@ capturas listas" sleep 1 /usr/bin/swaymsg exit 2>/dev/null ) & exec /usr/bin/sway -d 2>&1 ADENTRO if [ "$ATUQ" = "1" ]; then mkdir -p "$OUT/perfil" cat > "$OUT/perfil/user.js" <<'JS' // `dump()` es el canal por el que la página le habla al log del proceso: sin red y sin servidor. user_pref("browser.dom.window.dump.enabled", true); user_pref("devtools.console.stdout.content", true); // 1 = permitir sin preguntar. Sin esto la prueba mediría el POPUP DE PERMISOS del navegador, que // se dibuja igual y no prueba nada del sistema de notificaciones. user_pref("permissions.default.desktop-notification", 1); user_pref("browser.shell.checkDefaultBrowser", false); JS cat > "$OUT/notificar.html" <<'HTML' prueba de notificación

pidiendo permiso…

HTML fi # `--unshare-pid` para que un `ps` de adentro no vea la máquina entera, y el lab de alpine DEBAJO # porque el corpus no produce el intérprete musl (`ld-musl-x86_64.so.1`): sin él ni sway arranca. timeout 180 bwrap --overlay-src "$ALPINE" --overlay-src "$RFS" --tmp-overlay / \ --bind "$OUT" /salida --dev /dev --proc /proc --tmpfs /run --tmpfs /tmp --unshare-pid \ --uid 0 --gid 0 --setenv PATH /usr/bin:/bin:/usr/sbin:/sbin --setenv CONTROL "$CONTROL" \ --setenv ATUQ "$ATUQ" \ /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; cp "$OUT/log.txt" work/dunst-headless.log 2>/dev/null || true; exit 1; } # El diff se hace acá afuera: dentro de la imagen no hay python ni con qué mirar un PNG. CAMBIADOS="$(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]) # Sólo el cuadrante donde dunst dibuja por defecto (arriba a la derecha). Acotarlo saca del medio la # barra de swaynag y el reloj de swaybar, que cambian solos y darían un falso positivo. w, h = a.size caja = (w // 2, h // 12, w, h // 2) d = ImageChops.difference(a.crop(caja), b.crop(caja)).convert("L") # Por histograma y no recorriendo píxeles: el bin 0 son los que NO cambiaron. print(d.width * d.height - d.histogram()[0]) PY )" echo " píxeles cambiados en el cuadrante de dunst: $CAMBIADOS" if [ "$CONTROL" = "1" ]; then # El control se lee distinto según el modo, y no por comodidad: en `--via-atuq` el navegador ocupa # la pantalla y repinta solo —el diff de píxeles cambia miles aunque no haya UNA notificación—, así # que ahí lo que tiene que faltar es el `onshow` del motor. En el modo normal la pantalla está # quieta y el diff sí es el veredicto. if [ "$ATUQ" = "1" ]; then if grep -aq 'NOTIF-TEST MOSTRADA' "$OUT/atuq.log" 2>/dev/null; then echo "✗ control negativo: el navegador dijo MOSTRADA sin el .service — este guion no prueba lo que dice" >&2 exit 1 fi echo "✓ control negativo: sin el .service el navegador nunca llega a MOSTRADA" grep -a 'NOTIF-TEST' "$OUT/atuq.log" 2>/dev/null | tail -2 || true exit 0 fi if [ "$CAMBIADOS" -lt 500 ]; then echo "✓ control negativo: sin el .service no se dibuja nada (el guardián SÍ lo vería)"; exit 0 fi echo "✗ control negativo: se dibujó una notificación SIN el .service — este guion no prueba lo que dice" >&2 exit 1 fi if [ "$ATUQ" = "1" ]; then # El motor diciendo que el sistema ACEPTÓ la notificación. Es la mitad que el diff de píxeles no # puede distinguir de «el navegador repintó algo». grep -aq 'NOTIF-TEST MOSTRADA' "$OUT/atuq.log" || { echo "✗ el navegador nunca llegó a MOSTRADA — ¿libxul no encontró libnotify.so.4?" >&2 grep -a 'NOTIF-TEST' "$OUT/atuq.log" | tail -3 >&2 || true cp "$OUT/atuq.log" work/ 2>/dev/null || true; exit 1; } echo " el motor: $(grep -a 'NOTIF-TEST MOSTRADA' "$OUT/atuq.log" | tail -1)" fi [ "$CAMBIADOS" -ge 500 ] || { echo "✗ la notificación no se dibujó ($CAMBIADOS píxeles) — capturas en $OUT" >&2 cp "$OUT"/*.png "$OUT/log.txt" work/ 2>/dev/null || true; exit 1; } cp "$OUT/despues.png" work/dunst-headless.png 2>/dev/null || true echo "✓ el bus activó dunst y la notificación se dibujó (captura en work/dunst-headless.png)"