From 7e2c2b1b08da2183f8a1f021b8e98a7f1b94dac2 Mon Sep 17 00:00:00 2001 From: Jan-Stefan Janetzky Date: Wed, 26 Aug 2026 14:55:06 +0200 Subject: [PATCH] fix(browser): resolve snap and Flatpak Chromium profiles on Linux MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit real_profile_data_dir hard-wired Linux to $XDG_CONFIG_HOME/, and the xdg fragment map only knew the native package names. Ubuntu's default snap Chromium (xdg reports chromium_chromium.desktop, profile under ~/snap/chromium/common/chromium) and Flatpak builds (~/.var/app//config/…) therefore ended in 'profile directory was not found' for a browser the user runs every day, and Flatpak Chrome (com.google.Chrome.desktop) was reported as 'not a supported Chromium browser'. Try the native, snap and Flatpak locations and return the first that exists; fall back to the native path so the error message still names a concrete directory. Map the Flatpak application ids in the xdg lookup. Tests cover the xdg names for all four browsers in native and Flatpak form, and the directory preference order with a temp HOME. --- hermes_cli/browser_connect.py | 40 +++++++++-- .../test_browser_connect_default_chromium.py | 66 +++++++++++++++++++ 2 files changed, 101 insertions(+), 5 deletions(-) diff --git a/hermes_cli/browser_connect.py b/hermes_cli/browser_connect.py index 12b4565d1d..0d0b786ad6 100644 --- a/hermes_cli/browser_connect.py +++ b/hermes_cli/browser_connect.py @@ -104,14 +104,30 @@ _WINDOWS_PROGID_MAP = ( ) # Linux xdg default-web-browser .desktop name fragments → canonical key. +# Includes the Flatpak application ids (``com.google.Chrome.desktop`` etc.), +# which share none of the native package name fragments. _LINUX_DESKTOP_MAP = ( ("google-chrome", "chrome"), + ("com.google.chrome", "chrome"), ("chromium", "chromium"), ("brave", "brave"), ("microsoft-edge", "edge"), + ("com.microsoft.edge", "edge"), ("msedge", "edge"), ) +# Where sandboxed Linux packages keep the profile instead of $XDG_CONFIG_HOME. +_LINUX_FLATPAK_IDS = { + "chrome": "com.google.Chrome", + "chromium": "org.chromium.Chromium", + "brave": "com.brave.Browser", + "edge": "com.microsoft.Edge", +} +_LINUX_SNAP_PROFILE_PARTS = { + "chromium": ("snap", "chromium", "common", "chromium"), + "brave": ("snap", "brave", "current", ".config", "BraveSoftware", "Brave-Browser"), +} + # macOS LaunchServices bundle-id fragments → canonical key. _DARWIN_BUNDLE_MAP = ( ("com.google.chrome", "chrome"), @@ -150,10 +166,12 @@ def _real_profile_relparts(browser: str) -> tuple: def real_profile_data_dir(browser: str, system: str | None = None) -> str | None: """Return the default user-data-dir for a Chromium ``browser`` on ``system``. - Returns None for unknown browsers. Does not check existence — callers that - need that should stat the result. Paths are built with the TARGET system's - separator (posix for Darwin/Linux, backslash for Windows) so an explicit - ``system`` argument resolves correctly regardless of the host OS. + Returns None for unknown browsers. On Linux the native ($XDG_CONFIG_HOME), + snap and Flatpak locations are tried and the first existing one wins; the + native path is returned when none exists so the caller's error names it. + Darwin/Windows paths are not stat'ed. Paths are built with the TARGET + system's separator (posix for Darwin/Linux, backslash for Windows) so an + explicit ``system`` argument resolves correctly regardless of the host OS. """ if browser not in _CHROMIUM_BROWSERS: return None @@ -167,7 +185,19 @@ def real_profile_data_dir(browser: str, system: str | None = None) -> str | None return ntpath.join(local, *win_parts) # Linux / other POSIX config = os.environ.get("XDG_CONFIG_HOME") or posixpath.join(home, ".config") - return posixpath.join(config, *linux_name.split("/")) + candidates = [posixpath.join(config, *linux_name.split("/"))] + snap_parts = _LINUX_SNAP_PROFILE_PARTS.get(browser) + if snap_parts: + candidates.append(posixpath.join(home, *snap_parts)) + flatpak_id = _LINUX_FLATPAK_IDS.get(browser) + if flatpak_id: + candidates.append( + posixpath.join(home, ".var", "app", flatpak_id, "config", *linux_name.split("/")) + ) + for candidate in candidates: + if os.path.isdir(candidate): + return candidate + return candidates[0] def chromium_executable(browser: str, system: str | None = None) -> str | None: diff --git a/tests/hermes_cli/test_browser_connect_default_chromium.py b/tests/hermes_cli/test_browser_connect_default_chromium.py index a47f248b31..4a654d8e66 100644 --- a/tests/hermes_cli/test_browser_connect_default_chromium.py +++ b/tests/hermes_cli/test_browser_connect_default_chromium.py @@ -104,3 +104,69 @@ class TestDetectDefaultDarwin: def test_bundle_map(self, bundle, expected): with self._run_with(_ls_dump(_handler("https", bundle))): assert bc._detect_default_darwin() == expected + + +class TestDetectDefaultLinux: + def _run_with(self, output: str): + class _Proc: + stdout = output + + return patch.object(bc.subprocess, "run", return_value=_Proc()) + + @pytest.mark.parametrize( + "desktop,expected", + [ + ("google-chrome.desktop", "chrome"), + ("com.google.Chrome.desktop", "chrome"), + ("chromium_chromium.desktop", "chromium"), + ("org.chromium.Chromium.desktop", "chromium"), + ("brave-browser.desktop", "brave"), + ("com.brave.Browser.desktop", "brave"), + ("microsoft-edge.desktop", "edge"), + ("com.microsoft.Edge.desktop", "edge"), + ("firefox.desktop", None), + ("org.mozilla.firefox.desktop", None), + ("", None), + ], + ) + def test_xdg_desktop_names(self, desktop, expected): + with self._run_with(desktop + "\n"): + assert bc._detect_default_linux() == expected + + def test_missing_xdg_settings_fails_closed(self): + with patch.object(bc.subprocess, "run", side_effect=FileNotFoundError("xdg-settings")): + assert bc._detect_default_linux() is None + + +class TestLinuxProfileDir: + def _env(self, monkeypatch, home): + monkeypatch.setenv("HOME", str(home)) + monkeypatch.delenv("XDG_CONFIG_HOME", raising=False) + + def test_native_path_when_nothing_exists(self, tmp_path, monkeypatch): + self._env(monkeypatch, tmp_path) + assert bc.real_profile_data_dir("chromium", "Linux") == str(tmp_path / ".config" / "chromium") + + def test_snap_chromium_profile_is_found(self, tmp_path, monkeypatch): + self._env(monkeypatch, tmp_path) + snap = tmp_path / "snap" / "chromium" / "common" / "chromium" + snap.mkdir(parents=True) + assert bc.real_profile_data_dir("chromium", "Linux") == str(snap) + + def test_flatpak_chrome_profile_is_found(self, tmp_path, monkeypatch): + self._env(monkeypatch, tmp_path) + flatpak = tmp_path / ".var" / "app" / "com.google.Chrome" / "config" / "google-chrome" + flatpak.mkdir(parents=True) + assert bc.real_profile_data_dir("chrome", "Linux") == str(flatpak) + + def test_native_profile_wins_when_present(self, tmp_path, monkeypatch): + self._env(monkeypatch, tmp_path) + native = tmp_path / ".config" / "BraveSoftware" / "Brave-Browser" + native.mkdir(parents=True) + (tmp_path / ".var" / "app" / "com.brave.Browser" / "config" / "BraveSoftware" / "Brave-Browser").mkdir(parents=True) + assert bc.real_profile_data_dir("brave", "Linux") == str(native) + + def test_xdg_config_home_is_honoured(self, tmp_path, monkeypatch): + monkeypatch.setenv("HOME", str(tmp_path)) + monkeypatch.setenv("XDG_CONFIG_HOME", "/home/t/.config") + assert bc.real_profile_data_dir("edge", "Linux") == "/home/t/.config/microsoft-edge"