3.1.0: se instala solo en su sitio, manual dentro y el puerto para una sola copia

- instalar.py: instalar un paquete lo copia a la carpeta de programas del
  usuario (%LOCALAPPDATA%\Programs\OrganiLAB) con sus datos y su ffmpeg, lo da
  de alta desde ahí y lo vuelve a abrir desde su sitio. Del origen no se borra
  nada: queda instalado-en.txt, que además hace que abrir esa copia abra la
  instalada. Un destino ocupado no se pisa, y si ya hay un OrganiLAB instalado
  lo dice en vez de poner otro encima.
- static/index.html: vista «Manual», con cada función del programa, qué hace
  y cómo lo hace (41 entradas, un apartado por pestaña).
- organizador.py: ServidorLocal pide el puerto en exclusiva. Con «reutilizar
  dirección», en Windows dos copias escuchaban a la vez en el mismo puerto y
  se contestaban la una por la otra sin que nada fallara a la vista.
- Pruebas: probar.py 19, prueba_funcional.py 26 de 26, prueba_ventana.py 18
  desde el código y 17 con el paquete, incluida la instalación entera en un
  equipo de ensayo (ORGANILAB_ENSAYO) que no toca el Windows de verdad.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Este commit está contenido en:
Labstream
2026-10-04 12:28:03 -05:00
padre 6c21145b1c
commit 50562aeaab
Se han modificado 8 ficheros con 562 adiciones y 43 borrados

Ver fichero

