fix(panel-v3): bridge JS→Python, DETENER por PID, NVM, cierre limpio

- 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 <noreply@anthropic.com>
This commit is contained in:
SITO 2026-03-30 14:54:26 +02:00
parent 1ed0aa03d9
commit 68b3ceff8d
2 changed files with 61 additions and 58 deletions

View file

@ -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):
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)

View file

@ -5,9 +5,7 @@
// ── Bridge JS → Python ────────────────────────────────────────
function send(action) {
window.webkit.messageHandlers.bridge.postMessage(
JSON.stringify({ action })
);
window.location.href = 'oasis://' + action;
}
// ── Tabs ──────────────────────────────────────────────────────