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

@@ -22,6 +22,7 @@ import ctypes
import time
import queue
import shutil
import socket
import stat as stat_mod
import hashlib
import sqlite3
@@ -39,7 +40,7 @@ import actualizador
asistente.O = sys.modules[__name__]
actualizador.O = sys.modules[__name__]
VERSION = "3.0.0"
VERSION = "3.1.0"
BASE = os.path.dirname(os.path.abspath(sys.executable if getattr(sys, "frozen", False) else __file__))
# Empaquetado: el programa corre con el motor que viaja en su propia carpeta. OrganizadorLabstream.exe es
@@ -3751,6 +3752,19 @@ def estado_carpeta(ruta):
}
class ServidorLocal(ThreadingHTTPServer):
"""El servidor de la interfaz, con el puerto para él solo. ThreadingHTTPServer pide «reutilizar dirección»,
y en Windows eso significa que dos programas pueden escuchar a la vez en el mismo puerto y las peticiones
van a cualquiera de los dos: dos OrganiLAB con datos distintos (el instalado y una copia suelta, o el de
ensayo y el de verdad) se contestaban el uno por el otro sin que nada fallara a la vista."""
allow_reuse_address = False
def server_bind(self):
if hasattr(socket, "SO_EXCLUSIVEADDRUSE"):
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_EXCLUSIVEADDRUSE, 1)
super().server_bind()
class Manejador(BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1"
@@ -4132,7 +4146,7 @@ class Manejador(BaseHTTPRequestHandler):
AYUDA = """OrganiLAB (Organizador Labstream) — uso por comando
OrganizadorLabstream.exe abre OrganiLAB en su ventana
OrganizadorLabstream.exe instalar lo pone en el menú Inicio y en el Escritorio (desinstalar lo quita)
OrganizadorLabstream.exe instalar lo instala en su sitio: carpeta de programas, menú Inicio y Escritorio (desinstalar lo quita)
OrganizadorLabstream.exe "D:\\Clientes\\Carpeta" (o arrastra la carpeta encima del .exe) ASISTENTE: la escanea con barra
de progreso, da el diagnóstico por tipo de archivo y pregunta paso a paso qué hacer
OrganizadorLabstream.exe asistente "ruta" [--si] [--simular] lo mismo por comando (--si acepta lo recomendado sin preguntar)
@@ -4359,6 +4373,7 @@ SOLTADOS = {"ts": 0, "rutas": []}
SESION_PATH = os.path.join(DATOS, "sesion.json") # qué copia tiene abiertos estos datos, y en qué puerto atiende
_CANDADO = [] # el de «una sola copia abierta»; vive lo que el proceso
SILENCIO_NAVEGADOR = 180 # segundos sin que la página pregunte para dar el navegador por cerrado
RELEVO = [] # el .exe que toma el relevo al salir (tras instalarse en su sitio)
def _al_soltar(rutas):
@@ -4471,7 +4486,8 @@ def _vigilar(v, fin):
def _recien_abierta(v):
"""Con la interfaz ya a la vista: si el programa está dado de alta en Windows, pone al día su versión y
sus accesos directos; y la primera vez que se abre un paquete en un equipo, ofrece darlo de alta."""
sus accesos directos; y la primera vez que se abre un paquete en un equipo, ofrece instalarlo: copiarlo a
su sitio definitivo, darlo de alta y seguir desde allí."""
try:
import ventana
import instalar
@@ -4480,14 +4496,35 @@ def _recien_abierta(v):
oferta = CONFIG.get("instalacion") or {}
if not CONGELADO or os.environ.get("ORGANIZADOR_PUERTO") or oferta.get("ofrecida") or instalar.instalado_aqui(BASE):
return
if instalar.instalado() and not instalar.instalado_aqui(BASE):
return # ya hay otro instalado en este equipo: esta copia va por libre
CONFIG["instalacion"] = {**oferta, "ofrecida": True} # se pregunta una sola vez
guardar_json(CONFIG_PATH, CONFIG)
time.sleep(1.5)
if v.mensaje("¿Quieres tener OrganiLAB en el menú Inicio y en el Escritorio, como un programa más de Windows?\n\n"
"No copia nada: el programa se queda en esta carpeta. Se quita desde Configuración › Aplicaciones.",
ventana.MB_YESNO | ventana.MB_ICONQUESTION) == ventana.IDYES:
instalar.instalar(BASE, VERSION)
v.mensaje("Listo. Búscalo como «OrganiLAB» en el menú Inicio, o ábrelo desde el Escritorio.")
if not instalar.hay_que_mudar(BASE): # ya está en su sitio: solo falta darlo de alta
if v.mensaje("¿Quieres tener OrganiLAB en el menú Inicio y en el Escritorio, como un programa más de Windows?\n\n"
"Se quita desde Configuración › Aplicaciones.", ventana.MB_YESNO | ventana.MB_ICONQUESTION) == ventana.IDYES:
instalar.instalar(BASE, VERSION)
v.mensaje("Listo. Búscalo como «OrganiLAB» en el menú Inicio, o ábrelo desde el Escritorio.")
return
if v.mensaje("¿Instalar OrganiLAB en este equipo?\n\nSe copia a tu carpeta de programas, queda en el menú Inicio y en el "
"Escritorio, y se quita desde Configuración › Aplicaciones. La carpeta desde la que lo has abierto ya no hará falta.",
ventana.MB_YESNO | ventana.MB_ICONQUESTION) != ventana.IDYES:
return
v.evaluar("modal(" + json.dumps("Instalando OrganiLAB…") + ", " + json.dumps(
'<p><span class="spin"></span> Copiando el programa a su sitio definitivo. Tarda unos segundos; al terminar se abre solo desde allí.</p>') + ", null)")
try:
r = instalar.poner(BASE, VERSION, avance=v.progreso)
except Exception as ex: # noqa
v.progreso(None)
v.evaluar("document.querySelector('#modal').classList.remove('on')")
v.mensaje(f"No se pudo instalar: {ex}\n\nEl programa sigue funcionando desde esta carpeta.", ventana.MB_OK | ventana.MB_ICONWARNING)
return
v.progreso(None)
v.mensaje("OrganiLAB quedó instalado.\n\nAhora se cierra y se abre desde su sitio. La carpeta desde la que lo abriste "
"ya no hace falta: puedes borrarla.")
RELEVO.append(os.path.join(r["base"], "OrganizadorLabstream.exe"))
v.cerrar()
except Exception as ex: # noqa
print("no se pudo dar de alta en Windows:", ex)
@@ -4539,8 +4576,20 @@ def _atender_navegador():
def main():
global PUERTO
args_tmp = [a for a in sys.argv[1:] if not a.startswith("--")]
if CONGELADO and "--puente" not in sys.argv:
import instalar
instalada = instalar.mudado(BASE)
if instalada: # desde esta carpeta ya se instaló el programa: se abre el instalado
if sys.argv[1:]:
sys.exit(subprocess.call([instalada] + sys.argv[1:]))
subprocess.Popen([instalada], cwd=os.path.dirname(instalada), close_fds=True)
return
if args_tmp and args_tmp[0].lower() in ("instalar", "desinstalar") and "--puente" not in sys.argv:
import instalar # hablan con cuadros de mensaje: no hace falta consola
if args_tmp[0].lower() == "instalar" and instalar.hay_que_mudar(BASE) and not _soy_la_unica():
import ventana # la mudanza se lleva los datos: con el programa abierto se quedarían a medias
ctypes.windll.user32.MessageBoxW(None, "Cierra OrganiLAB antes de instalarlo.", "OrganiLAB", ventana.MB_OK | ventana.MB_ICONINFORMATION)
sys.exit(1)
sys.exit(instalar.principal(args_tmp[0].lower(), BASE, VERSION, silencioso="--si" in sys.argv))
if args_tmp and "--puente" not in sys.argv:
_asegurar_consola()
@@ -4583,13 +4632,14 @@ def main():
if huerfanas:
print(f"{huerfanas} conversiones quedaron a medias la última vez: vuelven a la cola en espera (Reanudar).")
try:
srv = ThreadingHTTPServer(("127.0.0.1", PUERTO), Manejador)
srv = ServidorLocal(("127.0.0.1", PUERTO), Manejador)
except OSError:
if os.environ.get("ORGANIZADOR_PUERTO"):
print(f"El puerto {PUERTO} está ocupado por otro programa.")
sys.exit(1)
# el puerto de siempre lo tiene otro programa: se toma uno libre (a la ventana le da igual cuál sea)
srv = ThreadingHTTPServer(("127.0.0.1", 0), Manejador)
# el puerto de siempre lo tiene otro programa (u otro OrganiLAB con otros datos): se toma uno libre,
# que a la ventana le da igual cuál sea
srv = ServidorLocal(("127.0.0.1", 0), Manejador)
PUERTO = srv.server_address[1]
url = f"http://127.0.0.1:{PUERTO}/"
guardar_json(SESION_PATH, {"pid": os.getpid(), "puerto": PUERTO})
@@ -4620,6 +4670,9 @@ def main():
while en_marcha(): # la ventana se fue con trabajo pendiente: se acaba antes de salir
time.sleep(1)
srv.shutdown()
srv.server_close()
if RELEVO: # se acaba de instalar en su sitio: sigue desde allí
subprocess.Popen([RELEVO[0]], cwd=os.path.dirname(RELEVO[0]), close_fds=True)
finally:
try:
os.remove(SESION_PATH)