99 lines
4.1 KiB
JavaScript
99 lines
4.1 KiB
JavaScript
/**
|
|
* Screenshot helper for this env: geckodriver + headless Firefox.
|
|
* node dev/wd-shot.mjs <url> <setupExpr> <shotFile> [width] [height] [timeoutMs]
|
|
* <setupExpr> is a WebDriver execute/sync script (function body) that runs on
|
|
* the page before the screenshot; may be '' to skip. The page is given time
|
|
* to finish loading (the expression is polled until it returns non-null).
|
|
*/
|
|
import { spawn, execFileSync } from 'node:child_process';
|
|
import { existsSync, readdirSync, writeFileSync } from 'node:fs';
|
|
|
|
const [url, setupExpr, shotFile, w = '1280', h = '720', timeoutMs = '30000'] = process.argv.slice(2);
|
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
|
|
let geckodriver;
|
|
try { geckodriver = execFileSync('sh', ['-c', 'command -v geckodriver'], { encoding: 'utf8' }).trim(); }
|
|
catch { console.error('need geckodriver on PATH'); process.exit(2); }
|
|
|
|
const firefoxCandidates = [];
|
|
try {
|
|
const ff = execFileSync('sh', ['-c', 'command -v firefox'], { encoding: 'utf8' }).trim();
|
|
if (ff) firefoxCandidates.push(ff);
|
|
} catch { /* ignore */ }
|
|
try {
|
|
const snapRev = readdirSync('/snap/firefox', { withFileTypes: true }).map((d) => d.name);
|
|
for (const rev of [null, ...snapRev]) {
|
|
const base = rev ? `/snap/firefox/${rev}` : '/snap/firefox/current';
|
|
const p = `${base}/usr/lib/firefox/firefox`;
|
|
if (existsSync(p)) firefoxCandidates.push(p);
|
|
}
|
|
} catch { /* no snap firefox */ }
|
|
if (firefoxCandidates.length === 0) { console.error('no firefox binary'); process.exit(2); }
|
|
|
|
const PORT = 4460 + (process.pid % 50);
|
|
const driver = spawn(geckodriver, ['--port', String(PORT), '--log', 'fatal'], { stdio: 'ignore' });
|
|
const base = `http://127.0.0.1:${PORT}`;
|
|
const fail = (msg, code = 1) => { try { driver.kill('SIGKILL'); } catch { /* ok */ } console.error(msg); process.exit(code); };
|
|
|
|
const wd = async (method, path, body) => {
|
|
const res = await fetch(base + path, { method, headers: { 'Content-Type': 'application/json' }, body: body === undefined ? undefined : JSON.stringify(body) });
|
|
const text = await res.text();
|
|
let data = null; try { data = JSON.parse(text); } catch { /* ignore */ }
|
|
if (!res.ok) throw new Error(`WebDriver ${method} ${path} → ${res.status}: ${text.slice(0, 300)}`);
|
|
return data;
|
|
};
|
|
|
|
try {
|
|
let up = false;
|
|
for (let i = 0; i < 60; i++) {
|
|
try { const v = await (await fetch(`${base}/status`)).json(); if (v?.value?.ready || v?.status === 'ok' || v?.value) { up = true; break; } } catch { /* not up */ }
|
|
await sleep(250);
|
|
}
|
|
if (!up) fail('geckodriver never came up');
|
|
|
|
let sess; let lastErr = null;
|
|
for (const ff of firefoxCandidates) {
|
|
try {
|
|
sess = await wd('POST', '/session', {
|
|
capabilities: { alwaysMatch: {
|
|
browserName: 'firefox',
|
|
'moz:firefoxOptions': { args: ['-headless'], binary: ff },
|
|
} },
|
|
});
|
|
break;
|
|
} catch (err) { lastErr = String(err?.message ?? err); }
|
|
}
|
|
if (!sess) fail(`no firefox candidate accepted: ${lastErr}`);
|
|
const sid = sess.value?.sessionId;
|
|
if (!sid) fail('no session id');
|
|
|
|
if (w && h) {
|
|
try { await wd('POST', `/session/${sid}/window`, { handle: 'current', state: { width: Number(w), height: Number(h) } }); } catch { /* keep default size */ }
|
|
}
|
|
await wd('POST', `/session/${sid}/url`, { url });
|
|
|
|
if (setupExpr) {
|
|
const deadline = Date.now() + Number(timeoutMs);
|
|
let last = null;
|
|
while (Date.now() < deadline) {
|
|
await sleep(250);
|
|
try {
|
|
const r = await wd('POST', `/session/${sid}/execute/sync`, { script: setupExpr, args: [] });
|
|
if (r.value !== undefined && r.value !== null) { last = r.value; break; }
|
|
} catch (err) { last = `ERR ${String(err?.message ?? err)}`; }
|
|
}
|
|
if (last === null) fail('setup expression never returned a value');
|
|
if (typeof last === 'string' && last.startsWith('ERR')) fail(last);
|
|
console.log('setup →', JSON.stringify(last).slice(0, 400));
|
|
await sleep(400);
|
|
}
|
|
|
|
const shot = await wd('GET', `/session/${sid}/screenshot`);
|
|
writeFileSync(shotFile, Buffer.from(shot.value, 'base64'));
|
|
console.log('saved', shotFile);
|
|
|
|
await wd('DELETE', `/session/${sid}`).catch(() => {});
|
|
} finally {
|
|
try { driver.kill('SIGKILL'); } catch { /* ok */ }
|
|
}
|