""" Launcher de la app GENPACT V2 - Utilerias RECON 2. Arranca Voila in-process (sin subprocess) para que funcione empaquetado con PyInstaller. Si el .exe es invocado como kernel de IPython, enruta al ipykernel_launcher en vez de relanzar Voila (evita el bucle infinito). """ import os import sys import time import socket import threading import webbrowser from pathlib import Path # ============================================================ # Diagnostico: escribir log al disco apenas arranque el launcher. # Si este archivo no aparece, el problema es PyInstaller / antivirus # bloqueando la extraccion antes de que Python siquiera empiece. # ============================================================ def _diag_log(msg): try: log_path = os.path.join(os.path.expanduser('~'), 'utilerias_recon_boot.log') with open(log_path, 'a', encoding='utf-8') as f: f.write(f'{time.strftime("%Y-%m-%d %H:%M:%S")} | {msg}\n') except Exception: pass _diag_log(f'=== LAUNCHER START ===') _diag_log(f'sys.executable={sys.executable}') _diag_log(f'sys.argv={sys.argv}') _diag_log(f'cwd={os.getcwd()}') _diag_log(f'frozen={getattr(sys, "frozen", False)}') try: if sys.stdout is not None: print(f'[boot] launcher iniciando, sys.executable={sys.executable}', flush=True) print(f'[boot] argv={sys.argv}', flush=True) print(f'[boot] cwd={os.getcwd()}', flush=True) except Exception: pass # ============================================================ # Cargar .env ANTES de cualquier cosa para que las variables # queden en os.environ y se hereden a Voila + kernels subprocess. # (En --onefile el cwd del kernel apunta al temp de PyInstaller, # no a la carpeta del .exe, asi que load_dotenv relativo falla.) # ============================================================ def _exe_dir() -> Path: if getattr(sys, 'frozen', False): return Path(sys.executable).parent return Path(__file__).resolve().parent _env_file = _exe_dir() / '.env' if _env_file.exists(): try: from dotenv import load_dotenv load_dotenv(_env_file, override=True) except ImportError: pass # ============================================================ # Router de modo: detectar invocacion como kernel de Jupyter # jupyter_client spawnea: sys.executable -m ipykernel_launcher -f conn.json # En el .exe eso seria: UtileriasReconV2.exe -m ipykernel_launcher -f conn.json # ============================================================ if 'ipykernel_launcher' in sys.argv: # En modo empaquetado, sys.stdout/stderr heredados del padre (Voila) # pueden ser invalidos para flush() y romper ipykernel.init_io. # ipykernel los reemplaza por canales ZMQ despues, asi que devnull es seguro. if getattr(sys, 'frozen', False): try: _devnull = open(os.devnull, 'w') sys.stdout = _devnull sys.stderr = _devnull except Exception: pass sys.argv = [a for a in sys.argv if a not in ('-m', 'ipykernel_launcher')] from ipykernel import kernelapp kernelapp.launch_new_instance() sys.exit(0) # ============================================================ # Localizar app.ipynb y .env # ============================================================ def resources_dir() -> Path: """Recursos embebidos (app.ipynb bundleado). En PyInstaller usa _MEIPASS.""" if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'): return Path(sys._MEIPASS) return Path(sys.executable).parent if getattr(sys, 'frozen', False) \ else Path(__file__).resolve().parent def user_dir() -> Path: """Archivos del usuario (.env). Siempre al lado del .exe.""" if getattr(sys, 'frozen', False): return Path(sys.executable).parent return Path(__file__).resolve().parent RES = resources_dir() USR = user_dir() # Buscar app.ipynb: primero al lado del .exe (override), luego en recursos APP_NB = USR / 'app.ipynb' if not APP_NB.exists(): APP_NB = RES / 'app.ipynb' # cwd donde este el .env (para que python-dotenv lo encuentre) if (USR / '.env').exists(): os.chdir(USR) elif (USR.parent / '.env').exists(): os.chdir(USR.parent) else: os.chdir(USR) def find_free_port(preferido: int = 8866) -> int: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: sock.bind(('127.0.0.1', preferido)) sock.close() return preferido except OSError: sock.close() s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.bind(('127.0.0.1', 0)) p = s.getsockname()[1] s.close() return p def fatal(msg: str): """Muestra error y sale. Si no hay consola, usa MessageBox de Windows.""" try: if sys.stdout is not None: print(f'ERROR: {msg}') except Exception: pass try: import tkinter from tkinter import messagebox root = tkinter.Tk() root.withdraw() messagebox.showerror('Utilerias RECON 2', msg) except Exception: pass sys.exit(1) def main(): if not APP_NB.exists(): fatal(f'No se encuentra app.ipynb en {APP_NB}') port = find_free_port(8866) url = f'http://127.0.0.1:{port}/' if sys.stdout is not None: print('=' * 60) print(' GENPACT V2 - Sistema de Descargas SCAII') print('=' * 60) print(f' URL: {url}') print(' Cierra esta ventana para detener la app.') print('=' * 60) # Abrir browser cuando Voila acepte conexiones def _open(): for _ in range(120): try: with socket.create_connection(('127.0.0.1', port), timeout=0.5): break except OSError: time.sleep(0.5) webbrowser.open(url) threading.Thread(target=_open, daemon=True).start() # Lanzar Voila in-process (sin subprocess) sys.argv = [ 'voila', str(APP_NB), f'--port={port}', '--no-browser', '--Voila.ip=127.0.0.1', '--strip_sources=True', ] try: from voila.app import Voila Voila.launch_instance() except SystemExit: pass except Exception as e: fatal(f'Voila fallo al iniciar: {e}') if __name__ == '__main__': main()