Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions Dockerfile.backend
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,6 @@ COPY server/tsconfig.json ./server/
# 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
Expand Down
5 changes: 5 additions & 0 deletions ENVEXAMPLE
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,8 @@ AIRTABLE_REDIRECT_URI=http://localhost:8080/auth/airtable/callback

# Telemetry Settings - Please keep it enabled. Keeping it enabled helps us understand how the product is used and assess the impact of any new changes.
MAXUN_TELEMETRY=true

# WebSocket port for browser CDP connections
BROWSER_WS_PORT=3001
BROWSER_HEALTH_PORT=3002
BROWSER_WS_HOST=browser
9 changes: 9 additions & 0 deletions browser/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
node_modules
npm-debug.log
.env
.git
.gitignore
dist
*.ts
!*.d.ts
tsconfig.json
30 changes: 30 additions & 0 deletions browser/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
FROM mcr.microsoft.com/playwright:v1.57.0-jammy

WORKDIR /app

# Copy package files
COPY browser/package*.json ./

# Install dependencies
RUN npm ci

# Copy TypeScript source and config
COPY browser/server.ts ./
COPY browser/tsconfig.json ./

# Build TypeScript
RUN npm run build

# Accept build arguments for ports (with defaults)
ARG BROWSER_WS_PORT=3001
ARG BROWSER_HEALTH_PORT=3002

# Set as environment variables
ENV BROWSER_WS_PORT=${BROWSER_WS_PORT}
ENV BROWSER_HEALTH_PORT=${BROWSER_HEALTH_PORT}

# Expose ports dynamically based on build args
EXPOSE ${BROWSER_WS_PORT} ${BROWSER_HEALTH_PORT}

# Start the browser service (run compiled JS)
CMD ["node", "dist/server.js"]
21 changes: 21 additions & 0 deletions browser/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "maxun-browser-service",
"version": "1.0.0",
"description": "Browser service that exposes Playwright browsers via WebSocket with stealth plugins",
"main": "dist/server.js",
"scripts": {
"build": "tsc",
"start": "node dist/server.js",
"dev": "ts-node server.ts"
},
"dependencies": {
"playwright": "1.57.0",
"playwright-extra": "^4.3.6",
"puppeteer-extra-plugin-stealth": "^2.11.2"
},
"devDependencies": {
"@types/node": "^22.7.9",
"typescript": "^5.0.0",
"ts-node": "^10.9.2"
}
}
92 changes: 92 additions & 0 deletions browser/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { chromium } from 'playwright-extra';
import stealthPlugin from 'puppeteer-extra-plugin-stealth';
import http from 'http';
import type { BrowserServer } from 'playwright';

// Apply stealth plugin to chromium
chromium.use(stealthPlugin());

let browserServer: BrowserServer | null = null;

// Configurable ports with defaults
const BROWSER_WS_PORT = parseInt(process.env.BROWSER_WS_PORT || '3001', 10);
const BROWSER_HEALTH_PORT = parseInt(process.env.BROWSER_HEALTH_PORT || '3002', 10);

async function start(): Promise<void> {
console.log('Starting Maxun Browser Service...');
console.log(`WebSocket port: ${BROWSER_WS_PORT}`);
console.log(`Health check port: ${BROWSER_HEALTH_PORT}`);

try {
// Launch browser server that exposes WebSocket endpoint
browserServer = await chromium.launchServer({
headless: true,
args: [
'--disable-blink-features=AutomationControlled',
'--disable-web-security',
'--disable-features=IsolateOrigins,site-per-process',
'--disable-site-isolation-trials',
'--disable-extensions',
'--no-sandbox',
'--disable-dev-shm-usage',
'--disable-gpu',
'--force-color-profile=srgb',
'--force-device-scale-factor=2',
'--ignore-certificate-errors',
'--mute-audio'
],
port: BROWSER_WS_PORT,
});

console.log(`✅ Browser WebSocket endpoint ready: ${browserServer.wsEndpoint()}`);
console.log(`✅ Stealth plugin enabled`);

// Health check HTTP server
const healthServer = http.createServer((req, res) => {
if (req.url === '/health') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
status: 'healthy',
wsEndpoint: browserServer?.wsEndpoint(),
wsPort: BROWSER_WS_PORT,
healthPort: BROWSER_HEALTH_PORT,
timestamp: new Date().toISOString()
}));
} else if (req.url === '/') {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end(`Maxun Browser Service\nWebSocket: ${browserServer?.wsEndpoint()}\nHealth: http://localhost:${BROWSER_HEALTH_PORT}/health`);
} else {
res.writeHead(404);
res.end('Not Found');
}
});

healthServer.listen(BROWSER_HEALTH_PORT, () => {
console.log(`✅ Health check server running on port ${BROWSER_HEALTH_PORT}`);
console.log('Browser service is ready to accept connections!');
});
} catch (error) {
console.error('❌ Failed to start browser service:', error);
process.exit(1);
}
}

// Graceful shutdown
async function shutdown(): Promise<void> {
console.log('Shutting down browser service...');
if (browserServer) {
try {
await browserServer.close();
console.log('Browser server closed');
} catch (error) {
console.error('Error closing browser server:', error);
}
}
process.exit(0);
}
Comment on lines +74 to +86
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Health server is not closed during shutdown.

The healthServer HTTP server is created but not tracked or closed in the shutdown() function. This could cause the process to hang during graceful shutdown.

