Merge pull request #720 from getmaxun/auto-restart

feat(maxun-core): auto restart services on crash
This commit is contained in:
Karishma Shukla
2025-08-08 16:10:57 +05:30
committed by GitHub
4 changed files with 76 additions and 14 deletions

View File

@@ -1,6 +1,7 @@
services: services:
postgres: postgres:
image: postgres:13 image: postgres:13
restart: unless-stopped
environment: environment:
POSTGRES_USER: ${DB_USER} POSTGRES_USER: ${DB_USER}
POSTGRES_PASSWORD: ${DB_PASSWORD} POSTGRES_PASSWORD: ${DB_PASSWORD}
@@ -17,6 +18,7 @@ services:
minio: minio:
image: minio/minio image: minio/minio
restart: unless-stopped
environment: environment:
MINIO_ROOT_USER: ${MINIO_ACCESS_KEY} MINIO_ROOT_USER: ${MINIO_ACCESS_KEY}
MINIO_ROOT_PASSWORD: ${MINIO_SECRET_KEY} MINIO_ROOT_PASSWORD: ${MINIO_SECRET_KEY}
@@ -32,6 +34,7 @@ services:
#context: . #context: .
#dockerfile: server/Dockerfile #dockerfile: server/Dockerfile
image: getmaxun/maxun-backend:latest image: getmaxun/maxun-backend:latest
restart: unless-stopped
ports: ports:
- "${BACKEND_PORT:-8080}:${BACKEND_PORT:-8080}" - "${BACKEND_PORT:-8080}:${BACKEND_PORT:-8080}"
env_file: .env env_file: .env
@@ -58,6 +61,7 @@ services:
#context: . #context: .
#dockerfile: Dockerfile #dockerfile: Dockerfile
image: getmaxun/maxun-frontend:latest image: getmaxun/maxun-frontend:latest
restart: unless-stopped
ports: ports:
- "${FRONTEND_PORT:-5173}:${FRONTEND_PORT:-5173}" - "${FRONTEND_PORT:-5173}:${FRONTEND_PORT:-5173}"
env_file: .env env_file: .env

View File

