mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 18:30:44 +00:00
feat(browser): configure local startup timeouts
This commit is contained in:
@@ -18,6 +18,8 @@ function buildResolvedConfig(): ResolvedBrowserConfig {
|
||||
cdpIsLoopback: true,
|
||||
remoteCdpTimeoutMs: 1500,
|
||||
remoteCdpHandshakeTimeoutMs: 3000,
|
||||
localLaunchTimeoutMs: 15_000,
|
||||
localCdpReadyTimeoutMs: 8_000,
|
||||
extraArgs: [],
|
||||
color: DEFAULT_OPENCLAW_BROWSER_COLOR,
|
||||
executablePath: undefined,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { DEFAULT_BROWSER_LOCAL_LAUNCH_TIMEOUT_MS } from "./constants.js";
|
||||
|
||||
export const CDP_HTTP_REQUEST_TIMEOUT_MS = 1500;
|
||||
export const CDP_WS_HANDSHAKE_TIMEOUT_MS = 5000;
|
||||
export const CDP_JSON_NEW_TIMEOUT_MS = 1500;
|
||||
@@ -8,7 +10,7 @@ export const CHROME_BOOTSTRAP_PREFS_TIMEOUT_MS = 10_000;
|
||||
export const CHROME_BOOTSTRAP_PREFS_POLL_MS = 100;
|
||||
export const CHROME_BOOTSTRAP_EXIT_TIMEOUT_MS = 5000;
|
||||
export const CHROME_BOOTSTRAP_EXIT_POLL_MS = 50;
|
||||
export const CHROME_LAUNCH_READY_WINDOW_MS = 15_000;
|
||||
export const CHROME_LAUNCH_READY_WINDOW_MS = DEFAULT_BROWSER_LOCAL_LAUNCH_TIMEOUT_MS;
|
||||
export const CHROME_LAUNCH_READY_POLL_MS = 200;
|
||||
export const CHROME_STOP_TIMEOUT_MS = 2500;
|
||||
export const CHROME_STOP_PROBE_TIMEOUT_MS = 200;
|
||||
|
||||
@@ -374,6 +374,8 @@ describe("chrome.ts internal", () => {
|
||||
headless: true,
|
||||
noSandbox: true,
|
||||
extraArgs: [],
|
||||
localLaunchTimeoutMs: 15_000,
|
||||
localCdpReadyTimeoutMs: 8_000,
|
||||
}) as unknown as ResolvedBrowserConfig;
|
||||
|
||||
it("rejects a remote profile before attempting to spawn", async () => {
|
||||
@@ -544,7 +546,11 @@ describe("chrome.ts internal", () => {
|
||||
try {
|
||||
vi.spyOn(fs, "existsSync").mockImplementation((p) => {
|
||||
const s = String(p);
|
||||
if (s.includes("google-chrome")) {
|
||||
if (
|
||||
s.includes("Google Chrome") ||
|
||||
s.includes("google-chrome") ||
|
||||
s.includes("/usr/bin/chromium")
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (s.endsWith("Local State") || s.endsWith("Preferences")) {
|
||||
@@ -574,6 +580,41 @@ describe("chrome.ts internal", () => {
|
||||
Object.defineProperty(process, "platform", { value: originalPlatform });
|
||||
}
|
||||
});
|
||||
|
||||
it("uses the configured local launch timeout while waiting for CDP discovery", async () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
const executablePath = path.join(tmpDir, "chrome");
|
||||
await fsp.writeFile(executablePath, "");
|
||||
const existsSync = fs.existsSync.bind(fs);
|
||||
vi.spyOn(fs, "existsSync").mockImplementation((p) => {
|
||||
const s = String(p);
|
||||
if (s.endsWith("Local State") || s.endsWith("Preferences")) {
|
||||
return true;
|
||||
}
|
||||
return existsSync(p);
|
||||
});
|
||||
const fakeProc = makeFakeProc();
|
||||
spawnMock.mockReturnValue(fakeProc);
|
||||
vi.stubGlobal("fetch", vi.fn().mockRejectedValue(new Error("ECONNREFUSED")));
|
||||
|
||||
const resolved = {
|
||||
...makeResolved(),
|
||||
executablePath,
|
||||
localLaunchTimeoutMs: 1,
|
||||
};
|
||||
const profile = makeProfile(55556);
|
||||
const rejection = expect(launchOpenClawChrome(resolved, profile)).rejects.toThrow(
|
||||
/Failed to start Chrome CDP/,
|
||||
);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(10);
|
||||
await rejection;
|
||||
expect(fakeProc.kill).toHaveBeenCalledWith("SIGKILL");
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("stopOpenClawChrome SIGKILL fallback", () => {
|
||||
|
||||
@@ -768,6 +768,8 @@ describe("browser chrome launch args", () => {
|
||||
evaluateEnabled: false,
|
||||
remoteCdpTimeoutMs: 1500,
|
||||
remoteCdpHandshakeTimeoutMs: 3000,
|
||||
localLaunchTimeoutMs: 15_000,
|
||||
localCdpReadyTimeoutMs: 8_000,
|
||||
actionTimeoutMs: 60_000,
|
||||
extraArgs: [],
|
||||
color: "#FF4500",
|
||||
|
||||
@@ -520,7 +520,8 @@ export async function launchOpenClawChrome(
|
||||
proc.stderr?.on("data", onStderr);
|
||||
|
||||
try {
|
||||
const readyDeadline = Date.now() + CHROME_LAUNCH_READY_WINDOW_MS;
|
||||
const readyDeadline =
|
||||
Date.now() + (resolved.localLaunchTimeoutMs ?? CHROME_LAUNCH_READY_WINDOW_MS);
|
||||
while (Date.now() < readyDeadline) {
|
||||
if (await isChromeReachable(profile.cdpUrl)) {
|
||||
break;
|
||||
|
||||
@@ -445,6 +445,35 @@ describe("browser config", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("managed browser startup timeouts", () => {
|
||||
it("uses defaults for local launch and post-launch readiness windows", () => {
|
||||
const resolved = resolveBrowserConfig({});
|
||||
|
||||
expect(resolved.localLaunchTimeoutMs).toBe(15_000);
|
||||
expect(resolved.localCdpReadyTimeoutMs).toBe(8_000);
|
||||
});
|
||||
|
||||
it("accepts custom local startup timeout values", () => {
|
||||
const resolved = resolveBrowserConfig({
|
||||
localLaunchTimeoutMs: 45_000,
|
||||
localCdpReadyTimeoutMs: 30_000,
|
||||
});
|
||||
|
||||
expect(resolved.localLaunchTimeoutMs).toBe(45_000);
|
||||
expect(resolved.localCdpReadyTimeoutMs).toBe(30_000);
|
||||
});
|
||||
|
||||
it("clamps oversized local startup timeout values", () => {
|
||||
const resolved = resolveBrowserConfig({
|
||||
localLaunchTimeoutMs: 999_999,
|
||||
localCdpReadyTimeoutMs: 999_999,
|
||||
});
|
||||
|
||||
expect(resolved.localLaunchTimeoutMs).toBe(120_000);
|
||||
expect(resolved.localCdpReadyTimeoutMs).toBe(120_000);
|
||||
});
|
||||
});
|
||||
|
||||
it("inherits executablePath from global browser config when profile override is not set", () => {
|
||||
const resolved = resolveBrowserConfig({
|
||||
executablePath: "~/bin/chrome-global",
|
||||
|
||||
@@ -24,6 +24,8 @@ import {
|
||||
DEFAULT_BROWSER_ACTION_TIMEOUT_MS,
|
||||
DEFAULT_BROWSER_DEFAULT_PROFILE_NAME,
|
||||
DEFAULT_BROWSER_EVALUATE_ENABLED,
|
||||
DEFAULT_BROWSER_LOCAL_CDP_READY_TIMEOUT_MS,
|
||||
DEFAULT_BROWSER_LOCAL_LAUNCH_TIMEOUT_MS,
|
||||
DEFAULT_BROWSER_TAB_CLEANUP_IDLE_MINUTES,
|
||||
DEFAULT_BROWSER_TAB_CLEANUP_MAX_TABS_PER_SESSION,
|
||||
DEFAULT_BROWSER_TAB_CLEANUP_SWEEP_MINUTES,
|
||||
@@ -39,6 +41,8 @@ export {
|
||||
DEFAULT_BROWSER_ACTION_TIMEOUT_MS,
|
||||
DEFAULT_BROWSER_DEFAULT_PROFILE_NAME,
|
||||
DEFAULT_BROWSER_EVALUATE_ENABLED,
|
||||
DEFAULT_BROWSER_LOCAL_CDP_READY_TIMEOUT_MS,
|
||||
DEFAULT_BROWSER_LOCAL_LAUNCH_TIMEOUT_MS,
|
||||
DEFAULT_OPENCLAW_BROWSER_COLOR,
|
||||
DEFAULT_OPENCLAW_BROWSER_ENABLED,
|
||||
DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME,
|
||||
@@ -69,6 +73,8 @@ export type ResolvedBrowserConfig = {
|
||||
cdpIsLoopback: boolean;
|
||||
remoteCdpTimeoutMs: number;
|
||||
remoteCdpHandshakeTimeoutMs: number;
|
||||
localLaunchTimeoutMs: number;
|
||||
localCdpReadyTimeoutMs: number;
|
||||
actionTimeoutMs: number;
|
||||
color: string;
|
||||
executablePath?: string;
|
||||
@@ -106,6 +112,7 @@ export type ResolvedBrowserProfile = {
|
||||
};
|
||||
|
||||
const DEFAULT_BROWSER_CDP_PORT_RANGE_START = 18800;
|
||||
const MAX_BROWSER_STARTUP_TIMEOUT_MS = 120_000;
|
||||
export const OPENCLAW_BROWSER_HEADLESS_ENV = "OPENCLAW_BROWSER_HEADLESS";
|
||||
|
||||
export type ManagedBrowserHeadlessSource =
|
||||
@@ -144,6 +151,14 @@ function normalizeTimeoutMs(raw: number | undefined, fallback: number): number {
|
||||
return value < 0 ? fallback : value;
|
||||
}
|
||||
|
||||
function normalizeStartupTimeoutMs(raw: number | undefined, fallback: number): number {
|
||||
const value = typeof raw === "number" && Number.isFinite(raw) ? Math.floor(raw) : fallback;
|
||||
if (value <= 0) {
|
||||
return fallback;
|
||||
}
|
||||
return Math.min(value, MAX_BROWSER_STARTUP_TIMEOUT_MS);
|
||||
}
|
||||
|
||||
function normalizeNonNegativeInteger(raw: number | undefined, fallback: number): number {
|
||||
const value = typeof raw === "number" && Number.isFinite(raw) ? Math.floor(raw) : fallback;
|
||||
return value < 0 ? fallback : value;
|
||||
@@ -297,6 +312,14 @@ export function resolveBrowserConfig(
|
||||
cfg?.remoteCdpHandshakeTimeoutMs,
|
||||
Math.max(2000, remoteCdpTimeoutMs * 2),
|
||||
);
|
||||
const localLaunchTimeoutMs = normalizeStartupTimeoutMs(
|
||||
cfg?.localLaunchTimeoutMs,
|
||||
DEFAULT_BROWSER_LOCAL_LAUNCH_TIMEOUT_MS,
|
||||
);
|
||||
const localCdpReadyTimeoutMs = normalizeStartupTimeoutMs(
|
||||
cfg?.localCdpReadyTimeoutMs,
|
||||
DEFAULT_BROWSER_LOCAL_CDP_READY_TIMEOUT_MS,
|
||||
);
|
||||
const actionTimeoutMs = normalizeTimeoutMs(
|
||||
cfg?.actionTimeoutMs,
|
||||
DEFAULT_BROWSER_ACTION_TIMEOUT_MS,
|
||||
@@ -382,6 +405,8 @@ export function resolveBrowserConfig(
|
||||
cdpIsLoopback: isLoopbackHost(cdpInfo.parsed.hostname),
|
||||
remoteCdpTimeoutMs,
|
||||
remoteCdpHandshakeTimeoutMs,
|
||||
localLaunchTimeoutMs,
|
||||
localCdpReadyTimeoutMs,
|
||||
actionTimeoutMs,
|
||||
color: defaultColor,
|
||||
executablePath,
|
||||
|
||||
@@ -4,6 +4,8 @@ export const DEFAULT_OPENCLAW_BROWSER_COLOR = "#FF4500";
|
||||
export const DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME = "openclaw";
|
||||
export const DEFAULT_BROWSER_DEFAULT_PROFILE_NAME = "openclaw";
|
||||
export const DEFAULT_BROWSER_ACTION_TIMEOUT_MS = 60_000;
|
||||
export const DEFAULT_BROWSER_LOCAL_LAUNCH_TIMEOUT_MS = 15_000;
|
||||
export const DEFAULT_BROWSER_LOCAL_CDP_READY_TIMEOUT_MS = 8_000;
|
||||
export const DEFAULT_BROWSER_SCREENSHOT_TIMEOUT_MS = 20_000;
|
||||
export const DEFAULT_BROWSER_TAB_CLEANUP_IDLE_MINUTES = 120;
|
||||
export const DEFAULT_BROWSER_TAB_CLEANUP_MAX_TABS_PER_SESSION = 8;
|
||||
|
||||
@@ -191,7 +191,8 @@ export function createProfileAvailability({
|
||||
const waitForCdpReadyAfterLaunch = async (): Promise<void> => {
|
||||
// launchOpenClawChrome() can return before Chrome is fully ready to serve /json/version + CDP WS.
|
||||
// If a follow-up call races ahead, we can hit PortInUseError trying to launch again on the same port.
|
||||
const deadlineMs = Date.now() + CDP_READY_AFTER_LAUNCH_WINDOW_MS;
|
||||
const deadlineMs =
|
||||
Date.now() + (state().resolved.localCdpReadyTimeoutMs ?? CDP_READY_AFTER_LAUNCH_WINDOW_MS);
|
||||
while (Date.now() < deadlineMs) {
|
||||
const remainingMs = Math.max(0, deadlineMs - Date.now());
|
||||
// Keep each attempt short; loopback profiles derive a WS timeout from this value.
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { DEFAULT_BROWSER_LOCAL_CDP_READY_TIMEOUT_MS } from "./constants.js";
|
||||
|
||||
export const MANAGED_BROWSER_PAGE_TAB_LIMIT = 8;
|
||||
|
||||
export const OPEN_TAB_DISCOVERY_WINDOW_MS = 2000;
|
||||
export const OPEN_TAB_DISCOVERY_POLL_MS = 100;
|
||||
|
||||
export const CDP_READY_AFTER_LAUNCH_WINDOW_MS = 8000;
|
||||
export const CDP_READY_AFTER_LAUNCH_WINDOW_MS = DEFAULT_BROWSER_LOCAL_CDP_READY_TIMEOUT_MS;
|
||||
export const CDP_READY_AFTER_LAUNCH_POLL_MS = 100;
|
||||
export const CDP_READY_AFTER_LAUNCH_MIN_TIMEOUT_MS = 75;
|
||||
export const CDP_READY_AFTER_LAUNCH_MAX_TIMEOUT_MS = 250;
|
||||
|
||||
@@ -90,6 +90,22 @@ describe("browser server-context ensureBrowserAvailable", () => {
|
||||
expect(stopOpenClawChrome).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("uses configured local CDP readiness timeout after launching", async () => {
|
||||
const { launchOpenClawChrome, stopOpenClawChrome, isChromeCdpReady, profile, state } =
|
||||
setupEnsureBrowserAvailableHarness();
|
||||
state.resolved.localCdpReadyTimeoutMs = 250;
|
||||
isChromeCdpReady.mockResolvedValue(false);
|
||||
mockLaunchedChrome(launchOpenClawChrome, 322);
|
||||
|
||||
const promise = profile.ensureBrowserAvailable();
|
||||
const rejected = expect(promise).rejects.toThrow("not reachable after start");
|
||||
await vi.advanceTimersByTimeAsync(300);
|
||||
await rejected;
|
||||
|
||||
expect(launchOpenClawChrome).toHaveBeenCalledTimes(1);
|
||||
expect(stopOpenClawChrome).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("deduplicates concurrent lazy-start calls to prevent PortInUseError", async () => {
|
||||
const { launchOpenClawChrome, stopOpenClawChrome, isChromeCdpReady, profile } =
|
||||
setupEnsureBrowserAvailableHarness();
|
||||
|
||||
@@ -44,6 +44,8 @@ function makeState(): BrowserServerState {
|
||||
cdpIsLoopback: true,
|
||||
remoteCdpTimeoutMs: 1500,
|
||||
remoteCdpHandshakeTimeoutMs: 3000,
|
||||
localLaunchTimeoutMs: 15_000,
|
||||
localCdpReadyTimeoutMs: 8_000,
|
||||
actionTimeoutMs: 60_000,
|
||||
color: "#FF4500",
|
||||
headless: false,
|
||||
|
||||
@@ -24,6 +24,8 @@ export function makeState(
|
||||
cdpIsLoopback: profile !== "remote",
|
||||
remoteCdpTimeoutMs: 1500,
|
||||
remoteCdpHandshakeTimeoutMs: 3000,
|
||||
localLaunchTimeoutMs: 15_000,
|
||||
localCdpReadyTimeoutMs: 8_000,
|
||||
actionTimeoutMs: 60_000,
|
||||
evaluateEnabled: false,
|
||||
extraArgs: [],
|
||||
|
||||
@@ -37,6 +37,8 @@ export function makeBrowserServerState(params?: {
|
||||
evaluateEnabled: false,
|
||||
remoteCdpTimeoutMs: 1500,
|
||||
remoteCdpHandshakeTimeoutMs: 3000,
|
||||
localLaunchTimeoutMs: 15_000,
|
||||
localCdpReadyTimeoutMs: 8_000,
|
||||
actionTimeoutMs: 60_000,
|
||||
extraArgs: [],
|
||||
color: profile.color,
|
||||
|
||||
Reference in New Issue
Block a user