FrameBridge 0.3.1 community source release

This commit is contained in:
FrameBridge Development committed 2026-10-26 22:09:18 -04:00
commit b23819fff1
1354 files changed
+343716

No files matched your search

@@ -0,0 +1,256 @@
"""Local authenticated Bash/FBTERM console, shared across desktop and TTY clients."""
from __future__ import annotations
import argparse
import hashlib
import os
from pathlib import Path
import pwd
import re
import shutil
import socket
import stat
import subprocess
import sys
from .config import load_config, private_dir, state_path
_NODE = re.compile(r"[A-Za-z0-9][A-Za-z0-9._-]{0,79}\Z")
def _node(state, value):
value = value or "LOCAL"
if type(value) is not str or _NODE.fullmatch(value) is None:
raise ValueError("CONSOLE_NODE_INVALID")
host = socket.gethostname()
names = {"local", "localhost", host.lower(), host.split(".")[0].lower()}
try:
config = load_config(state)
except (ValueError, OSError):
config = {}
name = config.get("name")
if isinstance(name, str):
names.add(name.lower())
if value.lower() not in names:
raise RuntimeError(
"REMOTE_CONSOLE_SSH_CONFIGURATION_REQUIRED: run fb console on the target node, "
"or use your explicitly configured SSH destination. Remote EXEC, CUA and TTY "
"operations remain available through the enrolled broker.")
return re.sub(r"[^A-Za-z0-9_-]", "_", host)[:64] or "NODE"
def _private_runtime(path):
path = Path(path)
info = path.lstat()
if (not stat.S_ISDIR(info.st_mode) or stat.S_ISLNK(info.st_mode)
or info.st_uid != os.getuid() or info.st_mode & 0o077):
raise RuntimeError("CONSOLE_RUNTIME_DIRECTORY_UNTRUSTED")
return path
def context(state, node="LOCAL", *, create=False):
state = state_path(state)
label = _node(state, node)
key = hashlib.sha256(str(state).encode()).hexdigest()[:20]
runtime = _private_runtime(Path("/run/user") / str(os.getuid()))
root = runtime / "framebridge-expanded-console"
profile = root / key
if create:
private_dir(state)
for path in (root, profile):
path.mkdir(mode=0o700, exist_ok=True)
_private_runtime(path)
else:
for path in (root, profile):
if path.exists() or path.is_symlink():
_private_runtime(path)
return {
"state": state, "node": label, "runtime": runtime,
"socket": profile / "console.sock",
"session": "FB-" + label + "-" + key[:10],
"jobs_root": state / "home/.local/share/framebridge/fbterm",
}
def _properties(text):
return dict(line.split("=", 1) for line in text.splitlines() if "=" in line)
def _active_graphical_display(env=None):
"""Auto GUI requires the actual active local X11 session, never a guessed Xvfb."""
env = os.environ if env is None else env
display = env.get("DISPLAY", "")
sid = env.get("XDG_SESSION_ID", "")
if (re.fullmatch(r":[0-9]+(?:\.[0-9]+)?", display) is None
or re.fullmatch(r"[A-Za-z0-9_-]{1,80}", sid) is None):
return False
try:
result = subprocess.run([
"loginctl", "show-session", sid, "--no-pager",
"-p", "User", "-p", "Type", "-p", "Active", "-p", "Remote",
"-p", "Display", "-p", "VTNr",
], capture_output=True, text=True, timeout=5)
if result.returncode:
return False
fields = _properties(result.stdout)
vt = fields.get("VTNr", "")
if not vt.isdigit() or int(vt) < 1:
return False
active = Path("/sys/class/tty/tty0/active").read_text().strip()
return (fields.get("User") == str(os.getuid())
and fields.get("Type") == "x11"
and fields.get("Active") == "yes"
and fields.get("Remote") == "no"
and fields.get("Display", "").split(".")[0] == display.split(".")[0]
and active == "tty" + str(int(vt)))
except (OSError, subprocess.SubprocessError):
return False
def _environment(ctx):
env = os.environ.copy()
real_home = pwd.getpwuid(os.getuid()).pw_dir
env.update({
"HOME": real_home,
"XDG_RUNTIME_DIR": str(ctx["runtime"]),
"FRAMEBRIDGE_EXPANDED_STATE": str(ctx["state"]),
"FRAMEBRIDGE_FBTERM_ROOT": str(ctx["jobs_root"]),
"FRAMEBRIDGE_FBTERM_ROOT_OVERRIDE": str(ctx["jobs_root"]),
"FBTERM_ROOT": str(ctx["jobs_root"]),
"LANG": "C.UTF-8", "LC_ALL": "C.UTF-8",
})
# The installed command adjusts sys.path only in its own process. Make the
# new graphical-terminal process resolve this same installed module.
package_root = str(Path(__file__).resolve().parent.parent)
env["PYTHONPATH"] = os.pathsep.join(
[package_root] + ([env["PYTHONPATH"]] if env.get("PYTHONPATH") else []))
env.pop("TMUX", None)
env.pop("TMUX_PANE", None)
return env
def _socket_exists(sock):
try:
info = sock.lstat()
except FileNotFoundError:
return False
if (not stat.S_ISSOCK(info.st_mode) or stat.S_ISLNK(info.st_mode)
or info.st_uid != os.getuid() or info.st_mode & 0o077):
raise RuntimeError("CONSOLE_SOCKET_UNTRUSTED")
return True
def status(state, node="LOCAL"):
ctx = context(state, node)
sock = ctx["socket"]
value = {"ok": True, "node": ctx["node"], "socket": str(sock),
"session": ctx["session"], "jobs_root": str(ctx["jobs_root"]),
"exists": False, "panes": []}
if not _socket_exists(sock):
return value
try:
result = subprocess.run([
"/usr/bin/tmux", "-S", str(sock), "list-panes",
"-t", "=" + ctx["session"],
"-F", "#{pane_id}|#{pane_pid}|#{pane_current_command}|#{pane_width}|#{pane_height}|#{@framebridge_role}",
], capture_output=True, text=True, timeout=5)
except (OSError, subprocess.SubprocessError):
value.update(ok=False, error="CONSOLE_TMUX_STATUS_FAILED")
return value
if result.returncode:
value.update(ok=False, error="CONSOLE_TMUX_STATUS_FAILED")
return value
value["exists"] = True
for line in result.stdout.splitlines():
fields = line.split("|")
if len(fields) != 6:
raise RuntimeError("CONSOLE_TMUX_STATUS_INVALID")
pane, pid, command, width, height, role = fields
if not re.fullmatch(r"%[0-9]+", pane) or not all(x.isdigit() for x in [pid, width, height]):
raise RuntimeError("CONSOLE_TMUX_STATUS_INVALID")
value["panes"].append({"pane": pane, "pid": int(pid), "command": command,
"cols": int(width), "rows": int(height), "role": role})
return value
def _prevent_recursive_attach(ctx, env):
"""Reject attaching a console from one of its own panes before reflow."""
marker = os.environ.get("TMUX", "")
if not marker:
return
parts = marker.rsplit(",", 2)
try:
same_socket = Path(parts[0]).resolve() == ctx["socket"].resolve()
except (OSError, RuntimeError):
raise RuntimeError("CONSOLE_TMUX_CONTEXT_UNVERIFIED") from None
if not same_socket:
return
pane = os.environ.get("TMUX_PANE", "")
if (len(parts) != 3 or not parts[1].isdigit() or not parts[2].isdigit()
or re.fullmatch(r"%[0-9]+", pane) is None):
raise RuntimeError("CONSOLE_TMUX_CONTEXT_UNVERIFIED")
try:
result = subprocess.run([
"/usr/bin/tmux", "-S", str(ctx["socket"]), "list-panes", "-s",
"-t", "=" + ctx["session"], "-F", "#{pane_id}",
], env=env, capture_output=True, text=True, timeout=5)
except (OSError, subprocess.SubprocessError):
raise RuntimeError("CONSOLE_TMUX_CONTEXT_UNVERIFIED") from None
panes = result.stdout.splitlines() if isinstance(result.stdout, str) else []
if (result.returncode or not panes
or any(re.fullmatch(r"%[0-9]+", value) is None for value in panes)):
raise RuntimeError("CONSOLE_TMUX_CONTEXT_UNVERIFIED")
if pane in panes:
raise RuntimeError(
"CONSOLE_ALREADY_ATTACHED: This terminal is already inside this "
"FrameBridge console. Use its existing shell or open a separate terminal.")
def run(state, node="LOCAL", *, inside=False, mode="auto"):
if mode not in {"auto", "gui", "tty"}:
raise ValueError("CONSOLE_MODE_INVALID")
ctx = context(state, node, create=True)
env = _environment(ctx)
_socket_exists(ctx["socket"])
ui = Path(__file__).resolve().parent / "console_ui/framebridge_console.py"
if not ui.is_file():
raise RuntimeError("CONSOLE_COMPONENT_NOT_INSTALLED")
graphical = _active_graphical_display(env)
if not inside and mode != "tty" and (mode == "gui" or graphical):
if not graphical:
raise RuntimeError("CONSOLE_ACTIVE_X11_DESKTOP_REQUIRED")
terminal = shutil.which("gnome-terminal")
if not terminal:
raise RuntimeError("CONSOLE_GRAPHICAL_TERMINAL_UNAVAILABLE")
try:
result = subprocess.run([
terminal, "--window", "--maximize",
"--title=FrameBridge Console | " + ctx["node"], "--",
sys.executable, "-m", "framebridge_expanded.console",
"--state-dir", str(ctx["state"]), "--node", node, "--inside", "--mode", "gui",
], env=env, capture_output=True, text=True, timeout=15)
except (OSError, subprocess.SubprocessError):
raise RuntimeError("CONSOLE_GRAPHICAL_TERMINAL_FAILED") from None
if result.returncode:
raise RuntimeError("CONSOLE_GRAPHICAL_TERMINAL_FAILED")
return 0
if not os.isatty(0):
raise RuntimeError("CONSOLE_INTERACTIVE_TERMINAL_REQUIRED")
_prevent_recursive_attach(ctx, env)
try:
result = subprocess.run([
sys.executable, str(ui), "--node", ctx["node"],
"--socket", str(ctx["socket"]), "--session", ctx["session"] ,
"--layout", "horizontal" if inside else "vertical", "--apply",
], env=env, capture_output=True, text=True, timeout=20)
except (OSError, subprocess.SubprocessError):
raise RuntimeError("CONSOLE_LAYOUT_FAILED") from None
if result.returncode:
raise RuntimeError("CONSOLE_LAYOUT_FAILED")
os.execve("/usr/bin/tmux", [
"tmux", "-S", str(ctx["socket"]), "attach-session", "-f", "ignore-size",
"-t", "=" + ctx["session"],
], env)
return 0
def main(argv=None):
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--state-dir")
parser.add_argument("--node", default="LOCAL")
parser.add_argument("--inside", action="store_true")
parser.add_argument("--mode", choices=["auto", "gui", "tty"], default="auto")
args = parser.parse_args(argv)
return run(state_path(args.state_dir), args.node, inside=args.inside, mode=args.mode)
if __name__ == "__main__":
raise SystemExit(main())