El README de la receta afirmaba (a) que las extensiones las instala la POLÍTICA porque el sideloading desde `distribution/extensions/` «ya no funciona», y (b) que el chrome de verdad —split view— exige entrar a `omni.ja` y escribirlo nosotros. Las dos son falsas, y ninguna observación del artefacto las distinguía: hubo que romper un mecanismo por vez. scripts/test-atuq-instalacion.py — tres escenarios: as-is nada roto ⇒ las dos extensiones puestas no-policy `policies.json` sin ExtensionSettings ⇒ SIGUEN puestas policy-only los XPI fuera de la carpeta ⇒ NINGUNA, y la home vuelve a about:home ⇒ instala el ESCANEO de la carpeta; `install_url` con `file://` no instala nada. El §7.bis del SDD 26 tenía razón. El control es por construcción: la sonda vive en la carpeta y ninguna política la nombra, así que una corrida muda se declara ROTA en vez de leerse como «no instaló». Y la segunda señal que probé NO discrimina, queda dicho: el `location` de `extensions.json` sale `app-profile` también cuando instala la carpeta. scripts/test-atuq-chrome.py — el chrome se programa desde `atuq.cfg`, sin abrir el zip: observando `browser-delayed-startup-finished` se toca el `gBrowser` de cada ventana. Y la vista dividida ya la trae el motor (fx 154) PRENDIDA de fábrica, ejercitada de verdad —`addTabSplitView` ⇒ `activeSplitView` + 2 navegadores—, no «el fichero está». Dos pendientes que eran de upstream. Control negativo del propio motor: con las pestañas fijadas devuelve null (WRAPPER null, ACTIVA no). Corolario para lo que venga: cada función del chrome que NO entre a `omni.ja` es una que no pelea con el orden del `jarlog` del PGO — que es justo lo que tiene al jarlog aparcado. Queda escrito lo que NO está: ninguno de los `test-atuq-*` corre en el latido (sólo lo hace `vigia-sonames.py`), y meterlos cuesta ~3 min por ciclo más el rootfs hidratado en el hub. Medido sobre atuq b3:fab2fbfb → b3:8f6d09c2 (el README entra en el hash: documentar la medición re-hashea el artefacto medido, y los guardianes se niegan a medir uno que no sea el vigente).
264 lines
12 KiB
Python
Executable File
264 lines
12 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""¿QUIÉN instala de verdad las extensiones de `atuq`: la política o la carpeta? (SDD 26 §7.bis)
|
|
|
|
python3 scripts/test-atuq-instalacion.py # los tres escenarios y el veredicto
|
|
python3 scripts/test-atuq-instalacion.py --scenario as-is # uno solo, para diagnosticar
|
|
|
|
── POR QUÉ EXISTE ESTA PRUEBA ────────────────────────────────────────────────────────────────
|
|
Dos documentos nuestros se contradecían sobre el mecanismo del que dependen TODAS las extensiones
|
|
de atuq —hoy `inicio` y `proxy`, mañana la de `sct`—:
|
|
|
|
· `recipes/atuq/README.md` decía que el *sideloading* desde `distribution/extensions/` «ya no
|
|
funciona» y que lo que instala es `ExtensionSettings.install_url` de la política;
|
|
· el §7.bis del SDD 26 midió lo contrario: que `install_url` con `file://` no instala nada y que
|
|
quien instala es el escaneo de la carpeta.
|
|
|
|
Los dos mecanismos APUNTAN A LOS MISMOS FICHEROS, así que ninguna observación del artefacto los
|
|
distingue: hay que romper uno por vez y mirar qué pasa. Eso es lo que hace este guión.
|
|
|
|
── LOS TRES ESCENARIOS ───────────────────────────────────────────────────────────────────────
|
|
as-is el artefacto tal cual ⇒ las extensiones tienen que estar
|
|
no-policy `policies.json` SIN `ExtensionSettings` ⇒ si siguen: instala LA CARPETA
|
|
policy-only los XPI mudados fuera de la carpeta y la
|
|
política apuntando a la ruta nueva ⇒ si no están: la política NO instala
|
|
|
|
En los tres se mira lo mismo, y por dos vías independientes:
|
|
|
|
el MOTOR una sonda con permiso `browserSettings` pregunta el `homepageOverride`; si el
|
|
override de `inicio` agarró, es `moz-extension://…/inicio.html`
|
|
el PERFIL `extensions.json` dice si el complemento ESTÁ y si está `active`
|
|
|
|
⚠ El `location` de `extensions.json` NO sirve para distinguir el mecanismo, y esto es medido: los
|
|
dos XPI de atuq salen `app-profile` **también cuando los instala el escaneo de la carpeta** y no hay
|
|
ninguna política que los nombre. Se lo imprime igual porque es barato, pero la única señal que
|
|
discrimina es la de arriba: romper un mecanismo por vez y ver si la extensión sigue.
|
|
|
|
La sonda es además el CONTROL de que la corrida sirvió: vive en `distribution/extensions/` y NO está
|
|
declarada en ninguna política, así que si habla, el escaneo de la carpeta está vivo en esa corrida.
|
|
Una corrida muda no se interpreta como «no instaló»: se declara rota.
|
|
|
|
REQUISITOS: el rootfs de `scripts/atuq-nested.sh` (que lo hidrata) y `atuq` sellado.
|
|
"""
|
|
import json
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import zipfile
|
|
|
|
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
RFS = os.environ.get("RFS", "/mnt/cosecha/escritorios/atuq-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"))
|
|
|
|
APPDIR = "/usr/lib/atuq"
|
|
EXTDIR = f"{APPDIR}/distribution/extensions"
|
|
ALTDIR = f"{APPDIR}/xpi-politica"
|
|
IDS = ("inicio@atuq.tawasuyu", "proxy@atuq.tawasuyu")
|
|
|
|
SONDA = "sonda-instalacion@atuq.tawasuyu"
|
|
MANIFIESTO = {
|
|
"manifest_version": 2,
|
|
"name": "sonda-instalacion",
|
|
"version": "1.0",
|
|
"browser_specific_settings": {"gecko": {"id": SONDA}},
|
|
"permissions": ["browserSettings"],
|
|
"background": {"scripts": ["fondo.js"]},
|
|
}
|
|
FONDO = """
|
|
function di(s) { try { dump("INSTALACION-TEST " + s + "\\n"); } catch (e) {} }
|
|
(async () => {
|
|
try {
|
|
const h = await browser.browserSettings.homepageOverride.get({});
|
|
di("HOMEPAGE " + h.value);
|
|
} catch (e) {
|
|
di("EXCEPCIÓN " + e.message);
|
|
}
|
|
})();
|
|
"""
|
|
|
|
ESCENARIOS = ("as-is", "no-policy", "policy-only")
|
|
|
|
|
|
def fatal(msg):
|
|
print(f"✗ {msg}", file=sys.stderr)
|
|
raise SystemExit(1)
|
|
|
|
|
|
def artefacto():
|
|
"""El artefacto VIGENTE de atuq, por `takana hash` y nunca por glob.
|
|
|
|
⚠ ESCOTILLA `ATUQ_DIR` con su aviso puesto: el corpus es compartido y otro frente puede
|
|
re-hashear `firefox` dejando el atuq vigente sin sellar durante las horas del build. Con la
|
|
escotilla se puede medir contra uno viejo A SABIENDAS, y por eso grita.
|
|
"""
|
|
forzado = os.environ.get("ATUQ_DIR")
|
|
if forzado:
|
|
if not os.path.isdir(forzado):
|
|
fatal(f"ATUQ_DIR={forzado} no existe")
|
|
print(f"⚠ ATUQ_DIR: se mide {os.path.basename(forzado)}, que NO es necesariamente el vigente")
|
|
return forzado
|
|
r = subprocess.run([HAMMER, "--store", STORE, "hash", "recipes/atuq.toml"],
|
|
cwd=ROOT, capture_output=True, text=True)
|
|
if r.returncode != 0:
|
|
fatal(f"no pude hashear atuq: {r.stderr.strip()}")
|
|
h = r.stdout.strip().splitlines()[-1].removeprefix("b3:")
|
|
d = os.path.join(STORE, f"{h}-atuq")
|
|
if not os.path.isdir(d):
|
|
fatal(f"atuq b3:{h[:8]} no está sellado — construilo antes")
|
|
return d
|
|
|
|
|
|
def politica_sin_extensionsettings(base):
|
|
p = json.load(open(base))
|
|
p["policies"].pop("ExtensionSettings", None)
|
|
return p
|
|
|
|
|
|
def politica_apuntando_a(base, directorio):
|
|
p = json.load(open(base))
|
|
for ident, cfg in p["policies"]["ExtensionSettings"].items():
|
|
cfg["install_url"] = f"file://{directorio}/{ident}.xpi"
|
|
return p
|
|
|
|
|
|
def correr(escenario, atuq):
|
|
"""Una corrida de atuq headless con perfil nuevo. Devuelve (homepage, locations, log)."""
|
|
tmp = tempfile.mkdtemp(prefix=f"atuq-inst-{escenario}-")
|
|
salida = os.path.join(tmp, "salida")
|
|
capa_ext = os.path.join(tmp, "capa", APPDIR.lstrip("/"), "distribution", "extensions")
|
|
perfil = os.path.join(salida, "perfil")
|
|
os.makedirs(perfil)
|
|
os.makedirs(capa_ext)
|
|
try:
|
|
with zipfile.ZipFile(os.path.join(capa_ext, f"{SONDA}.xpi"), "w") as z:
|
|
z.writestr("manifest.json", json.dumps(MANIFIESTO, indent=1))
|
|
z.writestr("fondo.js", FONDO)
|
|
|
|
with open(os.path.join(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')
|
|
|
|
base = os.path.join(atuq, APPDIR.lstrip("/"), "distribution", "policies.json")
|
|
preparar = "true"
|
|
if escenario == "no-policy":
|
|
nueva = politica_sin_extensionsettings(base)
|
|
elif escenario == "policy-only":
|
|
nueva = politica_apuntando_a(base, ALTDIR)
|
|
preparar = (f"mkdir -p {ALTDIR} && "
|
|
+ " && ".join(f"mv {EXTDIR}/{i}.xpi {ALTDIR}/{i}.xpi" for i in IDS))
|
|
else:
|
|
nueva = None
|
|
if nueva is not None:
|
|
capa_pol = os.path.join(tmp, "capa", APPDIR.lstrip("/"), "distribution")
|
|
with open(os.path.join(capa_pol, "policies.json"), "w") as fh:
|
|
json.dump(nueva, fh, indent=2)
|
|
|
|
guion = f"""
|
|
mkdir -p /root
|
|
[ -s /etc/machine-id ] || tr -d - < /proc/sys/kernel/random/uuid > /etc/machine-id
|
|
{preparar}
|
|
timeout 60 /usr/bin/atuq --profile /salida/perfil --no-remote --headless about:blank \
|
|
> /salida/consola.log 2>&1
|
|
cp /salida/perfil/extensions.json /salida/extensions.json 2>/dev/null || true
|
|
exit 0
|
|
"""
|
|
subprocess.run([
|
|
"bwrap",
|
|
"--overlay-src", ALPINE, "--overlay-src", RFS, "--overlay-src", atuq,
|
|
"--overlay-src", os.path.join(tmp, "capa"), "--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", "HOME", "/root",
|
|
"--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()
|
|
homepage = None
|
|
for l in log.splitlines():
|
|
if l.startswith("INSTALACION-TEST HOMEPAGE "):
|
|
homepage = l.split(" ", 2)[2]
|
|
locations = {}
|
|
ej = os.path.join(salida, "extensions.json")
|
|
if os.path.exists(ej):
|
|
try:
|
|
for a in json.load(open(ej)).get("addons", []):
|
|
if a.get("id") in IDS or a.get("id") == SONDA:
|
|
locations[a["id"]] = (a.get("location"), bool(a.get("active")))
|
|
except (ValueError, KeyError) as e:
|
|
print(f" ⚠ no pude leer extensions.json: {e}")
|
|
return homepage, locations, log
|
|
finally:
|
|
shutil.rmtree(tmp, ignore_errors=True)
|
|
|
|
|
|
def main():
|
|
argv = sys.argv[1:]
|
|
pedido = None
|
|
if "--scenario" in argv:
|
|
pedido = argv[argv.index("--scenario") + 1]
|
|
if pedido not in ESCENARIOS:
|
|
fatal(f"escenario desconocido: {pedido} (hay {', '.join(ESCENARIOS)})")
|
|
if not os.path.isdir(RFS):
|
|
fatal(f"no está el rootfs en {RFS} — corré antes `scripts/atuq-nested.sh`, que lo hidrata")
|
|
|
|
atuq = artefacto()
|
|
print(f"artefacto: {os.path.basename(atuq)}")
|
|
res = {}
|
|
for e in (ESCENARIOS if pedido is None else (pedido,)):
|
|
print(f"\n== escenario {e}")
|
|
homepage, locations, log = correr(e, atuq)
|
|
nuestra = bool(homepage and "inicio.html" in homepage)
|
|
print(f" MOTOR homepage = {homepage}"
|
|
f" ⇒ {'la de atuq' if nuestra else 'la de Firefox'}")
|
|
if not homepage:
|
|
destino = os.path.join(ROOT, "work", f"atuq-instalacion-{e}.log")
|
|
os.makedirs(os.path.dirname(destino), exist_ok=True)
|
|
open(destino, "w").write(log)
|
|
fatal(f"la sonda no dijo NADA en «{e}» — corrida rota, no resultado. Log en {destino}")
|
|
for ident, (loc, activo) in sorted(locations.items()):
|
|
print(f" PERFIL {ident:34s} location={loc} activo={activo}")
|
|
for ident in IDS:
|
|
if ident not in locations:
|
|
print(f" PERFIL {ident:34s} NO ESTÁ en extensions.json")
|
|
res[e] = (nuestra, locations)
|
|
|
|
if pedido is not None:
|
|
return 0
|
|
|
|
print()
|
|
fallos = []
|
|
if not res["as-is"][0]:
|
|
fallos.append("tal cual, la página de inicio NO es la de atuq — atuq está roto, "
|
|
"no es un problema del mecanismo")
|
|
if not res["no-policy"][0]:
|
|
fallos.append("sin `ExtensionSettings` las extensiones NO se instalan ⇒ el escaneo de "
|
|
"`distribution/extensions/` YA NO alcanza: hay que revisar el §7.bis y el "
|
|
"README, porque todas las extensiones de atuq cuelgan de ahí")
|
|
if res["policy-only"][0]:
|
|
fallos.append("con los XPI FUERA de la carpeta y sólo la política apuntándolos, las "
|
|
"extensiones SÍ se instalaron ⇒ `install_url` con `file://` ahora funciona "
|
|
"y el §7.bis quedó viejo")
|
|
if fallos:
|
|
for f in fallos:
|
|
print(f"✗ {f}", file=sys.stderr)
|
|
return 1
|
|
print("✓ quien instala es el ESCANEO de `distribution/extensions/`: sin la política siguen "
|
|
"puestas, y con la política sola no aparecen.")
|
|
print(" La política las FIJA y las CONFIGURA; instalarlas, no. (SDD 26 §7.bis)")
|
|
return 0
|
|
|
|
|
|
sys.exit(main())
|