Merge pull request #734 from getmaxun/browser-ready
feat: browser checks for run execution
This commit is contained in:
@@ -221,6 +221,12 @@ export class BrowserPool {
|
|||||||
return undefined;
|
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;
|
return poolInfo.browser || undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -607,8 +613,13 @@ export class BrowserPool {
|
|||||||
* @returns true if successful, false if slot wasn't reserved
|
* @returns true if successful, false if slot wasn't reserved
|
||||||
*/
|
*/
|
||||||
public upgradeBrowserSlot = (id: string, browser: RemoteBrowser): boolean => {
|
public upgradeBrowserSlot = (id: string, browser: RemoteBrowser): boolean => {
|
||||||
if (!this.pool[id] || this.pool[id].status !== "reserved") {
|
if (!this.pool[id]) {
|
||||||
logger.log('warn', `Cannot upgrade browser ${id}: slot not reserved`);
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -629,4 +640,17 @@ export class BrowserPool {
|
|||||||
this.deleteRemoteBrowser(id);
|
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;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
@@ -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`);
|
logger.log('warn', `No client connected to browser ${id} within timeout, proceeding with dummy socket`);
|
||||||
resolve(null);
|
resolve(null);
|
||||||
}
|
}
|
||||||
}, 10000);
|
}, 15000);
|
||||||
});
|
});
|
||||||
|
|
||||||
namespace.on('error', (error: any) => {
|
namespace.on('error', (error: any) => {
|
||||||
@@ -272,21 +272,25 @@ const initializeBrowserAsync = async (id: string, userId: string) => {
|
|||||||
browserSession = new RemoteBrowser(dummySocket, userId, id);
|
browserSession = new RemoteBrowser(dummySocket, userId, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.log('debug', `Starting browser initialization for ${id}`);
|
||||||
await browserSession.initialize(userId);
|
await browserSession.initialize(userId);
|
||||||
|
logger.log('debug', `Browser initialization completed for ${id}`);
|
||||||
|
|
||||||
const upgraded = browserPool.upgradeBrowserSlot(id, browserSession);
|
const upgraded = browserPool.upgradeBrowserSlot(id, browserSession);
|
||||||
if (!upgraded) {
|
if (!upgraded) {
|
||||||
throw new Error('Failed to upgrade reserved browser slot');
|
throw new Error('Failed to upgrade reserved browser slot');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
|
|
||||||
if (socket) {
|
if (socket) {
|
||||||
socket.emit('ready-for-run');
|
socket.emit('ready-for-run');
|
||||||
} else {
|
} else {
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
try {
|
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) {
|
} 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);
|
}, 100);
|
||||||
}
|
}
|
||||||
@@ -299,10 +303,12 @@ const initializeBrowserAsync = async (id: string, userId: string) => {
|
|||||||
if (socket) {
|
if (socket) {
|
||||||
socket.emit('error', { message: error.message });
|
socket.emit('error', { message: error.message });
|
||||||
}
|
}
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
logger.log('error', `Error setting up browser ${id}: ${error.message}`);
|
logger.log('error', `Error setting up browser ${id}: ${error.message}`);
|
||||||
browserPool.failBrowserSlot(id);
|
browserPool.failBrowserSlot(id);
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -215,7 +215,8 @@ async function triggerIntegrationUpdates(runId: string, robotMetaId: string): Pr
|
|||||||
* Modified processRunExecution function - only add browser reset
|
* Modified processRunExecution function - only add browser reset
|
||||||
*/
|
*/
|
||||||
async function processRunExecution(job: Job<ExecuteRunData>) {
|
async function processRunExecution(job: Job<ExecuteRunData>) {
|
||||||
const BROWSER_INIT_TIMEOUT = 30000;
|
const BROWSER_INIT_TIMEOUT = 60000;
|
||||||
|
const BROWSER_PAGE_TIMEOUT = 45000;
|
||||||
|
|
||||||
const data = job.data;
|
const data = job.data;
|
||||||
logger.log('info', `Processing run execution job for runId: ${data.runId}, browserId: ${data.browserId}`);
|
logger.log('info', `Processing run execution job for runId: ${data.runId}, browserId: ${data.browserId}`);
|
||||||
@@ -244,15 +245,28 @@ async function processRunExecution(job: Job<ExecuteRunData>) {
|
|||||||
|
|
||||||
let browser = browserPool.getRemoteBrowser(browserId);
|
let browser = browserPool.getRemoteBrowser(browserId);
|
||||||
const browserWaitStart = Date.now();
|
const browserWaitStart = Date.now();
|
||||||
|
let lastLogTime = 0;
|
||||||
|
|
||||||
while (!browser && (Date.now() - browserWaitStart) < BROWSER_INIT_TIMEOUT) {
|
while (!browser && (Date.now() - browserWaitStart) < BROWSER_INIT_TIMEOUT) {
|
||||||
logger.log('debug', `Browser ${browserId} not ready yet, waiting...`);
|
const currentTime = Date.now();
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
||||||
|
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);
|
browser = browserPool.getRemoteBrowser(browserId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!browser) {
|
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`);
|
logger.log('info', `Browser ${browserId} found and ready for execution`);
|
||||||
@@ -273,14 +287,22 @@ async function processRunExecution(job: Job<ExecuteRunData>) {
|
|||||||
let currentPage = browser.getCurrentPage();
|
let currentPage = browser.getCurrentPage();
|
||||||
|
|
||||||
const pageWaitStart = Date.now();
|
const pageWaitStart = Date.now();
|
||||||
while (!currentPage && (Date.now() - pageWaitStart) < 30000) {
|
let lastPageLogTime = 0;
|
||||||
logger.log('debug', `Page not ready for browser ${browserId}, waiting...`);
|
|
||||||
|
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));
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
currentPage = browser.getCurrentPage();
|
currentPage = browser.getCurrentPage();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!currentPage) {
|
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}`);
|
logger.log('info', `Starting workflow execution for run ${data.runId}`);
|
||||||
@@ -775,6 +797,10 @@ async function registerRunExecutionWorker() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
await checkForNewUserQueues();
|
await checkForNewUserQueues();
|
||||||
|
|
||||||
|
setInterval(async () => {
|
||||||
|
await checkForNewUserQueues();
|
||||||
|
}, 10000);
|
||||||
|
|
||||||
logger.log('info', 'Run execution worker registered successfully');
|
logger.log('info', 'Run execution worker registered successfully');
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
@@ -821,6 +847,10 @@ async function registerAbortRunWorker() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
await checkForNewAbortQueues();
|
await checkForNewAbortQueues();
|
||||||
|
|
||||||
|
setInterval(async () => {
|
||||||
|
await checkForNewAbortQueues();
|
||||||
|
}, 10000);
|
||||||
|
|
||||||
logger.log('info', 'Abort run worker registration system initialized');
|
logger.log('info', 'Abort run worker registration system initialized');
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
|
|||||||
@@ -2494,70 +2494,38 @@ class ClientSelectorGenerator {
|
|||||||
};
|
};
|
||||||
|
|
||||||
private getAllDescendantsIncludingShadow(
|
private getAllDescendantsIncludingShadow(
|
||||||
parentElement: HTMLElement
|
parentElement: HTMLElement,
|
||||||
|
maxDepth: number = 20
|
||||||
): HTMLElement[] {
|
): HTMLElement[] {
|
||||||
const allDescendants: HTMLElement[] = [];
|
const allDescendants: HTMLElement[] = [];
|
||||||
const visited = new Set<HTMLElement>();
|
const visited = new Set<HTMLElement>();
|
||||||
const shadowRootsSeen = new Set<ShadowRoot>();
|
|
||||||
|
|
||||||
const traverseShadowRoot = (shadowRoot: ShadowRoot, depth: number = 0) => {
|
const traverse = (element: HTMLElement, currentDepth: number) => {
|
||||||
if (depth > 10) return;
|
if (currentDepth >= maxDepth || visited.has(element)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
visited.add(element);
|
||||||
|
|
||||||
try {
|
if (element !== parentElement) {
|
||||||
const shadowElements = Array.from(
|
allDescendants.push(element);
|
||||||
shadowRoot.querySelectorAll("*")
|
}
|
||||||
) as HTMLElement[];
|
|
||||||
|
|
||||||
shadowElements.forEach((shadowElement) => {
|
// Traverse light DOM children
|
||||||
if (!visited.has(shadowElement)) {
|
const children = Array.from(element.children) as HTMLElement[];
|
||||||
visited.add(shadowElement);
|
for (const child of children) {
|
||||||
allDescendants.push(shadowElement);
|
traverse(child, currentDepth + 1);
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
// Traverse shadow DOM if it exists
|
||||||
shadowElement.shadowRoot &&
|
if (element.shadowRoot) {
|
||||||
!shadowRootsSeen.has(shadowElement.shadowRoot)
|
const shadowChildren = Array.from(element.shadowRoot.children) as HTMLElement[];
|
||||||
) {
|
for (const shadowChild of shadowChildren) {
|
||||||
shadowRootsSeen.add(shadowElement.shadowRoot);
|
traverse(shadowChild, currentDepth + 1);
|
||||||
traverseShadowRoot(shadowElement.shadowRoot, depth + 1);
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const regularDescendants = Array.from(
|
traverse(parentElement, 0);
|
||||||
parentElement.querySelectorAll("*")
|
|
||||||
) as HTMLElement[];
|
|
||||||
regularDescendants.forEach((descendant) => {
|
|
||||||
if (!visited.has(descendant)) {
|
|
||||||
visited.add(descendant);
|
|
||||||
allDescendants.push(descendant);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const elementsWithShadow = [parentElement, ...regularDescendants].filter(
|
|
||||||
(el) => el.shadowRoot
|
|
||||||
);
|
|
||||||
elementsWithShadow.forEach((element) => {
|
|
||||||
if (!shadowRootsSeen.has(element.shadowRoot!)) {
|
|
||||||
shadowRootsSeen.add(element.shadowRoot!);
|
|
||||||
traverseShadowRoot(element.shadowRoot!, 0);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return allDescendants;
|
return allDescendants;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2577,6 +2545,8 @@ class ClientSelectorGenerator {
|
|||||||
if (processedElements.has(descendant)) return;
|
if (processedElements.has(descendant)) return;
|
||||||
processedElements.add(descendant);
|
processedElements.add(descendant);
|
||||||
|
|
||||||
|
if (!this.isMeaningfulElement(descendant)) return;
|
||||||
|
|
||||||
const absolutePath = this.buildOptimizedAbsoluteXPath(
|
const absolutePath = this.buildOptimizedAbsoluteXPath(
|
||||||
descendant,
|
descendant,
|
||||||
listSelector,
|
listSelector,
|
||||||
@@ -2766,16 +2736,20 @@ class ClientSelectorGenerator {
|
|||||||
rootElement: HTMLElement,
|
rootElement: HTMLElement,
|
||||||
otherListElements: HTMLElement[] = []
|
otherListElements: HTMLElement[] = []
|
||||||
): string | null {
|
): string | null {
|
||||||
if (!this.elementContains(rootElement, targetElement) || targetElement === rootElement) {
|
if (
|
||||||
|
!this.elementContains(rootElement, targetElement) ||
|
||||||
|
targetElement === rootElement
|
||||||
|
) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const pathParts: string[] = [];
|
const pathParts: string[] = [];
|
||||||
let current: HTMLElement | null = targetElement;
|
let current: HTMLElement | null = targetElement;
|
||||||
|
let pathDepth = 0;
|
||||||
|
const MAX_PATH_DEPTH = 20;
|
||||||
|
|
||||||
// Build path from target up to root
|
// Build path from target up to root
|
||||||
while (current && current !== rootElement) {
|
while (current && current !== rootElement && pathDepth < MAX_PATH_DEPTH) {
|
||||||
// Calculate conflicts for each element in the path
|
|
||||||
const classes = this.getCommonClassesAcrossLists(
|
const classes = this.getCommonClassesAcrossLists(
|
||||||
current,
|
current,
|
||||||
otherListElements
|
otherListElements
|
||||||
@@ -2806,11 +2780,15 @@ class ClientSelectorGenerator {
|
|||||||
pathParts.unshift(pathPart);
|
pathParts.unshift(pathPart);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Move to parent (either regular parent or shadow host)
|
current =
|
||||||
current = current.parentElement ||
|
current.parentElement ||
|
||||||
((current.getRootNode() as ShadowRoot).host as HTMLElement | null);
|
((current.getRootNode() as ShadowRoot).host as HTMLElement | null);
|
||||||
|
|
||||||
|
pathDepth++;
|
||||||
|
}
|
||||||
|
|
||||||
if (!current) break;
|
if (current !== rootElement) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return pathParts.length > 0 ? "/" + pathParts.join("/") : null;
|
return pathParts.length > 0 ? "/" + pathParts.join("/") : null;
|
||||||
|
|||||||
Reference in New Issue
Block a user