From 126a422cfac151babfcc2e0c2e35ed385c0d2179 Mon Sep 17 00:00:00 2001 From: Charles Kerr Date: Mon, 27 Apr 2026 15:31:40 -0500 Subject: [PATCH] perf: use GIO for Browser::IsDefaultProtocolClient() on Linux (#51316) * perf: use GIO for Browser::IsDefaultProtocolClient() on Linux perf: use GIO for Browser::SetAsDefaultProtocolClient() on Linux Similar to 7d6227a, this speeds up app.isDefaultProtocolClient() by using the GIO library instead of spawning a shell command to get the info. * feat: log errors if g_app_info_set_as_default_for_type() fails --- shell/browser/browser_linux.cc | 103 ++++++++------------------ spec/api-app-spec.ts | 127 ++++++++++++--------------------- 2 files changed, 75 insertions(+), 155 deletions(-) diff --git a/shell/browser/browser_linux.cc b/shell/browser/browser_linux.cc index c546b482bb..75e2fe6338 100644 --- a/shell/browser/browser_linux.cc +++ b/shell/browser/browser_linux.cc @@ -4,19 +4,14 @@ #include "shell/browser/browser.h" -#include -#include - #if BUILDFLAG(IS_LINUX) +#include #include #include #endif -#include "base/command_line.h" #include "base/environment.h" #include "base/logging.h" -#include "base/process/launch.h" -#include "base/strings/strcat.h" #include "base/strings/utf_string_conversions.h" #include "electron/electron_version.h" #include "shell/browser/javascript_environment.h" @@ -24,7 +19,6 @@ #include "shell/browser/window_list.h" #include "shell/common/application_info.h" #include "shell/common/gin_converters/login_item_settings_converter.h" -#include "shell/common/thread_restrictions.h" #if BUILDFLAG(IS_LINUX) #include "shell/browser/linux/unity_service.h" @@ -34,64 +28,26 @@ namespace electron { namespace { -const char kXdgSettings[] = "xdg-settings"; -const char kXdgSettingsDefaultSchemeHandler[] = "default-url-scheme-handler"; - -// The use of the ForTesting flavors is a hack workaround to avoid having to -// patch these as friends into the associated guard classes. -class [[maybe_unused, nodiscard]] LaunchXdgUtilityScopedAllowBaseSyncPrimitives - : public base::ScopedAllowBaseSyncPrimitivesForTesting {}; - -bool LaunchXdgUtility(const std::vector& argv, int* exit_code) { - *exit_code = EXIT_FAILURE; - int devnull = open("/dev/null", O_RDONLY); - if (devnull < 0) - return false; - - base::LaunchOptions options; - options.fds_to_remap.emplace_back(devnull, STDIN_FILENO); - - base::Process process = base::LaunchProcess(argv, options); - close(devnull); - - if (!process.IsValid()) - return false; - LaunchXdgUtilityScopedAllowBaseSyncPrimitives allow_base_sync_primitives; - return process.WaitForExit(exit_code); -} - -std::optional GetXdgAppOutput( - const std::vector& argv) { - std::string reply; - int success_code; - ScopedAllowBlockingForElectron allow_blocking; - bool ran_ok = base::GetAppOutputWithExitCode(base::CommandLine(argv), &reply, - &success_code); - - if (!ran_ok || success_code != EXIT_SUCCESS) - return {}; - - return reply; -} - bool SetDefaultWebClient(const std::string& protocol) { - auto env = base::Environment::Create(); - - std::vector argv = {kXdgSettings, "set"}; - if (!protocol.empty()) { - argv.emplace_back(kXdgSettingsDefaultSchemeHandler); - argv.emplace_back(protocol); - } - - if (std::optional desktop_name = env->GetVar("CHROME_DESKTOP")) { - argv.emplace_back(desktop_name.value()); - } else { + const auto env = base::Environment::Create(); + const std::optional desktop_name = env->GetVar("CHROME_DESKTOP"); + if (!desktop_name) return false; - } - int exit_code; - bool ran_ok = LaunchXdgUtility(argv, &exit_code); - return ran_ok && exit_code == EXIT_SUCCESS; + GDesktopAppInfo* const app_info = + g_desktop_app_info_new(desktop_name->c_str()); + if (!app_info) + return false; + + const std::string content_type = "x-scheme-handler/" + protocol; + GError* error = nullptr; + const bool success = g_app_info_set_as_default_for_type( + G_APP_INFO(app_info), content_type.c_str(), &error); + if (error != nullptr) + LOG(ERROR) << error->message; + g_clear_error(&error); + g_object_unref(app_info); + return success; } } // namespace @@ -117,18 +73,19 @@ bool Browser::IsDefaultProtocolClient(const std::string& protocol, if (!IsValidProtocolScheme(protocol)) return false; - auto env = base::Environment::Create(); - - std::vector argv = {kXdgSettings, "check", - kXdgSettingsDefaultSchemeHandler, protocol}; - if (std::optional desktop_name = env->GetVar("CHROME_DESKTOP")) { - argv.emplace_back(desktop_name.value()); - } else { + const auto env = base::Environment::Create(); + const std::optional desktop_name = env->GetVar("CHROME_DESKTOP"); + if (!desktop_name) return false; - } - // Allow any reply that starts with "yes". - const std::optional output = GetXdgAppOutput(argv); - return output && output->starts_with("yes"); + + GAppInfo* app_info = g_app_info_get_default_for_uri_scheme(protocol.c_str()); + if (!app_info) + return false; + + const char* const app_id = g_app_info_get_id(app_info); + const bool is_default = app_id && app_id == desktop_name.value(); + g_object_unref(app_info); + return is_default; } // Todo implement diff --git a/spec/api-app-spec.ts b/spec/api-app-spec.ts index 76912a1573..a32e331a9a 100644 --- a/spec/api-app-spec.ts +++ b/spec/api-app-spec.ts @@ -20,21 +20,6 @@ import { defer, ifdescribe, ifit, isWayland, listen, waitUntil } from './lib/spe import { closeWindow, closeAllWindows } from './lib/window-helpers'; const fixturesPath = path.resolve(__dirname, 'fixtures'); -const xdgMockFixturePath = path.join(fixturesPath, 'api', 'xdg-mock'); - -function makeXdgMockDirectories(prefix: string) { - const xdgDir = fs.mkdtempSync(path.join(os.tmpdir(), prefix)); - fs.cpSync(xdgMockFixturePath, xdgDir, { recursive: true }); - - const xdgDataHome = path.join(xdgDir, 'data'); - const xdgConfigHome = path.join(xdgDir, 'config'); - const xdgBinDir = path.join(xdgDir, 'bin'); - - fs.chmodSync(path.join(xdgBinDir, 'xdg-mime'), 0o755); - fs.chmodSync(path.join(xdgBinDir, 'xdg-settings'), 0o755); - - return { xdgDir, xdgDataHome, xdgConfigHome, xdgBinDir }; -} const isMacOSx64 = process.platform === 'darwin' && process.arch === 'x64'; @@ -1635,92 +1620,70 @@ describe('app module', () => { }); }); - ifdescribe(process.platform === 'linux')('default protocol client APIs with mocked XDG settings', () => { + ifdescribe(process.platform === 'linux')('default protocol client APIs', () => { const protocol = 'electron-test-linux'; const desktopFileId = 'electron-test.desktop'; const protocolMimeType = `x-scheme-handler/${protocol}`; - let xdgDir: string; - let xdgDataHome: string; - let xdgConfigHome: string; - let xdgBinDir: string; - let oldEnv: Record; - - const getRegisteredHandler = () => { - for (const list of [ - path.join(xdgConfigHome, 'mimeapps.list'), - path.join(xdgDataHome, 'applications', 'mimeapps.list'), - path.join(xdgDataHome, 'applications', 'defaults.list') - ]) { - if (!fs.existsSync(list)) continue; - - const match = fs - .readFileSync(list, 'utf8') - .split('\n') - .find((line) => line.startsWith(`${protocolMimeType}=`)); - - // foo=bar.desktop; --> bar.desktop - if (match) return match.split('=', 2)[1].split(';', 1)[0]; - } - - return ''; - }; + // GIO caches XDG directory paths at process startup, so we must + // operate on the directories it is actually monitoring rather than + // creating isolated temp dirs. + const gioDataHome = process.env.XDG_DATA_HOME || path.join(os.homedir(), '.local', 'share'); + const gioConfigHome = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'); + const desktopFileDst = path.join(gioDataHome, 'applications', desktopFileId); + const mimeappsListPath = path.join(gioConfigHome, 'mimeapps.list'); beforeEach(() => { - ({ xdgDir, xdgDataHome, xdgConfigHome, xdgBinDir } = makeXdgMockDirectories('electron-xdg-default-client-')); + const oldDesktopName = process.env.CHROME_DESKTOP; - oldEnv = { - PATH: process.env.PATH, - CHROME_DESKTOP: process.env.CHROME_DESKTOP, - XDG_DATA_HOME: process.env.XDG_DATA_HOME, - XDG_DATA_DIRS: process.env.XDG_DATA_DIRS, - XDG_CONFIG_HOME: process.env.XDG_CONFIG_HOME - }; + // Install the test .desktop file where GIO can discover it. + fs.mkdirSync(path.dirname(desktopFileDst), { recursive: true }); + fs.copyFileSync( + path.join(fixturesPath, 'api', 'xdg-mock', 'data', 'applications', desktopFileId), + desktopFileDst + ); + + app.setDesktopName(desktopFileId); defer(() => { - for (const [key, value] of Object.entries(oldEnv)) { - if (value === undefined) { - delete process.env[key]; - } else { - process.env[key] = value; - } + // Restore CHROME_DESKTOP. + if (oldDesktopName !== undefined) { + process.env.CHROME_DESKTOP = oldDesktopName; + } else { + delete process.env.CHROME_DESKTOP; } - fs.rmSync(xdgDir, { recursive: true, force: true }); + // Remove the test .desktop file. + try { + fs.unlinkSync(desktopFileDst); + } catch {} + + // Remove any association for the test protocol from mimeapps.list. + if (fs.existsSync(mimeappsListPath)) { + const content = fs.readFileSync(mimeappsListPath, 'utf8'); + const cleaned = content + .split('\n') + .filter((line) => !line.includes(protocolMimeType)) + .join('\n'); + if (cleaned !== content) { + fs.writeFileSync(mimeappsListPath, cleaned); + } + } }); - - process.env.PATH = [xdgBinDir, oldEnv.PATH].filter(Boolean).join(':'); - process.env.XDG_DATA_HOME = xdgDataHome; - process.env.XDG_DATA_DIRS = [xdgDataHome, oldEnv.XDG_DATA_DIRS].filter(Boolean).join(':'); - process.env.XDG_CONFIG_HOME = xdgConfigHome; - app.setDesktopName(desktopFileId); }); - it('writes the default handler to the XDG association files', async () => { - expect(getRegisteredHandler()).to.equal(''); - - expect(app.setAsDefaultProtocolClient(protocol)).to.equal(true); - - await waitUntil(() => getRegisteredHandler() === desktopFileId); - expect(getRegisteredHandler()).to.equal(desktopFileId); - }); - - it('detects whether the app is the default protocol client', async () => { + it('sets and queries the default protocol client', async () => { expect(app.isDefaultProtocolClient(protocol)).to.equal(false); - fs.writeFileSync( - path.join(xdgConfigHome, 'mimeapps.list'), - ['[Default Applications]', `${protocolMimeType}=other.desktop`].join('\n') - ); - expect(app.isDefaultProtocolClient(protocol)).to.equal(false); - - fs.writeFileSync( - path.join(xdgConfigHome, 'mimeapps.list'), - ['[Default Applications]', `${protocolMimeType}=${desktopFileId}`].join('\n') - ); + // GIO needs to discover the newly installed .desktop file via inotify. + await waitUntil(() => app.setAsDefaultProtocolClient(protocol)); await waitUntil(() => app.isDefaultProtocolClient(protocol)); expect(app.isDefaultProtocolClient(protocol)).to.equal(true); + + // Changing identity should make the check return false. + app.setDesktopName('other-app.desktop'); + expect(app.isDefaultProtocolClient(protocol)).to.equal(false); }); });