@@ -537,6 +537,11 @@ function scrapableHeuristics(maxCountPerPage = 50, minArea = 20000, scrolls = 3,
const evaluateXPath = (document, xpath, isShadow = false) => { const evaluateXPath = (document, xpath, isShadow = false) => {
try { try {
if (!document || !xpath) {
console.warn('Invalid document or xpath provided to evaluateXPath');
return null;
}
const result = document.evaluate( const result = document.evaluate(
xpath, xpath,
document, document,
@@ -632,6 +637,7 @@ function scrapableHeuristics(maxCountPerPage = 50, minArea = 20000, scrolls = 3,
return null; return null;
} catch (err) { } catch (err) {
console.error("Critical XPath failure:", xpath, err); console.error("Critical XPath failure:", xpath, err);
// Return null instead of throwing to prevent crashes
return null; return null;
} }
}; };
@@ -694,16 +700,25 @@ function scrapableHeuristics(maxCountPerPage = 50, minArea = 20000, scrolls = 3,
for (let i = 0; i < parts.length; i++) { for (let i = 0; i < parts.length; i++) {
if (!currentElement) return null; if (!currentElement) return null;
// Handle iframe and frame traversal // Handle iframe and frame traversal with enhanced safety
if ( if (
currentElement.tagName === "IFRAME" || currentElement.tagName === "IFRAME" ||
currentElement.tagName === "FRAME" currentElement.tagName === "FRAME"
) { ) {
try { try {
// Check if frame is accessible
if (!currentElement.contentDocument && !currentElement.contentWindow) {
console.warn('Frame is not accessible (cross-origin or unloaded)');
return null;
}
const frameDoc = const frameDoc =
currentElement.contentDocument || currentElement.contentDocument ||
currentElement.contentWindow.document; currentElement.contentWindow?.document;
if (!frameDoc) return null; if (!frameDoc) {
console.warn('Frame document is not available');
return null;
}
if (isXPathSelector(parts[i])) { if (isXPathSelector(parts[i])) {
currentElement = evaluateXPath(frameDoc, parts[i]); currentElement = evaluateXPath(frameDoc, parts[i]);

View File

@@ -108,7 +108,9 @@ export default class Interpreter extends EventEmitter {
PlaywrightBlocker.fromLists(fetch, ['https://easylist.to/easylist/easylist.txt']).then(blocker => { PlaywrightBlocker.fromLists(fetch, ['https://easylist.to/easylist/easylist.txt']).then(blocker => {
this.blocker = blocker; this.blocker = blocker;
}).catch(err => { }).catch(err => {
this.log(`Failed to initialize ad-blocker:`, Level.ERROR); this.log(`Failed to initialize ad-blocker: ${err.message}`, Level.ERROR);
// Continue without ad-blocker rather than crashing
this.blocker = null;
}) })
} }
@@ -522,11 +524,16 @@ export default class Interpreter extends EventEmitter {
this.options.debugChannel.setActionType('script'); this.options.debugChannel.setActionType('script');
} }
const AsyncFunction: FunctionConstructor = Object.getPrototypeOf( try {
async () => { }, const AsyncFunction: FunctionConstructor = Object.getPrototypeOf(
).constructor; async () => { },
const x = new AsyncFunction('page', 'log', code); ).constructor;
await x(page, this.log); const x = new AsyncFunction('page', 'log', code);
await x(page, this.log);
} catch (error) {
this.log(`Script execution failed: ${error.message}`, Level.ERROR);
throw new Error(`Script execution error: ${error.message}`);
}
}, },
flag: async () => new Promise((res) => { flag: async () => new Promise((res) => {
@@ -590,11 +597,18 @@ export default class Interpreter extends EventEmitter {
try{ try{
await executeAction(invokee, methodName, [step.args[0], { force: true }]); await executeAction(invokee, methodName, [step.args[0], { force: true }]);
} catch (error) { } catch (error) {
continue this.log(`Click action failed: ${error.message}`, Level.WARN);
continue;
} }
} }
} else { } else {
await executeAction(invokee, methodName, step.args); try {
await executeAction(invokee, methodName, step.args);
} catch (error) {
this.log(`Action ${methodName} failed: ${error.message}`, Level.ERROR);
// Continue with next action instead of crashing
continue;
}
} }
} }
@@ -1132,7 +1146,16 @@ export default class Interpreter extends EventEmitter {
}); });
/* eslint no-constant-condition: ["warn", { "checkLoops": false }] */ /* eslint no-constant-condition: ["warn", { "checkLoops": false }] */
let loopIterations = 0;
const MAX_LOOP_ITERATIONS = 1000; // Circuit breaker
while (true) { while (true) {
// Circuit breaker to prevent infinite loops
if (++loopIterations > MAX_LOOP_ITERATIONS) {
this.log('Maximum loop iterations reached, terminating to prevent infinite loop', Level.ERROR);
return;
}
// Checks whether the page was closed from outside, // Checks whether the page was closed from outside,
// or the workflow execution has been stopped via `interpreter.stop()` // or the workflow execution has been stopped via `interpreter.stop()`
if (p.isClosed() || !this.stopper) { if (p.isClosed() || !this.stopper) {
@@ -1147,14 +1170,25 @@ export default class Interpreter extends EventEmitter {
} }
let pageState = {}; let pageState = {};
let getStateTest = "Hello";
try { try {
// Check if page is still valid before accessing state
if (p.isClosed()) {
this.log('Page was closed during execution', Level.WARN);
return;
}
pageState = await this.getState(p, workflowCopy, selectors); pageState = await this.getState(p, workflowCopy, selectors);
selectors = []; selectors = [];
console.log("Empty selectors:", selectors) console.log("Empty selectors:", selectors)
} catch (e: any) { } catch (e: any) {
this.log('The browser has been closed.'); this.log(`Failed to get page state: ${e.message}`, Level.ERROR);
return; // If state access fails, attempt graceful recovery
if (p.isClosed()) {
this.log('Browser has been closed, terminating workflow', Level.WARN);
return;
}
// For other errors, continue with empty state to avoid complete failure
pageState = { url: p.url(), selectors: [], cookies: {} };
} }
if (this.options.debug) { if (this.options.debug) {
@@ -1207,8 +1241,13 @@ export default class Interpreter extends EventEmitter {
selectors.push(selector); selectors.push(selector);
} }
}); });
// Reset loop iteration counter on successful action
loopIterations = 0;
} catch (e) { } catch (e) {
this.log(<Error>e, Level.ERROR); this.log(<Error>e, Level.ERROR);
// Don't crash on individual action failures - continue with next iteration
continue;
} }
} else { } else {
//await this.disableAdBlocker(p); //await this.disableAdBlocker(p);

View File

@@ -41,6 +41,10 @@ export default class Concurrency {
job().then(() => { job().then(() => {
// console.debug("Job finished, running the next waiting job..."); // console.debug("Job finished, running the next waiting job...");
this.runNextJob(); this.runNextJob();
}).catch((error) => {
console.error(`Job failed with error: ${error.message}`);
// Continue processing other jobs even if one fails
this.runNextJob();
}); });
} else { } else {
// console.debug("No waiting job found!"); // console.debug("No waiting job found!");