atuq §6.9: el guardián del torrent dejaba un daemon suelto y decía ✓
Encontrado con `ps` sobre la máquina, no por un test: al cerrar el sandbox quedaban vivos el daemon y su `bwrap` interno, sosteniendo overlays ya borrados. Es correcto que el daemon se quede —su torrent de prueba no tiene enjambre, así que nunca termina y nunca está «sin nada»—, lo que faltaba era despedirlo y MEDIR que se fue. - el guión de adentro le pide `puriy-costura-torrent stop` (el verbo del producto); - la huella del socket se anota adentro y ANTES del stop: el daemon lo borra al irse; - el guardián censa daemons antes/después con `ps -C` (nunca `pkill -f`) y falla nombrando el PID; el `finally` barre lo propio para que un test rojo no deje basura; - tope de 180 s al sandbox, como seguro contra un cuelgue. Comprobado en los dos sentidos con una copia rota fuera del repo: sin el `stop` el guardián daba ✓ igual, y con la aserción nueva sale ✗ nombrando el PID. La hipótesis de que se COLGARÍA era falsa: el `bwrap` externo vuelve; el que espera es el interno. Verde: positivo, control negativo y rotura a propósito.
This commit is contained in:
@@ -901,6 +901,28 @@ estaba bien: **el que no llegaba a llamarla era el bucle**. Una decisión correc
|
||||
igual que una que no existe. Arreglado (sin sesión = nada activo) y fijado con dos tests que corren el
|
||||
PROCESO y el reloj de verdad, el segundo comprobado volviendo a poner el `continue`.
|
||||
|
||||
⚠ **La pereza tiene un residuo, y el guardián lo daba por bueno (2026-09-10).** El daemon se va
|
||||
«tras 300 s **sin nada**», y un torrent sin enjambre nunca termina ⇒ nunca está sin nada: se queda,
|
||||
que es lo correcto para un cliente de torrent. Pero el guardián cerraba el sandbox y se iba, y
|
||||
**quedaba vivo el daemon Y el `bwrap` interno**, sosteniendo abiertos overlays ya borrados —
|
||||
encontrado con `ps` sobre la máquina, no por un test. La corrección tiene dos mitades y ninguna
|
||||
sobra:
|
||||
|
||||
1. el guión de adentro **despide al daemon** con `puriy-costura-torrent stop` — el propio verbo del
|
||||
producto, así que de paso se ejerce;
|
||||
2. el guardián **cuenta los daemons antes y después** (`ps -C`, nunca `pkill -f`, que empareja la
|
||||
línea de comandos y se lleva la shell del que corre el test) y **falla nombrando el PID** si quedó
|
||||
alguno; y barre lo propio en el `finally`, para que un test rojo tampoco deje basura.
|
||||
|
||||
Y el orden importa: la huella del socket se anota **adentro y antes** del `stop`, porque al irse el
|
||||
daemon lo borra — mirarlo desde el hub después mediría el reloj, no el hecho. Comprobado en los dos
|
||||
sentidos, quitando el `stop` en una copia fuera del repo: sin él el guardián decía ✓ igual y dejaba
|
||||
un daemon suelto; con la aserción nueva sale
|
||||
`✗ quedó un daemon vivo tras cerrar el sandbox: PID(s) [20157]`. La primera hipótesis —que sin `stop`
|
||||
el guardián se COLGARÍA, porque `bwrap --unshare-pid` espera a su namespace— **era falsa y la
|
||||
medición la descartó**: el `bwrap` externo vuelve, y el que se queda esperando es el interno. El tope
|
||||
de 180 s quedó igual, pero por lo que es: seguro contra un cuelgue, no el que detecta la fuga.
|
||||
|
||||
⚠ **Lo que sigue sin estar en ningún test automático: un transfer real entre peers.** Haría falta un
|
||||
sembrador y una espera que volverían la suite una que nadie corre. Está dicho también en el test del
|
||||
daemon, al pie, para que nadie lo lea como «probado de punta a punta».
|
||||
|
||||
@@ -32,11 +32,13 @@ import hashlib
|
||||
import http.server
|
||||
import os
|
||||
import shutil
|
||||
import signal
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
RFS = os.environ.get("RFS", "/mnt/cosecha/escritorios/atuq-rootfs")
|
||||
@@ -44,6 +46,9 @@ 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"))
|
||||
CLIENTE = "/usr/bin/puriy-costura-torrent"
|
||||
# Tope del sandbox entero: 60 s los pone `timeout` sobre el navegador, y el resto es
|
||||
# arranque + despedida del daemon. Si se pasa, no hay veredicto: hay algo colgado.
|
||||
TOPE_SANDBOX = 180
|
||||
|
||||
|
||||
def fatal(msg):
|
||||
@@ -119,6 +124,19 @@ class Servidor(http.server.SimpleHTTPRequestHandler):
|
||||
self.wfile.write(datos)
|
||||
|
||||
|
||||
def daemons_vivos():
|
||||
"""PIDs de daemons de torrent vivos AHORA, vistos desde el hub.
|
||||
|
||||
El daemon corre dentro del sandbox, pero `bwrap` no oculta los procesos al host:
|
||||
`ps -C` los ve con su PID de acá. Es por nombre de proceso a propósito — `pkill -f`
|
||||
empareja la línea de comandos entera y se lleva puesta la shell que corre el test
|
||||
(medido, sale 144 y lo que sigue no corre).
|
||||
"""
|
||||
r = subprocess.run(["ps", "-o", "pid=", "-C", "puriy-costura-torrent"],
|
||||
capture_output=True, text=True, check=False)
|
||||
return {int(l) for l in r.stdout.split()}
|
||||
|
||||
|
||||
def puerto_libre():
|
||||
s = socket.socket()
|
||||
s.bind(("127.0.0.1", 0))
|
||||
@@ -136,13 +154,22 @@ def correr(atuq, costura, daemon, salida, url, negativo):
|
||||
{borrar}
|
||||
timeout 60 /usr/bin/atuq --profile /salida/perfil --no-remote --headless '{url}' \
|
||||
> /salida/consola.log 2>&1
|
||||
# El socket se anota ACÁ ADENTRO, antes de despedir al daemon: al irse lo borra,
|
||||
# así que mirarlo después desde el hub no mediría nada.
|
||||
[ -S /salida/run/puriy-costura-torrent.sock ] && echo si > /salida/socket-visto
|
||||
# Y hay que despedirlo a mano. Medido (2026-09-10): el daemon se va «tras 300s sin
|
||||
# nada», pero un torrent sin enjambre NUNCA termina ⇒ nunca está «sin nada»; y
|
||||
# `bwrap --unshare-pid` no vuelve hasta que su namespace de PIDs queda vacío.
|
||||
# Sin este `stop` el guardián se cuelga PARA SIEMPRE, y de paso deja el sandbox
|
||||
# entero vivo (un `bwrap` huérfano pinneando el overlay borrado).
|
||||
command -v puriy-costura-torrent >/dev/null 2>&1 && puriy-costura-torrent stop >/dev/null 2>&1
|
||||
exit 0
|
||||
"""
|
||||
capas = ["--overlay-src", ALPINE, "--overlay-src", RFS, "--overlay-src", atuq,
|
||||
"--overlay-src", costura]
|
||||
if daemon:
|
||||
capas += ["--overlay-src", daemon]
|
||||
subprocess.run([
|
||||
proceso = subprocess.Popen([
|
||||
"bwrap", *capas, "--tmp-overlay", "/",
|
||||
"--dev-bind", "/dev", "/dev", "--proc", "/proc", "--bind", "/sys", "/sys",
|
||||
"--tmpfs", "/tmp", "--unshare-pid",
|
||||
@@ -156,7 +183,16 @@ def correr(atuq, costura, daemon, salida, url, negativo):
|
||||
"--setenv", "MOZ_DISABLE_SOCKET_PROCESS_SANDBOX", "1",
|
||||
"--setenv", "MOZ_DISABLE_UTILITY_SANDBOX", "1",
|
||||
"sh", "-c", guion,
|
||||
], check=False)
|
||||
])
|
||||
try:
|
||||
proceso.wait(timeout=TOPE_SANDBOX)
|
||||
except subprocess.TimeoutExpired:
|
||||
# No es un fallo del navegador: es que algo quedó vivo dentro del namespace.
|
||||
proceso.kill()
|
||||
proceso.wait(timeout=30)
|
||||
fatal(f"el sandbox no volvió en {TOPE_SANDBOX}s — quedó algo vivo adentro"
|
||||
" (el sospechoso es el daemon: `bwrap` espera a que el namespace de PIDs"
|
||||
" se vacíe)")
|
||||
log = open(os.path.join(salida, "consola.log"), errors="replace").read()
|
||||
return [l[len("TORRENT "):] for l in log.splitlines() if l.startswith("TORRENT ")], log
|
||||
|
||||
@@ -173,6 +209,10 @@ def main():
|
||||
print(f"artefactos: {os.path.basename(atuq)}\n {os.path.basename(costura)}"
|
||||
f"\n {os.path.basename(daemon)}")
|
||||
|
||||
# Censo previo: puede haber un daemon de otra corrida o de otro agente, y ése no
|
||||
# es nuestro. Lo que se juzga al final es la DIFERENCIA.
|
||||
ajenos = daemons_vivos()
|
||||
|
||||
puerto = puerto_libre()
|
||||
Servidor.puerto = puerto
|
||||
servidor = http.server.ThreadingHTTPServer(("127.0.0.1", puerto), Servidor)
|
||||
@@ -240,15 +280,39 @@ def main():
|
||||
|
||||
# Y la prueba de la PEREZA: el daemon no estaba corriendo, y su socket
|
||||
# apareció porque el cliente lo levantó.
|
||||
sock = os.path.join(salida, "run", "puriy-costura-torrent.sock")
|
||||
if not os.path.exists(sock):
|
||||
fatal("no hay socket del daemon: no llegó a levantarse")
|
||||
print(f"\nsocket del daemon: {os.path.relpath(sock, salida)}")
|
||||
# La huella la dejó el propio sandbox antes de despedir al daemon: el socket
|
||||
# se lo lleva al salir, así que buscarlo ahora sería medir el reloj, no el hecho.
|
||||
if not os.path.exists(os.path.join(salida, "socket-visto")):
|
||||
fatal("no hubo socket del daemon: no llegó a levantarse")
|
||||
print("\nsocket del daemon: run/puriy-costura-torrent.sock (visto adentro)")
|
||||
print("\n✓ el navegador le entrega el torrent a la suite y el daemon lo toma")
|
||||
print("✓ y NADIE lo arrancó: no hay servicio en la imagen — lo levantó su cliente")
|
||||
|
||||
# Y que se haya IDO. Esto no es prolijidad: el sandbox se cerró, sus overlays
|
||||
# están borrados, y un daemon vivo los sostiene abiertos indefinidamente —
|
||||
# su torrent de prueba no tiene enjambre, así que nunca termina y nunca está
|
||||
# «sin nada» que hacer. Medido: sin el `stop` de adentro el guardián decía ✓
|
||||
# igual y dejaba el daemon Y su `bwrap` corriendo. Un residuo que nadie mide
|
||||
# se ve igual que no haberlo dejado.
|
||||
for _ in range(15):
|
||||
quedaron = daemons_vivos() - ajenos
|
||||
if not quedaron:
|
||||
break
|
||||
time.sleep(1)
|
||||
if quedaron:
|
||||
fatal(f"quedó un daemon vivo tras cerrar el sandbox: PID(s) {sorted(quedaron)}"
|
||||
" — se despide con `puriy-costura-torrent stop` DENTRO del sandbox")
|
||||
print("✓ y se fue cuando se lo pidieron: el sandbox no deja daemon suelto")
|
||||
return 0
|
||||
finally:
|
||||
servidor.shutdown()
|
||||
# Barrer SÓLO lo que apareció durante esta corrida, y por PID: un test rojo no
|
||||
# tiene derecho a dejar procesos, ni a matar los de otro agente.
|
||||
for pid in sorted(daemons_vivos() - ajenos):
|
||||
try:
|
||||
os.kill(pid, signal.SIGTERM)
|
||||
except OSError:
|
||||
pass
|
||||
shutil.rmtree(tmp, ignore_errors=True)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user