Merge pull request #720 from getmaxun/auto-restart
feat(maxun-core): auto restart services on crash
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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]);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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!");
|
||||||
|
|||||||
Reference in New Issue
Block a user