Merge pull request #629 from getmaxun/abort-fix

fix: abort operation
This commit is contained in:
Karishma Shukla
2025-06-12 23:33:11 +05:30
committed by GitHub
4 changed files with 179 additions and 172 deletions

View File

@@ -1,51 +0,0 @@
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/playwright:v1.46.0-noble
# Set working directory
WORKDIR /app
# Install node dependencies
COPY package*.json ./
COPY src ./src
COPY public ./public
COPY server ./server
COPY tsconfig.json ./
COPY server/tsconfig.json ./server/
# COPY server/start.sh ./
# Install dependencies
RUN npm install --legacy-peer-deps
# Install Playwright browsers and dependencies
RUN npx playwright install --with-deps chromium
# Create the Chromium data directory with necessary permissions
RUN mkdir -p /tmp/chromium-data-dir && \
chmod -R 777 /tmp/chromium-data-dir
# Install dependencies
RUN apt-get update && apt-get install -y \
libgbm1 \
libnss3 \
libatk1.0-0 \
libatk-bridge2.0-0 \
libdrm2 \
libxkbcommon0 \
libglib2.0-0 \
libdbus-1-3 \
libx11-xcb1 \
libxcb1 \
libxcomposite1 \
libxcursor1 \
libxdamage1 \
libxext6 \
libxi6 \
libxtst6 \
&& rm -rf /var/lib/apt/lists/* \
&& mkdir -p /tmp/.X11-unix && chmod 1777 /tmp/.X11-unix
# Expose backend port
EXPOSE ${BACKEND_PORT:-8080}
# Run migrations & start backend using start script
#CMD ["npm", "run", "server"]
CMD ["sh", "-c", "npm run migrate && npm run server"]

View File

@@ -23,7 +23,7 @@ chromium.use(stealthPlugin());
export const router = Router();
export const processWorkflowActions = async (workflow: any[], checkLimit: boolean = false): Promise<any[]> => {
const processedWorkflow = JSON.parse(JSON.stringify(workflow));
const processedWorkflow = JSON.parse(JSON.stringify(workflow));
processedWorkflow.forEach((pair: any) => {
pair.what.forEach((action: any) => {
@@ -108,52 +108,52 @@ router.get('/recordings/:id', requireSignIn, async (req, res) => {
router.get(('/recordings/:id/runs'), requireSignIn, async (req, res) => {
try {
const runs = await Run.findAll({
where: {
robotMetaId: req.params.id
},
raw: true
where: {
robotMetaId: req.params.id
},
raw: true
});
const formattedRuns = runs.map(formatRunResponse);
const response = {
statusCode: 200,
messageCode: "success",
runs: {
statusCode: 200,
messageCode: "success",
runs: {
totalCount: formattedRuns.length,
items: formattedRuns,
},
},
};
res.status(200).json(response);
} catch (error) {
} catch (error) {
console.error("Error fetching runs:", error);
res.status(500).json({
statusCode: 500,
messageCode: "error",
message: "Failed to retrieve runs",
statusCode: 500,
messageCode: "error",
message: "Failed to retrieve runs",
});
}
}
})
function formatRunResponse(run: any) {
const formattedRun = {
id: run.id,
status: run.status,
name: run.name,
robotId: run.robotMetaId, // Renaming robotMetaId to robotId
startedAt: run.startedAt,
finishedAt: run.finishedAt,
runId: run.runId,
runByUserId: run.runByUserId,
runByScheduleId: run.runByScheduleId,
runByAPI: run.runByAPI,
data: {},
screenshot: null,
id: run.id,
status: run.status,
name: run.name,
robotId: run.robotMetaId, // Renaming robotMetaId to robotId
startedAt: run.startedAt,
finishedAt: run.finishedAt,
runId: run.runId,
runByUserId: run.runByUserId,
runByScheduleId: run.runByScheduleId,
runByAPI: run.runByAPI,
data: {},
screenshot: null,
};
if (run.serializableOutput && run.serializableOutput['item-0']) {
formattedRun.data = run.serializableOutput['item-0'];
formattedRun.data = run.serializableOutput['item-0'];
} else if (run.binaryOutput && run.binaryOutput['item-0']) {
formattedRun.screenshot = run.binaryOutput['item-0'];
formattedRun.screenshot = run.binaryOutput['item-0'];
}
return formattedRun;
@@ -170,81 +170,81 @@ interface Credentials {
function handleWorkflowActions(workflow: any[], credentials: Credentials) {
return workflow.map(step => {
if (!step.what) return step;
if (!step.what) return step;
const newWhat: any[] = [];
const processedSelectors = new Set<string>();
const newWhat: any[] = [];
const processedSelectors = new Set<string>();
for (let i = 0; i < step.what.length; i++) {
const action = step.what[i];
for (let i = 0; i < step.what.length; i++) {
const action = step.what[i];
if (!action?.action || !action?.args?.[0]) {
newWhat.push(action);
continue;
}
const selector = action.args[0];
const credential = credentials[selector];
if (!credential) {
newWhat.push(action);
continue;
}
if (action.action === 'click') {
newWhat.push(action);
if (!processedSelectors.has(selector) &&
i + 1 < step.what.length &&
(step.what[i + 1].action === 'type' || step.what[i + 1].action === 'press')) {
newWhat.push({
action: 'type',
args: [selector, encrypt(credential.value), credential.type]
});
newWhat.push({
action: 'waitForLoadState',
args: ['networkidle']
});
processedSelectors.add(selector);
while (i + 1 < step.what.length &&
(step.what[i + 1].action === 'type' ||
step.what[i + 1].action === 'press' ||
step.what[i + 1].action === 'waitForLoadState')) {
i++;
}
}
} else if ((action.action === 'type' || action.action === 'press') &&
!processedSelectors.has(selector)) {
newWhat.push({
action: 'type',
args: [selector, encrypt(credential.value), credential.type]
});
newWhat.push({
action: 'waitForLoadState',
args: ['networkidle']
});
processedSelectors.add(selector);
// Skip subsequent type/press/waitForLoadState actions for this selector
while (i + 1 < step.what.length &&
(step.what[i + 1].action === 'type' ||
step.what[i + 1].action === 'press' ||
step.what[i + 1].action === 'waitForLoadState')) {
i++;
}
}
if (!action?.action || !action?.args?.[0]) {
newWhat.push(action);
continue;
}
return {
...step,
what: newWhat
};
const selector = action.args[0];
const credential = credentials[selector];
if (!credential) {
newWhat.push(action);
continue;
}
if (action.action === 'click') {
newWhat.push(action);
if (!processedSelectors.has(selector) &&
i + 1 < step.what.length &&
(step.what[i + 1].action === 'type' || step.what[i + 1].action === 'press')) {
newWhat.push({
action: 'type',
args: [selector, encrypt(credential.value), credential.type]
});
newWhat.push({
action: 'waitForLoadState',
args: ['networkidle']
});
processedSelectors.add(selector);
while (i + 1 < step.what.length &&
(step.what[i + 1].action === 'type' ||
step.what[i + 1].action === 'press' ||
step.what[i + 1].action === 'waitForLoadState')) {
i++;
}
}
} else if ((action.action === 'type' || action.action === 'press') &&
!processedSelectors.has(selector)) {
newWhat.push({
action: 'type',
args: [selector, encrypt(credential.value), credential.type]
});
newWhat.push({
action: 'waitForLoadState',
args: ['networkidle']
});
processedSelectors.add(selector);
// Skip subsequent type/press/waitForLoadState actions for this selector
while (i + 1 < step.what.length &&
(step.what[i + 1].action === 'type' ||
step.what[i + 1].action === 'press' ||
step.what[i + 1].action === 'waitForLoadState')) {
i++;
}
}
}
return {
...step,
what: newWhat
};
});
}
@@ -630,6 +630,43 @@ router.put('/runs/:id', requireSignIn, async (req: AuthenticatedRequest, res) =>
robotMetaId: recording.recording_meta.id,
queued: true
});
} else {
const browserId = getActiveBrowserIdByState(req.user.id, "run")
if (browserId) {
// User has reached the browser limit, queue the run
try {
// Create the run record with 'queued' status
await Run.create({
status: 'queued',
name: recording.recording_meta.name,
robotId: recording.id,
robotMetaId: recording.recording_meta.id,
startedAt: new Date().toLocaleString(),
finishedAt: '',
browserId: browserId, // Random will be updated later
interpreterSettings: req.body,
log: 'Run queued - waiting for available browser slot',
runId,
runByUserId: req.user.id,
serializableOutput: {},
binaryOutput: {},
});
return res.send({
browserId: browserId,
runId: runId,
robotMetaId: recording.recording_meta.id,
queued: true,
});
} catch (queueError: any) {
logger.log('error', `Failed to queue run job: ${queueError.message}`);
return res.status(503).send({ error: 'Unable to queue run, please try again later' });
}
} else {
logger.log('info', "Browser id does not exist");
return res.send('');
}
}
} catch (e) {
const { message } = e as Error;
@@ -949,6 +986,26 @@ router.post('/runs/abort/:id', requireSignIn, async (req: AuthenticatedRequest,
});
}
if (!['running', 'queued'].includes(run.status)) {
return res.status(400).send({
error: `Cannot abort run with status: ${run.status}`
});
}
await run.update({
status: 'aborting'
});
if (run.status === 'queued') {
await run.update({
status: 'aborted',
finishedAt: new Date().toLocaleString(),
log: 'Run aborted while queued'
});
return res.send({ success: true, message: 'Queued run aborted' });
}
const userQueueName = `abort-run-user-${req.user.id}`;
await pgBoss.createQueue(userQueueName);
@@ -965,6 +1022,7 @@ router.post('/runs/abort/:id', requireSignIn, async (req: AuthenticatedRequest,
jobId,
isQueued: false
});
} catch (e) {
const { message } = e as Error;
logger.log('error', `Error aborting run ${req.params.id}: ${message}`);

View File

@@ -444,7 +444,7 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe
ref={logEndRef} />
</div>
</Box>
{row.status === 'running' ? <Button
{row.status === 'running' || row.status === 'queued' ? <Button
color="error"
onClick={abortRunHandler}
>