diff --git a/runner.js b/runner.js index d49598e..b1f0ed4 100644 --- a/runner.js +++ b/runner.js @@ -1,44 +1,20 @@ -import { spawn, execSync } from 'child_process'; +import { spawn } from 'child_process'; import { join, dirname } from 'path'; import { fileURLToPath } from 'url'; import { existsSync } from 'fs'; const __dirname = dirname(fileURLToPath(import.meta.url)); +const LOCAL_BIN = join(__dirname, '..', 'sitespeed.io', 'bin', 'sitespeed.js'); -// Resolve sitespeed.io binary. -// In Docker the global install is on PATH; outside Docker use the local clone. -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.'); +// Shell-escape a single argument (single-quote wrapping) +function q(arg) { + return `'${String(arg).replace(/'/g, "'\\''")}'`; } export function runTest(job, onLine) { return new Promise((resolve, reject) => { const outputFolder = join(__dirname, 'reports', job.id); - - let bin, scriptArg; - try { - ({ cmd: bin, scriptArg } = resolveBin()); - } catch (err) { - return reject(err); - } + const isDocker = !!process.env.IN_DOCKER; const sitespeedArgs = [ job.url, @@ -53,27 +29,34 @@ export function runTest(job, onLine) { if (job.mobile) sitespeedArgs.push('--mobile'); - // Chrome flags required in Docker - if (process.env.IN_DOCKER) { + if (isDocker) { sitespeedArgs.push('--browsertime.chrome.args', 'no-sandbox'); sitespeedArgs.push('--browsertime.chrome.args', 'disable-dev-shm-usage'); sitespeedArgs.push('--browsertime.chrome.args', 'disable-gpu'); } - // Build the final argv for spawn - const spawnArgs = scriptArg - ? [scriptArg, ...sitespeedArgs] // node /path/to/sitespeed.js - : sitespeedArgs; // sitespeed.io - const env = { ...process.env, DISPLAY: process.env.DISPLAY || ':99' }; - onLine(`[runner] binary: ${bin}${scriptArg ? ' ' + scriptArg : ''}`); - onLine(`[runner] DISPLAY: ${env.DISPLAY}`); - onLine(`[runner] outputFolder: ${outputFolder}`); + let child; - const child = spawn(bin, spawnArgs, { cwd: __dirname, env }); + if (isDocker) { + // In Docker, sitespeed.io is on PATH but execSync can't see it from Node's + // limited environment. Use 'sh -c' so the full shell PATH is used. + const shellCmd = ['sitespeed.io', ...sitespeedArgs].map(q).join(' '); + onLine(`[runner] sh -c ${shellCmd.slice(0, 120)}...`); + onLine(`[runner] DISPLAY=${env.DISPLAY}`); + child = spawn('sh', ['-c', shellCmd], { cwd: __dirname, env }); + } else { + if (!existsSync(LOCAL_BIN)) { + return reject(new Error( + `Local sitespeed.io not found at ${LOCAL_BIN}\n` + + `Run: cd /home/malin/c0ding/sitespeed.io && npm install` + )); + } + onLine(`[runner] node ${LOCAL_BIN.slice(-40)}...`); + child = spawn('node', [LOCAL_BIN, ...sitespeedArgs], { cwd: __dirname, env }); + } - // Collect all output lines — included in error if process fails const allLines = []; child.stdout.on('data', (data) => { @@ -90,14 +73,13 @@ export function runTest(job, onLine) { if (code === 0) { resolve(outputFolder); } else { - // 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) => { - reject(new Error(`Failed to spawn sitespeed.io (${bin}): ${err.message}`)); + reject(new Error(`Failed to spawn process: ${err.message}`)); }); }); }