tricu

An interpreted language for exploring Tree Calculus
Log | Files | Refs | README | LICENSE

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 }