diff --git a/server/src/browser-management/classes/BrowserPool.ts b/server/src/browser-management/classes/BrowserPool.ts index e6dcf6b81..eb99f2df7 100644 --- a/server/src/browser-management/classes/BrowserPool.ts +++ b/server/src/browser-management/classes/BrowserPool.ts @@ -221,6 +221,12 @@ export class BrowserPool { return undefined; } + // Return undefined for failed slots + if (poolInfo.status === "failed") { + logger.log('debug', `Browser ${id} has failed status`); + return undefined; + } + return poolInfo.browser || undefined; }; @@ -607,8 +613,13 @@ export class BrowserPool { * @returns true if successful, false if slot wasn't reserved */ public upgradeBrowserSlot = (id: string, browser: RemoteBrowser): boolean => { - if (!this.pool[id] || this.pool[id].status !== "reserved") { - logger.log('warn', `Cannot upgrade browser ${id}: slot not reserved`); + if (!this.pool[id]) { + logger.log('warn', `Cannot upgrade browser ${id}: slot does not exist in pool`); + return false; + } + + if (this.pool[id].status !== "reserved") { + logger.log('warn', `Cannot upgrade browser ${id}: slot not in reserved state (current: ${this.pool[id].status})`); return false; } @@ -629,4 +640,17 @@ export class BrowserPool { this.deleteRemoteBrowser(id); } }; + + /** + * Gets the current status of a browser slot. + * + * @param id browser ID to check + * @returns the status or null if browser doesn't exist + */ + public getBrowserStatus = (id: string): "reserved" | "initializing" | "ready" | "failed" | null => { + if (!this.pool[id]) { + return null; + } + return this.pool[id].status || null; + }; } \ No newline at end of file diff --git a/server/src/browser-management/controller.ts b/server/src/browser-management/controller.ts index 3a2c31e33..1c6ecb5cc 100644 --- a/server/src/browser-management/controller.ts +++ b/server/src/browser-management/controller.ts @@ -242,7 +242,7 @@ const initializeBrowserAsync = async (id: string, userId: string) => { logger.log('warn', `No client connected to browser ${id} within timeout, proceeding with dummy socket`); resolve(null); } - }, 10000); + }, 15000); }); namespace.on('error', (error: any) => { @@ -272,21 +272,25 @@ const initializeBrowserAsync = async (id: string, userId: string) => { browserSession = new RemoteBrowser(dummySocket, userId, id); } + logger.log('debug', `Starting browser initialization for ${id}`); await browserSession.initialize(userId); + logger.log('debug', `Browser initialization completed for ${id}`); const upgraded = browserPool.upgradeBrowserSlot(id, browserSession); if (!upgraded) { throw new Error('Failed to upgrade reserved browser slot'); } + await new Promise(resolve => setTimeout(resolve, 500)); + if (socket) { socket.emit('ready-for-run'); } else { setTimeout(async () => { try { - logger.log('info', `Starting execution for browser ${id} with dummy socket`); + logger.log('info', `Browser ${id} with dummy socket is ready for execution`); } catch (error: any) { - logger.log('error', `Error executing run for browser ${id}: ${error.message}`); + logger.log('error', `Error with dummy socket browser ${id}: ${error.message}`); } }, 100); } @@ -299,10 +303,12 @@ const initializeBrowserAsync = async (id: string, userId: string) => { if (socket) { socket.emit('error', { message: error.message }); } + throw error; } } catch (error: any) { logger.log('error', `Error setting up browser ${id}: ${error.message}`); browserPool.failBrowserSlot(id); + throw error; } }; diff --git a/server/src/pgboss-worker.ts b/server/src/pgboss-worker.ts index 2c3ae1ac1..c8baa9c0d 100644 --- a/server/src/pgboss-worker.ts +++ b/server/src/pgboss-worker.ts @@ -215,7 +215,8 @@ async function triggerIntegrationUpdates(runId: string, robotMetaId: string): Pr * Modified processRunExecution function - only add browser reset */ async function processRunExecution(job: Job) { - const BROWSER_INIT_TIMEOUT = 30000; + const BROWSER_INIT_TIMEOUT = 60000; + const BROWSER_PAGE_TIMEOUT = 45000; const data = job.data; logger.log('info', `Processing run execution job for runId: ${data.runId}, browserId: ${data.browserId}`); @@ -244,15 +245,28 @@ async function processRunExecution(job: Job) { let browser = browserPool.getRemoteBrowser(browserId); const browserWaitStart = Date.now(); + let lastLogTime = 0; while (!browser && (Date.now() - browserWaitStart) < BROWSER_INIT_TIMEOUT) { - logger.log('debug', `Browser ${browserId} not ready yet, waiting...`); - await new Promise(resolve => setTimeout(resolve, 1000)); + const currentTime = Date.now(); + + const browserStatus = browserPool.getBrowserStatus(browserId); + if (browserStatus === null) { + throw new Error(`Browser slot ${browserId} does not exist in pool`); + } + + if (currentTime - lastLogTime > 10000) { + logger.log('info', `Browser ${browserId} not ready yet (status: ${browserStatus}), waiting... (${Math.round((currentTime - browserWaitStart) / 1000)}s elapsed)`); + lastLogTime = currentTime; + } + + await new Promise(resolve => setTimeout(resolve, 2000)); browser = browserPool.getRemoteBrowser(browserId); } if (!browser) { - throw new Error(`Browser ${browserId} not found in pool after timeout`); + const finalStatus = browserPool.getBrowserStatus(browserId); + throw new Error(`Browser ${browserId} not found in pool after ${BROWSER_INIT_TIMEOUT/1000}s timeout (final status: ${finalStatus})`); } logger.log('info', `Browser ${browserId} found and ready for execution`); @@ -273,14 +287,22 @@ async function processRunExecution(job: Job) { let currentPage = browser.getCurrentPage(); const pageWaitStart = Date.now(); - while (!currentPage && (Date.now() - pageWaitStart) < 30000) { - logger.log('debug', `Page not ready for browser ${browserId}, waiting...`); + let lastPageLogTime = 0; + + while (!currentPage && (Date.now() - pageWaitStart) < BROWSER_PAGE_TIMEOUT) { + const currentTime = Date.now(); + + if (currentTime - lastPageLogTime > 5000) { + logger.log('info', `Page not ready for browser ${browserId}, waiting... (${Math.round((currentTime - pageWaitStart) / 1000)}s elapsed)`); + lastPageLogTime = currentTime; + } + await new Promise(resolve => setTimeout(resolve, 1000)); currentPage = browser.getCurrentPage(); } if (!currentPage) { - throw new Error(`No current page available for browser ${browserId} after timeout`); + throw new Error(`No current page available for browser ${browserId} after ${BROWSER_PAGE_TIMEOUT/1000}s timeout`); } logger.log('info', `Starting workflow execution for run ${data.runId}`); @@ -775,6 +797,10 @@ async function registerRunExecutionWorker() { }; await checkForNewUserQueues(); + + setInterval(async () => { + await checkForNewUserQueues(); + }, 10000); logger.log('info', 'Run execution worker registered successfully'); } catch (error: unknown) { @@ -821,6 +847,10 @@ async function registerAbortRunWorker() { }; await checkForNewAbortQueues(); + + setInterval(async () => { + await checkForNewAbortQueues(); + }, 10000); logger.log('info', 'Abort run worker registration system initialized'); } catch (error: unknown) { diff --git a/src/helpers/clientSelectorGenerator.ts b/src/helpers/clientSelectorGenerator.ts index cb782d444..9ec690b77 100644 --- a/src/helpers/clientSelectorGenerator.ts +++ b/src/helpers/clientSelectorGenerator.ts @@ -2494,70 +2494,38 @@ class ClientSelectorGenerator { }; private getAllDescendantsIncludingShadow( - parentElement: HTMLElement + parentElement: HTMLElement, + maxDepth: number = 20 ): HTMLElement[] { const allDescendants: HTMLElement[] = []; const visited = new Set(); - const shadowRootsSeen = new Set(); - - const traverseShadowRoot = (shadowRoot: ShadowRoot, depth: number = 0) => { - if (depth > 10) return; - - try { - const shadowElements = Array.from( - shadowRoot.querySelectorAll("*") - ) as HTMLElement[]; - - shadowElements.forEach((shadowElement) => { - if (!visited.has(shadowElement)) { - visited.add(shadowElement); - allDescendants.push(shadowElement); - if ( - shadowElement.shadowRoot && - !shadowRootsSeen.has(shadowElement.shadowRoot) - ) { - shadowRootsSeen.add(shadowElement.shadowRoot); - traverseShadowRoot(shadowElement.shadowRoot, depth + 1); - } - } - }); + const traverse = (element: HTMLElement, currentDepth: number) => { + if (currentDepth >= maxDepth || visited.has(element)) { + return; + } + visited.add(element); - Array.from(shadowRoot.children).forEach((child) => { - const htmlChild = child as HTMLElement; - if ( - htmlChild.shadowRoot && - !shadowRootsSeen.has(htmlChild.shadowRoot) - ) { - shadowRootsSeen.add(htmlChild.shadowRoot); - traverseShadowRoot(htmlChild.shadowRoot, depth + 1); - } - }); - } catch (error) { - console.warn(`Error traversing shadow root:`, error); + if (element !== parentElement) { + allDescendants.push(element); } - }; - const regularDescendants = Array.from( - parentElement.querySelectorAll("*") - ) as HTMLElement[]; - regularDescendants.forEach((descendant) => { - if (!visited.has(descendant)) { - visited.add(descendant); - allDescendants.push(descendant); + // Traverse light DOM children + const children = Array.from(element.children) as HTMLElement[]; + for (const child of children) { + traverse(child, currentDepth + 1); } - }); - const elementsWithShadow = [parentElement, ...regularDescendants].filter( - (el) => el.shadowRoot - ); - elementsWithShadow.forEach((element) => { - if (!shadowRootsSeen.has(element.shadowRoot!)) { - shadowRootsSeen.add(element.shadowRoot!); - traverseShadowRoot(element.shadowRoot!, 0); + // Traverse shadow DOM if it exists + if (element.shadowRoot) { + const shadowChildren = Array.from(element.shadowRoot.children) as HTMLElement[]; + for (const shadowChild of shadowChildren) { + traverse(shadowChild, currentDepth + 1); + } } - }); + }; + traverse(parentElement, 0); return allDescendants; } @@ -2577,6 +2545,8 @@ class ClientSelectorGenerator { if (processedElements.has(descendant)) return; processedElements.add(descendant); + if (!this.isMeaningfulElement(descendant)) return; + const absolutePath = this.buildOptimizedAbsoluteXPath( descendant, listSelector, @@ -2766,16 +2736,20 @@ class ClientSelectorGenerator { rootElement: HTMLElement, otherListElements: HTMLElement[] = [] ): string | null { - if (!this.elementContains(rootElement, targetElement) || targetElement === rootElement) { + if ( + !this.elementContains(rootElement, targetElement) || + targetElement === rootElement + ) { return null; } const pathParts: string[] = []; let current: HTMLElement | null = targetElement; + let pathDepth = 0; + const MAX_PATH_DEPTH = 20; // Build path from target up to root - while (current && current !== rootElement) { - // Calculate conflicts for each element in the path + while (current && current !== rootElement && pathDepth < MAX_PATH_DEPTH) { const classes = this.getCommonClassesAcrossLists( current, otherListElements @@ -2806,11 +2780,15 @@ class ClientSelectorGenerator { pathParts.unshift(pathPart); } - // Move to parent (either regular parent or shadow host) - current = current.parentElement || + current = + current.parentElement || ((current.getRootNode() as ShadowRoot).host as HTMLElement | null); + + pathDepth++; + } - if (!current) break; + if (current !== rootElement) { + return null; } return pathParts.length > 0 ? "/" + pathParts.join("/") : null;