UNPKG

@grasplabs/grasp

Version:

TypeScript SDK for browser automation and secure command execution in highly available and scalable cloud browser environments

268 lines 10.7 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.BrowserService = void 0; const sandbox_service_1 = require("./sandbox.service"); const logger_1 = require("../utils/logger"); const promises_1 = require("timers/promises"); const promises_2 = __importDefault(require("node:fs/promises")); const node_fs_1 = require("node:fs"); const node_path_1 = __importDefault(require("node:path")); /** * Browser service for managing Chromium browser with CDP access * Uses Grasp sandbox to run browser and expose CDP endpoint */ class BrowserService { /** * Gets or creates a default logger instance * @returns Logger instance */ getDefaultLogger() { try { return (0, logger_1.getLogger)().child('BrowserService'); } catch (error) { // If logger is not initialized, create a default one const defaultLogger = new logger_1.Logger({ level: this.sandboxService.isDebug ? 'debug' : 'info', console: true, }); return defaultLogger.child('BrowserService'); } } constructor(sandboxConfig, browserConfig = {}) { this.cdpConnection = null; this.browserProcess = null; this.sandboxService = new sandbox_service_1.SandboxService(sandboxConfig); this.config = { headless: true, launchTimeout: 30000, args: [], envs: {}, ...browserConfig, }; // 需要把 APIKey 存进去记 log this.config.envs.APIKEY = sandboxConfig.key; this.logger = this.getDefaultLogger(); } /** * Initialize the Grasp sandbox * @returns Promise that resolves when sandbox is ready */ async initialize(type) { this.logger.info('Initializing Browser service'); await this.sandboxService.createSandbox(`grasp-run-${type}-v2`, { BROWSER_ARGS: JSON.stringify(this.config.args), LAUNCH_TIMEOUT: this.config.launchTimeout.toString(), SANBOX_TIMEOUT: this.sandboxService.timeout.toString(), HEADLESS: this.config.headless.toString(), NODE_ENV: 'production', // SANDBOX_ID: this.id, WORKSPACE: this.sandboxService.workspace, PLAYWRIGHT_BROWSERS_PATH: '0', BROWSER_TYPE: type, ...this.config.envs, }); this.logger.info('Grasp sandbox initialized successfully'); } async connect(sandboxId) { this.logger.info('Initializing Browser service'); await this.sandboxService.connectSandbox(sandboxId); this.logger.info('Browser service initialized successfully'); const cdpConnection = await this.sandboxService.sandbox.files.read('/home/user/.grasp-cdp.json'); return JSON.parse(cdpConnection); } /** * Launch Chromium browser with CDP server * @returns Promise with CDP connection information * @throws {Error} If browser launch fails */ async launchBrowser() { if (!this.sandboxService) { throw new Error('Grasp service not initialized. Call initialize() first.'); } try { this.logger.info('Launching Chromium browser with CDP', { port: 9222, headless: this.config.headless, }); // Generate the Playwright script to launch Chromium with CDP let playwrightScript = '/home/user/http-proxy.js'; const localPath = node_path_1.default.resolve(__dirname, `../sandbox/http-proxy.js`); if ((0, node_fs_1.existsSync)(localPath)) { this.logger.info(`🗂️ Load local file: ${localPath}`); playwrightScript = await promises_2.default.readFile(node_path_1.default.resolve(__dirname, `../sandbox/http-proxy.js`), 'utf8'); } // console.log(playwrightScript); // Run the Playwright script in background to keep browser alive this.browserProcess = await this.sandboxService.runScript(playwrightScript, { type: 'cjs', background: true, nohup: !this.sandboxService.isDebug, // Keep running even if parent process exits timeoutMs: 0, preCommand: '', // 废弃掉了,因为不需要在容器内部运行 xvfb }); // Set up event listeners for browser process this.setupBrowserProcessListeners(); // Wait for browser to start and CDP to be available const result = await this.waitForCDPReady(); // Create CDP connection info this.cdpConnection = { id: this.sandboxService.id, ...result, }; await this.sandboxService.sandbox.files.write('/home/user/.grasp-cdp.json', JSON.stringify(this.cdpConnection)); this.logger.info('Chromium browser launched successfully', { cdpPort: 9222, wsUrl: this.cdpConnection?.wsUrl, }); const timer = setInterval(async () => { try { const res = await fetch(`${this.cdpConnection?.httpUrl}/json/version`, { method: 'HEAD', }); if (!res.ok) { clearInterval(timer); this.logger.info('Browser process exited with code 0'); this.cleanup(); } } catch (ex) { clearInterval(timer); this.logger.info('Browser process exited with code 1'); this.cleanup(); } }, 5000); return this.cdpConnection; } catch (error) { this.logger.error('Failed to launch Chromium browser', error); throw new Error(`Failed to launch browser: ${error}`); } } /** * Set up event listeners for browser process */ setupBrowserProcessListeners() { if (!this.browserProcess) return; this.browserProcess.on('stdout', (data) => { this.logger.debug('Browser stdout:', data); }); this.browserProcess.on('stderr', (data) => { this.logger.debug('Browser stderr:', data); }); // this.browserProcess.on('exit', (exitCode: number) => { // this.logger.info('Browser process exited', { exitCode }); // this.cdpConnection = null; // this.browserProcess = null; // this.sandboxService.destroy(); // }); this.browserProcess.on('error', (error) => { this.logger.error('Browser process error:', error); }); } /** * Wait for CDP server to be ready * @returns Promise that resolves when CDP is available */ async waitForCDPReady() { const delayMs = 100; const maxAttempts = this.config.launchTimeout / delayMs; const host = this.sandboxService.getSandboxHost(9223); for (let attempt = 1; attempt <= maxAttempts; attempt++) { try { this.logger.debug(`Checking CDP availability (attempt ${attempt}/${maxAttempts})`); // Check if CDP endpoint is responding const response = await fetch(`https://${host}/json/version`); if (response.ok) { const responseText = await response.text(); if (responseText.includes('Browser')) { const metaData = JSON.parse(responseText); metaData.wsUrl = metaData.webSocketDebuggerUrl .replace(/^ws:\/\//, 'wss://') .replace(`localhost:9222`, host); metaData.httpUrl = `https://${host}`; delete metaData.webSocketDebuggerUrl; this.logger.info('CDP server is ready', metaData); return metaData; } } } catch (error) { this.logger.debug(`CDP check failed (attempt ${attempt}):`, error); } if (attempt < maxAttempts) { await (0, promises_1.setTimeout)(delayMs); } } throw new Error('CDP server failed to become ready within timeout'); } /** * Get current CDP connection information * @returns CDP connection info or null if not connected */ getCDPConnection() { return this.cdpConnection; } /** * Check if browser is running * @returns True if browser process is active */ isBrowserRunning() { return this.browserProcess !== null && this.cdpConnection !== null; } /** * Stop the browser and cleanup resources * @returns Promise that resolves when cleanup is complete */ async stopBrowser() { if (!this.browserProcess) { this.logger.info('No browser process to stop'); return; } try { this.logger.info('Stopping Chromium browser'); // Kill the browser process if (this.browserProcess) { // this.browserProcess.removeAllListeners(); await this.browserProcess.kill(); } this.browserProcess = null; this.cdpConnection = null; this.logger.info('Chromium browser stopped successfully'); } catch (error) { this.logger.debug('Error stopping browser:', error); // throw error; } } /** * Cleanup all resources including Grasp sandbox * @returns Promise that resolves when cleanup is complete */ async cleanup() { this.logger.info('Cleaning up Playwright service'); // Stop browser first if (this.isBrowserRunning()) { await this.stopBrowser(); } // Cleanup Grasp sandbox await this.sandboxService.destroy(); this.logger.info('Playwright service cleanup completed'); } get id() { return this.sandboxService.id; } /** * Get the underlying Grasp service instance * @returns Grasp service instance */ getSandbox() { return this.sandboxService; } } exports.BrowserService = BrowserService; //# sourceMappingURL=browser.service.js.map