fix(browser): avoid cold mac chrome version timeouts (#85460)

This commit is contained in:
Gio Della-Libera
2026-05-23 20:39:47 -07:00
committed by GitHub
parent 76221b53c2
commit 2e8dee7f28
3 changed files with 119 additions and 1 deletions

View File

@@ -159,6 +159,7 @@ Docs: https://docs.openclaw.ai
- CLI/agents: default new omitted-account bindings to all accounts when the channel has multiple configured accounts, and clarify account-scope docs. (#49769) Thanks @Gcaufy.
- Codex app-server: let authorized `/codex` control commands such as `/codex detach` escape plugin-owned conversation bindings while keeping unknown or unauthorized slash text routed to the bound plugin. Fixes #85157. (#85188) Thanks @TurboTheTurtle.
- Auto-reply/models: keep `/models` browse replies fast by sharing the bounded read-only catalog path with Gateway model listing. (#84735) Thanks @safrano9999.
- Browser/Doctor: read macOS Chrome app bundle versions from `Info.plist` before spawning Chrome and extend the fallback version probe timeout, avoiding false cold-cache warnings from Gatekeeper latency. Fixes #85418. Thanks @davidcittadini.
- Codex app-server: disable native Code Mode when the effective exec host is `node` and keep OpenClaw `exec`/`process` available, so `/exec host=node` routes shell commands through the selected node instead of the gateway. Fixes #85012. (#85090) Thanks @sahilsatralkar.
- Agents: bound embedded auto-compaction session write-lock watchdogs to the compaction timeout instead of the full run timeout, so stuck compaction cannot hold the live session lock for the whole run window. (#84949) Thanks @luoyanglang.
- Gateway/agents: return phase-aware `agent.wait` timeout attribution and only cool auth profiles on provider-started timeouts. Refs #65504. Thanks @100yenadmin.

View File

@@ -15,6 +15,8 @@ export type BrowserExecutable = {
const CHROME_VERSION_RE = /\b(\d+)(?:\.\d+){1,3}\b/g;
const PLAYWRIGHT_BROWSERS_PATH_ENV = "PLAYWRIGHT_BROWSERS_PATH";
const BROWSER_VERSION_TIMEOUT_MS = 6000;
const MAC_PLISTBUDDY_TIMEOUT_MS = 800;
const CHROMIUM_BUNDLE_IDS = new Set([
"com.google.Chrome",
@@ -732,13 +734,42 @@ export function resolveGoogleChromeExecutableForPlatform(
}
export function readBrowserVersion(executablePath: string): string | null {
const output = execText(executablePath, ["--version"], 2000);
if (process.platform === "darwin") {
const bundleVersion = readMacBundleBrowserVersion(executablePath);
if (bundleVersion) {
return bundleVersion;
}
}
const output = execText(executablePath, ["--version"], BROWSER_VERSION_TIMEOUT_MS);
if (!output) {
return null;
}
return output.replace(/\s+/g, " ").trim();
}
function readMacBundleBrowserVersion(executablePath: string): string | null {
const appBundlePath = resolveMacAppBundlePath(executablePath);
if (!appBundlePath) {
return null;
}
const plistPath = path.join(appBundlePath, "Contents", "Info.plist");
return execText(
"/usr/libexec/PlistBuddy",
["-c", "Print :CFBundleShortVersionString", plistPath],
MAC_PLISTBUDDY_TIMEOUT_MS,
);
}
function resolveMacAppBundlePath(executablePath: string): string | null {
const parts = path.normalize(executablePath).split(path.sep);
const appIndex = parts.findIndex((part) => part.endsWith(".app"));
if (appIndex < 0) {
return null;
}
return parts.slice(0, appIndex + 1).join(path.sep) || path.sep;
}
export function parseBrowserMajorVersion(rawVersion: string | null | undefined): number | null {
const matches = [...(rawVersion ?? "").matchAll(CHROME_VERSION_RE)];
const match = matches.at(-1);

View File

@@ -0,0 +1,86 @@
import { afterEach, describe, expect, it, vi } from "vitest";
const execFileSyncMock = vi.hoisted(() => vi.fn());
vi.mock("node:child_process", async () => {
const actual = await vi.importActual<typeof import("node:child_process")>("node:child_process");
return {
...actual,
execFileSync: (...args: unknown[]) => execFileSyncMock(...args),
};
});
import { readBrowserVersion } from "./chrome.executables.js";
function stubPlatform(platform: NodeJS.Platform): void {
Object.defineProperty(process, "platform", {
configurable: true,
value: platform,
});
}
describe("readBrowserVersion", () => {
const originalPlatform = process.platform;
afterEach(() => {
stubPlatform(originalPlatform);
execFileSyncMock.mockReset();
vi.restoreAllMocks();
});
it("reads macOS app bundle versions from Info.plist before spawning Chrome", () => {
stubPlatform("darwin");
execFileSyncMock.mockReturnValue("148.0.7778.179\n");
const version = readBrowserVersion(
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
);
expect(version).toBe("148.0.7778.179");
expect(execFileSyncMock).toHaveBeenCalledTimes(1);
expect(execFileSyncMock).toHaveBeenCalledWith(
"/usr/libexec/PlistBuddy",
[
"-c",
"Print :CFBundleShortVersionString",
"/Applications/Google Chrome.app/Contents/Info.plist",
],
expect.objectContaining({ timeout: 800 }),
);
});
it("falls back to a slower --version probe when macOS bundle metadata is unavailable", () => {
stubPlatform("darwin");
execFileSyncMock
.mockImplementationOnce(() => {
throw new Error("plist unavailable");
})
.mockReturnValueOnce("Google Chrome 148.0.7778.179\n");
const version = readBrowserVersion(
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
);
expect(version).toBe("Google Chrome 148.0.7778.179");
expect(execFileSyncMock).toHaveBeenNthCalledWith(
2,
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
["--version"],
expect.objectContaining({ timeout: 6000 }),
);
});
it("uses the slower --version probe for non-bundle paths", () => {
stubPlatform("darwin");
execFileSyncMock.mockReturnValue("Chromium 148.0.7778.179\n");
const version = readBrowserVersion("/opt/chromium/chrome");
expect(version).toBe("Chromium 148.0.7778.179");
expect(execFileSyncMock).toHaveBeenCalledWith(
"/opt/chromium/chrome",
["--version"],
expect.objectContaining({ timeout: 6000 }),
);
});
});