From 68b3ceff8d2a72fe2c95496e59f58113dc0c1ce4 Mon Sep 17 00:00:00 2001 From: SITO Date: Mon, 30 Mar 2026 14:54:26 +0200 Subject: [PATCH] =?UTF-8?q?fix(panel-v3):=20bridge=20JS=E2=86=92Python,=20?= =?UTF-8?q?DETENER=20por=20PID,=20NVM,=20cierre=20limpio?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Bridge reescrito: usa oasis:// URI en lugar de UserContentManager - bash -lc para cargar NVM al lanzar OASIS (Node v22) - DETENER mata el grupo de procesos por PID (no pkill genérico) - _oasis_proc guarda el proceso activo entre INICIAR/DETENER - Flag _alive evita MemoryError al llamar GLib.idle_add tras cerrar - _on_destroy desconecta el main loop limpiamente Co-Authored-By: Claude Sonnet 4.6 --- INSTALLER_V3/panel.py | 115 +++++++++++++++++++++-------------------- INSTALLER_V3/ui/app.js | 4 +- 2 files changed, 61 insertions(+), 58 deletions(-) diff --git a/INSTALLER_V3/panel.py b/INSTALLER_V3/panel.py index 3bc90ab..5c7fc6b 100755 --- a/INSTALLER_V3/panel.py +++ b/INSTALLER_V3/panel.py @@ -138,12 +138,15 @@ def node_version() -> str: class OasisPanel: def __init__(self): + self._alive = True + self._oasis_proc = None # proceso OASIS activo + self.win = Gtk.Window() self.win.set_title("SOLAR NET HUB") self.win.set_default_size(390, 620) self.win.set_resizable(False) self.win.set_position(Gtk.WindowPosition.CENTER) - self.win.connect("destroy", Gtk.main_quit) + self.win.connect("destroy", self._on_destroy) # Fondo negro en la ventana GTK para evitar parpadeo blanco al cargar css = b"window { background-color: #000000; }" @@ -154,24 +157,21 @@ class OasisPanel: ) # ── WebKit2 ─────────────────────────────────────────────────────── - self.manager = WebKit2.UserContentManager() - self.manager.connect("script-message-received::bridge", self._on_message) - self.manager.register_script_message_handler("bridge") - settings = WebKit2.Settings() settings.set_enable_javascript(True) - settings.set_enable_developer_extras(True) # activa inspector por si hace falta depurar settings.set_allow_file_access_from_file_urls(True) settings.set_allow_universal_access_from_file_urls(True) - settings.set_enable_page_cache(False) # sin caché de página + settings.set_enable_page_cache(False) - self.webview = WebKit2.WebView.new_with_user_content_manager(self.manager) + self.webview = WebKit2.WebView() self.webview.set_settings(settings) self.webview.set_background_color(Gdk.RGBA(0, 0, 0, 1)) + # Bridge JS→Python: interceptar navegaciones a oasis://accion + self.webview.connect("decide-policy", self._on_policy) + self.win.add(self.webview) - # Cargar HTML directamente por URI (evita caché de recursos) html_file = SCRIPT_DIR / "ui" / "index.html" self.webview.load_uri(f"file://{html_file}") @@ -182,6 +182,10 @@ class OasisPanel: # Poll continuo cada 3 s GLib.timeout_add_seconds(3, self._poll) + def _on_destroy(self, *_): + self._alive = False + Gtk.main_quit() + # ── Polling ─────────────────────────────────────────────────────────── def _initial_poll(self): self._poll() @@ -219,24 +223,26 @@ class OasisPanel: # ── JS helpers ──────────────────────────────────────────────────────── def _js(self, code): - # evaluate_javascript (WebKit2 4.1+) o run_javascript (4.0 fallback) - if hasattr(self.webview, "evaluate_javascript"): - self.webview.evaluate_javascript(code, -1, None, None, None, None, None) - else: + if self._alive: self.webview.run_javascript(code, None, None, None) return False def _log(self, text): - self._js(f"appendLog({json.dumps(text)})") + if self._alive: + self._js(f"appendLog({json.dumps(text)})") - # ── Mensaje desde JS ────────────────────────────────────────────────── - def _on_message(self, _manager, result): - try: - data = json.loads(result.get_value().to_string()) - action = data.get("action", "") - except Exception: - return - self._dispatch(action) + # ── Bridge JS→Python via navegación oasis://accion ─────────────────── + def _on_policy(self, webview, decision, decision_type): + if decision_type == WebKit2.PolicyDecisionType.NAVIGATION_ACTION: + uri = decision.get_navigation_action().get_request().get_uri() + if uri.startswith("oasis://"): + action = uri[len("oasis://"):] + print(f"[bridge] action={action}", flush=True) + GLib.idle_add(self._dispatch, action) + decision.ignore() + return True + decision.use() + return False def _dispatch(self, action): installer_cmds = { @@ -284,57 +290,56 @@ class OasisPanel: def _start_oasis(self): import time - PORT = 3000 oasis_dir = find_oasis_dir() - oasis_sh = oasis_dir / "oasis.sh" - - if not oasis_sh.is_file(): - GLib.idle_add(self._log, f"[Error]: no se encontró oasis.sh en {oasis_dir}") - return - - # ¿Ya está escuchando? - if oasis_running(PORT): - GLib.idle_add(self._log, f"OASIS ya está escuchando en el puerto {PORT}") - subprocess.Popen(["xdg-open", f"http://localhost:{PORT}"]) - GLib.idle_add(self._poll) - return - - # Lanzar en background igual que el installer original - cmd = f'cd "{oasis_dir}" && exec bash oasis.sh' - GLib.idle_add(self._log, f"$ nohup bash -lc '{cmd}' &") + GLib.idle_add(self._log, f"$ cd {oasis_dir} && bash oasis.sh") try: - subprocess.Popen( - ["bash", "-lc", cmd], - stdout=open("/tmp/oasis_gui.log", "w"), + proc = subprocess.Popen( + ["bash", "-lc", f"cd '{oasis_dir}' && bash oasis.sh"], + stdout=subprocess.PIPE, stderr=subprocess.STDOUT, start_new_session=True, ) except Exception as e: - GLib.idle_add(self._log, f"[Error al lanzar]: {e}") + GLib.idle_add(self._log, f"[Error]: {e}") return - # Esperar hasta 30 s a que el puerto esté activo - GLib.idle_add(self._log, "Esperando que OASIS levante en el puerto 3000...") + self._oasis_proc = proc + GLib.idle_add(self._log, f"PID {proc.pid} — esperando puerto 3000...") + + # Leer salida del proceso y mostrarla en el log + def _read_output(): + for line in iter(proc.stdout.readline, b""): + if self._alive: + GLib.idle_add(self._log, line.decode("utf-8", errors="replace").rstrip()) + threading.Thread(target=_read_output, daemon=True).start() + + # Esperar hasta 30s a que el puerto esté activo y abrir navegador for i in range(30): time.sleep(1) - if oasis_running(PORT): - GLib.idle_add(self._log, f"OASIS levantado en {PORT}. Abriendo navegador...") - subprocess.Popen(["xdg-open", f"http://localhost:{PORT}"]) + if oasis_running(): + GLib.idle_add(self._log, "Puerto 3000 activo — abriendo navegador") + subprocess.Popen(["xdg-open", "http://localhost:3000"]) GLib.idle_add(self._poll) return - if (i + 1) % 5 == 0: - GLib.idle_add(self._log, f" ... {i+1}s") - - GLib.idle_add(self._log, "[Error]: OASIS no levantó en 30 s. Revisa /tmp/oasis_gui.log") + GLib.idle_add(self._log, "[Timeout] OASIS no levantó en 30s") GLib.idle_add(self._poll) def _kill_oasis(self): GLib.idle_add(self._log, "Deteniendo OASIS...") try: - subprocess.run(["pkill", "-f", "node.*server.js"], timeout=5) - GLib.idle_add(self._log, "Servidor OASIS detenido.") + import os, signal as sig + if self._oasis_proc and self._oasis_proc.poll() is None: + # Matar el grupo de procesos completo (bash -lc + node hijo) + os.killpg(os.getpgid(self._oasis_proc.pid), sig.SIGTERM) + self._oasis_proc.wait(timeout=5) + self._oasis_proc = None + GLib.idle_add(self._log, "OASIS detenido.") + else: + # Fallback: matar cualquier node backend.js corriendo + subprocess.run(["pkill", "-f", "node.*backend.js"], timeout=5) + GLib.idle_add(self._log, "OASIS detenido.") except Exception as e: - GLib.idle_add(self._log, f"[Error]: {e}") + GLib.idle_add(self._log, f"[Error al detener]: {e}") GLib.idle_add(self._poll) diff --git a/INSTALLER_V3/ui/app.js b/INSTALLER_V3/ui/app.js index 239e1d7..5d61ecc 100644 --- a/INSTALLER_V3/ui/app.js +++ b/INSTALLER_V3/ui/app.js @@ -5,9 +5,7 @@ // ── Bridge JS → Python ──────────────────────────────────────── function send(action) { - window.webkit.messageHandlers.bridge.postMessage( - JSON.stringify({ action }) - ); + window.location.href = 'oasis://' + action; } // ── Tabs ──────────────────────────────────────────────────────