From b1ff87dd813a25beb6e033563c9e193e93a525db Mon Sep 17 00:00:00 2001 From: Gerardo Betancourt Date: Wed, 28 Oct 2026 12:07:52 -0400 Subject: [PATCH] Prepare 0.3.3 and migrate recognized legacy console wrappers --- docs/CONSOLE-DESKTOP-UPGRADE.md | 2 +- .../expanded/framebridge_expanded/__init__.py | 2 +- .../framebridge_expanded/console_desktop.py | 38 ++++++++++++++++++- source/packaging/build_profiles.py | 2 +- source/tests/test_console_desktop.py | 15 ++++++++ 5 files changed, 55 insertions(+), 4 deletions(-) diff --git a/docs/CONSOLE-DESKTOP-UPGRADE.md b/docs/CONSOLE-DESKTOP-UPGRADE.md index e0f21c3..52fe22c 100644 --- a/docs/CONSOLE-DESKTOP-UPGRADE.md +++ b/docs/CONSOLE-DESKTOP-UPGRADE.md @@ -4,7 +4,7 @@ The desktop console and its authenticated tty1 view share the existing local Bas Older private installations used a user-level desktop launcher. That launcher could hide the package launcher and reject a valid console after other work windows were added. Version 0.3.3 targets the existing named `Console` window, while preserving the other windows. -During an upgrade, a running profile can migrate the exact recognized old launcher to the packaged CLI. Custom launchers and changed legacy components are preserved. The old desktop entry is backed up privately in the profile; the console socket/session binding is recorded there. No login or boot policy changes. +During an upgrade, a running profile can migrate the exact recognized old launcher to the packaged CLI. Custom launchers and changed legacy components are preserved. The exact recognized legacy wrapper is also redirected to the packaged CLI, so desktop or panel copies that invoke it keep working. The old desktop entry and wrapper are backed up privately in the profile; the console socket/session binding is recorded there. No login or boot policy changes. For an explicit owner repair, use the real configured profile: diff --git a/source/expanded/framebridge_expanded/__init__.py b/source/expanded/framebridge_expanded/__init__.py index 6fd3ae5..e19434e 100644 --- a/source/expanded/framebridge_expanded/__init__.py +++ b/source/expanded/framebridge_expanded/__init__.py @@ -1 +1 @@ -__version__ = "0.3.2+private.20261028.2" +__version__ = "0.3.3" diff --git a/source/expanded/framebridge_expanded/console_desktop.py b/source/expanded/framebridge_expanded/console_desktop.py index 4ef5d37..af507a2 100644 --- a/source/expanded/framebridge_expanded/console_desktop.py +++ b/source/expanded/framebridge_expanded/console_desktop.py @@ -78,6 +78,38 @@ def _new_entry(state): "StartupWMClass=Gnome-terminal\n").encode() +def _new_wrapper(state): + command = ["/usr/bin/fb", "--state-dir", str(state), "console"] + return ("#!/usr/bin/python3\n" + '"""Open the packaged FrameBridge console using the retained local profile."""\n' + "import os\nimport sys\n" + "command = " + repr(command) + "\n" + "if sys.argv[1:] == ['--inside']:\n" + " command += ['--inside', '--mode', 'gui']\n" + "elif sys.argv[1:]:\n" + " raise SystemExit('Usage: framebridge-console')\n" + "os.execv(command[0], command)\n").encode() + + +def _migrate_wrapper(state, home): + path = home / ".local/bin/framebridge-console" + before, mode = _snapshot(path) + after = _new_wrapper(state) + if before == after: + return False + _need(hashlib.sha256(before).hexdigest() == LEGACY_WRAPPER_SHA256, + "CONSOLE_DESKTOP_LEGACY_WRAPPER_CHANGED") + backup = state / "console-wrapper-legacy.backup" + if backup.exists() or backup.is_symlink(): + data, backup_mode = _snapshot(backup) + _need(data == before and backup_mode == 0o600, "CONSOLE_DESKTOP_WRAPPER_BACKUP_CHANGED") + else: + _atomic(backup, before) + _need(_snapshot(path)[0] == before, "CONSOLE_DESKTOP_WRAPPER_CHANGED_DURING_MIGRATION") + _atomic(path, after, mode) + return True + + def _legacy_identity(node, runtime): return {"socket": runtime / (node.lower() + "-tty1") / "console.sock", "session": node.upper() + "-FBTERM"} @@ -123,7 +155,9 @@ def migrate_legacy(state, *, home=None, node=None, runtime=None): after = _new_entry(state) if before == after: _need(legacy_context(state, node, runtime) is not None, "CONSOLE_DESKTOP_RECORD_MISSING") - return {"ok": True, "changed": False, "reason": "ALREADY_MIGRATED"} + wrapper_changed = _migrate_wrapper(state, home) + return {"ok": True, "changed": wrapper_changed, "reason": "ALREADY_MIGRATED", + "wrapper_changed": wrapper_changed} if before != _legacy_entry(home): return {"ok": True, "changed": False, "reason": "CUSTOM_DESKTOP_ENTRY_PRESERVED"} wrapper, _ = _snapshot(home / ".local/bin/framebridge-console") @@ -166,6 +200,8 @@ def migrate_legacy(state, *, home=None, node=None, runtime=None): _atomic(state / RECORD, (json.dumps(record, sort_keys=True, indent=2) + "\n").encode()) _need(_snapshot(desktop)[0] == before, "CONSOLE_DESKTOP_CHANGED_DURING_MIGRATION") _atomic(desktop, after, mode) + wrapper_changed = _migrate_wrapper(state, home) return {"ok": True, "changed": True, "desktop": str(desktop), + "wrapper_changed": wrapper_changed, "session": identity["session"], "socket": str(identity["socket"]), "backup": str(backup), "runtime_changed": False} diff --git a/source/packaging/build_profiles.py b/source/packaging/build_profiles.py index 912a177..2124307 100644 --- a/source/packaging/build_profiles.py +++ b/source/packaging/build_profiles.py @@ -11,7 +11,7 @@ from pathlib import Path import re -CANDIDATE_VERSION = "0.3.2" +CANDIDATE_VERSION = "0.3.3" VENDOR_RELATIVE = Path("original/geranas/FrameBridge-Fleet-Hub/vendor") SOURCE_VENDOR = Path("sources/original-geranas/FrameBridge-Fleet-Hub/vendor") GLIBC_BASELINE = (2, 34) diff --git a/source/tests/test_console_desktop.py b/source/tests/test_console_desktop.py index b9d9d2d..a6bf5c8 100644 --- a/source/tests/test_console_desktop.py +++ b/source/tests/test_console_desktop.py @@ -56,6 +56,9 @@ class DesktopMigrationTests(unittest.TestCase): self.assertFalse(value["runtime_changed"]) self.assertEqual((self.state / "console-desktop-legacy.backup").read_bytes(), before) self.assertEqual(self.desktop.read_bytes(), console_desktop._new_entry(self.state)) + self.assertEqual(self.wrapper.read_bytes(), console_desktop._new_wrapper(self.state)) + self.assertEqual((self.state / "console-wrapper-legacy.backup").read_bytes(), b"#!/usr/bin/python3\npass\n") + self.assertTrue(value["wrapper_changed"]) context = console_desktop.legacy_context(self.state, "TestHost", self.runtime) self.assertEqual(context["session"], "TESTHOST-FBTERM") self.assertEqual(context["socket"], self.runtime / "testhost-tty1/console.sock") @@ -122,6 +125,18 @@ class ConsoleWindowSelectionTests(unittest.TestCase): value = console.exists() return console, value + def test_migrated_wrapper_targets_the_packaged_profile_without_a_shell(self): + state = Path("/home/user/profile") + with mock.patch("sys.argv", ["framebridge-console"]), mock.patch("os.execv") as execute: + exec(compile(console_desktop._new_wrapper(state), "wrapper", "exec"), {}) + execute.assert_called_once_with("/usr/bin/fb", ["/usr/bin/fb", "--state-dir", str(state), "console"]) + + def test_migrated_inside_wrapper_does_not_create_another_viewer(self): + state = Path("/home/user/profile") + with mock.patch("sys.argv", ["framebridge-console", "--inside"]), mock.patch("os.execv") as execute: + exec(compile(console_desktop._new_wrapper(state), "wrapper", "exec"), {}) + self.assertEqual(execute.call_args.args[1][-3:], ["--inside", "--mode", "gui"]) + def test_monitor_receives_profile_jobs_root_as_an_explicit_argument(self): args = SimpleNamespace(socket="/owned/socket", session="NODE-FBTERM", node="NODE", jobs_root=Path("/profile with space/jobs"))