#!/usr/bin/env python3 """ OASIS Control Panel v3 — WebKit2GTK UI en HTML/CSS/JS dentro de ventana GTK nativa. Sin interferencias del tema GTK — control visual total como Electron. """ import gi gi.require_version('Gtk', '3.0') # Intentar WebKit2 4.1 primero, luego 4.0 _wk = None for _v in ('4.1', '4.0'): try: gi.require_version('WebKit2', _v) _wk = _v break except ValueError: pass if _wk is None: print("ERROR: WebKit2GTK no encontrado. Instala: gir1.2-webkit2-4.0") raise SystemExit(1) from gi.repository import Gtk, WebKit2, GLib, Gdk import subprocess import sys import signal import threading import json from pathlib import Path # ── Rutas base ───────────────────────────────────────────────────────────── SCRIPT_DIR = Path(__file__).parent.resolve() INSTALLER_SH = SCRIPT_DIR / "installer.sh" ECOIN_DIR = Path.home() / "ecoin" # Estado persistente (mismo fichero que usa el installer original) _STATE_FILE = Path.home() / ".config" / "oasis-installer" / "state" # Candidatos donde puede estar OASIS instalado _OASIS_CANDIDATES = [ Path.home() / "COFRE" / "CODERS" / "oasis", Path.home() / "oasis", Path.home() / "Projects" / "oasis", Path.home() / "Proyectos" / "oasis", Path.home() / "Documentos" / "oasis", Path.home() / "Documents" / "oasis", Path.home() / "src" / "oasis", ] # ── Detección OASIS ──────────────────────────────────────────────────────── def _oasis_valid(d: Path) -> bool: """Flexible: cualquiera de estas señales indica que OASIS está ahí.""" return ( (d / "oasis.sh").is_file() or (d / "src" / "server" / "server.js").is_file() or (d / "src" / "server" / "node_modules").is_dir() ) def _read_saved_dir() -> Path | None: """Lee la ruta guardada por el installer original.""" if _STATE_FILE.is_file(): for line in _STATE_FILE.read_text().splitlines(): if line.startswith("OASIS_DIR="): p = Path(line[10:].strip()) if p.is_dir(): return p return None def find_oasis_dir() -> Path: """Devuelve la ruta de OASIS encontrada o ~/oasis como fallback.""" candidates = [] saved = _read_saved_dir() if saved: candidates.append(saved) candidates.extend(_OASIS_CANDIDATES) for d in candidates: if d.is_dir() and _oasis_valid(d): return d return Path.home() / "oasis" def oasis_installed() -> bool: return _oasis_valid(find_oasis_dir()) def oasis_version() -> str: pkg = find_oasis_dir() / "src" / "server" / "package.json" if pkg.is_file(): try: return json.loads(pkg.read_text()).get("version", "") except Exception: pass return "" def oasis_running(port: int = 3000) -> bool: """Comprueba si hay algo escuchando en el puerto de OASIS (igual que el installer).""" try: r = subprocess.run( ["ss", "-lnt", f"sport = :{port}"], capture_output=True, text=True, timeout=2 ) return str(port) in r.stdout except Exception: return False # ── Detección ECOIN ──────────────────────────────────────────────────────── _ECOIN_CANDIDATES = [ Path.home() / "ecoin", Path.home() / "COFRE" / "CODERS" / "ecoin", Path.home() / "Projects" / "ecoin", ] def find_ecoin_dir() -> Path: for d in _ECOIN_CANDIDATES: if (d / "ecoin" / "ecoin-qt").is_file() or \ (d / "ecoin" / "src" / "ecoind").is_file(): return d return Path.home() / "ecoin" def ecoin_installed() -> bool: d = find_ecoin_dir() return (d / "ecoin" / "ecoin-qt").is_file() or \ (d / "ecoin" / "src" / "ecoind").is_file() def ecoin_wallet_exists() -> bool: return (Path.home() / ".ecoin" / "wallet.dat").is_file() def node_version() -> str: try: r = subprocess.run(["node", "--version"], capture_output=True, text=True, timeout=3) return r.stdout.strip() except Exception: return "" # ── Panel principal ──────────────────────────────────────────────────────── class OasisPanel: def __init__(self): 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) # Fondo negro en la ventana GTK para evitar parpadeo blanco al cargar css = b"window { background-color: #000000; }" provider = Gtk.CssProvider() provider.load_from_data(css) self.win.get_style_context().add_provider( provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION ) # ── 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 self.webview = WebKit2.WebView.new_with_user_content_manager(self.manager) self.webview.set_settings(settings) self.webview.set_background_color(Gdk.RGBA(0, 0, 0, 1)) 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}") self.win.show_all() # Poll inicial tras 1.5 s (tiempo para que cargue el HTML) GLib.timeout_add(1500, self._initial_poll) # Poll continuo cada 3 s GLib.timeout_add_seconds(3, self._poll) # ── Polling ─────────────────────────────────────────────────────────── def _initial_poll(self): self._poll() return False def _poll(self): threading.Thread(target=self._poll_thread, daemon=True).start() return True def _poll_thread(self): o_dir = find_oasis_dir() o_inst = _oasis_valid(o_dir) o_run = oasis_running() o_ver = oasis_version() o_node = node_version() e_dir = find_ecoin_dir() e_inst = ecoin_installed() e_wall = ecoin_wallet_exists() e_qt = (e_dir / "ecoin" / "ecoin-qt").is_file() e_dmn = (e_dir / "ecoin" / "src" / "ecoind").is_file() data = { "oasis_installed": o_inst, "oasis_running": o_run, "oasis_version": o_ver, "node_version": o_node, "oasis_dir": str(o_dir) if o_inst else "", "ecoin_installed": e_inst, "ecoin_wallet": e_wall, "ecoin_qt": e_qt, "ecoin_daemon": e_dmn, "ecoin_dir": str(e_dir) if e_inst else "", } GLib.idle_add(self._js, f"updateStatus({json.dumps(data)})") # ── 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: self.webview.run_javascript(code, None, None, None) return False def _log(self, text): 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) def _dispatch(self, action): installer_cmds = { "oasis-install": ["--oasis-install"], "ecoin-install": ["--ecoin-install"], "ecoin-gui": ["--ecoin-gui"], "ecoin-wallet": ["--wallet-create"], "ecoin-connect": ["--wallet-connect"], } if action == "oasis-stop": threading.Thread(target=self._kill_oasis, daemon=True).start() GLib.idle_add(self._js, "switchTab('sistema')") elif action == "oasis-start": GLib.idle_add(self._js, "switchTab('sistema')") threading.Thread(target=self._start_oasis, daemon=True).start() elif action == "oasis-browser": subprocess.Popen(["xdg-open", "http://localhost:3000"]) GLib.idle_add(self._log, "Abriendo http://localhost:3000 ...") elif action in installer_cmds: GLib.idle_add(self._js, "switchTab('sistema')") args = installer_cmds[action] threading.Thread(target=self._run_cmd, args=(args,), daemon=True).start() def _run_cmd(self, args): cmd = [str(INSTALLER_SH)] + args GLib.idle_add(self._log, "$ " + " ".join(cmd)) try: proc = subprocess.Popen( cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1 ) for line in iter(proc.stdout.readline, ""): line = line.rstrip() if line: GLib.idle_add(self._log, line) proc.wait() GLib.idle_add(self._log, f"── fin (código {proc.returncode}) ──") except Exception as e: GLib.idle_add(self._log, f"[Error]: {e}") GLib.idle_add(self._poll) 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}' &") try: subprocess.Popen( ["bash", "-lc", cmd], stdout=open("/tmp/oasis_gui.log", "w"), stderr=subprocess.STDOUT, start_new_session=True, ) except Exception as e: GLib.idle_add(self._log, f"[Error al lanzar]: {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...") 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}"]) 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._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.") except Exception as e: GLib.idle_add(self._log, f"[Error]: {e}") GLib.idle_add(self._poll) # ── Main ─────────────────────────────────────────────────────────────────── def main(): signal.signal(signal.SIGINT, signal.SIG_DFL) OasisPanel() Gtk.main() if __name__ == "__main__": main()