OASIS-USABILITY/INSTALLER_V3/panel.py
SITO 68b3ceff8d 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>
2026-03-30 14:54:26 +02:00

354 lines
13 KiB
Python
Executable file

#!/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._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()
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):
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"],
"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
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)
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()