Files
utilerias-recon-2-exe/app/launcher.py
Kevin_Ramirez 71ef0b390a feat: empaquetado .exe con instalador y fix conexion SCAII
- launcher.py reescrito para PyInstaller (Voila in-process, kernel router)
- build_exe.bat: script PyInstaller (onedir + collect-all dependencias Jupyter)
- build_installer.bat + installer.iss: instalador Inno Setup
- schema_registro_sqlite.sql: schema DataStage en SQLite
- _make_conn_str ahora respeta SCAII_TRUSTED (Windows Auth)
- .env.example: quitar variables DB_* (Postgres ya no usado por DataStage)
- requirements.txt: quitar psycopg2-binary

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 15:57:25 -05:00

200 lines
6.2 KiB
Python

"""
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()