@@ -5,7 +5,7 @@ sin abrirlo; esto abre OrganiLAB sobre una copia temporal (programa y datos apar
comprueba desde otro hilo lo que solo se puede comprobar con la ventana delante:
py -3.12 prueba_ventana.py el programa desde el código
py -3.12 prueba_ventana.py --paquete dist\OrganizadorLabstream el paquete, arrancado por su .exe
py -3.12 prueba_ventana.py --paquete dist\OrganizadorLabstream el paquete, arrancado por su .exe, y su instalación
… --captura ruta.png deja una imagen de la ventana entera
Durante medio minuto se ve una ventana que se abre, se mueve sola y se cierra. No hay que tocar nada.
@@ -138,6 +138,21 @@ def marca_de(datos):
return (zlib.crc32(os.path.normcase(os.path.abspath(datos)).encode("utf-8")) & 0x7FFFFFFF) or 1
def cerrar_las_de(V, *marcas):
"""Cierra las copias de ensayo que sigan abiertas (con sus cuadros de diálogo), pase lo que pase en la
prueba: una copia olvidada se queda en la pantalla y, peor, atendiendo."""
for _ in range(3):
abiertas = [h for m in marcas for h in V._ventanas(m)]
if not abiertas:
return
for h in abiertas:
d = dialogo_de(h)
if d:
U.PostMessageW(d, WM_CLOSE, 0, 0)
U.PostMessageW(h, WM_CLOSE, 0, 0)
time.sleep(3)
def captura_pedida():
return sys.argv[sys.argv.index("--captura") + 1] if "--captura" in sys.argv else ""
@@ -206,6 +221,10 @@ def desde_el_codigo(tmp):
return r
aj = v.en_ui(ajustes)
anota("lo que es de navegador está apagado", not any(aj.values()), ", ".join(k for k, x in aj.items() if x) or "todo apagado")
v.evaluar("irA('manual')")
entradas = v.evaluar("document.querySelector('#t-manual').classList.contains('on') ? document.querySelectorAll('#t-manual dt').length : 0")
anota("el manual está dentro del programa", (entradas or 0) > 35, f"{entradas} entradas")
v.evaluar("irA('carpeta')")
anota("la interfaz sabe que está en la ventana", v.evaluar("document.documentElement.classList.contains('nativo')") is True)
# el avance en la barra de tareas
@@ -348,10 +367,89 @@ def el_paquete(tmp, origen):
proc.kill()
anota("al cerrar la ventana se cierra el programa", proc.returncode == 0, f"código {proc.returncode}")
anota("y deja de atender", estado() is None)
cerrar_las_de(V, marca)
p = subprocess.run([exe, "ayuda"], capture_output=True, timeout=60, stdin=subprocess.DEVNULL, env=entorno)
anota("por consola responde a sus comandos", p.returncode == 0 and b"asistente" in p.stdout)
def instalar_el_paquete(tmp, origen):
"""La instalación de punta a punta, sin tocar el Windows de verdad: con ORGANILAB_ENSAYO el «equipo» es una
carpeta temporal. El paquete se abre desde unas «Descargas», ofrece instalarse, se copia a su sitio, se da
de alta, se cierra y vuelve a abrirse desde allí; y abrir después la copia de Descargas abre la instalada."""
import winreg
equipo = os.path.join(tmp, "equipo")
os.environ["ORGANILAB_ENSAYO"] = equipo
os.environ.pop("ORGANIZADOR_PUERTO", None)
sys.path.insert(0, AQUI)
import ventana as V
import instalar
descarga = os.path.join(tmp, "Descargas", "OrganizadorLabstream")
shutil.copytree(origen, descarga, ignore=shutil.ignore_patterns("ffmpeg", "actualizacion", "__pycache__"))
os.makedirs(os.path.join(descarga, "ffmpeg"))
with open(os.path.join(descarga, "ffmpeg", "LICENSE.txt"), "w") as fh:
fh.write("en el paquete completo aquí va ffmpeg")
destino = instalar.sitio_definitivo()
exe_d, exe_i = os.path.join(descarga, "OrganizadorLabstream.exe"), os.path.join(destino, "OrganizadorLabstream.exe")
marca_d, marca_i = marca_de(os.path.join(descarga, "datos")), marca_de(os.path.join(destino, "datos"))
try:
proc = subprocess.Popen([exe_d], cwd=tmp)
hs = espera(lambda: V._ventanas(marca_d), 40)
if not hs:
anota("el paquete abre desde Descargas", False)
proc.kill()
return
h = hs[0]
d = espera(lambda: dialogo_de(h), 25)
pregunta = " ".join(texto(x) for x in hijos(d)) if d else ""
anota("la primera vez ofrece instalarlo en el equipo", "Instalar OrganiLAB en este equipo" in pregunta, pregunta[8:70])
if d:
U.PostMessageW(d, WM_COMMAND, 6, 0) # «Sí»
def aviso_final():
x = dialogo_de(h)
return x if x and "quedó instalado" in " ".join(texto(y) for y in hijos(x)) else None
d = espera(aviso_final, 90)
anota("se copia a su sitio y lo dice", bool(d))
if d:
U.PostMessageW(d, WM_CLOSE, 0, 0) # solo tiene «Aceptar»
hi = espera(lambda: V._ventanas(marca_i), 60)
anota("se cierra y vuelve a abrirse solo desde su sitio", bool(hi) and espera(lambda: not V._ventanas(marca_d), 30) is True)
try:
proc.wait(30)
except subprocess.TimeoutExpired:
proc.kill()
anota("en su sitio están el programa, sus datos y su ffmpeg",
all(os.path.exists(os.path.join(destino, x)) for x in ("OrganizadorLabstream.exe", "runtime", "static", "datos\\config.json", "ffmpeg\\LICENSE.txt")))
a = instalar.leer_acceso(instalar._accesos()[0]) if os.path.exists(instalar._accesos()[0]) else None
anota("queda dado de alta desde su sitio (Inicio, Escritorio y Aplicaciones)",
instalar.instalado_aqui(destino) and bool(a) and instalar._misma(a["exe"], exe_i) and os.path.exists(instalar._accesos()[1]),
a["exe"] if a else "sin acceso directo")
anota("del origen no se borra nada, y queda la nota de la mudanza", os.path.exists(exe_d) and bool(instalar.mudado(descarga)))
if hi:
try:
with open(os.path.join(destino, "datos", "sesion.json"), encoding="utf-8") as fh:
puerto = json.load(fh)["puerto"]
e = json.loads(urllib.request.urlopen(f"http://127.0.0.1:{puerto}/api/estado", timeout=5).read())
except Exception: # noqa
e = {}
anota("el instalado atiende con sus datos", e.get("congelado") is True and (e.get("config") or {}).get("instalacion", {}).get("ofrecida") is True,
f"versión {e.get('version')}")
subprocess.run([exe_d], cwd=tmp, timeout=40)
time.sleep(2.5)
anota("abrir la copia de Descargas abre la instalada", not V._ventanas(marca_d) and len(V._ventanas(marca_i)) == 1)
U.PostMessageW(hi[0], WM_CLOSE, 0, 0)
anota("y se cierra como siempre", espera(lambda: not V._ventanas(marca_i), 25) is True)
time.sleep(2)
finally:
cerrar_las_de(V, marca_d, marca_i)
for clave in (instalar.CLAVE, "Software\\OrganiLAB-ensayo"):
try:
winreg.DeleteKey(winreg.HKEY_CURRENT_USER, clave)
except OSError:
pass
os.environ.pop("ORGANILAB_ENSAYO", None)
def main():
guardia = threading.Timer(300, lambda: os._exit(3)) # pase lo que pase, no se queda colgada
guardia.daemon = True
@@ -360,6 +458,7 @@ def main():
try:
if "--paquete" in sys.argv:
el_paquete(tmp, os.path.abspath(sys.argv[sys.argv.index("--paquete") + 1]))
instalar_el_paquete(tmp, os.path.abspath(sys.argv[sys.argv.index("--paquete") + 1]))
else:
desde_el_codigo(tmp)
finally: