atuq: un vídeo se abre en el reproductor de la distro, no en una pestaña

El 6.6 de la unidad 9 del SDD 26. Una navegación de PRIMER NIVEL a un medio
(`Content-Type: video/*` o `audio/*`) se cancela y la URL se la lleva `mpv`, que ya viaja en las
cuatro imágenes de escritorio: esto no agrega ni una receta, es cablear lo que ya estaba.

    MEDIO http://…/audio.wav
    CANCELADA la navegación http://…/audio.wav
    ABIERTO /usr/bin/mpv pid=266 http://…/audio.wav
    mpv: AO: [null] 8000Hz mono 1ch u8        ← su propio log: DECODIFICÓ, no sólo arrancó

LA REGLA ES ESTRECHA A PROPÓSITO: sólo el primer nivel. Un `<video>` embebido es parte de la página y
sacarlo de ahí rompería el sitio que lo puso. Las reglas anchas en el camino de cada petición son las
que terminan rompiendo la web de alguien.

FAIL-OPEN, y es lo que prueba el control negativo: cancelar es SÍNCRONO y la respuesta del host llega
después, así que no se puede saber en el momento si el reproductor arrancó. Se cancela sólo con el
puerto vivo y, si el host contesta que no pudo, la URL VUELVE al navegador con una marca para no
entrar en bucle:

    SIN REPRODUCTOR no hay reproductor en /usr/bin/mpv: esta imagen no lo trae
    VUELVE AL NAVEGADOR http://…/audio.wav?atuq-medios=no

Quedarse sin vídeo Y sin pestaña es el único resultado inaceptable, y es justo el que se consigue si
uno confía en que salió bien.

⚠ EL BUG QUE ESTE GUARDIÁN DESTAPÓ VALE MÁS QUE LA FUNCIÓN, y estaba DESPUÉS del éxito aparente: la
primera corrida detectó el medio, canceló y recibió el pid… y después el puerto murió con «Native
application tried to send a message of 546281442 bytes». **Un hijo hereda los descriptores del padre,
y los del padre SON la tubería de native messaging**: `mpv` escribía su salida ahí y el navegador la
leía como un marco. Arreglado en tawasuyu (`2e99d216`, pineado acá) con tres redirecciones, cada una
contra un fallo distinto — `stdin` a null porque mpv LEE stdin y se comería los mensajes del
navegador; `stdout` al stderr del host y no a `/dev/null`, porque apagarlo arreglaría el bug y se
llevaría puesto el diagnóstico; `stderr` heredado por lo mismo.

Y dos cosas del ARNÉS, las dos ya vistas antes en esta misma sesión:
· la URL NO se pasa por la línea de comandos: una petición del arranque compite con la
  inicialización de la extensión (la misma carrera del guardián de `sct`). El guardián navega a una
  página que redirige a los 3 s, que además es lo que hace una persona: seguir un enlace;
· la evidencia de que el reproductor CORRIÓ se le pide a su propio `log-file`, porque su salida ya no
  va al stdout del host y Gecko no vuelca el stderr del host al del navegador. Sin eso sólo se sabría
  que hubo un `spawn`.

Del §6 quedan el foco (6.5), la IA local (6.7) —que además es la que traería el motor que le falta al
archivo del §6.3— y el torrent (6.9). Medido de paso: **el corpus no tiene ninguna receta de LLM ni de
embeddings**, así que 6.7 hoy no se puede pagar.

Medido sobre `atuq b3:a7080058` y `puriy-costura b3:060314e6`.
This commit is contained in:
Sergio
2026-09-10 15:45:08 +00:00
parent f9dae780fa
commit 928d5eb119
8 changed files with 476 additions and 3 deletions
+49 -1
View File
@@ -681,6 +681,54 @@ eso; el modo incógnito es teatro. ⚠ Con la advertencia que el propio SDD de `
**hay que repetir acá, no esconder**: en una máquina con swap sin cifrar, el documento no aplica.
Se promete lo que se puede probar.
### 6.6 Medios fuera del navegador — HECHO (2026-09-10)
Una navegación de PRIMER NIVEL a un medio (`Content-Type: video/*` o `audio/*`) se cancela y la URL se
la lleva el reproductor de la distro. `mpv` ya viaja en las cuatro imágenes de escritorio, así que
esto no agrega ni una receta: es cablear lo que ya estaba.
**La regla es estrecha a propósito.** Un `<video>` embebido es parte de la página y sacarlo de ahí
rompería el sitio que lo puso; lo que se cambia es el caso en que el navegador no estaba pintando una
página sino haciendo de reproductor —seguiste un enlace a un `.mp4`—. Las reglas anchas en el camino
de cada petición son las que terminan rompiendo la web de alguien.
**La seguridad del verbo es que el binario NO viene del cable**: es una constante del host
(`/usr/bin/mpv`). Si el programa viajara en el mensaje, esto dejaría de ser «abrir un vídeo» y sería
un lanzador de procesos arbitrarios manejado, al final de la cadena, por una página web. Sólo viaja la
URL, sólo `http(s)`, y detrás de un `--`.
**Fail-open, y es la promesa que el control negativo prueba.** Cancelar es SÍNCRONO y la respuesta del
host llega después, así que no se puede saber en el momento si el reproductor arrancó. Se cancela sólo
si el puerto está vivo, y si el host contesta que no pudo, la URL **vuelve al navegador** (con una
marca para no entrar en bucle). Medido:
SIN REPRODUCTOR no hay reproductor en /usr/bin/mpv: esta imagen no lo trae
VUELVE AL NAVEGADOR http://…/audio.wav?atuq-medios=no
Quedarse sin vídeo Y sin pestaña es el único resultado inaceptable, y es el que se consigue si uno
confía en que salió bien.
**⚠ Y el bug que esto destapó vale más que la función.** La primera corrida hizo todo bien —detectó el
medio, canceló, el host contestó con el pid— y después:
DESCONECTADO Native application tried to send a message of 546281442 bytes,
which exceeds the limit of 1048576 bytes.
**Un hijo hereda los descriptores del padre, y los del padre SON la tubería de native messaging.** El
reproductor escribía su salida ahí y el navegador la leía como un marco. Tres redirecciones, cada una
contra un fallo distinto: `stdin` a null (`mpv` lee stdin para sus comandos: heredándolo **se come los
mensajes del navegador**), `stdout` al **`stderr` del host** —no a `/dev/null`: apagarlo arreglaría el
bug y se llevaría puesto el diagnóstico, y Gecko manda ese `stderr` al log del navegador, que es donde
uno mira cuando «el vídeo no abrió»— y `stderr` heredado por lo mismo.
El test que lo fija **arranca el binario, no la lib**, y ésa es la parte reutilizable: en proceso, el
`stdout` del host es el del arnés y la contaminación no se ve — el test habría pasado con el bug
puesto. Se comprobó rompiéndolo a propósito.
⚠ Y una nota del arnés que ya apareció dos veces: la URL no se pasa por la línea de comandos, porque
una petición del ARRANQUE compite con la inicialización de la extensión. El guardián navega a una
página que redirige a los tres segundos — que además es lo que hace una persona: seguir un enlace.
### 6.8 Proxy por contenedor — HECHO (v0.5, 2026-09-06)
**Es el primer diferenciador del §6 que se paga entero**, y se pudo pagar ahora porque es el único
@@ -1304,7 +1352,7 @@ siguiente.
| 6 | **`sct` v1 — HECHA 2026-09-10.** Extensión `sct@atuq.tawasuyu` (`filterResponseData` → native messaging) + el manifiesto en `/usr/lib/mozilla/native-messaging-hosts/` + el lanzador que le pone el `--state` que el manifiesto no puede llevar. Guardián `scripts/test-atuq-sct.py`: servidor HTTP real, seis cargas, **una carga = una visita** vigilado, la insignia releída, y dos controles (la 5ª repite el mismo script y no alerta; sin manifiesto no hay veredicto) | el diferenciador que nadie tiene, andando | 5 ✅ |
| 7 | **Descargas al CAS — HECHA 2026-09-10.** `cas.ingest`/`cas.list` en el host + la extensión `descargas@atuq.tawasuyu`; ingesta en STREAMING (`arje-cas::almacenar_fichero_en`, nueva) para no cargar una ISO entera en RAM; raíz propia porque el `GcCas` de arje borraría las descargas en silencio; el fichero del usuario no se toca. Guardián `scripts/test-atuq-descargas.py` con descarga real y control negativo | 6.2, y alimenta 6.9 | 5 ✅ |
| 8 | **Archivo personal — la mitad de congelar y buscar, HECHA 2026-09-10.** `archive.add`/`archive.search` + la extensión `archivo@atuq.tawasuyu`: el HTML ya ejecutado al CAS, el índice buscable en JSON, y nada de ventanas privadas. Guardián `scripts/test-atuq-archivo.py`, cuyo control negativo además **dice quién** impidió archivar lo privado. **Falta la mitad semántica**: un motor `rag-motor::RagMotor` (como `willay-rag`), que exige daemon de embeddings + LLM real | 6.3 | 5 ✅, 7 ✅ |
| 9 | Torrent, medios, foco | 6.56.7, 6.9 | 5 |
| 9 | **Medios fuera del navegador (6.6) — HECHA 2026-09-10**: `media.open` + la extensión `medios@atuq.tawasuyu`; una navegación de primer nivel a `video/*`/`audio/*` la abre `mpv`, que ya viaja en las cuatro imágenes. Fail-open probado: sin reproductor la navegación VUELVE al navegador. Destapó que un hijo hereda la tubería del protocolo. **Faltan** el foco (6.5), la IA local (6.7) y el torrent (6.9) | 6.56.7, 6.9 | 5 |
| 9.a | **Proxy por contenedor ✅ v0.5** — contenedores por política + extensión con `proxy.onRequest` | 6.8, y NO dependía de 5: es API de Firefox | — |
Las unidades 2 y 4 son **paralelizables**: la toolchain no toca el chrome y el chrome no toca la
+20
View File
@@ -27,6 +27,7 @@ el vigente (por eso el ciclo es: editar todo → resellar una vez → medir), y
| `extensions/sct/` | `<appdir>/distribution/extensions/sct@atuq.tawasuyu.xpi` | **Transparencia de scripts** (SDD 26 §6.1). |
| `extensions/descargas/` | `<appdir>/distribution/extensions/descargas@atuq.tawasuyu.xpi` | **Descargas con identidad** (SDD 26 §6.2). |
| `extensions/archivo/` | `<appdir>/distribution/extensions/archivo@atuq.tawasuyu.xpi` | **Archivo personal** (SDD 26 §6.3). |
| `extensions/medios/` | `<appdir>/distribution/extensions/medios@atuq.tawasuyu.xpi` | **Medios fuera del navegador** (SDD 26 §6.6). |
| `native-messaging/*.json` | `/usr/lib/mozilla/native-messaging-hosts/` | El manifiesto del host nativo. **Fuera del appdir** — ver abajo. |
| `bin/puriy-costura-host` | `<appdir>/puriy-costura-host` | El lanzador del host: le pone el `--state` que el manifiesto no puede. |
@@ -219,6 +220,25 @@ del mismo lado y esto no cambia.
ventana privada, exige que no se archive nada **y dice quién lo impidió** — nuestro código o el
navegador—, porque las dos protegen pero sólo una es nuestra.
## `medios` — el vídeo no se ve en una pestaña (§6.6)
Cuando una navegación de **primer nivel** resulta ser un medio —el servidor contesta `video/*` o
`audio/*`—, la navegación se cancela y la URL se la lleva `mpv`, que ya viaja en las cuatro imágenes
de escritorio. El navegador vuelve a ser un navegador en vez de un reproductor a medias metido en una
pestaña.
**Sólo el primer nivel**: un `<video>` embebido es parte de la página, y sacarlo de ahí rompería el
sitio que lo puso. Las reglas anchas en el camino de cada petición son las que terminan rompiendo la
web de alguien.
**Si no hay reproductor, la navegación VUELVE al navegador.** Cancelar es síncrono y la respuesta del
host llega después, así que no se puede saber en el momento si arrancó; se cancela sólo con el puerto
vivo y, ante un fallo, se reabre la URL con una marca para no entrar en bucle. Quedarse sin vídeo y
sin pestaña es el único resultado inaceptable, y el control negativo del guardián lo prueba.
⚠ El binario **no viene del mensaje**: es una constante del host. Lo único que viaja es la URL, sólo
`http(s)`, y detrás de un `--`.
## Por qué el CSS no es `userChrome.css`
`userChrome.css` vive en el PERFIL del usuario y exige que el usuario prenda
+4
View File
@@ -47,6 +47,10 @@
"archivo@atuq.tawasuyu": {
"installation_mode": "normal_installed",
"install_url": "file:///usr/lib/atuq/distribution/extensions/archivo@atuq.tawasuyu.xpi"
},
"medios@atuq.tawasuyu": {
"installation_mode": "normal_installed",
"install_url": "file:///usr/lib/atuq/distribution/extensions/medios@atuq.tawasuyu.xpi"
}
}
}
+121
View File
@@ -0,0 +1,121 @@
"use strict";
// medios — el vídeo no se ve en una pestaña (SDD 26 §6.6), v1.
//
// Cuando una navegación de PRIMER NIVEL resulta ser un medio —el servidor contesta
// `Content-Type: video/*` o `audio/*`—, la navegación se cancela y la URL se le pasa
// al host, que abre el reproductor de la distro. El navegador vuelve a ser un
// navegador: no un reproductor a medias metido en una pestaña.
//
// ── POR QUÉ SÓLO EL PRIMER NIVEL, Y NO LOS `<video>` DE UNA PÁGINA ────────────
// Un `<video>` embebido es parte de la página: sacarlo de ahí rompería el sitio que
// lo puso. Lo que esta extensión cambia es el caso en que el navegador NO estaba
// pintando una página, sino haciendo de reproductor: seguiste un enlace a un `.mp4`.
// Es una regla estrecha a propósito — las reglas anchas en el camino de cada
// petición son las que terminan rompiendo la web de alguien.
//
// ── LA REGLA DE ORO: SI NO HAY REPRODUCTOR, NO SE CANCELA NADA ────────────────
// La decisión de cancelar es SÍNCRONA y la respuesta del host llega después, así que
// no se puede saber en el momento si el reproductor arrancó. Se cancela de forma
// optimista **sólo si el puerto está vivo**, y si el host contesta que no pudo, la
// URL se vuelve a abrir en el navegador. Quedarse sin vídeo y sin pestaña sería el
// peor resultado posible, y es el que se consigue si uno confía en que salió bien.
const HOST = "puriy_costura";
// Los tipos que se sacan del navegador. Deliberadamente cortos: `application/*` no
// entra —hay demasiadas cosas que se sirven así— y los manifiestos de streaming
// tampoco por ahora, porque el reproductor los pide con otras cabeceras.
const MEDIOS = ["video/", "audio/"];
let puerto = null;
// url → tabId, para poder devolver al navegador lo que el host no pudo abrir.
const enVuelo = new Map();
function di(s) {
try { dump("MEDIOS " + s + "\n"); } catch (e) {}
}
function conectar() {
try {
puerto = browser.runtime.connectNative(HOST);
puerto.onMessage.addListener(alRecibir);
puerto.onDisconnect.addListener(p => {
di("DESCONECTADO " + (p.error ? p.error.message : "sin error"));
puerto = null;
});
di("CONECTADO " + HOST);
} catch (e) {
di("SIN HOST " + e.message);
puerto = null;
}
}
function alRecibir(r) {
if (!r || r.verb !== "media.open") {
if (r && r.ok === false) { fracaso(r); }
return;
}
if (r.ok === true && r.player) {
di("ABIERTO " + r.player.program + " pid=" + r.player.pid + " " + r.player.url);
enVuelo.delete(r.player.url);
return;
}
fracaso(r);
}
// El host no pudo. La navegación ya está cancelada, así que hay que devolverla: sin
// esto el usuario se queda sin vídeo Y sin pestaña.
function fracaso(r) {
di("SIN REPRODUCTOR " + (r && r.error ? r.error : "sin error"));
for (const [url, tabId] of enVuelo) {
enVuelo.delete(url);
try {
// `?atuq-medios=no` marca la reapertura para no volver a cancelarla: sin esa
// marca esto sería un bucle —cancelar, reabrir, cancelar— con la pestaña
// parpadeando y sin vídeo.
const vuelta = url + (url.includes("?") ? "&" : "?") + "atuq-medios=no";
browser.tabs.update(tabId, { url: vuelta });
di("VUELVE AL NAVEGADOR " + vuelta);
} catch (e) {
di("NO PUDE DEVOLVERLA " + e.message);
}
}
}
function esMedio(cabeceras) {
for (const h of cabeceras || []) {
if (h.name.toLowerCase() === "content-type") {
const v = (h.value || "").toLowerCase();
return MEDIOS.some(m => v.startsWith(m));
}
}
return false;
}
browser.webRequest.onHeadersReceived.addListener(
d => {
try {
if (d.url.includes("atuq-medios=no")) { return {}; } // ya volvió una vez
if (!esMedio(d.responseHeaders)) { return {}; }
if (!puerto) { conectar(); }
if (!puerto) {
// Sin host no se cancela: mejor un vídeo en una pestaña que ningún vídeo.
di("SIN HOST, no se cancela " + d.url);
return {};
}
di("MEDIO " + d.url);
enVuelo.set(d.url, d.tabId);
puerto.postMessage({ verb: "media.open", url: d.url });
di("CANCELADA la navegación " + d.url);
return { cancel: true };
} catch (e) {
di("FALLÓ el filtro " + e.message);
return {};
}
},
{ urls: ["http://*/*", "https://*/*"], types: ["main_frame"] },
["blocking", "responseHeaders"]
);
conectar();
di("LISTA v1 saca del navegador sólo la navegación de primer nivel");
@@ -0,0 +1,29 @@
{
"manifest_version": 2,
"name": "atuq — medios fuera del navegador",
"description": "Un vídeo o un audio se abren en el reproductor de la distro, no en una pestaña (SDD 26 §6.6).",
"version": "1.0",
"browser_specific_settings": {
"gecko": {
"id": "medios@atuq.tawasuyu",
"strict_min_version": "115.0"
}
},
"permissions": [
"webRequest",
"webRequestBlocking",
"nativeMessaging",
"tabs",
"<all_urls>"
],
"background": {
"scripts": ["fondo.js"]
},
"browser_action": {
"default_title": "Medios fuera del navegador",
"default_icon": "atuq128.png"
},
"icons": {
"128": "atuq128.png"
}
}
@@ -6,6 +6,7 @@
"allowed_extensions": [
"sct@atuq.tawasuyu",
"descargas@atuq.tawasuyu",
"archivo@atuq.tawasuyu"
"archivo@atuq.tawasuyu",
"medios@atuq.tawasuyu"
]
}
+1 -1
View File
@@ -43,7 +43,7 @@ license = "MIT OR Apache-2.0"
[source]
repo = "gitea@git.tawasuyu.net:tawasuyu/tawasuyu.git"
commit = "357791a851b8b917c59532af4705c3db705e867f"
commit = "2e99d21675de7aa3b60bf4d9d22fd4c8b76ffe0c"
# ⚠ SIN ESTO EL BUILD MUERE EN UN SITIO QUE NO NOMBRA LA CAUSA — medido acá el 2026-09-10.
# tawasuyu COMMITEA su propio `vendor/`: adentro tiene un `smithay` parcheado a mano (tearing/flip
# async para el compositor), y lo enchufa con `[patch.crates-io] smithay = { path = "vendor/smithay" }`.
+250
View File
@@ -0,0 +1,250 @@
#!/usr/bin/env python3
"""¿Un medio se abre en el reproductor de la distro y no en una pestaña? (SDD 26 §6.6)
python3 scripts/test-atuq-medios.py # el vídeo sale por mpv
python3 scripts/test-atuq-medios.py --negative-control # SIN mpv: la navegación vuelve al navegador
── QUÉ SE EJERCITA ───────────────────────────────────────────────────────────────────────────
Un servidor HTTP sirve un audio real (`audio/wav`); el navegador navega a él; la extensión ve por la
cabecera que es un medio, **cancela la navegación** y le pasa la URL al host, que lanza el reproductor
de la distro. Se comprueban las tres cosas: que la extensión lo detectó y canceló, que el host
contestó con el **pid** del proceso, y que **el reproductor de verdad abrió esa URL** — su propia
salida aparece en el log, porque hereda la del navegador.
── EL CONTROL NEGATIVO PRUEBA LA PROMESA QUE MÁS IMPORTA ─────────────────────────────────────
`--negative-control` borra `/usr/bin/mpv` dentro de la jaula y exige que la navegación **vuelva al
navegador**. La decisión de cancelar es síncrona y la respuesta del host llega después, así que el
único modo de fallo inaceptable es quedarse sin vídeo Y sin pestaña. Sin este control, una extensión
que cancelara siempre se vería idéntica a una que funciona — hasta el día que falte el reproductor.
REQUISITOS: el rootfs de un escritorio (el mínimo de `atuq-nested.sh` NO trae `mpv`), y `atuq` +
`puriy-costura` sellados.
"""
import http.server
import os
import shutil
import socket
import struct
import subprocess
import sys
import tempfile
import threading
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
RFS = os.environ.get("RFS", "/mnt/cosecha/escritorios/sway-rootfs")
ALPINE = os.path.join(ROOT, ".dev-fs/alpine")
HAMMER = os.path.join(ROOT, "target/release/takana")
STORE = os.environ.get("HAMMER_STORE", os.path.join(ROOT, "store"))
MPV = "/usr/bin/mpv"
def fatal(msg):
print(f"{msg}", file=sys.stderr)
raise SystemExit(1)
def artefacto(receta, nombre, escotilla=None):
forzado = os.environ.get(escotilla) if escotilla else None
if forzado:
if not os.path.isdir(forzado):
fatal(f"{escotilla}={forzado} no existe")
print(f"{escotilla}: se mide {os.path.basename(forzado)}, que NO es necesariamente el vigente")
return forzado
r = subprocess.run([HAMMER, "--store", STORE, "hash", f"recipes/{receta}.toml"],
cwd=ROOT, capture_output=True, text=True)
if r.returncode != 0:
fatal(f"no pude hashear {receta}: {r.stderr.strip()}")
h = r.stdout.strip().splitlines()[-1].removeprefix("b3:")
d = os.path.join(STORE, f"{h}-{nombre}")
if not os.path.isdir(d):
fatal(f"{nombre} b3:{h[:8]} no está sellado — construilo antes")
return d
def wav(segundos=0.3, hz=8000):
"""Un WAV real, hecho a mano: sin esto habría que traer un binario de prueba al
repo, y un fichero opaco en un test es una dependencia que nadie revisa."""
n = int(segundos * hz)
datos = b"\x00" * n
cab = (b"RIFF" + (36 + len(datos)).to_bytes(4, "little") + b"WAVEfmt "
+ (16).to_bytes(4, "little") + (1).to_bytes(2, "little") + (1).to_bytes(2, "little")
+ hz.to_bytes(4, "little") + hz.to_bytes(4, "little")
+ (1).to_bytes(2, "little") + (8).to_bytes(2, "little")
+ b"data" + len(datos).to_bytes(4, "little"))
return cab + datos
AUDIO = wav()
class Servidor(http.server.SimpleHTTPRequestHandler):
def log_message(self, *a):
pass
def do_GET(self):
if self.path.startswith("/audio.wav"):
datos, tipo = AUDIO, "audio/wav"
elif self.path.startswith("/ir.html"):
# ⚠ NO se navega al medio desde la línea de comandos, y la razón está
# medida: una URL en el argumento se pide en el ARRANQUE y compite con la
# inicialización de la extensión — la misma carrera que ya apareció en el
# guardián de `sct`, sólo que acá hay una sola carga y no hay segunda
# oportunidad. Esta página espera tres segundos y recién entonces va al
# medio, que además es lo que hace una persona: seguir un enlace.
datos = (b'<!doctype html><html><head><meta charset="utf-8">'
b'<meta http-equiv="refresh" content="3;url=/audio.wav">'
b"</head><body>yendo al medio</body></html>")
tipo = "text/html; charset=utf-8"
else:
datos, tipo = b"<html><body>no soy un medio</body></html>", "text/html"
self.send_response(200)
self.send_header("Content-Type", tipo)
self.send_header("Content-Length", str(len(datos)))
self.send_header("Cache-Control", "no-store")
self.end_headers()
self.wfile.write(datos)
def puerto_libre():
s = socket.socket()
s.bind(("127.0.0.1", 0))
p = s.getsockname()[1]
s.close()
return p
def main():
negativo = "--negative-control" in sys.argv[1:]
if negativo:
print("== MODO CONTROL NEGATIVO: sin mpv; la navegación tiene que VOLVER al navegador")
if not os.path.isdir(RFS):
fatal(f"no está el rootfs en {RFS} — hidratá un perfil de escritorio (trae `mpv`)")
if not os.path.isfile(os.path.join(RFS, MPV.lstrip("/"))):
fatal(f"{RFS} no trae {MPV}: este guardián necesita el reproductor de la imagen")
atuq = artefacto("atuq", "atuq", "ATUQ_DIR")
costura = artefacto("puriy-costura", "puriy-costura")
print(f"artefactos: {os.path.basename(atuq)}\n {os.path.basename(costura)}")
puerto = puerto_libre()
servidor = http.server.ThreadingHTTPServer(("127.0.0.1", puerto), Servidor)
threading.Thread(target=servidor.serve_forever, daemon=True).start()
url = f"http://127.0.0.1:{puerto}/audio.wav"
partida = f"http://127.0.0.1:{puerto}/ir.html"
tmp = tempfile.mkdtemp(prefix="atuq-medios-")
salida = os.path.join(tmp, "salida")
os.makedirs(os.path.join(salida, "perfil"))
# La configuración del reproductor la pone el TEST, no el producto: sin salida de
# vídeo ni de audio (no hay pantalla ni tarjeta en la jaula) y con un tope de
# duración para que termine solo. Meter esto en el host sería cablear el arnés
# dentro del artefacto.
conf = os.path.join(salida, "hogar", ".config", "mpv")
os.makedirs(conf)
with open(os.path.join(conf, "mpv.conf"), "w") as fh:
# `log-file` es la evidencia: la salida del reproductor ya NO va al stdout del
# host —eso era el bug que este guardián encontró, ver §6.6— y Gecko no vuelca
# el stderr del host al del navegador, así que hay que pedirle al propio
# reproductor que deje su rastro donde se pueda leer.
fh.write("vo=null\nao=null\nlength=0.2\nmsg-level=all=info\nlog-file=/salida/mpv.log\n")
try:
with open(os.path.join(salida, "perfil", "user.js"), "w") as fh:
fh.write('user_pref("browser.dom.window.dump.enabled", true);\n')
fh.write('user_pref("browser.shell.checkDefaultBrowser", false);\n')
borrar = f"rm -f {MPV}" if negativo else "true"
guion = f"""
mkdir -p /root /salida/hogar
[ -s /etc/machine-id ] || tr -d - < /proc/sys/kernel/random/uuid > /etc/machine-id
export HOME=/salida/hogar
{borrar}
timeout 50 /usr/bin/atuq --profile /salida/perfil --no-remote --headless '{partida}' \
> /salida/consola.log 2>&1
exit 0
"""
subprocess.run([
"bwrap",
"--overlay-src", ALPINE, "--overlay-src", RFS,
"--overlay-src", atuq, "--overlay-src", costura,
"--tmp-overlay", "/",
"--dev-bind", "/dev", "/dev", "--proc", "/proc", "--bind", "/sys", "/sys",
"--tmpfs", "/tmp", "--tmpfs", "/run", "--unshare-pid",
"--bind", salida, "/salida",
"--uid", "0", "--gid", "0",
"--setenv", "PATH", "/usr/bin:/bin:/usr/sbin:/sbin",
"--setenv", "XDG_RUNTIME_DIR", "/run",
"--setenv", "XDG_DATA_DIRS", "/usr/share", "--setenv", "MOZ_HEADLESS", "1",
"--setenv", "MOZ_DISABLE_CONTENT_SANDBOX", "1",
"--setenv", "MOZ_DISABLE_GMP_SANDBOX", "1",
"--setenv", "MOZ_DISABLE_RDD_SANDBOX", "1",
"--setenv", "MOZ_DISABLE_SOCKET_PROCESS_SANDBOX", "1",
"--setenv", "MOZ_DISABLE_UTILITY_SANDBOX", "1",
"sh", "-c", guion,
], check=False)
log = open(os.path.join(salida, "consola.log"), errors="replace").read()
lineas = [l[len("MEDIOS "):] for l in log.splitlines() if l.startswith("MEDIOS ")]
for l in lineas:
print(" " + l)
if not lineas:
destino = os.path.join(ROOT, "work", "atuq-medios.log")
os.makedirs(os.path.dirname(destino), exist_ok=True)
open(destino, "w").write(log)
fatal(f"la extensión no dijo nada — log en {destino}")
detecto = any(l.startswith("MEDIO ") for l in lineas)
cancelo = any(l.startswith("CANCELADA ") for l in lineas)
abierto = [l for l in lineas if l.startswith("ABIERTO ")]
volvio = [l for l in lineas if l.startswith("VUELVE AL NAVEGADOR")]
sin_repro = [l for l in lineas if l.startswith("SIN REPRODUCTOR")]
if not detecto:
fatal("la extensión no reconoció el `audio/wav` como medio")
if negativo:
if abierto:
fatal(f"sin mpv dijo que abrió algo: {abierto}")
if not sin_repro:
fatal("sin mpv, el host tenía que fallar DICIÉNDOLO y la extensión decirlo")
if not volvio:
fatal("sin reproductor, la navegación NO volvió al navegador —"
" el usuario se quedaría sin vídeo y sin pestaña")
print("\n✓ control negativo: sin reproductor no se finge nada y la navegación vuelve"
" al navegador")
return 0
if not cancelo:
fatal("la extensión no canceló la navegación: el vídeo seguiría en una pestaña")
if not abierto:
fatal("el host no confirmó haber abierto el reproductor")
if MPV not in abierto[0]:
fatal(f"abrió algo que no es el reproductor de la imagen: {abierto[0]}")
if url not in abierto[0]:
fatal(f"abrió otra URL: {abierto[0]}")
if volvio:
fatal(f"volvió al navegador habiendo reproductor: {volvio}")
# Y la prueba de que el reproductor CORRIÓ de verdad, y no que hubo un `spawn`:
# su propio log. Su salida ya no va al stdout del host (eso era el bug) y Gecko
# no vuelca el stderr del host al del navegador, así que la evidencia se le pide
# al reproductor.
ruta_mpv = os.path.join(salida, "mpv.log")
if not os.path.isfile(ruta_mpv):
destino = os.path.join(ROOT, "work", "atuq-medios.log")
os.makedirs(os.path.dirname(destino), exist_ok=True)
open(destino, "w").write(log)
fatal(f"el reproductor no dejó su log: no llegó a correr — consola en {destino}")
propio = open(ruta_mpv, errors="replace").read()
if url not in propio:
fatal(f"el reproductor corrió pero no abrió esa URL — su log dice:\n{propio[:400]}")
for l in [x for x in propio.splitlines() if "Playing" in x or "AO:" in x][:3]:
print(" mpv: " + l.strip()[:110])
print("\n✓ una navegación a un medio se CANCELA y la abre el reproductor de la distro")
print("✓ y el reproductor corrió de verdad: su propia salida lo dice, no un `spawn` a ciegas")
return 0
finally:
servidor.shutdown()
shutil.rmtree(tmp, ignore_errors=True)
sys.exit(main())