import { tradingViewAutomation, TradingViewAutomation, TradingViewCredentials, NavigationOptions } from './tradingview-automation' import fs from 'fs/promises' import path from 'path' import puppeteer from 'puppeteer' import { Browser, Page } from 'puppeteer' import { progressTracker, ProgressStep } from './progress-tracker' export interface ScreenshotConfig { symbol: string timeframe: string layouts?: string[] sessionId?: string analyze?: boolean } // Layout URL mappings for direct navigation const LAYOUT_URLS = { 'ai': 'Z1TzpUrf', 'diy': 'vWVvjLhP', 'Diy module': 'vWVvjLhP' // Alternative mapping for 'Diy module' } export class EnhancedScreenshotService { private static readonly OPERATION_TIMEOUT = 120000 // 2 minutes timeout for Docker private static aiSession: TradingViewAutomation | null = null private static diySession: TradingViewAutomation | null = null // Track active sessions for guaranteed cleanup private activeSessions: Set = new Set() async captureWithLogin(config: ScreenshotConfig): Promise { console.log('πŸš€ Enhanced Screenshot Service - Docker Environment (Dual Session with Robust Cleanup)') console.log('πŸ“‹ Config:', config) const screenshotFiles: string[] = [] const { sessionId } = config console.log('πŸ” Enhanced Screenshot Service received sessionId:', sessionId) // Cleanup tracker for all sessions created in this operation const sessionCleanupTasks: Array<() => Promise> = [] try { // Ensure screenshots directory exists const screenshotsDir = path.join(process.cwd(), 'screenshots') await fs.mkdir(screenshotsDir, { recursive: true }) const timestamp = Date.now() const layoutsToCapture = config.layouts || ['ai', 'diy'] console.log(`\nπŸ”„ Starting parallel capture of ${layoutsToCapture.length} layouts...`) if (sessionId) { progressTracker.updateStep(sessionId, 'init', 'completed', `Started ${layoutsToCapture.length} browser sessions`) progressTracker.updateStep(sessionId, 'auth', 'active', 'Authenticating with TradingView...') } // Create parallel session promises with proper cleanup tracking const sessionPromises = layoutsToCapture.map(async (layout, index) => { const layoutKey = layout.toLowerCase() let layoutSession: TradingViewAutomation | null = null try { console.log(`\nπŸ”§ Initializing ${layout.toUpperCase()} session (parallel)...`) // Get layout URL with better error handling let layoutUrl = LAYOUT_URLS[layoutKey as keyof typeof LAYOUT_URLS] // Try alternative key for 'Diy module' if (!layoutUrl && layout === 'Diy module') { layoutUrl = LAYOUT_URLS['diy'] } if (!layoutUrl) { throw new Error(`No URL mapping found for layout: ${layout} (tried keys: ${layoutKey}, diy)`) } console.log(`πŸ—ΊοΈ ${layout.toUpperCase()}: Using layout URL ${layoutUrl}`) // Create a dedicated automation instance for this layout layoutSession = new TradingViewAutomation() // CRITICAL: Add to active sessions for guaranteed cleanup this.activeSessions.add(layoutSession) // Add cleanup task for this session sessionCleanupTasks.push(async () => { if (layoutSession) { console.log(`🧹 Cleaning up ${layout} session...`) try { await layoutSession.forceCleanup() this.activeSessions.delete(layoutSession) console.log(`βœ… ${layout} session cleaned up`) } catch (cleanupError) { console.error(`❌ Error cleaning up ${layout} session:`, cleanupError) } } }) console.log(`🐳 Starting ${layout} browser session...`) await layoutSession.init() // Check login status and login if needed const isLoggedIn = await layoutSession.checkLoginStatus() if (!isLoggedIn) { console.log(`πŸ” Logging in to ${layout} session...`) if (sessionId && index === 0) { progressTracker.updateStep(sessionId, 'auth', 'active', `Logging into ${layout} session...`) } const credentials: TradingViewCredentials = { email: process.env.TRADINGVIEW_EMAIL!, password: process.env.TRADINGVIEW_PASSWORD! } const loginSuccess = await layoutSession.login(credentials) if (!loginSuccess) { throw new Error(`Failed to login to ${layout} session`) } } else { console.log(`βœ… ${layout.toUpperCase()}: Already logged in`) } // Get page from the session const page = (layoutSession as any).page if (!page) { throw new Error(`Failed to get page for ${layout} session`) } // Navigate directly to the layout URL with retries and progressive timeout strategy let navigationSuccess = false for (let attempt = 1; attempt <= 3; attempt++) { try { console.log(`πŸ”„ ${layout.toUpperCase()}: Navigation attempt ${attempt}/3`) // Progressive waiting strategy: first try domcontentloaded, then networkidle if that fails const waitUntilStrategy = attempt === 1 ? 'domcontentloaded' : 'networkidle0' const timeoutDuration = attempt === 1 ? 30000 : (60000 + (attempt - 1) * 30000) const directUrl = `https://www.tradingview.com/chart/?symbol=${config.symbol}&interval=${config.timeframe}&layout=${layoutUrl}` console.log(`πŸ”— ${layout.toUpperCase()}: Direct navigation to: ${directUrl}`) console.log(`πŸ“‹ ${layout.toUpperCase()}: Using waitUntil: ${waitUntilStrategy}, timeout: ${timeoutDuration}ms`) await page.goto(directUrl, { waitUntil: waitUntilStrategy, timeout: timeoutDuration }) // If we used domcontentloaded, wait a bit more for dynamic content if (waitUntilStrategy === 'domcontentloaded') { console.log(`⏳ ${layout.toUpperCase()}: Waiting additional 5s for dynamic content...`) await new Promise(resolve => setTimeout(resolve, 5000)) } navigationSuccess = true break } catch (navError: any) { console.warn(`⚠️ ${layout.toUpperCase()}: Navigation attempt ${attempt} failed:`, navError?.message || navError) if (attempt === 3) { throw new Error(`Failed to navigate to ${layout} layout after 3 attempts: ${navError?.message || navError}`) } // Progressive backoff const waitTime = 2000 * attempt console.log(`⏳ ${layout.toUpperCase()}: Waiting ${waitTime}ms before retry...`) await new Promise(resolve => setTimeout(resolve, waitTime)) } } if (!navigationSuccess) { throw new Error(`Navigation to ${layout} layout failed after all attempts`) } // Wait for chart to load console.log(`⏳ ${layout.toUpperCase()}: Waiting for chart to load...`) if (sessionId && index === 0) { progressTracker.updateStep(sessionId, 'auth', 'completed', 'Authentication successful') progressTracker.updateStep(sessionId, 'loading', 'active', 'Loading chart data...') } try { // Wait for canvas elements (charts) await page.waitForSelector('canvas', { timeout: 30000 }) console.log(`βœ… ${layout.toUpperCase()}: Chart canvas found`) } catch (canvasError) { console.warn(`⚠️ ${layout.toUpperCase()}: Chart canvas not found, continuing anyway...`) } // Check if it's the primary session for loading progress updates if (sessionId && index === 0) { // Wait for chart data to fully load console.log(`⏳ ${layout.toUpperCase()}: Ensuring chart data is loaded...`) await new Promise(resolve => setTimeout(resolve, 8000)) } else { // Additional stabilization wait after chart loads console.log(`⏳ ${layout.toUpperCase()}: Chart stabilization (3s)...`) await new Promise(resolve => setTimeout(resolve, 3000)) } // Update loading progress when first session completes loading if (sessionId && index === 0) { progressTracker.updateStep(sessionId, 'loading', 'completed', 'Chart data loaded successfully') progressTracker.updateStep(sessionId, 'capture', 'active', 'Capturing screenshots...') } // Take screenshot with better error handling const filename = `${config.symbol}_${config.timeframe}_${layout}_${timestamp}.png` console.log(`πŸ“Έ Taking ${layout} screenshot: ${filename}`) let screenshotFile = null try { screenshotFile = await layoutSession.takeScreenshot({ filename }) if (screenshotFile) { console.log(`βœ… ${layout.toUpperCase()}: Screenshot saved: ${screenshotFile}`) return screenshotFile } else { console.error(`❌ ${layout.toUpperCase()}: Screenshot failed - no file returned`) return null } } catch (screenshotError: any) { console.error(`❌ ${layout.toUpperCase()}: Screenshot capture failed:`, screenshotError?.message || screenshotError) return null } } catch (error: any) { console.error(`❌ Error capturing ${layout} layout:`, error?.message || error) console.error(`❌ Full ${layout} error details:`, error) console.error(`❌ ${layout} error stack:`, error?.stack) // Attempt to capture browser state for debugging try { const page = (layoutSession as any)?.page if (page) { const url = await page.url() const title = await page.title() console.error(`❌ ${layout} browser state - URL: ${url}, Title: ${title}`) // Try to get page content for debugging const bodyText = await page.evaluate(() => document.body.innerText.slice(0, 200)) console.error(`❌ ${layout} page content preview:`, bodyText) } } catch (debugError: any) { console.error(`❌ Failed to capture ${layout} browser state:`, debugError?.message || debugError) } throw error // Re-throw to be caught by Promise.allSettled } }) // Execute all sessions in parallel and wait for completion console.log(`\n⚑ Executing ${layoutsToCapture.length} sessions in parallel...`) const results = await Promise.allSettled(sessionPromises) // Collect successful screenshots results.forEach((result, index) => { const layout = layoutsToCapture[index] if (result.status === 'fulfilled' && result.value) { screenshotFiles.push(result.value) console.log(`βœ… ${layout} parallel session completed successfully`) } else { console.error(`❌ ${layout} parallel session failed:`, result.status === 'rejected' ? result.reason : 'Unknown error') } }) if (sessionId) { progressTracker.updateStep(sessionId, 'capture', 'completed', `Captured ${screenshotFiles.length}/${layoutsToCapture.length} screenshots`) } console.log(`\n🎯 Parallel capture completed: ${screenshotFiles.length}/${layoutsToCapture.length} screenshots`) return screenshotFiles } catch (error) { console.error('Enhanced parallel screenshot capture failed:', error) if (sessionId) { // Mark the current active step as error const progress = progressTracker.getProgress(sessionId) if (progress) { const activeStep = progress.steps.find(step => step.status === 'active') if (activeStep) { progressTracker.updateStep(sessionId, activeStep.id, 'error', error instanceof Error ? error.message : 'Unknown error') } } } throw error } finally { // CRITICAL: Guaranteed cleanup of all sessions created in this operation console.log(`🧹 FINALLY BLOCK: Cleaning up ${sessionCleanupTasks.length} sessions...`) try { // Execute all cleanup tasks in parallel with timeout const cleanupPromises = sessionCleanupTasks.map(task => Promise.race([ task(), new Promise((_, reject) => setTimeout(() => reject(new Error('Cleanup timeout')), 10000) ) ]).catch(error => { console.error('Session cleanup error:', error) }) ) await Promise.allSettled(cleanupPromises) console.log('βœ… FINALLY BLOCK: All session cleanups completed') // Additional aggressive cleanup to ensure no processes remain await this.forceKillRemainingProcesses() } catch (cleanupError) { console.error('❌ FINALLY BLOCK: Error during session cleanup:', cleanupError) } } } // Enhanced cleanup with force process termination private async forceKillRemainingProcesses(): Promise { try { const { exec } = require('child_process') const { promisify } = require('util') const execAsync = promisify(exec) console.log('πŸ”§ Force killing any remaining browser processes...') // Multiple kill strategies for thorough cleanup const killCommands = [ 'pkill -f "chromium.*--remote-debugging-port" 2>/dev/null || true', 'pkill -f "chromium.*--user-data-dir" 2>/dev/null || true', 'pkill -f "/usr/lib/chromium/chromium" 2>/dev/null || true', 'pkill -f "chrome.*--remote-debugging-port" 2>/dev/null || true', // Clean up any zombie processes 'pkill -9 -f "chromium.*defunct" 2>/dev/null || true' ] for (const command of killCommands) { try { await execAsync(command) } catch (killError) { // Ignore errors from kill commands as processes might not exist } } console.log('βœ… Force process cleanup completed') } catch (error) { console.error('Error in force process cleanup:', error) } } async cleanup(): Promise { console.log('🧹 Cleaning up parallel browser sessions...') const cleanupPromises = [] // Cleanup all active sessions tracked in this instance for (const session of this.activeSessions) { cleanupPromises.push( session.forceCleanup().catch((err: any) => console.error('Active session cleanup error:', err) ) ) } this.activeSessions.clear() // Cleanup dedicated AI session if exists if (EnhancedScreenshotService.aiSession) { console.log('πŸ”§ Cleaning up AI session...') cleanupPromises.push( EnhancedScreenshotService.aiSession.forceCleanup().catch((err: any) => console.error('AI session cleanup error:', err) ) ) EnhancedScreenshotService.aiSession = null } // Cleanup dedicated DIY session if exists if (EnhancedScreenshotService.diySession) { console.log('πŸ”§ Cleaning up DIY session...') cleanupPromises.push( EnhancedScreenshotService.diySession.forceCleanup().catch((err: any) => console.error('DIY session cleanup error:', err) ) ) EnhancedScreenshotService.diySession = null } // Also cleanup the main singleton session cleanupPromises.push( tradingViewAutomation.forceCleanup().catch((err: any) => console.error('Main session cleanup error:', err) ) ) // Wait for all cleanup operations to complete await Promise.allSettled(cleanupPromises) // Give browsers time to fully close await new Promise(resolve => setTimeout(resolve, 3000)) console.log('βœ… All parallel browser sessions cleaned up') // Force kill any remaining browser processes await this.forceKillRemainingProcesses() } // Keep existing methods for compatibility async captureQuick(symbol: string, timeframe: string, credentials?: TradingViewCredentials): Promise { const automation = tradingViewAutomation try { await automation.init() if (credentials) { const loginSuccess = await automation.login(credentials) if (!loginSuccess) { throw new Error('Failed to login to TradingView') } } // Navigate to symbol and timeframe await automation.navigateToSymbol(symbol, timeframe) const filename = `${symbol}_${timeframe}_${Date.now()}.png` return await automation.takeScreenshot({ filename }) } catch (error) { console.error('Quick capture failed:', error) throw error } finally { // Cleanup after quick capture try { await automation.forceCleanup() } catch (cleanupError) { console.error('Error in quick capture cleanup:', cleanupError) } } } async capture(symbol: string, filename: string): Promise { let browser: Browser | null = null try { console.log(`Starting enhanced screenshot capture for ${symbol}...`); // Launch browser browser = await puppeteer.launch({ headless: true, executablePath: process.env.PUPPETEER_EXECUTABLE_PATH || undefined, args: [ '--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-accelerated-2d-canvas', '--no-first-run', '--no-zygote', '--disable-gpu' ] }); const page = await browser.newPage(); await page.setViewport({ width: 1920, height: 1080 }); // Navigate to TradingView chart await page.goto('https://www.tradingview.com/chart/', { waitUntil: 'networkidle0', timeout: 30000 }); // Wait for chart to load await page.waitForSelector('canvas', { timeout: 30000 }); await new Promise(resolve => setTimeout(resolve, 3000)); // Ensure screenshots directory exists const screenshotsDir = path.join(process.cwd(), 'screenshots') await fs.mkdir(screenshotsDir, { recursive: true }) // Take screenshot const screenshotPath = path.join(screenshotsDir, filename); await page.screenshot({ path: screenshotPath as `${string}.png`, type: 'png', fullPage: false }); console.log(`Screenshot saved to: ${screenshotPath}`); return [screenshotPath]; } catch (error) { console.error('Error capturing screenshot:', error); throw error; } finally { // CRITICAL: Always cleanup browser in finally block if (browser) { try { await browser.close(); console.log('βœ… Browser closed in finally block'); } catch (cleanupError) { console.error('Error closing browser in finally block:', cleanupError); } } } } async healthCheck(): Promise<{ status: 'healthy' | 'error', message?: string }> { let browser: Browser | null = null try { // Simple health check - try to launch a browser instance browser = await puppeteer.launch({ headless: true, executablePath: process.env.PUPPETEER_EXECUTABLE_PATH || undefined, args: [ '--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', ] }); return { status: 'healthy' }; } catch (error) { return { status: 'error', message: error instanceof Error ? error.message : 'Unknown error' }; } finally { // CRITICAL: Always cleanup browser in finally block if (browser) { try { await browser.close(); } catch (cleanupError) { console.error('Error closing browser in health check:', cleanupError); } } } } } export const enhancedScreenshotService = new EnhancedScreenshotService()