fix: surface full sitespeed.io error output + robust binary resolution
- runner.js: auto-detects sitespeed.io via which/candidates instead of hardcoded path; collects all stderr and appends last 20 lines to the error so the real failure is visible in the UI log - running.pug: render multi-line error messages line-by-line Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
82
runner.js
82
runner.js
@@ -1,19 +1,46 @@
|
|||||||
import { spawn } from 'child_process';
|
import { spawn, execSync } from 'child_process';
|
||||||
import { join, dirname } from 'path';
|
import { join, dirname } from 'path';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
|
import { existsSync } from 'fs';
|
||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
// Path to sitespeed.io — works both locally and in Docker
|
// Resolve sitespeed.io binary.
|
||||||
const SITESPEED_BIN = process.env.SITESPEED_BIN ||
|
// In Docker the global install is on PATH; outside Docker use the local clone.
|
||||||
join(__dirname, '..', 'sitespeed.io', 'bin', 'sitespeed.js');
|
function resolveBin() {
|
||||||
|
if (process.env.SITESPEED_BIN) return { cmd: 'node', scriptArg: process.env.SITESPEED_BIN };
|
||||||
|
|
||||||
|
// Try global install on PATH (works in Docker image)
|
||||||
|
try {
|
||||||
|
const globalPath = execSync('which sitespeed.io', { encoding: 'utf8' }).trim();
|
||||||
|
if (globalPath) return { cmd: globalPath, scriptArg: null };
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
// Try common global npm location
|
||||||
|
const candidates = [
|
||||||
|
'/usr/local/lib/node_modules/sitespeed.io/bin/sitespeed.js',
|
||||||
|
'/usr/lib/node_modules/sitespeed.io/bin/sitespeed.js',
|
||||||
|
join(__dirname, '..', 'sitespeed.io', 'bin', 'sitespeed.js'),
|
||||||
|
];
|
||||||
|
for (const p of candidates) {
|
||||||
|
if (existsSync(p)) return { cmd: 'node', scriptArg: p };
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('sitespeed.io binary not found. Set SITESPEED_BIN env var.');
|
||||||
|
}
|
||||||
|
|
||||||
export function runTest(job, onLine) {
|
export function runTest(job, onLine) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const outputFolder = join(__dirname, 'reports', job.id);
|
const outputFolder = join(__dirname, 'reports', job.id);
|
||||||
|
|
||||||
const args = [
|
let bin, scriptArg;
|
||||||
SITESPEED_BIN,
|
try {
|
||||||
|
({ cmd: bin, scriptArg } = resolveBin());
|
||||||
|
} catch (err) {
|
||||||
|
return reject(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sitespeedArgs = [
|
||||||
job.url,
|
job.url,
|
||||||
'--browser', job.browser,
|
'--browser', job.browser,
|
||||||
'-n', String(job.runs),
|
'-n', String(job.runs),
|
||||||
@@ -24,46 +51,53 @@ export function runTest(job, onLine) {
|
|||||||
'--coach',
|
'--coach',
|
||||||
];
|
];
|
||||||
|
|
||||||
if (job.mobile) {
|
if (job.mobile) sitespeedArgs.push('--mobile');
|
||||||
args.push('--mobile');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Chrome needs these flags when running inside Docker
|
// Chrome flags required in Docker
|
||||||
if (process.env.IN_DOCKER) {
|
if (process.env.IN_DOCKER) {
|
||||||
args.push('--browsertime.chrome.args', 'no-sandbox');
|
sitespeedArgs.push('--browsertime.chrome.args', 'no-sandbox');
|
||||||
args.push('--browsertime.chrome.args', 'disable-dev-shm-usage');
|
sitespeedArgs.push('--browsertime.chrome.args', 'disable-dev-shm-usage');
|
||||||
// Disable GPU compositing — not needed in Xvfb and can cause crashes
|
sitespeedArgs.push('--browsertime.chrome.args', 'disable-gpu');
|
||||||
args.push('--browsertime.chrome.args', 'disable-gpu');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onLine(`[runner] Starting: node ${args.slice(0, 3).join(' ')} ...`);
|
// Build the final argv for spawn
|
||||||
onLine(`[runner] Output folder: ${outputFolder}`);
|
const spawnArgs = scriptArg
|
||||||
|
? [scriptArg, ...sitespeedArgs] // node /path/to/sitespeed.js <args>
|
||||||
|
: sitespeedArgs; // sitespeed.io <args>
|
||||||
|
|
||||||
const child = spawn('node', args, {
|
const env = { ...process.env, DISPLAY: process.env.DISPLAY || ':99' };
|
||||||
cwd: __dirname,
|
|
||||||
env: { ...process.env, DISPLAY: process.env.DISPLAY || ':99' },
|
onLine(`[runner] binary: ${bin}${scriptArg ? ' ' + scriptArg : ''}`);
|
||||||
});
|
onLine(`[runner] DISPLAY: ${env.DISPLAY}`);
|
||||||
|
onLine(`[runner] outputFolder: ${outputFolder}`);
|
||||||
|
|
||||||
|
const child = spawn(bin, spawnArgs, { cwd: __dirname, env });
|
||||||
|
|
||||||
|
// Collect all output lines — included in error if process fails
|
||||||
|
const allLines = [];
|
||||||
|
|
||||||
child.stdout.on('data', (data) => {
|
child.stdout.on('data', (data) => {
|
||||||
const lines = data.toString().split('\n').filter(Boolean);
|
const lines = data.toString().split('\n').filter(Boolean);
|
||||||
for (const line of lines) onLine(line);
|
for (const line of lines) { allLines.push(line); onLine(line); }
|
||||||
});
|
});
|
||||||
|
|
||||||
child.stderr.on('data', (data) => {
|
child.stderr.on('data', (data) => {
|
||||||
const lines = data.toString().split('\n').filter(Boolean);
|
const lines = data.toString().split('\n').filter(Boolean);
|
||||||
for (const line of lines) onLine(`[stderr] ${line}`);
|
for (const line of lines) { allLines.push('[stderr] ' + line); onLine('[stderr] ' + line); }
|
||||||
});
|
});
|
||||||
|
|
||||||
child.on('close', (code) => {
|
child.on('close', (code) => {
|
||||||
if (code === 0) {
|
if (code === 0) {
|
||||||
resolve(outputFolder);
|
resolve(outputFolder);
|
||||||
} else {
|
} else {
|
||||||
reject(new Error(`sitespeed.io exited with code ${code}`));
|
// Surface the last 20 lines of output so the error is visible in the UI
|
||||||
|
const tail = allLines.slice(-20).join('\n');
|
||||||
|
reject(new Error(`sitespeed.io exited with code ${code}\n${tail}`));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
child.on('error', (err) => {
|
child.on('error', (err) => {
|
||||||
reject(new Error(`Failed to spawn sitespeed.io: ${err.message}`));
|
reject(new Error(`Failed to spawn sitespeed.io (${bin}): ${err.message}`));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,9 +65,14 @@ block content
|
|||||||
badge.className = 'text-xs px-3 py-1 rounded-full bg-red-100 text-red-800 font-semibold';
|
badge.className = 'text-xs px-3 py-1 rounded-full bg-red-100 text-red-800 font-semibold';
|
||||||
try {
|
try {
|
||||||
const d = JSON.parse(e.data);
|
const d = JSON.parse(e.data);
|
||||||
|
// Render each line of the error message separately so multi-line errors are readable
|
||||||
|
const lines = d.message.split('\n');
|
||||||
|
for (const line of lines) {
|
||||||
const el = document.createElement('div');
|
const el = document.createElement('div');
|
||||||
el.className = 'text-red-400 log-line';
|
el.className = 'text-red-400 log-line';
|
||||||
el.textContent = '[ERROR] ' + d.message;
|
el.textContent = line;
|
||||||
logBox.appendChild(el);
|
logBox.appendChild(el);
|
||||||
|
}
|
||||||
|
logBox.scrollTop = logBox.scrollHeight;
|
||||||
} catch {}
|
} catch {}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user