From fa11a93d50b8422cff2e630d173c708d8cfdcd8a Mon Sep 17 00:00:00 2001 From: amhsirak Date: Fri, 24 Jan 2025 19:47:57 +0530 Subject: [PATCH 01/15] feat: check if robot is running --- .../browser-management/classes/BrowserPool.ts | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/server/src/browser-management/classes/BrowserPool.ts b/server/src/browser-management/classes/BrowserPool.ts index cd4962a16..16b427e20 100644 --- a/server/src/browser-management/classes/BrowserPool.ts +++ b/server/src/browser-management/classes/BrowserPool.ts @@ -15,6 +15,8 @@ interface BrowserPoolInfo { * @default false */ active: boolean, + + isRobotRun?: boolean; } /** @@ -46,17 +48,29 @@ export class BrowserPool { * @param browser remote browser instance * @param active states if the browser's instance is being actively used */ - public addRemoteBrowser = (id: string, browser: RemoteBrowser, active: boolean = false): void => { + public addRemoteBrowser = (id: string, browser: RemoteBrowser, active: boolean = false, isRobotRun: boolean = false): void => { this.pool = { ...this.pool, [id]: { browser, active, + isRobotRun }, } logger.log('debug', `Remote browser with id: ${id} added to the pool`); }; + public hasActiveRobotRun(): boolean { + return Object.values(this.pool).some(info => info.isRobotRun); + } + + public clearRobotRunState(id: string): void { + if (this.pool[id]) { + this.pool[id].isRobotRun = false; + logger.log('debug', `Robot run state cleared for browser ${id}`); + } + } + /** * Removes the remote browser instance from the pool. * @param id remote browser instance's id @@ -67,6 +81,8 @@ export class BrowserPool { logger.log('warn', `Remote browser with id: ${id} does not exist in the pool`); return false; } + + this.clearRobotRunState(id); delete (this.pool[id]); logger.log('debug', `Remote browser with id: ${id} deleted from the pool`); return true; @@ -97,4 +113,4 @@ export class BrowserPool { logger.log('warn', `No active browser in the pool`); return null; }; -} +} \ No newline at end of file From 7328ec3934fa35a9333599c2a4e68daa6065e4e0 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Fri, 24 Jan 2025 19:48:23 +0530 Subject: [PATCH 02/15] feat: route to check if robot is running --- server/src/routes/record.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/server/src/routes/record.ts b/server/src/routes/record.ts index 51d3ff922..9584d6f9d 100644 --- a/server/src/routes/record.ts +++ b/server/src/routes/record.ts @@ -16,6 +16,7 @@ import stealthPlugin from 'puppeteer-extra-plugin-stealth'; import logger from "../logger"; import { getDecryptedProxyConfig } from './proxy'; import { requireSignIn } from '../middlewares/auth'; +import { browserPool } from '../server'; export const router = Router(); chromium.use(stealthPlugin()); @@ -33,6 +34,17 @@ router.all('/', requireSignIn, (req, res, next) => { next() // pass control to the next handler }) +router.use('/', requireSignIn, (req: AuthenticatedRequest, res: Response, next) => { + if (browserPool.hasActiveRobotRun()) { + logger.log('debug', 'Preventing browser initialization - robot run in progress'); + return res.status(403).json({ + error: 'Cannot initialize recording browser while a robot run is in progress' + }); + } + next(); +}); + + /** * GET endpoint for starting the remote browser recording session. * returns session's id @@ -131,4 +143,4 @@ router.get('/interpret', requireSignIn, async (req, res) => { router.get('/interpret/stop', requireSignIn, async (req, res) => { await stopRunningInterpretation(); return res.send('interpretation stopped'); -}); +}); \ No newline at end of file From 0093340ce159266cd64859af92fac1a2e8b92b9d Mon Sep 17 00:00:00 2001 From: amhsirak Date: Fri, 24 Jan 2025 19:48:48 +0530 Subject: [PATCH 03/15] feat: notify on robot run after page reload --- src/pages/MainPage.tsx | 77 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 70 insertions(+), 7 deletions(-) diff --git a/src/pages/MainPage.tsx b/src/pages/MainPage.tsx index b9a4f24fe..c80669515 100644 --- a/src/pages/MainPage.tsx +++ b/src/pages/MainPage.tsx @@ -68,13 +68,14 @@ export const MainPage = ({ handleEditRecording, initialContent }: MainPageProps) const readyForRunHandler = useCallback((browserId: string, runId: string) => { interpretStoredRecording(runId).then(async (interpretation: boolean) => { if (!aborted) { - if (interpretation) { - notify('success', t('main_page.notifications.interpretation_success', { name: runningRecordingName })); - } else { - notify('success', t('main_page.notifications.interpretation_failed', { name: runningRecordingName })); - // destroy the created browser - await stopRecording(browserId); - } + // if (interpretation) { + // notify('success', t('main_page.notifications.interpretation_success', { name: runningRecordingName })); + // } else { + // notify('success', t('main_page.notifications.interpretation_failed', { name: runningRecordingName })); + // // destroy the created browser + // await stopRecording(browserId); + // } + if (!interpretation) await stopRecording(browserId); } setRunningRecordingName(''); setCurrentInterpretationLog(''); @@ -89,6 +90,12 @@ export const MainPage = ({ handleEditRecording, initialContent }: MainPageProps) const handleRunRecording = useCallback((settings: RunSettings) => { createRunForStoredRecording(runningRecordingId, settings).then(({ browserId, runId }: CreateRunResponse) => { + localStorage.setItem('runInfo', JSON.stringify({ + browserId, + runId, + recordingName: runningRecordingName + })); + setIds({ browserId, runId }); const socket = io(`${apiUrl}/${browserId}`, { @@ -98,6 +105,18 @@ export const MainPage = ({ handleEditRecording, initialContent }: MainPageProps) setSockets(sockets => [...sockets, socket]); socket.on('ready-for-run', () => readyForRunHandler(browserId, runId)); socket.on('debugMessage', debugMessageHandler); + + socket.on('run-completed', (status) => { + if (status === 'success') { + notify('success', t('main_page.notifications.interpretation_success', { name: runningRecordingName })); + } else { + notify('error', t('main_page.notifications.interpretation_failed', { name: runningRecordingName })); + } + setRunningRecordingName(''); + setCurrentInterpretationLog(''); + setRerenderRuns(true); + }); + setContent('runs'); if (browserId) { notify('info', t('main_page.notifications.run_started', { name: runningRecordingName })); @@ -108,6 +127,7 @@ export const MainPage = ({ handleEditRecording, initialContent }: MainPageProps) return (socket: Socket, browserId: string, runId: string) => { socket.off('ready-for-run', () => readyForRunHandler(browserId, runId)); socket.off('debugMessage', debugMessageHandler); + socket.off('run-completed'); } }, [runningRecordingName, sockets, ids, readyForRunHandler, debugMessageHandler]) @@ -122,6 +142,49 @@ export const MainPage = ({ handleEditRecording, initialContent }: MainPageProps) }); } + useEffect(() => { + const storedRunInfo = localStorage.getItem('runInfo'); + console.log('storedRunInfo', storedRunInfo); + + if (storedRunInfo) { + // Parse the stored info + const { browserId, runId, recordingName } = JSON.parse(storedRunInfo); + + // Reconnect to the specific browser's namespace + setIds({ browserId, runId }); + const socket = io(`${apiUrl}/${browserId}`, { + transports: ["websocket"], + rejectUnauthorized: false + }); + + // Update component state with stored info + setRunningRecordingName(recordingName); + setSockets(sockets => [...sockets, socket]); + + // Set up event listeners + socket.on('ready-for-run', () => readyForRunHandler(browserId, runId)); + socket.on('debugMessage', debugMessageHandler); + socket.on('run-completed', (status) => { + if (status === 'success') { + notify('success', t('main_page.notifications.interpretation_success', { name: recordingName })); + } else { + notify('error', t('main_page.notifications.interpretation_failed', { name: recordingName })); + } + setRunningRecordingName(''); + setCurrentInterpretationLog(''); + setRerenderRuns(true); + localStorage.removeItem('runInfo'); // Clean up stored info + }); + + // Cleanup function + return () => { + socket.off('ready-for-run', () => readyForRunHandler(browserId, runId)); + socket.off('debugMessage', debugMessageHandler); + socket.off('run-completed'); + }; + } + }, []); + const DisplayContent = () => { switch (content) { case 'robots': From 31840c87e9faf7cf2fdcdc8b3f6c1ffe0b1760b1 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Fri, 24 Jan 2025 19:49:17 +0530 Subject: [PATCH 04/15] feat: update status as true on run creation --- server/src/browser-management/controller.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/browser-management/controller.ts b/server/src/browser-management/controller.ts index 24a677ce1..bc9d173fd 100644 --- a/server/src/browser-management/controller.ts +++ b/server/src/browser-management/controller.ts @@ -59,7 +59,7 @@ export const createRemoteBrowserForRun = (userId: string): string => { async (socket: Socket) => { const browserSession = new RemoteBrowser(socket); await browserSession.initialize(userId); - browserPool.addRemoteBrowser(id, browserSession, true); + browserPool.addRemoteBrowser(id, browserSession, true, true); socket.emit('ready-for-run'); }); return id; @@ -154,4 +154,4 @@ export const stopRunningInterpretation = async () => { } else { logger.log('error', 'Cannot stop interpretation: No active browser or generator.'); } -}; +}; \ No newline at end of file From 3fe203335f1c11ebd38d4a11c2ae74afcf7ec270 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Fri, 24 Jan 2025 19:49:38 +0530 Subject: [PATCH 05/15] feat: emit run completed socket event --- server/src/workflow-management/classes/Interpreter.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server/src/workflow-management/classes/Interpreter.ts b/server/src/workflow-management/classes/Interpreter.ts index c8aec13c4..dde3daf6b 100644 --- a/server/src/workflow-management/classes/Interpreter.ts +++ b/server/src/workflow-management/classes/Interpreter.ts @@ -332,6 +332,8 @@ export class WorkflowInterpreter { }, {}) } + this.socket.emit('run-completed', "success"); + logger.log('debug', `Interpretation finished`); this.clearState(); return result; @@ -354,4 +356,4 @@ export class WorkflowInterpreter { this.socket = socket; this.subscribeToPausing(); }; -} +} \ No newline at end of file From 5d636adf48e1e7c127e7e4c569f047c3159038bb Mon Sep 17 00:00:00 2001 From: Rohit Date: Sat, 25 Jan 2025 15:00:08 +0530 Subject: [PATCH 06/15] feat: execute robot run as a seperate job --- server/src/routes/storage.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/server/src/routes/storage.ts b/server/src/routes/storage.ts index fb587d172..657d61a46 100644 --- a/server/src/routes/storage.ts +++ b/server/src/routes/storage.ts @@ -477,7 +477,7 @@ router.put('/runs/:id', requireSignIn, async (req: AuthenticatedRequest, res) => console.log(`Proxy config for run: ${JSON.stringify(proxyOptions)}`) - const id = createRemoteBrowserForRun(req.user.id); + // const id = createRemoteBrowserForRun(req.user.id); const runId = uuid(); @@ -488,7 +488,7 @@ router.put('/runs/:id', requireSignIn, async (req: AuthenticatedRequest, res) => robotMetaId: recording.recording_meta.id, startedAt: new Date().toLocaleString(), finishedAt: '', - browserId: id, + browserId: req.params.id, interpreterSettings: req.body, log: '', runId, @@ -497,10 +497,15 @@ router.put('/runs/:id', requireSignIn, async (req: AuthenticatedRequest, res) => binaryOutput: {}, }); + const job = await workflowQueue.add( + 'run workflow', + { id: req.params.id, runId, userId: req.user.id, isScheduled: false }, + ); + const plainRun = run.toJSON(); return res.send({ - browserId: id, + browserId: req.params.id, runId: plainRun.runId, }); } catch (e) { From 1a550900dbc5bd481049c0dcd538b8b1fbdcad3e Mon Sep 17 00:00:00 2001 From: Rohit Date: Sat, 25 Jan 2025 15:01:14 +0530 Subject: [PATCH 07/15] feat: add support to execute manual run --- server/src/worker.ts | 44 +++++++++++++++++++++++++------------------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/server/src/worker.ts b/server/src/worker.ts index 3a82ee737..c6e5de607 100644 --- a/server/src/worker.ts +++ b/server/src/worker.ts @@ -1,7 +1,8 @@ import { Queue, Worker } from 'bullmq'; import IORedis from 'ioredis'; import logger from './logger'; -import { handleRunRecording } from "./workflow-management/scheduler"; +import { handleRunRecording as handleScheduledRunRecording } from "./workflow-management/scheduler"; +import { handleRunRecording } from './api/record'; import Robot from './models/Robot'; import { computeNextRun } from './utils/schedule'; @@ -22,9 +23,11 @@ connection.on('error', (err) => { const workflowQueue = new Queue('workflow', { connection }); const worker = new Worker('workflow', async job => { - const { runId, userId, id } = job.data; + const { runId, userId, id, isScheduled = true } = job.data; try { - const result = await handleRunRecording(id, userId); + const result = isScheduled ? + await handleScheduledRunRecording(id, userId) : + await handleRunRecording(id, userId); return result; } catch (error) { logger.error('Error running workflow:', error); @@ -34,23 +37,26 @@ const worker = new Worker('workflow', async job => { worker.on('completed', async (job: any) => { logger.log(`info`, `Job ${job.id} completed for ${job.data.runId}`); - const robot = await Robot.findOne({ where: { 'recording_meta.id': job.data.id } }); - if (robot) { - // Update `lastRunAt` to the current time - const lastRunAt = new Date(); + + if (job.data.isScheduled) { + const robot = await Robot.findOne({ where: { 'recording_meta.id': job.data.id } }); + if (robot) { + // Update `lastRunAt` to the current time + const lastRunAt = new Date(); - // Compute the next run date - if (robot.schedule && robot.schedule.cronExpression && robot.schedule.timezone) { - const nextRunAt = computeNextRun(robot.schedule.cronExpression, robot.schedule.timezone) || undefined; - await robot.update({ - schedule: { - ...robot.schedule, - lastRunAt, - nextRunAt, - }, - }); - } else { - logger.error('Robot schedule, cronExpression, or timezone is missing.'); + // Compute the next run date + if (robot.schedule && robot.schedule.cronExpression && robot.schedule.timezone) { + const nextRunAt = computeNextRun(robot.schedule.cronExpression, robot.schedule.timezone) || undefined; + await robot.update({ + schedule: { + ...robot.schedule, + lastRunAt, + nextRunAt, + }, + }); + } else { + logger.error('Robot schedule, cronExpression, or timezone is missing.'); + } } } }); From f957a7ced76db2cba940a9a29ae4cb9ef8c45a39 Mon Sep 17 00:00:00 2001 From: Rohit Date: Sat, 25 Jan 2025 15:10:42 +0530 Subject: [PATCH 08/15] feat: rm readyRunHandler --- src/pages/MainPage.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/pages/MainPage.tsx b/src/pages/MainPage.tsx index c80669515..8e56df690 100644 --- a/src/pages/MainPage.tsx +++ b/src/pages/MainPage.tsx @@ -103,7 +103,6 @@ export const MainPage = ({ handleEditRecording, initialContent }: MainPageProps) rejectUnauthorized: false }); setSockets(sockets => [...sockets, socket]); - socket.on('ready-for-run', () => readyForRunHandler(browserId, runId)); socket.on('debugMessage', debugMessageHandler); socket.on('run-completed', (status) => { @@ -125,11 +124,10 @@ export const MainPage = ({ handleEditRecording, initialContent }: MainPageProps) } }) return (socket: Socket, browserId: string, runId: string) => { - socket.off('ready-for-run', () => readyForRunHandler(browserId, runId)); socket.off('debugMessage', debugMessageHandler); socket.off('run-completed'); } - }, [runningRecordingName, sockets, ids, readyForRunHandler, debugMessageHandler]) + }, [runningRecordingName, sockets, ids, debugMessageHandler]) const handleScheduleRecording = (settings: ScheduleSettings) => { scheduleStoredRecording(runningRecordingId, settings) @@ -162,7 +160,6 @@ export const MainPage = ({ handleEditRecording, initialContent }: MainPageProps) setSockets(sockets => [...sockets, socket]); // Set up event listeners - socket.on('ready-for-run', () => readyForRunHandler(browserId, runId)); socket.on('debugMessage', debugMessageHandler); socket.on('run-completed', (status) => { if (status === 'success') { @@ -178,7 +175,6 @@ export const MainPage = ({ handleEditRecording, initialContent }: MainPageProps) // Cleanup function return () => { - socket.off('ready-for-run', () => readyForRunHandler(browserId, runId)); socket.off('debugMessage', debugMessageHandler); socket.off('run-completed'); }; From 9d3c55b66601e2319583a02b1f645e885a4edb64 Mon Sep 17 00:00:00 2001 From: Rohit Date: Mon, 27 Jan 2025 15:22:33 +0530 Subject: [PATCH 09/15] feat: rm local storage run info --- src/pages/MainPage.tsx | 47 ------------------------------------------ 1 file changed, 47 deletions(-) diff --git a/src/pages/MainPage.tsx b/src/pages/MainPage.tsx index 8e56df690..de85ff317 100644 --- a/src/pages/MainPage.tsx +++ b/src/pages/MainPage.tsx @@ -90,12 +90,6 @@ export const MainPage = ({ handleEditRecording, initialContent }: MainPageProps) const handleRunRecording = useCallback((settings: RunSettings) => { createRunForStoredRecording(runningRecordingId, settings).then(({ browserId, runId }: CreateRunResponse) => { - localStorage.setItem('runInfo', JSON.stringify({ - browserId, - runId, - recordingName: runningRecordingName - })); - setIds({ browserId, runId }); const socket = io(`${apiUrl}/${browserId}`, { @@ -140,47 +134,6 @@ export const MainPage = ({ handleEditRecording, initialContent }: MainPageProps) }); } - useEffect(() => { - const storedRunInfo = localStorage.getItem('runInfo'); - console.log('storedRunInfo', storedRunInfo); - - if (storedRunInfo) { - // Parse the stored info - const { browserId, runId, recordingName } = JSON.parse(storedRunInfo); - - // Reconnect to the specific browser's namespace - setIds({ browserId, runId }); - const socket = io(`${apiUrl}/${browserId}`, { - transports: ["websocket"], - rejectUnauthorized: false - }); - - // Update component state with stored info - setRunningRecordingName(recordingName); - setSockets(sockets => [...sockets, socket]); - - // Set up event listeners - socket.on('debugMessage', debugMessageHandler); - socket.on('run-completed', (status) => { - if (status === 'success') { - notify('success', t('main_page.notifications.interpretation_success', { name: recordingName })); - } else { - notify('error', t('main_page.notifications.interpretation_failed', { name: recordingName })); - } - setRunningRecordingName(''); - setCurrentInterpretationLog(''); - setRerenderRuns(true); - localStorage.removeItem('runInfo'); // Clean up stored info - }); - - // Cleanup function - return () => { - socket.off('debugMessage', debugMessageHandler); - socket.off('run-completed'); - }; - } - }, []); - const DisplayContent = () => { switch (content) { case 'robots': From 302ccd1231d96e318ee63879127f4c09b754b446 Mon Sep 17 00:00:00 2001 From: Rohit Date: Mon, 27 Jan 2025 18:24:04 +0530 Subject: [PATCH 10/15] feat: revert browser id changes --- server/src/routes/storage.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/server/src/routes/storage.ts b/server/src/routes/storage.ts index 657d61a46..f04fb22e7 100644 --- a/server/src/routes/storage.ts +++ b/server/src/routes/storage.ts @@ -477,7 +477,7 @@ router.put('/runs/:id', requireSignIn, async (req: AuthenticatedRequest, res) => console.log(`Proxy config for run: ${JSON.stringify(proxyOptions)}`) - // const id = createRemoteBrowserForRun(req.user.id); + const id = createRemoteBrowserForRun(req.user.id); const runId = uuid(); @@ -488,7 +488,7 @@ router.put('/runs/:id', requireSignIn, async (req: AuthenticatedRequest, res) => robotMetaId: recording.recording_meta.id, startedAt: new Date().toLocaleString(), finishedAt: '', - browserId: req.params.id, + browserId: id, interpreterSettings: req.body, log: '', runId, @@ -499,13 +499,13 @@ router.put('/runs/:id', requireSignIn, async (req: AuthenticatedRequest, res) => const job = await workflowQueue.add( 'run workflow', - { id: req.params.id, runId, userId: req.user.id, isScheduled: false }, + { id, runId, userId: req.user.id, isScheduled: false }, ); const plainRun = run.toJSON(); return res.send({ - browserId: req.params.id, + browserId: id, runId: plainRun.runId, }); } catch (e) { From de2d828a65c5e8996bff3281be5eebd4855f42a6 Mon Sep 17 00:00:00 2001 From: Rohit Date: Mon, 27 Jan 2025 18:25:08 +0530 Subject: [PATCH 11/15] feat: modify worker to handle manual run --- server/src/worker.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/worker.ts b/server/src/worker.ts index c6e5de607..60eb3b4d4 100644 --- a/server/src/worker.ts +++ b/server/src/worker.ts @@ -2,7 +2,7 @@ import { Queue, Worker } from 'bullmq'; import IORedis from 'ioredis'; import logger from './logger'; import { handleRunRecording as handleScheduledRunRecording } from "./workflow-management/scheduler"; -import { handleRunRecording } from './api/record'; +import { handleRunRecording } from './workflow-management/record'; import Robot from './models/Robot'; import { computeNextRun } from './utils/schedule'; @@ -27,7 +27,7 @@ const worker = new Worker('workflow', async job => { try { const result = isScheduled ? await handleScheduledRunRecording(id, userId) : - await handleRunRecording(id, userId); + await handleRunRecording(id, userId, runId); return result; } catch (error) { logger.error('Error running workflow:', error); From 279612d5d38453f51db197ae4720a86764d2120c Mon Sep 17 00:00:00 2001 From: Rohit Date: Mon, 27 Jan 2025 18:54:40 +0530 Subject: [PATCH 12/15] feat: handle robot run for workers --- server/src/workflow-management/record.ts | 229 +++++++++++++++++++++++ 1 file changed, 229 insertions(+) create mode 100644 server/src/workflow-management/record.ts diff --git a/server/src/workflow-management/record.ts b/server/src/workflow-management/record.ts new file mode 100644 index 000000000..02a15a71d --- /dev/null +++ b/server/src/workflow-management/record.ts @@ -0,0 +1,229 @@ +// Import core dependencies +import { chromium } from 'playwright-extra'; +import stealthPlugin from 'puppeteer-extra-plugin-stealth'; +import { Page } from "playwright"; + +// Import local utilities and services +import { destroyRemoteBrowser } from '../browser-management/controller'; +import logger from '../logger'; +import { browserPool } from "../server"; +import { googleSheetUpdateTasks, processGoogleSheetUpdates } from "./integrations/gsheet"; +import { BinaryOutputService } from "../storage/mino"; +import { capture } from "../utils/analytics"; + +// Import models and types +import Robot from "../models/Robot"; +import Run from "../models/Run"; +import { WorkflowFile } from "maxun-core"; +import { io, Socket } from 'socket.io-client'; + +// Enable stealth mode for chromium +chromium.use(stealthPlugin()); + +async function readyForRunHandler(browserId: string, id: string) { + try { + const result = await executeRun(id); + + const socket = io(`${process.env.BACKEND_URL ? process.env.BACKEND_URL : 'http://localhost:8080'}/${browserId}`, { + transports: ['websocket'], + rejectUnauthorized: false + }); + + if (result && result.success) { + logger.info(`Interpretation of ${id} succeeded`); + socket.emit('run-completed', 'success'); + resetRecordingState(browserId, id); + return result.interpretationInfo; + } else { + logger.error(`Interpretation of ${id} failed`); + socket.emit('run-completed', 'failed'); + await destroyRemoteBrowser(browserId); + resetRecordingState(browserId, id); + return null; + } + + } catch (error: any) { + logger.error(`Error during readyForRunHandler: ${error.message}`); + await destroyRemoteBrowser(browserId); + return null; + } +} + +function resetRecordingState(browserId: string, id: string) { + browserId = ''; + id = ''; +} + +function AddGeneratedFlags(workflow: WorkflowFile) { + const copy = JSON.parse(JSON.stringify(workflow)); + for (let i = 0; i < workflow.workflow.length; i++) { + copy.workflow[i].what.unshift({ + action: 'flag', + args: ['generated'], + }); + } + return copy; +} + +async function executeRun(id: string) { + try { + const run = await Run.findOne({ where: { runId: id } }); + if (!run) { + return { + success: false, + error: 'Run not found' + }; + } + + const plainRun = run.toJSON(); + + const recording = await Robot.findOne({ + where: { 'recording_meta.id': plainRun.robotMetaId }, + raw: true + }); + if (!recording) { + return { + success: false, + error: 'Recording not found' + }; + } + + const browser = browserPool.getRemoteBrowser(plainRun.browserId); + if (!browser) { + throw new Error('Could not access browser'); + } + + let currentPage = await browser.getCurrentPage(); + if (!currentPage) { + throw new Error('Could not create a new page'); + } + + const workflow = AddGeneratedFlags(recording.recording); + const interpretationInfo = await browser.interpreter.InterpretRecording( + workflow, + currentPage, + (newPage: Page) => currentPage = newPage, + plainRun.interpreterSettings + ); + + const binaryOutputService = new BinaryOutputService('maxun-run-screenshots'); + const uploadedBinaryOutput = await binaryOutputService.uploadAndStoreBinaryOutput( + run, + interpretationInfo.binaryOutput + ); + + await destroyRemoteBrowser(plainRun.browserId); + + const updatedRun = await run.update({ + ...run, + status: 'success', + finishedAt: new Date().toLocaleString(), + browserId: plainRun.browserId, + log: interpretationInfo.log.join('\n'), + serializableOutput: interpretationInfo.serializableOutput, + binaryOutput: uploadedBinaryOutput, + }); + + let totalRowsExtracted = 0; + let extractedScreenshotsCount = 0; + let extractedItemsCount = 0; + + if (updatedRun.dataValues.binaryOutput && updatedRun.dataValues.binaryOutput["item-0"]) { + extractedScreenshotsCount = 1; + } + + if (updatedRun.dataValues.serializableOutput && updatedRun.dataValues.serializableOutput["item-0"]) { + const itemsArray = updatedRun.dataValues.serializableOutput["item-0"]; + extractedItemsCount = itemsArray.length; + totalRowsExtracted = itemsArray.reduce((total: number, item: any) => { + return total + Object.keys(item).length; + }, 0); + } + + logger.info(`Extracted Items Count: ${extractedItemsCount}`); + logger.info(`Extracted Screenshots Count: ${extractedScreenshotsCount}`); + logger.info(`Total Rows Extracted: ${totalRowsExtracted}`); + + capture('maxun-oss-run-created-manual', { + runId: id, + created_at: new Date().toISOString(), + status: 'success', + extractedItemsCount, + totalRowsExtracted, + extractedScreenshotsCount, + }); + + // Handle Google Sheets integration + try { + googleSheetUpdateTasks[plainRun.runId] = { + robotId: plainRun.robotMetaId, + runId: plainRun.runId, + status: 'pending', + retries: 5, + }; + await processGoogleSheetUpdates(); + } catch (err: any) { + logger.error(`Failed to update Google Sheet for run: ${plainRun.runId}: ${err.message}`); + } + + return { + success: true, + interpretationInfo: updatedRun.toJSON() + }; + + } catch (error: any) { + logger.error(`Error running robot: ${error.message}`); + const run = await Run.findOne({ where: { runId: id } }); + if (run) { + await run.update({ + status: 'failed', + finishedAt: new Date().toLocaleString(), + }); + } + + capture('maxun-oss-run-created-manual', { + runId: id, + created_at: new Date().toISOString(), + status: 'failed', + error_message: error.message, + }); + + return { + success: false, + error: error.message, + }; + } +} + +/** + * Main function to handle running a recording through the worker process + */ +export async function handleRunRecording(id: string, userId: string, runId: string) { + try { + if (!id || !runId || !userId) { + throw new Error('browserId or runId or userId is undefined'); + } + + const socket = io(`${process.env.BACKEND_URL ? process.env.BACKEND_URL : 'http://localhost:8080'}/${id}`, { + transports: ['websocket'], + rejectUnauthorized: false + }); + + socket.on('ready-for-run', () => readyForRunHandler(id, runId)); + + logger.info(`Running Robot: ${id}`); + + socket.on('disconnect', () => { + cleanupSocketListeners(socket, id, runId); + }); + + } catch (error: any) { + logger.error('Error running robot:', error); + throw error; + } +} + +function cleanupSocketListeners(socket: Socket, browserId: string, id: string) { + socket.off('ready-for-run', () => readyForRunHandler(browserId, id)); + logger.info(`Cleaned up listeners for browserId: ${browserId}, runId: ${id}`); +} From 13d52a445b10fb4499925722d1d28ac8f53cd94d Mon Sep 17 00:00:00 2001 From: Rohit Date: Tue, 28 Jan 2025 12:33:14 +0530 Subject: [PATCH 13/15] feat: emit socket event to display run status --- server/src/workflow-management/record.ts | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/server/src/workflow-management/record.ts b/server/src/workflow-management/record.ts index 02a15a71d..36049595d 100644 --- a/server/src/workflow-management/record.ts +++ b/server/src/workflow-management/record.ts @@ -16,6 +16,7 @@ import Robot from "../models/Robot"; import Run from "../models/Run"; import { WorkflowFile } from "maxun-core"; import { io, Socket } from 'socket.io-client'; +import { io as serverIo } from "../server"; // Enable stealth mode for chromium chromium.use(stealthPlugin()); @@ -24,19 +25,12 @@ async function readyForRunHandler(browserId: string, id: string) { try { const result = await executeRun(id); - const socket = io(`${process.env.BACKEND_URL ? process.env.BACKEND_URL : 'http://localhost:8080'}/${browserId}`, { - transports: ['websocket'], - rejectUnauthorized: false - }); - if (result && result.success) { logger.info(`Interpretation of ${id} succeeded`); - socket.emit('run-completed', 'success'); resetRecordingState(browserId, id); return result.interpretationInfo; } else { logger.error(`Interpretation of ${id} failed`); - socket.emit('run-completed', 'failed'); await destroyRemoteBrowser(browserId); resetRecordingState(browserId, id); return null; @@ -124,14 +118,15 @@ async function executeRun(id: string) { binaryOutput: uploadedBinaryOutput, }); + let totalRowsExtracted = 0; let extractedScreenshotsCount = 0; let extractedItemsCount = 0; - + if (updatedRun.dataValues.binaryOutput && updatedRun.dataValues.binaryOutput["item-0"]) { extractedScreenshotsCount = 1; } - + if (updatedRun.dataValues.serializableOutput && updatedRun.dataValues.serializableOutput["item-0"]) { const itemsArray = updatedRun.dataValues.serializableOutput["item-0"]; extractedItemsCount = itemsArray.length; @@ -139,11 +134,12 @@ async function executeRun(id: string) { return total + Object.keys(item).length; }, 0); } - + logger.info(`Extracted Items Count: ${extractedItemsCount}`); logger.info(`Extracted Screenshots Count: ${extractedScreenshotsCount}`); logger.info(`Total Rows Extracted: ${totalRowsExtracted}`); + capture('maxun-oss-run-created-manual', { runId: id, created_at: new Date().toISOString(), @@ -152,7 +148,7 @@ async function executeRun(id: string) { totalRowsExtracted, extractedScreenshotsCount, }); - + // Handle Google Sheets integration try { googleSheetUpdateTasks[plainRun.runId] = { @@ -166,6 +162,8 @@ async function executeRun(id: string) { logger.error(`Failed to update Google Sheet for run: ${plainRun.runId}: ${err.message}`); } + serverIo.of(plainRun.browserId).emit('run-completed', 'success'); + return { success: true, interpretationInfo: updatedRun.toJSON() @@ -179,6 +177,9 @@ async function executeRun(id: string) { status: 'failed', finishedAt: new Date().toLocaleString(), }); + + const plainRun = run.toJSON(); + serverIo.of(plainRun.browserId).emit('run-completed', 'failed'); } capture('maxun-oss-run-created-manual', { @@ -188,6 +189,7 @@ async function executeRun(id: string) { error_message: error.message, }); + return { success: false, error: error.message, From 8c0a83293bfb42ea3f8aab9ecedf1676812f9250 Mon Sep 17 00:00:00 2001 From: Rohit Date: Tue, 28 Jan 2025 12:57:27 +0530 Subject: [PATCH 14/15] feat: notify on page reload --- src/pages/MainPage.tsx | 59 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 57 insertions(+), 2 deletions(-) diff --git a/src/pages/MainPage.tsx b/src/pages/MainPage.tsx index de85ff317..2f6cb2066 100644 --- a/src/pages/MainPage.tsx +++ b/src/pages/MainPage.tsx @@ -53,6 +53,7 @@ export const MainPage = ({ handleEditRecording, initialContent }: MainPageProps) if (response) { notify('success', t('main_page.notifications.abort_success', { name: runningRecordingName })); await stopRecording(ids.browserId); + localStorage.removeItem('runningRobot'); } else { notify('error', t('main_page.notifications.abort_failed', { name: runningRecordingName })); } @@ -91,20 +92,29 @@ export const MainPage = ({ handleEditRecording, initialContent }: MainPageProps) const handleRunRecording = useCallback((settings: RunSettings) => { createRunForStoredRecording(runningRecordingId, settings).then(({ browserId, runId }: CreateRunResponse) => { setIds({ browserId, runId }); + + localStorage.setItem('runningRobot', JSON.stringify({ + browserId, + runId, + recordingName: runningRecordingName + })); + const socket = io(`${apiUrl}/${browserId}`, { transports: ["websocket"], rejectUnauthorized: false }); setSockets(sockets => [...sockets, socket]); + socket.on('debugMessage', debugMessageHandler); - socket.on('run-completed', (status) => { if (status === 'success') { notify('success', t('main_page.notifications.interpretation_success', { name: runningRecordingName })); } else { notify('error', t('main_page.notifications.interpretation_failed', { name: runningRecordingName })); } + + localStorage.removeItem('runningRobot'); setRunningRecordingName(''); setCurrentInterpretationLog(''); setRerenderRuns(true); @@ -121,7 +131,52 @@ export const MainPage = ({ handleEditRecording, initialContent }: MainPageProps) socket.off('debugMessage', debugMessageHandler); socket.off('run-completed'); } - }, [runningRecordingName, sockets, ids, debugMessageHandler]) + }, [runningRecordingName, sockets, ids, notify, debugMessageHandler]) + + useEffect(() => { + const storedRobotInfo = localStorage.getItem('runningRobot'); + + if (storedRobotInfo) { + try { + const { browserId, runId, recordingName } = JSON.parse(storedRobotInfo); + + setIds({ browserId, runId }); + setRunningRecordingName(recordingName); + setContent('runs'); + + const socket = io(`${apiUrl}/${browserId}`, { + transports: ["websocket"], + rejectUnauthorized: false + }); + + socket.on('debugMessage', debugMessageHandler); + socket.on('run-completed', (status) => { + if (status === 'success') { + notify('success', t('main_page.notifications.interpretation_success', { name: recordingName })); + } else { + notify('error', t('main_page.notifications.interpretation_failed', { name: recordingName })); + } + + localStorage.removeItem('runningRobot'); + setRunningRecordingName(''); + setCurrentInterpretationLog(''); + setRerenderRuns(true); + }); + + setSockets(prevSockets => [...prevSockets, socket]); + } catch (error) { + console.error('Error restoring robot state:', error); + localStorage.removeItem('runningRobot'); + } + } + + return () => { + sockets.forEach(socket => { + socket.off('debugMessage', debugMessageHandler); + socket.off('run-completed'); + }); + }; + }, []); const handleScheduleRecording = (settings: ScheduleSettings) => { scheduleStoredRecording(runningRecordingId, settings) From 8bb2ed357d58691fec0dc2a05655d721155d4380 Mon Sep 17 00:00:00 2001 From: Rohit Date: Tue, 4 Feb 2025 23:25:26 +0530 Subject: [PATCH 15/15] feat: add robot met id in local storage --- src/pages/MainPage.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pages/MainPage.tsx b/src/pages/MainPage.tsx index 90c5a12c0..c2c63af85 100644 --- a/src/pages/MainPage.tsx +++ b/src/pages/MainPage.tsx @@ -100,6 +100,7 @@ export const MainPage = ({ handleEditRecording, initialContent }: MainPageProps) localStorage.setItem('runningRobot', JSON.stringify({ browserId, runId, + robotMetaId, recordingName: runningRecordingName })); @@ -144,9 +145,9 @@ export const MainPage = ({ handleEditRecording, initialContent }: MainPageProps) if (storedRobotInfo) { try { - const { browserId, runId, recordingName } = JSON.parse(storedRobotInfo); + const { browserId, runId, robotMetaId, recordingName } = JSON.parse(storedRobotInfo); - setIds({ browserId, runId }); + setIds({ browserId, runId, robotMetaId }); setRunningRecordingName(recordingName); setContent('runs');