fix(panel-v3): detección de puerto, botones y limpieza de warnings
- oasis_running() usa ss en lugar de pgrep (evita falsos positivos) - _start_oasis() lanza oasis.sh en background y espera el puerto - INSTALAR se deshabilita cuando OASIS ya está instalado/activo - Corrige DeprecationWarning: override_background_color → CssProvider - Corrige DeprecationWarning: run_javascript → evaluate_javascript - Desactiva caché de WebKit, carga por URI directa - Inspector de WebKit activado para depuración Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
ae79e45c19
commit
67acbf1add
17 changed files with 2692 additions and 0 deletions
349
INSTALLER_V3/panel.py
Executable file
349
INSTALLER_V3/panel.py
Executable file
|
|
@ -0,0 +1,349 @@
|
|||
#!/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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue