UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

233 lines • 8.54 kB
/** * @license * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import { execSync } from 'node:child_process'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import { puppeteer } from './third_party/index.js'; import { logger, puppeteerLogger } from './utils/logger.js'; import { isAllowedUrl } from './utils/url.js'; let browser; let browserMode; export function makeTargetFilter(enableExtensions = false) { return function targetFilter(target) { const url = target.url(); if (!url) { return true; } return isAllowedUrl(url, { categoryExtensions: enableExtensions }); }; } export async function ensureBrowserConnected(options) { const { channel, enableExtensions } = options; if (browser?.connected) { return browser; } const connectOptions = { targetFilter: makeTargetFilter(enableExtensions), defaultViewport: null, handleDevToolsAsPage: true, blocklist: options.blocklist, allowlist: options.allowlist, logger: puppeteerLogger, }; let autoConnect = false; if (options.wsEndpoint) { connectOptions.browserWSEndpoint = options.wsEndpoint; if (options.wsHeaders) { connectOptions.headers = options.wsHeaders; } } else if (options.browserURL) { connectOptions.browserURL = options.browserURL; } else if (channel || options.userDataDir) { const userDataDir = options.userDataDir; if (userDataDir) { autoConnect = true; // TODO: re-expose this logic via Puppeteer. const portPath = path.join(userDataDir, 'DevToolsActivePort'); try { const fileContent = await fs.promises.readFile(portPath, 'utf8'); const [rawPort, rawPath] = fileContent .split('\n') .map(line => { return line.trim(); }) .filter(line => { return !!line; }); if (!rawPort || !rawPath) { throw new Error(`Invalid DevToolsActivePort '${fileContent}' found`); } const port = parseInt(rawPort, 10); if (isNaN(port) || port <= 0 || port > 65535) { throw new Error(`Invalid port '${rawPort}' found`); } const browserWSEndpoint = `ws://127.0.0.1:${port}${rawPath}`; connectOptions.browserWSEndpoint = browserWSEndpoint; } catch (error) { throw new Error(`Could not connect to Chrome in ${userDataDir}. Check if Chrome is running and remote debugging is enabled by going to chrome://inspect/#remote-debugging.`, { cause: error, }); } } else { if (!channel) { throw new Error('Channel must be provided if userDataDir is missing'); } connectOptions.channel = (channel === 'stable' ? 'chrome' : `chrome-${channel}`); } } else { throw new Error('Either browserURL, wsEndpoint, channel or userDataDir must be provided'); } logger?.('Connecting Puppeteer to ', JSON.stringify(connectOptions)); try { // Assign mode before browser so a concurrent closeBrowser() never sees // `browser` set with `browserMode` still undefined (would fall through // to the disconnect() path and orphan a launched Chrome). const connected = await puppeteer.connect(connectOptions); browserMode = 'connected'; browser = connected; } catch (err) { throw new Error(`Could not connect to Chrome. ${autoConnect ? `Check if Chrome is running and remote debugging is enabled by going to chrome://inspect/#remote-debugging.` : `Check if Chrome is running.`}`, { cause: err, }); } logger?.('Connected Puppeteer'); return browser; } export function detectDisplay() { // Only detect display on Linux/UNIX. if (os.platform() === 'win32' || os.platform() === 'darwin') { return; } if (!process.env['DISPLAY']) { try { const result = execSync(`ps -u $(id -u) -o pid= | xargs -I{} cat /proc/{}/environ 2>/dev/null | tr '\\0' '\\n' | grep -m1 '^DISPLAY=' | cut -d= -f2`); const display = result.toString('utf8').trim(); process.env['DISPLAY'] = display; } catch { // no-op } } } export async function launch(options) { const { channel, executablePath, headless, isolated } = options; const profileDirName = channel && channel !== 'stable' ? `chrome-profile-${channel}` : 'chrome-profile'; let userDataDir = options.userDataDir; if (!isolated && !userDataDir) { userDataDir = path.join(os.homedir(), '.cache', options.viaCli ? 'chrome-devtools-mcp-cli' : 'chrome-devtools-mcp', profileDirName); await fs.promises.mkdir(userDataDir, { recursive: true, }); } const args = [ ...(options.chromeArgs ?? []), '--hide-crash-restore-bubble', ]; const ignoreDefaultArgs = options.ignoreDefaultChromeArgs ?? false; if (headless) { args.push('--screen-info={3840x2160}'); } let puppeteerChannel; if (options.devtools) { args.push('--auto-open-devtools-for-tabs'); } if (!executablePath) { puppeteerChannel = channel && channel !== 'stable' ? `chrome-${channel}` : 'chrome'; } if (!headless) { detectDisplay(); } try { const browser = await puppeteer.launch({ channel: puppeteerChannel, targetFilter: makeTargetFilter(options.enableExtensions), executablePath, defaultViewport: null, userDataDir, pipe: true, headless, args, ignoreDefaultArgs: ignoreDefaultArgs, acceptInsecureCerts: options.acceptInsecureCerts, handleDevToolsAsPage: true, enableExtensions: options.enableExtensions, blocklist: options.blocklist, allowlist: options.allowlist, logger: puppeteerLogger, }); if (options.logFile) { // FIXME: we are probably subscribing too late to catch startup logs. We // should expose the process earlier or expose the getRecentLogs() getter. browser.process()?.stderr?.pipe(options.logFile); browser.process()?.stdout?.pipe(options.logFile); } if (options.viewport) { const [page] = await browser.pages(); await page?.resize({ contentWidth: options.viewport.width, contentHeight: options.viewport.height, }); } return browser; } catch (error) { if (userDataDir && error.message.includes('The browser is already running')) { throw new Error(`The browser is already running for ${userDataDir}. Use --isolated to run multiple browser instances.`, { cause: error, }); } throw error; } } export async function ensureBrowserLaunched(options) { if (browser?.connected) { return browser; } // Assign mode before browser; see the connect path above for rationale. const launched = await launch(options); browserMode = 'launched'; browser = launched; return browser; } /** * Shutdown hook for the active browser. Closes a launched browser (so the * Chrome subprocess is reaped) or disconnects from an attached browser (so * the user's Chrome instance stays alive). No-op if no browser is active or * the connection has already been dropped. Called from the server entrypoint * on stdin EOF / SIGTERM / SIGINT. */ export async function closeBrowser() { const b = browser; const mode = browserMode; browser = undefined; browserMode = undefined; if (!b || !b.connected) { return; } if (mode === 'launched') { await b.close().catch(err => { logger?.('Failed to close browser', err); }); return; } await b.disconnect().catch(err => { logger?.('Failed to disconnect from browser', err); }); } //# sourceMappingURL=browser.js.map