#!/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 ecoin_rpc_config() -> dict: """Lee credenciales RPC de ~/.ecoin/ecoin.conf.""" conf = Path.home() / ".ecoin" / "ecoin.conf" cfg = {"rpcuser": "", "rpcpassword": "", "rpcport": "7474", "rpchost": "127.0.0.1"} if conf.is_file(): for line in conf.read_text().splitlines(): line = line.strip() if line.startswith("#") or "=" not in line: continue k, v = line.split("=", 1) k = k.strip() if k in cfg: cfg[k] = v.strip() return cfg def ecoin_rpc(method, params=None): """Llama al RPC de ecoind. Devuelve (result, error).""" import http.client, base64 as _b64 cfg = ecoin_rpc_config() auth = _b64.b64encode(f"{cfg['rpcuser']}:{cfg['rpcpassword']}".encode()).decode() body = json.dumps({"method": method, "params": params or [], "id": 1}).encode() try: conn = http.client.HTTPConnection(cfg["rpchost"], int(cfg["rpcport"]), timeout=3) conn.request("POST", "/", body, {"Content-Type": "application/json", "Authorization": f"Basic {auth}"}) resp = conn.getresponse() data = json.loads(resp.read()) return data.get("result"), data.get("error") except Exception as e: return None, str(e) def ecoin_running() -> bool: result, _ = ecoin_rpc("getinfo") return result is not None 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._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", self._on_destroy) # 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 ─────────────────────────────────────────────────────── settings = WebKit2.Settings() settings.set_enable_javascript(True) settings.set_allow_file_access_from_file_urls(True) settings.set_allow_universal_access_from_file_urls(True) settings.set_enable_page_cache(False) 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) 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) def _on_destroy(self, *_): self._alive = False Gtk.main_quit() # ── 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() e_run = ecoin_running() # Info RPC solo si el daemon está corriendo e_info = {} e_balance = None if e_run: info, _ = ecoin_rpc("getinfo") bal, _ = ecoin_rpc("getbalance") e_info = info or {} e_balance = bal 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_running": e_run, "ecoin_wallet": e_wall, "ecoin_qt": e_qt, "ecoin_daemon": e_dmn, "ecoin_dir": str(e_dir) if e_inst else "", "ecoin_balance": e_balance, "ecoin_blocks": e_info.get("blocks", None), "ecoin_connections": e_info.get("connections", None), } GLib.idle_add(self._js, f"updateStatus({json.dumps(data)})") # ── JS helpers ──────────────────────────────────────────────────────── def _js(self, code): 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)})") # ── 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 = { "oasis-install": ["--oasis-install"], "ecoin-install": ["--ecoin-install"], } 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 == "ecoin-start": GLib.idle_add(self._js, "switchTab('sistema')") threading.Thread(target=self._start_ecoin, daemon=True).start() elif action == "ecoin-stop": GLib.idle_add(self._js, "switchTab('sistema')") threading.Thread(target=self._stop_ecoin, daemon=True).start() elif action == "ecoin-gui": GLib.idle_add(self._js, "switchTab('sistema')") threading.Thread(target=self._open_ecoin_gui, daemon=True).start() elif action == "ecoin-info": GLib.idle_add(self._js, "switchTab('sistema')") threading.Thread(target=self._ecoin_info, daemon=True).start() 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 oasis_dir = find_oasis_dir() GLib.idle_add(self._log, f"$ cd {oasis_dir} && bash oasis.sh") try: 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]: {e}") return 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(): GLib.idle_add(self._log, "Puerto 3000 activo — abriendo navegador") subprocess.Popen(["xdg-open", "http://localhost:3000"]) GLib.idle_add(self._poll) return GLib.idle_add(self._log, "[Timeout] OASIS no levantó en 30s") GLib.idle_add(self._poll) # ── ECOIN actions ───────────────────────────────────────────────────── def _start_ecoin(self): if ecoin_running(): GLib.idle_add(self._log, "ecoind ya está corriendo.") GLib.idle_add(self._poll) return e_dir = find_ecoin_dir() ecoind = e_dir / "ecoin" / "src" / "ecoind" if not ecoind.is_file(): GLib.idle_add(self._log, f"[Error]: ecoind no encontrado en {ecoind}") return GLib.idle_add(self._log, f"$ {ecoind} -daemon") try: subprocess.Popen( [str(ecoind), "-daemon"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, start_new_session=True, ) import time for i in range(15): time.sleep(1) if ecoin_running(): GLib.idle_add(self._log, "ecoind iniciado correctamente.") GLib.idle_add(self._poll) return GLib.idle_add(self._log, "[Timeout] ecoind no respondió en 15s.") except Exception as e: GLib.idle_add(self._log, f"[Error]: {e}") GLib.idle_add(self._poll) def _stop_ecoin(self): GLib.idle_add(self._log, "Deteniendo ecoind...") result, err = ecoin_rpc("stop") if err: GLib.idle_add(self._log, f"[Error RPC]: {err}") else: GLib.idle_add(self._log, "ecoind detenido.") GLib.idle_add(self._poll) def _open_ecoin_gui(self): e_dir = find_ecoin_dir() ecoin_qt = e_dir / "ecoin" / "ecoin-qt" src_dir = e_dir / "ecoin" if ecoin_qt.is_file(): # Si ecoind está corriendo, pararlo antes (no pueden coexistir) if ecoin_running(): GLib.idle_add(self._log, "Parando ecoind antes de abrir la GUI...") ecoin_rpc("stop") import time for _ in range(10): time.sleep(1) if not ecoin_running(): break GLib.idle_add(self._log, f"$ {ecoin_qt}") subprocess.Popen([str(ecoin_qt)], start_new_session=True) GLib.idle_add(self._poll) return # No compilado — compilar primero GLib.idle_add(self._log, "ecoin-qt no encontrado. Compilando...") # Comprobar que qmake está disponible qmake = None for q in ("qmake", "qmake-qt5", "qmake6"): r = subprocess.run(["which", q], capture_output=True) if r.returncode == 0: qmake = q break if not qmake: GLib.idle_add(self._log, "[Error]: qmake no encontrado.") GLib.idle_add(self._log, "Instala Qt5: sudo apt install qtbase5-dev qt5-qmake") return GLib.idle_add(self._log, f"$ cd {src_dir} && {qmake} && make -j$(nproc)") try: proc = subprocess.Popen( ["bash", "-c", f"cd '{src_dir}' && {qmake} && make -j$(nproc)"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, ) for line in iter(proc.stdout.readline, b""): if self._alive: GLib.idle_add(self._log, line.decode("utf-8", errors="replace").rstrip()) proc.wait() except Exception as e: GLib.idle_add(self._log, f"[Error compilando]: {e}") return if ecoin_qt.is_file(): GLib.idle_add(self._log, "Compilación completada. Lanzando ecoin-qt...") subprocess.Popen([str(ecoin_qt)], start_new_session=True) else: GLib.idle_add(self._log, "[Error]: compilación falló. Revisa las dependencias.") GLib.idle_add(self._log, "Dependencias necesarias:") GLib.idle_add(self._log, " sudo apt install qtbase5-dev qt5-qmake libboost-all-dev libssl-dev libdb++-dev") def _ecoin_info(self): if not ecoin_running(): GLib.idle_add(self._log, "ecoind no está corriendo. Inicia el daemon primero.") return info, err = ecoin_rpc("getinfo") if err: GLib.idle_add(self._log, f"[RPC Error]: {err}") return bal, _ = ecoin_rpc("getbalance") GLib.idle_add(self._log, "── ECOIN INFO ──────────────────") GLib.idle_add(self._log, f" Balance: {bal} ECO") GLib.idle_add(self._log, f" Bloques: {info.get('blocks')}") GLib.idle_add(self._log, f" Conexiones: {info.get('connections')}") GLib.idle_add(self._log, f" Versión: {info.get('version')}") GLib.idle_add(self._log, f" Dificultad: {info.get('difficulty')}") GLib.idle_add(self._log, "────────────────────────────────") GLib.idle_add(self._poll) def _kill_oasis(self): GLib.idle_add(self._log, "Deteniendo OASIS...") try: 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 al detener]: {e}") GLib.idle_add(self._poll) # ── Main ─────────────────────────────────────────────────────────────────── def main(): signal.signal(signal.SIGINT, signal.SIG_DFL) OasisPanel() Gtk.main() if __name__ == "__main__": main()