mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 14:54:31 +00:00
fix(browser): harden CDP readiness
This commit is contained in:
@@ -1,14 +1,20 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import type { Server } from "node:http";
|
||||
|
||||
import { createTargetViaCdp } from "./cdp.js";
|
||||
import {
|
||||
isChromeCdpReady,
|
||||
isChromeReachable,
|
||||
launchClawdChrome,
|
||||
type RunningChrome,
|
||||
resolveClawdUserDataDir,
|
||||
stopClawdChrome,
|
||||
} from "./chrome.js";
|
||||
import type { ResolvedBrowserConfig } from "./config.js";
|
||||
import { resolveTargetIdFromTabs } from "./target-id.js";
|
||||
import { runExec } from "../process/exec.js";
|
||||
|
||||
export type BrowserTab = {
|
||||
targetId: string;
|
||||
@@ -30,12 +36,18 @@ export type BrowserRouteContext = {
|
||||
state: () => BrowserServerState;
|
||||
ensureBrowserAvailable: () => Promise<void>;
|
||||
ensureTabAvailable: (targetId?: string) => Promise<BrowserTab>;
|
||||
isHttpReachable: (timeoutMs?: number) => Promise<boolean>;
|
||||
isReachable: (timeoutMs?: number) => Promise<boolean>;
|
||||
listTabs: () => Promise<BrowserTab[]>;
|
||||
openTab: (url: string) => Promise<BrowserTab>;
|
||||
focusTab: (targetId: string) => Promise<void>;
|
||||
closeTab: (targetId: string) => Promise<void>;
|
||||
stopRunningBrowser: () => Promise<{ stopped: boolean }>;
|
||||
resetProfile: () => Promise<{
|
||||
moved: boolean;
|
||||
from: string;
|
||||
to?: string;
|
||||
}>;
|
||||
mapTabError: (err: unknown) => { status: number; message: string } | null;
|
||||
};
|
||||
|
||||
@@ -157,27 +169,64 @@ export function createBrowserRouteContext(
|
||||
};
|
||||
|
||||
const isReachable = async (timeoutMs = 300) => {
|
||||
const current = state();
|
||||
const wsTimeout = Math.max(200, Math.min(2000, timeoutMs * 2));
|
||||
return await isChromeCdpReady(current.cdpPort, timeoutMs, wsTimeout);
|
||||
};
|
||||
|
||||
const isHttpReachable = async (timeoutMs = 300) => {
|
||||
const current = state();
|
||||
return await isChromeReachable(current.cdpPort, timeoutMs);
|
||||
};
|
||||
|
||||
const attachRunning = (running: RunningChrome) => {
|
||||
opts.setRunning(running);
|
||||
running.proc.on("exit", () => {
|
||||
const live = opts.getState();
|
||||
if (live?.running?.pid === running.pid) {
|
||||
opts.setRunning(null);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const ensureBrowserAvailable = async (): Promise<void> => {
|
||||
const current = state();
|
||||
const httpReachable = await isHttpReachable();
|
||||
if (!httpReachable) {
|
||||
if (current.resolved.attachOnly) {
|
||||
throw new Error(
|
||||
"Browser attachOnly is enabled and no browser is running.",
|
||||
);
|
||||
}
|
||||
const launched = await launchClawdChrome(current.resolved);
|
||||
attachRunning(launched);
|
||||
}
|
||||
|
||||
if (await isReachable()) return;
|
||||
|
||||
if (current.resolved.attachOnly) {
|
||||
throw new Error(
|
||||
"Browser attachOnly is enabled and no browser is running.",
|
||||
"Browser attachOnly is enabled and CDP websocket is not reachable.",
|
||||
);
|
||||
}
|
||||
|
||||
const launched = await launchClawdChrome(current.resolved);
|
||||
opts.setRunning(launched);
|
||||
launched.proc.on("exit", () => {
|
||||
const live = opts.getState();
|
||||
if (live?.running?.pid === launched.pid) {
|
||||
opts.setRunning(null);
|
||||
}
|
||||
});
|
||||
if (!current.running) {
|
||||
throw new Error(
|
||||
"CDP port responds but websocket handshake failed. Ensure the clawd browser owns the port or stop the conflicting process.",
|
||||
);
|
||||
}
|
||||
|
||||
await stopClawdChrome(current.running);
|
||||
opts.setRunning(null);
|
||||
|
||||
const relaunched = await launchClawdChrome(current.resolved);
|
||||
attachRunning(relaunched);
|
||||
|
||||
if (!(await isReachable(600))) {
|
||||
throw new Error(
|
||||
"Chrome CDP websocket is not reachable after restart.",
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const ensureTabAvailable = async (targetId?: string): Promise<BrowserTab> => {
|
||||
@@ -244,6 +293,36 @@ export function createBrowserRouteContext(
|
||||
return { stopped: true };
|
||||
};
|
||||
|
||||
const resetProfile = async () => {
|
||||
const current = state();
|
||||
const userDataDir = resolveClawdUserDataDir();
|
||||
|
||||
const httpReachable = await isHttpReachable(300);
|
||||
if (httpReachable && !current.running) {
|
||||
throw new Error(
|
||||
"Browser appears to be running but is not owned by clawd. Stop it before resetting the profile.",
|
||||
);
|
||||
}
|
||||
|
||||
if (current.running) {
|
||||
await stopRunningBrowser();
|
||||
}
|
||||
|
||||
try {
|
||||
const mod = await import("./pw-ai.js");
|
||||
await mod.closePlaywrightBrowserConnection();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
if (!fs.existsSync(userDataDir)) {
|
||||
return { moved: false, from: userDataDir };
|
||||
}
|
||||
|
||||
const moved = await movePathToTrash(userDataDir);
|
||||
return { moved: true, from: userDataDir, to: moved };
|
||||
};
|
||||
|
||||
const mapTabError = (err: unknown) => {
|
||||
const msg = String(err);
|
||||
if (msg.includes("ambiguous target id prefix")) {
|
||||
@@ -259,12 +338,31 @@ export function createBrowserRouteContext(
|
||||
state,
|
||||
ensureBrowserAvailable,
|
||||
ensureTabAvailable,
|
||||
isHttpReachable,
|
||||
isReachable,
|
||||
listTabs,
|
||||
openTab,
|
||||
focusTab,
|
||||
closeTab,
|
||||
stopRunningBrowser,
|
||||
resetProfile,
|
||||
mapTabError,
|
||||
};
|
||||
}
|
||||
|
||||
async function movePathToTrash(targetPath: string): Promise<string> {
|
||||
try {
|
||||
await runExec("trash", [targetPath], { timeoutMs: 10_000 });
|
||||
return targetPath;
|
||||
} catch {
|
||||
const trashDir = path.join(os.homedir(), ".Trash");
|
||||
fs.mkdirSync(trashDir, { recursive: true });
|
||||
const base = path.basename(targetPath);
|
||||
let dest = path.join(trashDir, `${base}-${Date.now()}`);
|
||||
if (fs.existsSync(dest)) {
|
||||
dest = path.join(trashDir, `${base}-${Date.now()}-${Math.random()}`);
|
||||
}
|
||||
fs.renameSync(targetPath, dest);
|
||||
return dest;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user