3.0.0: OrganiLAB pasa a ser un programa de Windows
- ventana.py: la ventana es un marco Win32 propio con WebView2 dentro, manejado por COM con ctypes. Fuera pywebview, pythonnet y clr_loader y sus binarios sin firmar. Identidad en la barra de tareas, una sola copia abierta por carpeta de datos, selector de carpetas de Windows, rutas completas al soltar, avance en la barra de tareas, memoria de posición, aviso al cerrar con trabajo en marcha, y nada de navegador (menú, F5, zoom, permisos). Si el motor se cae, se monta de nuevo solo. - arranque.py y empaquetar.py: el paquete ya no lleva binario propio. OrganizadorLabstream.exe es pythonw.exe (firmado por la PSF) con otro nombre y arranca el programa desde sitecustomize; abre con Smart App Control encendido. Fuera PyInstaller. Al empaquetar se comprueba la firma de cada binario con Windows y se arranca el motor del paquete. - instalar.py: accesos directos en Inicio y Escritorio con la identidad de la ventana, y la línea en Configuración › Aplicaciones. Se ofrece una vez, la primera que se abre un paquete en un equipo. - Dos copias sobre los mismos datos ya no se pisan: la segunda trae la ventana al frente (antes abría el navegador y daba por huérfanas las conversiones en marcha de la primera). Si el puerto 8765 está ocupado por otro programa, se toma uno libre. - Por consola, el paquete ya no consulta si hay versión nueva con las actualizaciones automáticas apagadas. - Pruebas: probar.py 17, prueba_funcional.py 26 de 26 y prueba_ventana.py (nueva), que abre la ventana de verdad: 17 comprobaciones desde el código y 8 arrancando el paquete por su .exe. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Este commit está contenido en:
301
organizador.py
301
organizador.py
@@ -2,10 +2,12 @@
|
||||
"""
|
||||
Organizador Labstream — inventario, mapa, orden y optimización del material.
|
||||
|
||||
Solo librería estándar de Python (3.10+). Arranca un servidor local y abre la
|
||||
interfaz en el navegador:
|
||||
Solo librería estándar de Python (3.10+). Es un programa de Windows: abre su
|
||||
propia ventana (ventana.py) y la interfaz habla con este módulo por un servidor
|
||||
que solo escucha en este equipo.
|
||||
|
||||
py -3.12 organizador.py (o doble clic en Iniciar.bat)
|
||||
OrganizadorLabstream.exe (el paquete: lo arma empaquetar.py)
|
||||
py -3.12 organizador.py (desde el código)
|
||||
|
||||
Nunca borra nada: lo que sobra va a _Para borrar dentro de la misma raíz y cada
|
||||
movimiento queda en datos/bitacora.jsonl para poder deshacerlo.
|
||||
@@ -37,11 +39,14 @@ import actualizador
|
||||
asistente.O = sys.modules[__name__]
|
||||
actualizador.O = sys.modules[__name__]
|
||||
|
||||
VERSION = "2.6.2"
|
||||
VERSION = "3.0.0"
|
||||
|
||||
CONGELADO = getattr(sys, "frozen", False)
|
||||
SIN_VENTANA = getattr(subprocess, "CREATE_NO_WINDOW", 0) # ffmpeg/ffprobe/net sin abrir consolas negras # empaquetado como .exe con PyInstaller
|
||||
BASE = os.path.dirname(os.path.abspath(sys.executable if CONGELADO else __file__))
|
||||
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
|
||||
# el intérprete de Python tal como lo firma la Python Software Foundation, con otro nombre (arranque.py
|
||||
# cuenta cómo). Hasta la 2.x era un .exe de PyInstaller, sin firma, y Smart App Control lo bloqueaba.
|
||||
CONGELADO = bool(getattr(sys, "frozen", False)) or os.path.normcase(os.path.dirname(os.path.abspath(sys.executable))) == os.path.normcase(BASE)
|
||||
SIN_VENTANA = getattr(subprocess, "CREATE_NO_WINDOW", 0) # ffmpeg/ffprobe/net sin abrir consolas negras
|
||||
DATOS = os.path.join(BASE, "datos")
|
||||
os.makedirs(DATOS, exist_ok=True)
|
||||
DB_PATH = os.path.join(DATOS, "inventario.sqlite")
|
||||
@@ -2239,11 +2244,8 @@ def correr_resolve(trabajos):
|
||||
lote = os.path.join(DATOS, f"resolve_lote_{int(time.time())}.json")
|
||||
with open(lote, "w", encoding="utf-8") as fh:
|
||||
json.dump(jobs, fh, ensure_ascii=False)
|
||||
if CONGELADO and not o.get("python_resolve"):
|
||||
cmd = [sys.executable, "--puente", lote] # el mismo .exe hace de puente
|
||||
else:
|
||||
py = o.get("python_resolve") or sys.executable
|
||||
cmd = [py, os.path.join(BASE, "resolve_bridge.py"), lote]
|
||||
# el puente corre con el mismo motor que el programa (en el paquete, su propio .exe haciendo de intérprete)
|
||||
cmd = [o.get("python_resolve") or sys.executable, os.path.join(BASE, "resolve_bridge.py"), lote]
|
||||
try:
|
||||
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, encoding="utf-8", errors="replace", creationflags=SIN_VENTANA)
|
||||
except OSError as ex:
|
||||
@@ -2889,13 +2891,20 @@ def _clave(ruta):
|
||||
|
||||
|
||||
_ELIGIENDO = threading.Lock()
|
||||
VENTANA = None # la ventana del programa (ventana.Ventana) mientras está abierta
|
||||
ULTIMA_PETICION = [0.0] # cuándo preguntó algo la interfaz por última vez
|
||||
|
||||
|
||||
def elegir_carpeta():
|
||||
"""Abre el selector de carpetas de Windows (en un PowerShell aparte, para no bloquear el servidor) y devuelve la ruta o None."""
|
||||
"""Abre el selector de carpetas de Windows y devuelve la ruta o None. Con la ventana del programa abierta
|
||||
es el suyo, modal sobre ella; si la interfaz va en el navegador, uno aparte en PowerShell."""
|
||||
if not _ELIGIENDO.acquire(blocking=False):
|
||||
raise ValueError("ya hay un selector de carpetas abierto")
|
||||
try:
|
||||
v = VENTANA
|
||||
if v is not None:
|
||||
ruta = v.elegir_carpeta("Elige la carpeta del cliente")
|
||||
return os.path.normpath(ruta) if ruta and os.path.isdir(ruta) else None
|
||||
ps = ("Add-Type -AssemblyName System.Windows.Forms; "
|
||||
"$d = New-Object System.Windows.Forms.FolderBrowserDialog; "
|
||||
"$d.Description = 'Elige la carpeta del cliente'; $d.ShowNewFolderButton = $false; "
|
||||
@@ -3777,6 +3786,7 @@ class Manejador(BaseHTTPRequestHandler):
|
||||
return True
|
||||
|
||||
def do_GET(self):
|
||||
ULTIMA_PETICION[0] = time.time()
|
||||
if self.path.startswith("/api/") and not self._es_local():
|
||||
return self._json({"error": "petición no permitida"}, 403)
|
||||
u = urlparse(self.path)
|
||||
@@ -3917,6 +3927,7 @@ class Manejador(BaseHTTPRequestHandler):
|
||||
return self._json({"error": repr(ex), "traza": traceback.format_exc()}, 500)
|
||||
|
||||
def do_POST(self):
|
||||
ULTIMA_PETICION[0] = time.time()
|
||||
u = urlparse(self.path)
|
||||
ruta = u.path
|
||||
if not self._es_local() or not (self.headers.get("Content-Type") or "").lower().startswith("application/json"):
|
||||
@@ -4120,7 +4131,8 @@ class Manejador(BaseHTTPRequestHandler):
|
||||
|
||||
AYUDA = """OrganiLAB (Organizador Labstream) — uso por comando
|
||||
|
||||
OrganizadorLabstream.exe abre la interfaz en el navegador
|
||||
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 "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)
|
||||
@@ -4344,20 +4356,20 @@ def _asegurar_consola():
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
def _al_soltar(e):
|
||||
try:
|
||||
rutas = [f.get("pywebviewFullPath") for f in (e.get("dataTransfer") or {}).get("files", []) if f.get("pywebviewFullPath")]
|
||||
except Exception: # noqa
|
||||
rutas = []
|
||||
def _al_soltar(rutas):
|
||||
"""Lo que se acaba de soltar en la ventana, con su ruta completa: la ventana la sabe, la interfaz no."""
|
||||
if rutas:
|
||||
SOLTADOS.update(ts=time.time(), rutas=[os.path.normpath(r) for r in rutas])
|
||||
|
||||
|
||||
def soltado(nombre, espera=2.5):
|
||||
"""Ruta completa de lo que se acaba de soltar en la ventana (llega por el puente WebView2). Espera un poco
|
||||
porque el drop del navegador y el aviso del puente no llegan en el mismo orden."""
|
||||
"""Ruta completa de lo que se acaba de soltar en la ventana. Espera un poco porque el aviso de la ventana
|
||||
y la pregunta de la interfaz no llegan siempre en el mismo orden."""
|
||||
fin = time.time() + espera
|
||||
while time.time() < fin:
|
||||
if time.time() - SOLTADOS["ts"] < 8:
|
||||
@@ -4368,46 +4380,181 @@ def soltado(nombre, espera=2.5):
|
||||
return None
|
||||
|
||||
|
||||
def _ventana(url):
|
||||
"""Ventana propia con WebView2 (Edge). Si no está disponible, se abre en el navegador."""
|
||||
try:
|
||||
import webview
|
||||
except Exception: # noqa
|
||||
webbrowser.open(url)
|
||||
return False
|
||||
try:
|
||||
v = webview.create_window("OrganiLAB", url, width=1280, height=820, min_size=(960, 640), background_color="#ffffff")
|
||||
def en_marcha():
|
||||
"""Qué está haciendo el programa ahora mismo, dicho en llano, o "" si no hace nada. Mientras haya algo,
|
||||
cerrar la ventana no lo corta."""
|
||||
que = actualizador.tarea_en_marcha()
|
||||
if not que and APLICANDO.get("total") and APLICANDO.get("hechas", 0) < APLICANDO["total"]:
|
||||
que = "se están moviendo archivos"
|
||||
return que
|
||||
|
||||
def _al_cargar():
|
||||
# WebView2 entrega la ruta completa de lo que se suelta solo por este puente (el navegador no la da):
|
||||
# se guarda y la interfaz la pide en /api/soltado en cuanto ve el drop.
|
||||
try:
|
||||
v.dom.document.events.drop += _al_soltar
|
||||
except Exception as ex: # noqa
|
||||
print("sin puente de arrastre:", ex)
|
||||
v.events.loaded += _al_cargar
|
||||
webview.start(gui="edgechromium", private_mode=False, storage_path=os.path.join(DATOS, "ventana"))
|
||||
|
||||
def avance():
|
||||
"""Cuánto lleva lo que está en marcha: de 0 a 1, -1 si no se puede saber y None si no hay nada. Es lo que
|
||||
se ve en el botón de la barra de tareas."""
|
||||
if COLA["activa"]:
|
||||
try:
|
||||
con = db()
|
||||
espera = con.execute("SELECT COUNT(*) FROM trabajos WHERE estado = 'pendiente'").fetchone()[0]
|
||||
t = con.execute("SELECT pct FROM trabajos WHERE id = ?", (COLA["actual"],)).fetchone() if COLA["actual"] else None
|
||||
con.close()
|
||||
except sqlite3.Error:
|
||||
return -1
|
||||
total = COLA["hechos"] + (1 if t else 0) + espera
|
||||
return (COLA["hechos"] + ((t[0] or 0) / 100 if t else 0)) / total if total else -1
|
||||
if RESPALDO.get("estado") == "corriendo":
|
||||
return RESPALDO["bytes"] / RESPALDO["bytes_total"] if RESPALDO.get("bytes_total") else -1
|
||||
if APLICANDO.get("total") and APLICANDO.get("hechas", 0) < APLICANDO["total"]:
|
||||
return APLICANDO["hechas"] / APLICANDO["total"]
|
||||
if asistente.ESTADO.get("activo"):
|
||||
return asistente.ESTADO["pct"] / 100 if asistente.ESTADO.get("total") else -1
|
||||
for a in list(ANALISIS.values()):
|
||||
if (a or {}).get("estado") == "corriendo":
|
||||
return a["hechos"] / a["total"] if a.get("total") else -1
|
||||
if any((p or {}).get("estado") == "corriendo" for p in list(PROGRESO.values())):
|
||||
return -1
|
||||
if actualizador.ESTADO.get("estado") == "descargando":
|
||||
return (actualizador.ESTADO.get("pct") or 0) / 100
|
||||
return None
|
||||
|
||||
|
||||
def _marca():
|
||||
"""Un número que identifica esta instalación por su carpeta de datos."""
|
||||
return (zlib.crc32(os.path.normcase(os.path.abspath(DATOS)).encode("utf-8")) & 0x7FFFFFFF) or 1
|
||||
|
||||
|
||||
def _soy_la_unica():
|
||||
"""False si ya hay otra copia del programa abierta sobre estos mismos datos: dos a la vez se pisarían el
|
||||
inventario, y la segunda daría por huérfanas las conversiones que la primera tiene en marcha."""
|
||||
if os.name != "nt":
|
||||
return True
|
||||
except Exception as ex: # noqa
|
||||
print("Sin WebView2; abro el navegador:", ex)
|
||||
webbrowser.open(url)
|
||||
k = ctypes.WinDLL("kernel32", use_last_error=True)
|
||||
k.CreateMutexW.argtypes, k.CreateMutexW.restype = [ctypes.c_void_p, ctypes.c_int, ctypes.c_wchar_p], ctypes.c_void_p
|
||||
h = k.CreateMutexW(None, False, f"OrganiLAB-{_marca():08x}")
|
||||
if h and ctypes.get_last_error() == 183: # ya existía
|
||||
return False
|
||||
_CANDADO.append(h)
|
||||
return True
|
||||
|
||||
|
||||
def _limpiar_restos():
|
||||
"""Lo que dejó el .exe de PyInstaller de las versiones hasta la 2.x: su carpeta _internal, que ya no usa
|
||||
nadie (el motor de ahora va en la carpeta runtime)."""
|
||||
viejo = os.path.join(BASE, "_internal")
|
||||
if CONGELADO and not getattr(sys, "frozen", False) and os.path.isfile(os.path.join(viejo, "base_library.zip")):
|
||||
shutil.rmtree(viejo, ignore_errors=True)
|
||||
|
||||
|
||||
def _vigilar(v, fin):
|
||||
"""Mientras la ventana existe: lleva el avance al botón de la barra de tareas, avisa cuando termina algo
|
||||
largo y, si la ventana se cerró con trabajo pendiente, cierra el programa en cuanto acaba."""
|
||||
antes, desde = None, 0.0
|
||||
while not fin.wait(1.0):
|
||||
try:
|
||||
f = avance()
|
||||
except Exception: # noqa
|
||||
f = None
|
||||
if f is None:
|
||||
if antes is not None:
|
||||
v.progreso(None)
|
||||
if time.time() - desde > 20 and not v.oculta:
|
||||
v.avisar()
|
||||
if v.oculta and not en_marcha():
|
||||
v.cerrar()
|
||||
else:
|
||||
if antes is None:
|
||||
desde = time.time()
|
||||
if not v.oculta:
|
||||
v.progreso(f)
|
||||
antes = f
|
||||
|
||||
|
||||
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."""
|
||||
try:
|
||||
import ventana
|
||||
import instalar
|
||||
if instalar.al_dia(BASE, VERSION):
|
||||
return
|
||||
oferta = CONFIG.get("instalacion") or {}
|
||||
if not CONGELADO or os.environ.get("ORGANIZADOR_PUERTO") or oferta.get("ofrecida") or instalar.instalado_aqui(BASE):
|
||||
return
|
||||
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.")
|
||||
except Exception as ex: # noqa
|
||||
print("no se pudo dar de alta en Windows:", ex)
|
||||
|
||||
|
||||
def _ventana(url):
|
||||
"""Abre la ventana del programa y no vuelve hasta que se cierra. Devuelve False si este Windows no puede
|
||||
abrirla (le falta WebView2): quien llama abre entonces la interfaz en el navegador."""
|
||||
global VENTANA
|
||||
try:
|
||||
import ventana
|
||||
import instalar
|
||||
ok, detalle = ventana.disponible(BASE)
|
||||
if not ok:
|
||||
print("Sin ventana propia:", detalle)
|
||||
return False
|
||||
v = ventana.Ventana(url, titulo="OrganiLAB", icono=os.path.join(BASE, "static", "organilab.ico"),
|
||||
perfil=os.path.join(DATOS, "ventana"), memoria=os.path.join(DATOS, "ventana.json"),
|
||||
base=BASE, marca=_marca(), relanzar=instalar.orden(BASE),
|
||||
depurar=bool(os.environ.get("ORGANIZADOR_DEPURAR")))
|
||||
v.al_cerrar, v.al_soltar = en_marcha, _al_soltar
|
||||
v.al_abrir = lambda: _recien_abierta(v)
|
||||
fin = threading.Event()
|
||||
threading.Thread(target=_vigilar, args=(v, fin), daemon=True).start()
|
||||
VENTANA = v
|
||||
try:
|
||||
abierta = v.mostrar()
|
||||
finally:
|
||||
VENTANA = None
|
||||
fin.set()
|
||||
if not abierta:
|
||||
print("La ventana no llegó a abrirse:", v.fallo)
|
||||
return abierta
|
||||
except Exception as ex: # noqa
|
||||
print("Sin ventana propia:", ex)
|
||||
return False
|
||||
|
||||
|
||||
def _atender_navegador():
|
||||
"""Sin ventana no hay nada que cerrar: el programa sigue mientras la página del navegador pregunte (lo hace
|
||||
cada pocos segundos) y se va cuando lleva tres minutos callada y no queda trabajo en marcha."""
|
||||
ULTIMA_PETICION[0] = time.time()
|
||||
try:
|
||||
while time.time() - ULTIMA_PETICION[0] < SILENCIO_NAVEGADOR or en_marcha():
|
||||
time.sleep(2)
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
|
||||
|
||||
def main():
|
||||
global PUERTO
|
||||
args_tmp = [a for a in sys.argv[1:] if not a.startswith("--")]
|
||||
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
|
||||
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()
|
||||
try:
|
||||
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
||||
except Exception: # noqa
|
||||
pass
|
||||
if "--puente" in sys.argv: # modo puente con Resolve (lo usa el propio .exe)
|
||||
if "--puente" in sys.argv: # puente con Resolve, como lo llamaban los .exe hasta la 2.x
|
||||
import resolve_bridge
|
||||
sys.exit(resolve_bridge.main(sys.argv[sys.argv.index("--puente") + 1]))
|
||||
args = [a for a in sys.argv[1:] if a not in ("--sin-navegador", "--navegador")]
|
||||
if args:
|
||||
if CONGELADO and args[0].lower() not in ("actualizar", "actualizacion", "ayuda", "-h", "--help", "/?"):
|
||||
if (CONGELADO and CONFIG.get("actualizaciones", {}).get("auto") is True
|
||||
and args[0].lower() not in ("actualizar", "actualizacion", "ayuda", "-h", "--help", "/?")):
|
||||
actualizador.comprobar_en_segundo_plano()
|
||||
codigo = cli(args)
|
||||
d = actualizador.ESTADO.get("disponible")
|
||||
@@ -4419,6 +4566,16 @@ def main():
|
||||
except EOFError:
|
||||
pass
|
||||
sys.exit(codigo)
|
||||
sin_ventana = "--sin-navegador" in sys.argv or "--navegador" in sys.argv
|
||||
if not _soy_la_unica():
|
||||
import ventana
|
||||
if not sin_ventana and ventana.traer_al_frente(_marca()):
|
||||
return # ya estaba abierto: se enseña esa ventana y nada más
|
||||
otra = cargar_json(SESION_PATH, {}).get("puerto")
|
||||
print("Ya hay un OrganiLAB abierto con estos datos" + (f", en el puerto {otra}." if otra else "."))
|
||||
if otra and "--sin-navegador" not in sys.argv:
|
||||
webbrowser.open(f"http://127.0.0.1:{otra}/") # esa copia no tiene ventana: va en el navegador
|
||||
return
|
||||
h = herramientas()
|
||||
print("ffmpeg:", "OK" if h["ffmpeg"]["ok"] else "NO ENCONTRADO", h["ffmpeg"]["ruta"])
|
||||
print("Resolve:", "OK" if h["resolve"]["ok"] else "no instalado (solo hace falta para BRAW)")
|
||||
@@ -4428,34 +4585,46 @@ def main():
|
||||
try:
|
||||
srv = ThreadingHTTPServer(("127.0.0.1", PUERTO), Manejador)
|
||||
except OSError:
|
||||
print(f"Ya hay un Organizador abierto en el puerto {PUERTO}; abro esa ventana.")
|
||||
webbrowser.open(f"http://127.0.0.1:{PUERTO}/")
|
||||
return
|
||||
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)
|
||||
PUERTO = srv.server_address[1]
|
||||
url = f"http://127.0.0.1:{PUERTO}/"
|
||||
guardar_json(SESION_PATH, {"pid": os.getpid(), "puerto": PUERTO})
|
||||
if sys.stdout:
|
||||
print(f"OrganiLAB {VERSION} en {url} (Ctrl+C para cerrar)")
|
||||
print(f"OrganiLAB {VERSION} en {url}")
|
||||
if CONGELADO and CONFIG.get("actualizaciones", {}).get("auto") is True:
|
||||
actualizador.comprobar_en_segundo_plano()
|
||||
if "--sin-navegador" in sys.argv: # solo el servidor (desarrollo / vista previa)
|
||||
try:
|
||||
if "--sin-navegador" in sys.argv: # solo el servidor (pruebas y desarrollo)
|
||||
try:
|
||||
srv.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
return
|
||||
hilo = threading.Thread(target=srv.serve_forever, daemon=True)
|
||||
hilo.start()
|
||||
threading.Thread(target=_limpiar_restos, daemon=True).start()
|
||||
if "--navegador" in sys.argv:
|
||||
webbrowser.open(url)
|
||||
try:
|
||||
hilo.join()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
return
|
||||
if not _ventana(url):
|
||||
webbrowser.open(url)
|
||||
_atender_navegador()
|
||||
while en_marcha(): # la ventana se fue con trabajo pendiente: se acaba antes de salir
|
||||
time.sleep(1)
|
||||
srv.shutdown()
|
||||
finally:
|
||||
try:
|
||||
srv.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
os.remove(SESION_PATH)
|
||||
except OSError:
|
||||
pass
|
||||
return
|
||||
hilo = threading.Thread(target=srv.serve_forever, daemon=True)
|
||||
hilo.start()
|
||||
if "--navegador" in sys.argv:
|
||||
webbrowser.open(url)
|
||||
try:
|
||||
hilo.join()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
return
|
||||
# ventana propia: al cerrarla se cierra el programa (salvo que haya trabajo en marcha)
|
||||
_ventana(url)
|
||||
while COLA["activa"] or RESPALDO.get("estado") == "corriendo" or asistente.ESTADO.get("activo"):
|
||||
time.sleep(1)
|
||||
srv.shutdown()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Referencia en una nueva incidencia
Block a user