import { type ChildProcess, type ChildProcessWithoutNullStreams, spawn } from "node:child_process"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { prepareOomScoreAdjustedSpawn } from "openclaw/plugin-sdk/process-runtime"; import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime"; import type { SsrFPolicy } from "../infra/net/ssrf.js"; import { ensurePortAvailable } from "../infra/ports.js"; import { createSubsystemLogger } from "../logging/subsystem.js"; import { CONFIG_DIR } from "../utils.js"; import { hasChromeProxyControlArg, omitChromeProxyEnv } from "./browser-proxy-mode.js"; import {
CHROME_BOOTSTRAP_EXIT_POLL_MS,
CHROME_BOOTSTRAP_EXIT_TIMEOUT_MS,
CHROME_BOOTSTRAP_PREFS_POLL_MS,
CHROME_BOOTSTRAP_PREFS_TIMEOUT_MS,
CHROME_LAUNCH_READY_POLL_MS,
CHROME_LAUNCH_READY_WINDOW_MS,
CHROME_REACHABILITY_TIMEOUT_MS,
CHROME_STDERR_HINT_MAX_CHARS,
CHROME_STOP_PROBE_TIMEOUT_MS,
CHROME_STOP_TIMEOUT_MS,
CHROME_WS_READY_TIMEOUT_MS,
} from "./cdp-timeouts.js"; import {
assertCdpEndpointAllowed,
isDirectCdpWebSocketEndpoint,
isWebSocketUrl,
normalizeCdpHttpBaseForJsonEndpoints,
openCdpWebSocket,
} from "./cdp.helpers.js"; import { normalizeCdpWsUrl } from "./cdp.js"; import {
diagnoseChromeCdp,
formatChromeCdpDiagnostic,
type ChromeVersion,
readChromeVersion,
safeChromeCdpErrorMessage,
} from "./chrome.diagnostics.js"; import {
type BrowserExecutable,
resolveBrowserExecutableForPlatform,
} from "./chrome.executables.js"; import {
decorateOpenClawProfile,
ensureProfileCleanExit,
isProfileDecorated,
} from "./chrome.profile-decoration.js"; import type { ResolvedBrowserConfig, ResolvedBrowserProfile } from "./config.js"; import {
DEFAULT_OPENCLAW_BROWSER_COLOR,
DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME,
} from "./constants.js";
const log = createSubsystemLogger("browser").child("chrome"); const CHROME_SINGLETON_LOCK_PATHS = [ "SingletonLock", "SingletonSocket", "SingletonCookie",
] as const; const CHROME_SINGLETON_IN_USE_PATTERN = /profile appears to be in use by another chromium process/i; const CHROME_MISSING_DISPLAY_PATTERN = /missing x server|\$DISPLAY/i;
export type { BrowserExecutable } from "./chrome.executables.js";
export {
diagnoseChromeCdp,
formatChromeCdpDiagnostic,
type ChromeCdpDiagnostic,
type ChromeCdpDiagnosticCode,
} from "./chrome.diagnostics.js";
export {
findChromeExecutableLinux,
findChromeExecutableMac,
findChromeExecutableWindows,
resolveBrowserExecutableForPlatform,
} from "./chrome.executables.js";
export {
decorateOpenClawProfile,
ensureProfileCleanExit,
isProfileDecorated,
} from "./chrome.profile-decoration.js";
function chromeLaunchHints(params: {
stderrOutput: string;
resolved: ResolvedBrowserConfig;
profile: ResolvedBrowserProfile;
}): string { const hints: string[] = []; if (process.platform === "linux" && !params.resolved.noSandbox) {
hints.push("If running in a container or as root, try setting browser.noSandbox: true.");
} if (CHROME_MISSING_DISPLAY_PATTERN.test(params.stderrOutput) && !params.profile.headless) {
hints.push( "No DISPLAY/X server was detected. Enable browser.headless: true, start Xvfb, or run the Gateway in a desktop session.",
);
} if (CHROME_SINGLETON_IN_USE_PATTERN.test(params.stderrOutput)) {
hints.push(
`The Chromium profile "${params.profile.name}" is locked. Stop the existing browser or remove stale Singleton* lock files under ~/.openclaw/browser/${params.profile.name}/user-data.`,
);
} return hints.length > 0 ? `\nHint: ${hints.join("\nHint: ")}` : "";
}
export async function isChromeReachable(
cdpUrl: string,
timeoutMs = CHROME_REACHABILITY_TIMEOUT_MS,
ssrfPolicy?: SsrFPolicy,
): Promise<boolean> { try {
await assertCdpEndpointAllowed(cdpUrl, ssrfPolicy); if (isDirectCdpWebSocketEndpoint(cdpUrl)) { // Handshake-ready direct WS endpoint — probe via WS handshake. return await canOpenWebSocket(cdpUrl, timeoutMs);
} // Either an http(s) discovery URL or a bare ws/wss root. Try // /json/version discovery first. For bare ws/wss URLs, fall back to a // direct WS handshake when discovery is unavailable — some providers // (e.g. Browserless/Browserbase) expose a direct WebSocket root without // a /json/version endpoint. const discoveryUrl = isWebSocketUrl(cdpUrl)
? normalizeCdpHttpBaseForJsonEndpoints(cdpUrl)
: cdpUrl; const version = await fetchChromeVersion(discoveryUrl, timeoutMs, ssrfPolicy); if (version) { returntrue;
} if (isWebSocketUrl(cdpUrl)) { return await canOpenWebSocket(cdpUrl, timeoutMs);
} returnfalse;
} catch { returnfalse;
}
}
export async function getChromeWebSocketUrl(
cdpUrl: string,
timeoutMs = CHROME_REACHABILITY_TIMEOUT_MS,
ssrfPolicy?: SsrFPolicy,
): Promise<string | null> {
await assertCdpEndpointAllowed(cdpUrl, ssrfPolicy); if (isDirectCdpWebSocketEndpoint(cdpUrl)) { // Handshake-ready direct WebSocket endpoint — the cdpUrl is already // the WebSocket URL. return cdpUrl;
} // Either an http(s) endpoint or a bare ws/wss root; discover the // actual WebSocket URL via /json/version. Normalise the scheme so // fetch() can reach the endpoint. const discoveryUrl = isWebSocketUrl(cdpUrl)
? normalizeCdpHttpBaseForJsonEndpoints(cdpUrl)
: cdpUrl; const version = await fetchChromeVersion(discoveryUrl, timeoutMs, ssrfPolicy); const wsUrl = normalizeOptionalString(version?.webSocketDebuggerUrl) ?? ""; if (!wsUrl) { // /json/version unavailable or returned no WebSocket URL. For bare // ws/wss inputs, the URL itself may be a direct WebSocket endpoint // (e.g. Browserless/Browserbase-style providers without /json/version). // The SSRF check on cdpUrl was already performed at the start of this // function, so we can return it directly. if (isWebSocketUrl(cdpUrl)) { return cdpUrl;
} returnnull;
} const normalizedWsUrl = normalizeCdpWsUrl(wsUrl, discoveryUrl);
await assertCdpEndpointAllowed(normalizedWsUrl, ssrfPolicy); return normalizedWsUrl;
}
export async function launchOpenClawChrome(
resolved: ResolvedBrowserConfig,
profile: ResolvedBrowserProfile,
): Promise<RunningChrome> { if (!profile.cdpIsLoopback) { thrownew Error(`Profile "${profile.name}" is remote; cannot launch local Chrome.`);
}
await ensurePortAvailable(profile.cdpPort);
const exe = resolveBrowserExecutable(resolved, profile); if (!exe) { thrownew Error( "No supported browser found (Chrome/Brave/Edge/Chromium on macOS, Linux, or Windows).",
);
}
// Collect stderr for diagnostics in case Chrome fails to start. // The listener is removed on success to avoid unbounded memory growth // from a long-lived Chrome process that emits periodic warnings. const stderrChunks: Buffer[] = []; const onStderr = (chunk: Buffer) => {
stderrChunks.push(chunk);
};
proc.stderr?.on("data", onStderr);
try { const readyDeadline = Date.now() + CHROME_LAUNCH_READY_WINDOW_MS; while (Date.now() < readyDeadline) { if (await isChromeReachable(profile.cdpUrl)) { break;
}
await new Promise((r) => setTimeout(r, CHROME_LAUNCH_READY_POLL_MS));
}
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.