-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathscreenshot-server.js
More file actions
132 lines (111 loc) · 3.57 KB
/
Copy pathscreenshot-server.js
File metadata and controls
132 lines (111 loc) · 3.57 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
const express = require('express');
const cors = require('cors');
const fs = require('node:fs');
const puppeteer = require('puppeteer-core');
const app = express();
const PORT = 3001;
// Middleware
app.use(cors());
app.use(express.json({ limit: '50mb' }));
// Browser instance (reused for performance)
let browser = null;
function getChromePath() {
const candidates = [
process.env.CHROME_PATH,
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
'/Applications/Chromium.app/Contents/MacOS/Chromium',
'/usr/bin/google-chrome-stable',
'/usr/bin/google-chrome',
'/usr/bin/chromium-browser',
'/usr/bin/chromium',
].filter(Boolean);
const executablePath = candidates.find(candidate => fs.existsSync(candidate));
if (!executablePath) {
throw new Error('Chrome executable not found. Install Google Chrome or set CHROME_PATH to a Chromium executable.');
}
return executablePath;
}
async function getBrowser() {
if (!browser) {
browser = await puppeteer.launch({
executablePath: getChromePath(),
headless: 'new',
args: ['--no-sandbox', '--disable-setuid-sandbox']
});
}
return browser;
}
// Screenshot endpoint
app.post('/screenshot', async (req, res) => {
const { html, width = 1200, height = 800 } = req.body;
if (!html) {
return res.status(400).json({ error: 'HTML content is required' });
}
let page = null;
try {
const browserInstance = await getBrowser();
page = await browserInstance.newPage();
await page.setViewport({ width, height });
// Set content and wait for network idle
await page.setContent(html, {
waitUntil: ['load', 'networkidle0'],
timeout: 30000
});
// Small delay to ensure any animations/transitions complete
await new Promise(resolve => setTimeout(resolve, 500));
// Capture screenshot
const screenshot = await page.screenshot({
type: 'png',
fullPage: false,
encoding: 'base64'
});
res.json({ screenshot });
} catch (error) {
console.error('Screenshot error:', error);
res.status(500).json({ error: error.message });
} finally {
if (page) {
await page.close();
}
}
});
// Health check
app.get('/health', (req, res) => {
let chromePath = null;
let chromeAvailable = false;
try {
chromePath = getChromePath();
chromeAvailable = true;
} catch (error) {
chromePath = error.message;
}
res.json({
status: 'ok',
service: 'skilleval-local-runner',
browser: browser ? 'running' : 'not started',
chromeAvailable,
chromePath,
});
});
app.get('/api/health', (req, res) => {
res.redirect(307, '/health');
});
// Graceful shutdown
process.on('SIGINT', async () => {
console.log('\nShutting down...');
if (browser) {
await browser.close();
}
clearInterval(keepAlive);
server.close();
process.exit(0);
});
const server = app.listen(PORT, '127.0.0.1', () => {
console.log(`SkillEval local runner running on http://localhost:${PORT}`);
console.log('Endpoints:');
console.log(' POST /screenshot - Capture screenshot of HTML');
console.log(' GET /health - Health check');
});
// Some desktop sandbox launchers do not keep the Node event loop alive for the
// HTTP server handle alone, so keep an explicit heartbeat for foreground runs.
const keepAlive = setInterval(() => {}, 60_000);