UNPKG

mycoder-agent

Version:

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

213 lines 8.9 kB
import { chromium, firefox, webkit } from '@playwright/test'; import { v4 as uuidv4 } from 'uuid'; import { BrowserDetector } from './BrowserDetector.js'; import { BrowserError, BrowserErrorCode, } from './types.js'; export class SessionManager { sessions = new Map(); defaultConfig = { headless: true, defaultTimeout: 30000, useSystemBrowsers: true, preferredType: 'chromium', }; detectedBrowsers = []; browserDetectionPromise = null; constructor() { // 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.setupGlobalCleanup(); // Start browser detection in the background this.browserDetectionPromise = this.detectBrowsers(); } /** * Detect available browsers on the system */ async detectBrowsers() { try { this.detectedBrowsers = await BrowserDetector.detectBrowsers(); console.log(`Detected ${this.detectedBrowsers.length} browsers on the system`); if (this.detectedBrowsers.length > 0) { console.log('Available browsers:'); this.detectedBrowsers.forEach((browser) => { console.log(`- ${browser.name} (${browser.type}) at ${browser.path}`); }); } } catch (error) { console.error('Failed to detect system browsers:', error); this.detectedBrowsers = []; } } async createSession(config) { try { // Wait for browser detection to complete if it's still running if (this.browserDetectionPromise) { await this.browserDetectionPromise; this.browserDetectionPromise = null; } const sessionConfig = { ...this.defaultConfig, ...config }; // Determine if we should try to use system browsers const useSystemBrowsers = sessionConfig.useSystemBrowsers !== false; // If a specific executable path is provided, use that if (sessionConfig.executablePath) { console.log(`Using specified browser executable: ${sessionConfig.executablePath}`); return this.launchWithExecutablePath(sessionConfig.executablePath, sessionConfig.preferredType || 'chromium', sessionConfig); } // Try to use a system browser if enabled and any were detected if (useSystemBrowsers && this.detectedBrowsers.length > 0) { const preferredType = sessionConfig.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}`); return this.launchWithExecutablePath(browserInfo.path, browserInfo.type, sessionConfig); } } // Fall back to Playwright's bundled browser console.log('Using Playwright bundled browser'); const browser = await chromium.launch({ headless: sessionConfig.headless, }); // 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); const session = { browser, page, id: uuidv4(), }; this.sessions.set(session.id, session); this.setupCleanup(session); return session; } catch (error) { throw new BrowserError('Failed to create browser session', BrowserErrorCode.LAUNCH_FAILED, error); } } /** * Launch a browser with a specific executable path */ async launchWithExecutablePath(executablePath, browserType, config) { let browser; // Launch the browser using the detected executable path switch (browserType) { case 'chromium': browser = await chromium.launch({ headless: config.headless, executablePath: executablePath, }); break; case 'firefox': browser = await firefox.launch({ headless: config.headless, executablePath: executablePath, }); break; case 'webkit': browser = await webkit.launch({ headless: config.headless, executablePath: executablePath, }); break; default: throw new BrowserError(`Unsupported browser type: ${browserType}`, BrowserErrorCode.LAUNCH_FAILED); } // 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(config.defaultTimeout ?? 30000); const session = { browser, page, id: uuidv4(), }; this.sessions.set(session.id, session); this.setupCleanup(session); return session; } async closeSession(sessionId) { const session = this.sessions.get(sessionId); if (!session) { throw new BrowserError('Session not found', BrowserErrorCode.SESSION_ERROR); } try { // In Playwright, we should close the context which will automatically close its pages await session.page.context().close(); await session.browser.close(); this.sessions.delete(sessionId); } catch (error) { throw new BrowserError('Failed to close session', BrowserErrorCode.SESSION_ERROR, error); } } setupCleanup(session) { // Handle browser disconnection session.browser.on('disconnected', () => { this.sessions.delete(session.id); }); // No need to add individual process handlers for each session // We'll handle all sessions in the global cleanup } /** * Sets up global cleanup handlers for all browser sessions */ setupGlobalCleanup() { // Use beforeExit for async cleanup process.on('beforeExit', () => { this.closeAllSessions().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 for (const session of this.sessions.values()) { try { // Attempt synchronous close - may not fully work session.browser.close(); // eslint-disable-next-line unused-imports/no-unused-vars } catch (e) { // Ignore errors during exit } } }); // Handle SIGINT (Ctrl+C) process.on('SIGINT', () => { // eslint-disable-next-line promise/catch-or-return this.closeAllSessions() .catch(() => { return false; }) .finally(() => { // Give a moment for cleanup to complete setTimeout(() => process.exit(0), 500); }); }); } async closeAllSessions() { const closePromises = Array.from(this.sessions.keys()).map((sessionId) => this.closeSession(sessionId).catch(() => { })); await Promise.all(closePromises); } getSession(sessionId) { const session = this.sessions.get(sessionId); if (!session) { throw new BrowserError('Session not found', BrowserErrorCode.SESSION_ERROR); } return session; } } //# sourceMappingURL=SessionManager.js.map