+let healthServer: http.Server | null = null;
+
 async function start(): Promise<void> {
     // ... existing code ...
-        const healthServer = http.createServer((req, res) => {
+        healthServer = http.createServer((req, res) => {
             // ... handlers ...
         });
     // ...
 }

 async function shutdown(): Promise<void> {
     console.log('Shutting down browser service...');
+    if (healthServer) {
+        healthServer.close();
+        console.log('Health server closed');
+    }
     if (browserServer) {
         // ... existing code ...
     }
     process.exit(0);
 }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In browser/server.ts around lines 74 to 86, the shutdown() function closes
browserServer but never closes the healthServer HTTP server created elsewhere,
which can leave the process hanging; update shutdown() to check if healthServer
exists, call await healthServer.close() inside the try/catch (log success and
errors similarly to browserServer), and ensure any references are cleaned up
(e.g., set healthServer = undefined) before calling process.exit(0); also make
sure healthServer is in module scope so shutdown() can access it.


process.on('SIGTERM', shutdown);
process.on('SIGINT', shutdown);

// Start the service
start().catch(console.error);
24 changes: 24 additions & 0 deletions browser/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": [
"ES2020"
],
"outDir": "./dist",
"rootDir": "./",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"moduleResolution": "node"
},
"include": [
"server.ts"
],
"exclude": [
"node_modules",
"dist"
]
}
36 changes: 36 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,42 @@ services:
depends_on:
- backend

browser:
build:
context: .
dockerfile: browser/Dockerfile
args:
BROWSER_WS_PORT: ${BROWSER_WS_PORT:-3001}
BROWSER_HEALTH_PORT: ${BROWSER_HEALTH_PORT:-3002}
ports:
- "${BROWSER_WS_PORT:-3001}:${BROWSER_WS_PORT:-3001}"
- "${BROWSER_HEALTH_PORT:-3002}:${BROWSER_HEALTH_PORT:-3002}"
environment:
- NODE_ENV=production
- DEBUG=pw:browser*
- BROWSER_WS_PORT=${BROWSER_WS_PORT:-3001}
- BROWSER_HEALTH_PORT=${BROWSER_HEALTH_PORT:-3002}
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:${BROWSER_HEALTH_PORT:-3002}/health"]
interval: 10s
timeout: 5s
retries: 3
start_period: 10s
deploy:
resources:
limits:
memory: 2G
cpus: '1.5'
reservations:
memory: 1G
cpus: '1.0'
security_opt:
- seccomp:unconfined
shm_size: 2gb
cap_add:
- SYS_ADMIN

volumes:
postgres_data:
minio_data:
8 changes: 4 additions & 4 deletions maxun-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,10 @@
"license": "AGPL-3.0-or-later",
"dependencies": {
"@cliqz/adblocker-playwright": "^1.31.3",
"@types/node": "22.7.9",
"cross-fetch": "^4.0.0",
"joi": "^17.6.0",
"playwright": "^1.20.1",
"playwright-extra": "^4.3.6",
"puppeteer-extra-plugin-stealth": "^2.11.2"
"playwright-core": "1.57.0",
"turndown": "^7.2.2"
}
}
}
6 changes: 3 additions & 3 deletions maxun-core/src/interpret.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* eslint-disable no-await-in-loop, no-restricted-syntax */
import { ElementHandle, Page, PageScreenshotOptions } from 'playwright';
import { ElementHandle, Page, PageScreenshotOptions } from 'playwright-core';
import { PlaywrightBlocker } from '@cliqz/adblocker-playwright';
import fetch from 'cross-fetch';
import path from 'path';
Expand Down Expand Up @@ -144,7 +144,7 @@ export default class Interpreter extends EventEmitter {
private async applyAdBlocker(page: Page): Promise<void> {
if (this.blocker) {
try {
await this.blocker.enableBlockingInPage(page);
await this.blocker.enableBlockingInPage(page as any);
} catch (err) {
this.log(`Ad-blocker operation failed:`, Level.ERROR);
}
Expand All @@ -154,7 +154,7 @@ export default class Interpreter extends EventEmitter {
private async disableAdBlocker(page: Page): Promise<void> {
if (this.blocker) {
try {
await this.blocker.disableBlockingInPage(page);
await this.blocker.disableBlockingInPage(page as any);
} catch (err) {
this.log(`Ad-blocker operation failed:`, Level.ERROR);
}
Expand Down
2 changes: 1 addition & 1 deletion maxun-core/src/types/workflow.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Page } from 'playwright';
import { Page } from 'playwright-core';
import {
naryOperators, unaryOperators, operators, meta,
} from './logic';
Expand Down
7 changes: 3 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,9 @@
"node-cron": "^3.0.3",
"pg": "^8.13.0",
"pg-boss": "^10.1.6",
"playwright": "^1.48.2",
"playwright-extra": "^4.3.6",
"pkce-challenge": "^4.1.0",
"playwright-core": "1.57.0",
"posthog-node": "^4.2.1",
"puppeteer-extra-plugin-stealth": "^2.11.2",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"react-i18next": "^15.1.3",
Expand Down Expand Up @@ -129,4 +128,4 @@
"vite": "^5.4.10",
"zod": "^3.25.62"
}
}
}
4 changes: 0 additions & 4 deletions server/src/api/record.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import { Router, Request, Response } from 'express';
import { chromium } from "playwright-extra";
import stealthPlugin from 'puppeteer-extra-plugin-stealth';
import { requireAPIKey } from "../middlewares/api";
import Robot from "../models/Robot";
import Run from "../models/Run";
Expand All @@ -20,8 +18,6 @@ import { airtableUpdateTasks, processAirtableUpdates } from "../workflow-managem
import { sendWebhook } from "../routes/webhook";
import { convertPageToHTML, convertPageToMarkdown } from '../markdownify/scrape';

chromium.use(stealthPlugin());

const router = Router();

const formatRecording = (recordingData: any) => {
Expand Down
Loading