UNPKG

@measey/mycoder-agent

Version:

Agent module for mycoder - an AI-powered software development assistant

287 lines 11.1 kB
import { chromium, firefox, webkit, } from '@playwright/test'; import { v4 as uuidv4 } from 'uuid'; import { BrowserError, BrowserErrorCode } from './lib/types.js'; // Status of a browser session export var SessionStatus; (function (SessionStatus) { SessionStatus["RUNNING"] = "running"; SessionStatus["COMPLETED"] = "completed"; SessionStatus["ERROR"] = "error"; SessionStatus["TERMINATED"] = "terminated"; })(SessionStatus || (SessionStatus = {})); /** * Creates, manages, and tracks browser sessions */ export class SessionTracker { ownerAgentId; logger; // Map to track session info for reporting sessions = new Map(); browser = null; defaultConfig = { headless: true, defaultTimeout: 30000, useSystemBrowsers: true, preferredType: 'chromium', }; detectedBrowsers = []; browserDetectionPromise = null; currentConfig = null; constructor(ownerAgentId, logger) { this.ownerAgentId = ownerAgentId; this.logger = logger; // Store a reference to the instance globally for cleanup // This allows the CLI to access the instance for cleanup globalThis.__BROWSER_MANAGER__ = this; // Set up cleanup handlers for graceful shutdown this.setupOnExitCleanup(); } // Update the status of a browser session updateSessionStatus(sessionId, status, metadata) { const session = this.sessions.get(sessionId); if (!session) { return false; } session.status = status; if (status === SessionStatus.COMPLETED || status === SessionStatus.ERROR || status === SessionStatus.TERMINATED) { session.endTime = new Date(); } if (metadata) { session.metadata = { ...session.metadata, ...metadata }; } return true; } // Get all browser sessions info getSessions() { return Array.from(this.sessions.values()); } // Get a specific browser session info by ID getSessionById(id) { return this.sessions.get(id); } // Filter sessions by status getSessionsByStatus(status) { return this.getSessions().filter((session) => session.status === status); } /** * Create a new browser session */ async createSession(config) { try { const sessionConfig = { ...this.defaultConfig, ...config }; // Initialize browser if needed const browser = await this.initializeBrowser(sessionConfig); // Create a new context (equivalent to incognito) const context = await browser.newContext({ viewport: null, userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', }); const page = await context.newPage(); page.setDefaultTimeout(sessionConfig.defaultTimeout ?? 30000); // Create session info const sessionId = uuidv4(); const sessionInfo = { sessionId, status: SessionStatus.RUNNING, startTime: new Date(), page, metadata: {}, }; this.sessions.set(sessionId, sessionInfo); return sessionId; } catch (error) { throw new BrowserError('Failed to create browser session', BrowserErrorCode.LAUNCH_FAILED, error); } } /** * Get a page from a session by ID */ getSessionPage(sessionId) { const sessionInfo = this.sessions.get(sessionId); if (!sessionInfo || !sessionInfo.page) { console.log('getting session, but here are the sessions', this.sessions); throw new BrowserError('Session not found', BrowserErrorCode.SESSION_ERROR); } return sessionInfo.page; } /** * Close a specific browser session */ async closeSession(sessionId) { const sessionInfo = this.sessions.get(sessionId); if (!sessionInfo || !sessionInfo.page) { console.log('closing session, but here are the sessions', this.sessions); throw new BrowserError('Session not found', BrowserErrorCode.SESSION_ERROR); } try { // In Playwright, we should close the context which will automatically close its pages await sessionInfo.page.context().close(); // Remove the page reference sessionInfo.page = undefined; // Update status this.updateSessionStatus(sessionId, SessionStatus.COMPLETED, { closedExplicitly: true, }); } catch (error) { this.updateSessionStatus(sessionId, SessionStatus.ERROR, { error: error instanceof Error ? error.message : String(error), }); throw new BrowserError('Failed to close session', BrowserErrorCode.SESSION_ERROR, error); } } /** * Cleans up all browser sessions and the browser itself */ async cleanup() { await this.closeAllSessions(); // Close the browser if it exists if (this.browser) { try { await this.browser.close(); this.browser = null; this.currentConfig = null; } catch (error) { console.error('Error closing browser:', error); } } } /** * Close all browser sessions */ async closeAllSessions() { const closePromises = Array.from(this.sessions.keys()) .filter((sessionId) => { const sessionInfo = this.sessions.get(sessionId); return sessionInfo && sessionInfo.page; }) .map((sessionId) => this.closeSession(sessionId).catch(() => { })); await Promise.all(closePromises); } /** * Sets up global cleanup handlers for all browser sessions */ /** * Lazily initializes the browser instance */ async initializeBrowser(config) { if (this.browser) { // If we already have a browser with the same config, reuse it if (this.currentConfig && this.currentConfig.headless === config.headless && this.currentConfig.executablePath === config.executablePath && this.currentConfig.preferredType === config.preferredType) { return this.browser; } // Otherwise, close the existing browser before creating a new one await this.browser.close(); this.browser = null; } // Wait for browser detection to complete if it's still running if (this.browserDetectionPromise) { await this.browserDetectionPromise; this.browserDetectionPromise = null; } // Determine if we should try to use system browsers const useSystemBrowsers = config.useSystemBrowsers !== false; // If a specific executable path is provided, use that if (config.executablePath) { console.log(`Using specified browser executable: ${config.executablePath}`); this.browser = await this.launchBrowserWithExecutablePath(config.executablePath, config.preferredType || 'chromium', config); } // Try to use a system browser if enabled and any were detected else if (useSystemBrowsers && this.detectedBrowsers.length > 0) { const preferredType = config.preferredType || 'chromium'; // First try to find a browser of the preferred type let browserInfo = this.detectedBrowsers.find((b) => b.type === preferredType); // If no preferred browser type found, use any available browser if (!browserInfo) { browserInfo = this.detectedBrowsers[0]; } if (browserInfo) { console.log(`Using system browser: ${browserInfo.name} (${browserInfo.type}) at ${browserInfo.path}`); this.browser = await this.launchBrowserWithExecutablePath(browserInfo.path, browserInfo.type, config); } } // Fall back to Playwright's bundled browser if no browser was created if (!this.browser) { console.log('Using Playwright bundled browser'); this.browser = await chromium.launch({ headless: config.headless, }); } // Store the current config this.currentConfig = { ...config }; // Set up event handlers for the browser this.browser.on('disconnected', () => { this.browser = null; this.currentConfig = null; }); return this.browser; } /** * Launch a browser with a specific executable path */ async launchBrowserWithExecutablePath(executablePath, browserType, config) { // Launch the browser using the detected executable path switch (browserType) { case 'chromium': return await chromium.launch({ headless: config.headless, executablePath: executablePath, }); case 'firefox': return await firefox.launch({ headless: config.headless, executablePath: executablePath, }); case 'webkit': return await webkit.launch({ headless: config.headless, executablePath: executablePath, }); default: throw new BrowserError(`Unsupported browser type: ${browserType}`, BrowserErrorCode.LAUNCH_FAILED); } } setupOnExitCleanup() { // Use beforeExit for async cleanup process.on('beforeExit', () => { this.cleanup().catch((err) => { console.error('Error closing browser sessions:', err); }); }); // Use exit for synchronous cleanup (as a fallback) process.on('exit', () => { // Can only do synchronous operations here if (this.browser) { try { // Attempt synchronous close - may not fully work this.browser.close(); } catch { // Ignore errors during exit } } }); // Handle SIGINT (Ctrl+C) process.on('SIGINT', () => { this.cleanup() .catch(() => { return false; }) .finally(() => { // Give a moment for cleanup to complete setTimeout(() => process.exit(0), 500); }) .catch(() => { // Additional catch for any unexpected errors in the finally block }); }); } } //# sourceMappingURL=SessionTracker.js.map