Files
framebridge/source/packaging/framebridge-original
T

259 lines
13 KiB
Python
Executable File

#!/usr/bin/python3
"""Manual launcher for the captured FrameBridge Original programs."""
import argparse
import hashlib
import json
import os
from pathlib import Path
import shutil
import signal
import stat
import socket
import subprocess
import sys
import time
PREFIX = Path(__file__).resolve().parents[1]
REAL_HOME = Path.home()
DEFAULT_STATE = Path(os.environ.get("XDG_STATE_HOME", str(REAL_HOME / ".local/state"))) / "framebridge-original"
LAYOUT = json.loads((PREFIX / "layout.json").read_text())
CORE_COMMANDS = ["bash", "tmux", "tee", "flock", "awk", "sha256sum", "timeout",
"gnome-terminal", "wmctrl"]
def settings(args):
state = Path(args.state_dir).expanduser().absolute()
home = state / "home"
runtime_base = Path("/run/user") / str(os.getuid())
if not runtime_base.is_dir() or runtime_base.stat().st_uid != os.getuid():
runtime_base = Path("/tmp")
runtime = runtime_base / ("fbo-" + str(os.getuid()) + "-" + hashlib.sha256(str(state).encode()).hexdigest()[:16])
env = os.environ.copy()
authority = env.get("XAUTHORITY")
if not authority and (REAL_HOME / ".Xauthority").is_file():
authority = str(REAL_HOME / ".Xauthority")
env.update({
"HOME": str(home), "XDG_CONFIG_HOME": str(home / ".config"),
"XDG_DATA_HOME": str(home / ".local/share"),
"XDG_STATE_HOME": str(home / ".local/state"), "XDG_CACHE_HOME": str(home / ".cache"),
"XDG_RUNTIME_DIR": str(runtime), "TMPDIR": str(runtime / "tmp"),
"TMUX_TMPDIR": str(runtime / "tmux"), "PYTHONDONTWRITEBYTECODE": "1",
"FBTERM_ROOT": str(home / ".local/share/framebridge/fbterm"),
"FRAMEBRIDGE_FBTERM_ROOT_OVERRIDE": str(home / ".local/share/framebridge/fbterm"),
"FBTERM_SESSION": "framebridge-original-fbterm",
"FBTERM_TITLE": "FrameBridge Original",
"FRAMEBRIDGE_FBTERM_ROOT": str(home / ".local/share/framebridge/fbterm"),
"FRAMEBRIDGE_FBTERM_ENGINE": str(PREFIX / LAYOUT["fbterm_bin"] / "fbterm_engine.py"),
"FRAMEBRIDGE_FBTERM_SESSION": "framebridge-original-fbterm",
"FRAMEBRIDGE_FBTERM_TITLE": "FrameBridge Original",
"FRAMEBRIDGE_EXEC_SESSION": "framebridge-original",
"FRAMEBRIDGE_RESCUE_SESSION": "framebridge-original-rescue",
"FRAMEBRIDGE_UI_STATE_DIR": str(home / ".local/share/framebridge/ui-state"),
"FRAMEBRIDGE_PROGRESS_DIR": str(home / ".local/state/framebridge/progress"),
"FRAMEBRIDGE_PROGRESS_LOG": str(home / ".local/state/framebridge/progress/framebridge-progress.log"),
"FRAMEBRIDGE_TOOL_ROOT": str(runtime / "adapter"),
"FRAMEBRIDGE_NATIVE_HOST": str(PREFIX / LAYOUT["native_host"]),
"FRAMEBRIDGE_FRONT_DOOR_HOST": "127.0.0.1",
"FRAMEBRIDGE_FRONT_DOOR_PORT": str(args.port),
"FRAMEBRIDGE_FLEET_HUB_HOST": "127.0.0.1",
"FRAMEBRIDGE_FLEET_HUB_PORT": str(args.hub_port),
"FRAMEBRIDGE_HUB_MCP_URL": "http://127.0.0.1:" + str(args.hub_port) + "/mcp",
"FRAMEBRIDGE_BROKER_CONTROL": "http://127.0.0.1:" + str(args.broker_port),
"FRAMEBRIDGE_ASUS_NODE_ID": "", "FRAMEBRIDGE_GERAMEG_NODE_ID": "",
"FRAMEBRIDGE_CLOUD_DSM_SOCKET": str(runtime / "cloud-unconfigured.sock"),
"FRAMEBRIDGE_NODE_REGISTRY": str(home / ".config/framebridge/nodes.json"),
"FRAMEBRIDGE_ACCOUNT_BINDING": str(home / ".config/framebridge/account-binding.json"),
"FRAMEBRIDGE_PAIRED_DISCOVERY": str(home / ".config/framebridge/paired-nodes.json"),
"FRAMEBRIDGE_ENABLE_GERAMEG_CUA_TTY": "0",
"PYTHONPATH": os.pathsep.join(str(PREFIX / p) for p in [
"original/geranas/FrameBridge-Fleet-Hub/vendor",
"original/geranas/FrameBridge-Fleet-Hub/app", "portable"]),
})
env.pop("TMUX", None)
env.pop("TMUX_PANE", None)
if authority:
env["XAUTHORITY"] = authority
return state, home, env
def prepare(args):
state, home, env = settings(args)
runtime = Path(env["XDG_RUNTIME_DIR"])
try:
runtime.mkdir(mode=0o700)
except FileExistsError:
pass
st = runtime.lstat()
if not stat.S_ISDIR(st.st_mode) or st.st_uid != os.getuid() or stat.S_IMODE(st.st_mode) != 0o700:
raise RuntimeError("Runtime directory must be private and owned by this user: " + str(runtime))
for p in [runtime, runtime / "tmp", runtime / "tmux", runtime / "adapter", state, home, state / "run", state / "tmp", state / "tmux",
state / "logs", home / ".config/framebridge",
home / ".local/share/framebridge/fbterm",
home / ".local/share/framebridge/tool-adapter",
home / ".local/lib/framebridge-native",
home / "Descargas/framebridge_v0_1"]:
p.mkdir(parents=True, exist_ok=True)
p.chmod(0o700)
links = {
home / ".local/share/framebridge/fbterm/bin": PREFIX / LAYOUT["fbterm_bin"],
home / ".local/share/framebridge/tool-adapter/bin": PREFIX / LAYOUT["adapter_bin"],
home / ".local/lib/framebridge-native/host": PREFIX / LAYOUT["native_host_dir"],
home / "Descargas/framebridge_v0_1/host": PREFIX / LAYOUT["native_host_dir"],
}
for dst, src in links.items():
if dst.is_symlink():
if dst.resolve() != src.resolve():
raise RuntimeError("Unexpected link: " + str(dst))
elif dst.exists():
raise RuntimeError("Runtime program path already exists: " + str(dst))
else:
dst.symlink_to(src, target_is_directory=True)
registry = home / ".config/framebridge/nodes.json"
if not registry.exists():
registry.write_text(json.dumps({"nodes": {}}, indent=2) + "\n")
registry.chmod(0o600)
return state, home, env
def birth(pid):
try:
fields = Path("/proc/" + str(pid) + "/stat").read_text().rsplit(") ", 1)[1].split()
if fields[0] in {"Z", "X"}:
return None
return fields[19]
except (OSError, IndexError):
return None
def live(record):
expected = record.get("birth")
return expected is not None and birth(record["pid"]) == expected
def load_record(state):
p = state / "processes.json"
if not p.exists():
return {"processes": []}
return json.loads(p.read_text())
def stop(args):
state, home, env = settings(args)
records = load_record(state)
for rec in reversed(records.get("processes", [])):
if live(rec):
os.killpg(rec["pid"], signal.SIGTERM)
deadline = time.monotonic() + 5
while any(live(r) for r in records.get("processes", [])) and time.monotonic() < deadline:
time.sleep(.1)
for rec in records.get("processes", []):
if live(rec):
os.killpg(rec["pid"], signal.SIGKILL)
if Path(env["TMUX_TMPDIR"]).is_dir() and shutil.which("tmux"):
subprocess.run(["tmux", "kill-server"], env=env, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
(state / "processes.json").unlink(missing_ok=True)
print("FrameBridge Original detenido. Estado: " + str(state))
def start(args):
missing = [name for name in CORE_COMMANDS if not shutil.which(name)]
if missing:
raise RuntimeError("Faltan comandos para ejecutar FrameBridge Original: " + ", ".join(missing))
state, home, env = prepare(args)
records = load_record(state)
if any(live(r) for r in records.get("processes", [])):
raise RuntimeError("Este perfil ya está ejecutándose; usa status o stop.")
for port in (args.port, args.hub_port):
with socket.socket() as probe:
probe.bind(("127.0.0.1", port))
commands = [
("adapter", [sys.executable, str(PREFIX / LAYOUT["adapter"]), "serve"]),
("fleet", [sys.executable, str(PREFIX / "original/geranas/FrameBridge-Fleet-Hub/app/fleet_hub_mcp_server.py")]),
("mcp", [sys.executable, str(PREFIX / "portable/front_door_mcp.py")]),
]
record = {"state_dir": str(state), "mcp_url": "http://127.0.0.1:" + str(args.port) + "/mcp",
"port": args.port, "hub_port": args.hub_port, "processes": []}
try:
for role, command in commands:
log = (state / "logs" / (role + ".log")).open("ab")
proc = subprocess.Popen(command, env=env, cwd=home, stdin=subprocess.DEVNULL,
stdout=log, stderr=subprocess.STDOUT, start_new_session=True, umask=0o077)
log.close()
record["processes"].append({"role": role, "pid": proc.pid, "birth": birth(proc.pid)})
(state / "processes.json").write_text(json.dumps(record, indent=2) + "\n")
(state / "processes.json").chmod(0o600)
deadline = time.monotonic() + 15
while time.monotonic() < deadline:
if not all(live(r) for r in record["processes"]):
raise RuntimeError("Falló un proceso. Consulta los logs del perfil: " + str(state / "logs"))
ready = (Path(env["FRAMEBRIDGE_TOOL_ROOT"]) / "adapter.sock").is_socket()
for port in (args.port, args.hub_port):
try:
with socket.create_connection(("127.0.0.1", port), timeout=.2):
pass
except OSError:
ready = False
if ready:
print("FrameBridge Original iniciado: " + record["mcp_url"])
print("Conexión a ChatGPT: manual; usa tunnel --help y las instrucciones del paquete.")
print("Perfil local: " + str(state))
return
time.sleep(.15)
raise RuntimeError("No se completó el arranque; consulta los logs del perfil.")
except BaseException:
stop(args)
raise
def status(args):
state, home, env = settings(args)
record = load_record(state)
print(json.dumps({"state_dir": str(state), "mcp_url": record.get("mcp_url"),
"processes": [{"role": x["role"], "pid": x["pid"], "running": live(x)}
for x in record.get("processes", [])]}, indent=2))
def doctor(args):
state, home, env = settings(args)
required = CORE_COMMANDS
gui = ["xdotool", "xwininfo", "xclip", "import", "convert", "identify", "xterm"]
imports = subprocess.run([sys.executable, "-c", "import mcp,pydantic_core,cryptography"], env=env, capture_output=True)
report = {"python": sys.version.split()[0], "python_abi_expected": "3.13",
"mcp_imports_ok": imports.returncode == 0,
"core_commands": {x: bool(shutil.which(x)) for x in required},
"cua_commands": {x: bool(shutil.which(x)) for x in gui},
"display": os.environ.get("DISPLAY", ""),
"tunnel_sha256": hashlib.sha256((PREFIX / "original/geranas/FrameBridge-MCP-Private/bin/tunnel-client").read_bytes()).hexdigest()}
print(json.dumps(report, indent=2))
return 0 if sys.version_info[:2] == (3, 13) and imports.returncode == 0 and all(report["core_commands"].values()) else 1
def main():
parser = argparse.ArgumentParser(description="FrameBridge Original: ejecución local, CUA y conexión manual a ChatGPT.")
parser.add_argument("--state-dir", default=str(DEFAULT_STATE))
parser.add_argument("--port", type=int, default=19865)
parser.add_argument("--hub-port", type=int, default=19877)
parser.add_argument("--broker-port", type=int, default=19878)
sub = parser.add_subparsers(dest="command", required=True)
for name in ["start", "stop", "status", "doctor"]:
sub.add_parser(name)
tunnel = sub.add_parser("tunnel", help="Ejecuta el cliente de túnel original; configuración manual independiente.")
tunnel.add_argument("arguments", nargs=argparse.REMAINDER)
args = parser.parse_args()
if args.command == "tunnel":
state, home, env = prepare(args)
rest = args.arguments
if rest[:1] == ["--"]:
rest = rest[1:]
explicit_refs = {x[4:] for x in rest if x.startswith("env:")}
for x in rest:
if "=env:" in x:
explicit_refs.add(x.split("=env:", 1)[1])
for key in list(env):
if (key.startswith(("TUNNEL_CLIENT_", "CONTROL_PLANE_", "OPENAI_", "MCP_", "HEALTH_", "LOG_", "CLOUDFLARED_", "HARPOON_"))
or key in {"TUNNEL_ID", "TUNNEL_BIN", "CRED_FILE", "OPEN_WEB_UI", "PID_FILE",
"ALLOW_REMOTE_UI", "ADMIN_UI_LOG_BUFFER_EVENTS", "PROXY_CHECK_INTERVAL"}):
if key not in explicit_refs:
env.pop(key, None)
os.execve(str(PREFIX / "original/geranas/FrameBridge-MCP-Private/bin/tunnel-client"),
["tunnel-client"] + rest, env)
result = globals()[args.command](args)
return result or 0
if __name__ == "__main__":
try:
sys.exit(main())
except (OSError, ValueError, RuntimeError) as exc:
print(str(exc), file=sys.stderr)
sys.exit(1)