@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
JavaScript
"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