check-firefox.mjs (4824B)
1 // Dependency-free stock Firefox check, using the same page/Worker assertions 2 // as Chromium. The localhost harness receives the report; no patched browser, 3 // extension, WebDriver or application changes are needed. 4 import assert from 'node:assert/strict'; 5 import {spawn,execFileSync} from 'node:child_process'; 6 import {createServer} from 'node:http'; 7 import {readFile,mkdtemp,rm,writeFile} from 'node:fs/promises'; 8 import {resolve,extname,sep} from 'node:path'; 9 import {tmpdir} from 'node:os'; 10 import {createHash,randomBytes} from 'node:crypto'; 11 import {gzipSync} from 'node:zlib'; 12 const [directory,output,firefox='firefox']=process.argv.slice(2),root=resolve(directory); 13 const token=randomBytes(24).toString('hex'); 14 let deliver,fail; 15 const completion=new Promise((resolve,reject)=>{deliver=resolve;fail=reject;}); 16 // Register a rejection handler before asynchronous startup steps. 17 completion.catch(()=>{}); 18 const server=createServer(async(req,res)=>{ 19 try{ 20 const pathname=decodeURIComponent(new URL(req.url,'http://localhost').pathname); 21 if(pathname==='/report/'+token&&req.method==='POST'){ 22 const chunks=[];let size=0; 23 for await(const chunk of req){size+=chunk.length;if(size>2*1024*1024)throw Error('Report too large');chunks.push(chunk);} 24 const result=JSON.parse(Buffer.concat(chunks).toString('utf8')); 25 res.writeHead(200);res.end('received'); 26 result.ok?deliver(result.report):fail(Error(result.error));return; 27 } 28 if(req.method!=='GET')throw Error('Unsupported method'); 29 res.setHeader('Cache-Control','no-store'); 30 if(pathname==='/'){ 31 res.setHeader('Content-Type','text/html'); 32 res.end(`<!doctype html><meta charset="utf-8"><title>Firefox Haskell WASM check</title><script type="module"> 33 let sent=false; 34 async function finish(result){if(sent)return;sent=true;await fetch('/report/${token}',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(result)});} 35 addEventListener('error',e=>finish({ok:false,error:e.message})); 36 addEventListener('unhandledrejection',e=>finish({ok:false,error:String(e.reason?.stack||e.reason)})); 37 try{await import('./page.mjs');await finish({ok:true,report:await window.runProbe()});} 38 catch(e){await finish({ok:false,error:String(e.stack||e)});} 39 </script>`);return; 40 } 41 const module=pathname.match(/^\/_arboricx\/bundle\/hash\/([0-9a-f]{64})$/); 42 const file=module?resolve(root,'parity/modules',module[1]+'.arboricx'):resolve(root,'.'+pathname); 43 if(!file.startsWith(root+sep))throw Error('Outside root'); 44 res.setHeader('Content-Type',({'.html':'text/html','.mjs':'text/javascript','.js':'text/javascript','.json':'application/json','.wasm':'application/wasm'})[extname(file)]||'application/octet-stream'); 45 res.end(await readFile(file)); 46 }catch(e){res.writeHead(404);res.end();if(req.url.startsWith('/report/'))fail(e);} 47 }); 48 await new Promise(r=>server.listen(0,'127.0.0.1',r)); 49 const profile=await mkdtemp(resolve(tmpdir(),'haskell-wasm-firefox-')); 50 let browser,timer,log=''; 51 try{ 52 const version=execFileSync(firefox,['--version'],{encoding:'utf8',timeout:20000}).trim(); 53 browser=spawn(firefox,['--headless','--no-remote','--new-instance','--profile',profile,`http://127.0.0.1:${server.address().port}/`], 54 {stdio:['ignore','pipe','pipe'],detached:true}); 55 browser.stdout.on('data',b=>{log=(log+b).slice(-16000);}); 56 browser.stderr.on('data',b=>{log=(log+b).slice(-16000);}); 57 browser.on('error',fail); 58 browser.on('exit',code=>fail(Error('Firefox exited '+code+'\n'+log))); 59 timer=setTimeout(()=>fail(Error('Firefox test timeout\n'+log)),120000); 60 const report=await completion; 61 assert.match(report.userAgent,/Firefox\//); 62 report.package=root;report.browser={product:version}; 63 report.versions=await readFile(resolve(root,'versions.txt'),'utf8'); 64 report.manifest=JSON.parse(await readFile(resolve(root,'data/manifest.json'))); 65 const bytes=await readFile(resolve(root,'reactor.wasm')); 66 report.artifact={bytes:bytes.length,gzipBytes:gzipSync(bytes).length,sha256:createHash('sha256').update(bytes).digest('hex')}; 67 await writeFile(output,JSON.stringify(report,null,2)+'\n'); 68 console.log(JSON.stringify({browser:report.browser,checks:report.checks,parity:report.parity,initialization:report.initialization,termination:report.termination,artifact:report.artifact,memory:report.memory},null,2)); 69 }finally{ 70 clearTimeout(timer); 71 if(browser?.pid){ 72 const exited=browser.exitCode===null?new Promise(r=>browser.once('exit',r)):Promise.resolve(); 73 try{process.kill(-browser.pid,'SIGKILL');}catch(e){if(e.code!=='ESRCH')throw e;} 74 await exited; 75 } 76 server.closeAllConnections();await new Promise(r=>server.close(r)); 77 await rm(profile,{recursive:true,force:true,maxRetries:5,retryDelay:100}); 78 }