tricu

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

check-browser.mjs (4538B)


      1 // Opt-in dependency-free Chromium CDP check. Run via the pinned Nix shell.
      2 import assert from 'node:assert/strict';
      3 import {spawn} from 'node:child_process';
      4 import {createServer} from 'node:http';
      5 import {readFile,mkdtemp,rm,writeFile} from 'node:fs/promises';
      6 import {resolve,extname,sep} from 'node:path';
      7 import {tmpdir} from 'node:os';
      8 import {createHash} from 'node:crypto';
      9 import {gzipSync} from 'node:zlib';
     10 const [directory,output,chromium='chromium']=process.argv.slice(2),root=resolve(directory);
     11 const server=createServer(async(req,res)=>{
     12   try{
     13     const pathname=decodeURIComponent(new URL(req.url,'http://localhost').pathname);
     14     const module=pathname.match(/^\/_arboricx\/bundle\/hash\/([0-9a-f]{64})$/);
     15     const file=module?resolve(root,'parity/modules',module[1]+'.arboricx'):resolve(root,'.'+(pathname==='/'?'/index.html':pathname));
     16     if(!file.startsWith(root+sep))throw Error('Outside root');
     17     res.setHeader('Cache-Control','no-store');
     18     res.setHeader('Content-Type',({'.html':'text/html','.mjs':'text/javascript','.js':'text/javascript','.json':'application/json','.wasm':'application/wasm'})[extname(file)]||'application/octet-stream');
     19     res.end(await readFile(file));
     20   }catch{res.writeHead(404);res.end();}
     21 });
     22 await new Promise(r=>server.listen(0,'127.0.0.1',r));
     23 const profile=await mkdtemp(resolve(tmpdir(),'haskell-wasm-browser-'));
     24 let browser,socket,sequence=0;const pending=new Map(),exceptions=[];
     25 try{
     26   browser=spawn(chromium,['--headless','--no-sandbox','--disable-dev-shm-usage','--remote-debugging-port=0',`--user-data-dir=${profile}`,'about:blank'],{stdio:['ignore','ignore','pipe']});
     27   const endpoint=await new Promise((resolve,reject)=>{
     28     let log='';const timer=setTimeout(()=>reject(Error('Browser startup timeout: '+log)),20000);
     29     browser.on('error',e=>{clearTimeout(timer);reject(e);});
     30     browser.on('exit',code=>{clearTimeout(timer);reject(Error('Browser exited '+code+': '+log));});
     31     browser.stderr.on('data',b=>{log=(log+b).slice(-16000);const m=log.match(/DevTools listening on (ws:\/\/\S+)/);if(m){clearTimeout(timer);resolve(new URL(m[1]));}});
     32   });
     33   const tab=await (await fetch(`http://${endpoint.host}/json/new?${encodeURIComponent('http://127.0.0.1:'+server.address().port+'/')}`,{method:'PUT'})).json();
     34   socket=new WebSocket(tab.webSocketDebuggerUrl);
     35   await new Promise((r,j)=>{socket.onopen=r;socket.onerror=j;});
     36   socket.onmessage=({data})=>{
     37     const m=JSON.parse(data);
     38     if(m.id){const p=pending.get(m.id);if(!p)return;pending.delete(m.id);clearTimeout(p.timer);m.error?p.reject(Error(JSON.stringify(m.error))):p.resolve(m.result);}
     39     else if(m.method==='Runtime.exceptionThrown')exceptions.push(m.params);
     40   };
     41   const send=(method,params={})=>new Promise((resolve,reject)=>{
     42     const id=++sequence,timer=setTimeout(()=>{pending.delete(id);reject(Error('CDP timeout: '+method));},120000);
     43     pending.set(id,{resolve,reject,timer});socket.send(JSON.stringify({id,method,params}));
     44   });
     45   await send('Runtime.enable');
     46   for(let i=0;i<100;i++){
     47     const r=await send('Runtime.evaluate',{expression:'typeof window.runProbe'});
     48     if(r.result.value==='function')break;
     49     await new Promise(r=>setTimeout(r,100));
     50   }
     51   const result=await send('Runtime.evaluate',{expression:'window.runProbe()',awaitPromise:true,returnByValue:true});
     52   assert.ok(!result.exceptionDetails,JSON.stringify(result.exceptionDetails));
     53   assert.equal(exceptions.length,0,JSON.stringify(exceptions));
     54   const report=result.result.value;
     55   report.package=root;
     56   report.browser=await send('Browser.getVersion');
     57   report.versions=await readFile(resolve(root,'versions.txt'),'utf8');
     58   report.manifest=JSON.parse(await readFile(resolve(root,'data/manifest.json')));
     59   const bytes=await readFile(resolve(root,'reactor.wasm'));
     60   report.artifact={bytes:bytes.length,gzipBytes:gzipSync(bytes).length,sha256:createHash('sha256').update(bytes).digest('hex')};
     61   await writeFile(output,JSON.stringify(report,null,2)+'\n');
     62   console.log(JSON.stringify({checks:report.checks,parity:report.parity,initialization:report.initialization,termination:report.termination,artifact:report.artifact,memory:report.memory},null,2));
     63 }finally{
     64   if(socket)socket.close();
     65   for(const p of pending.values())clearTimeout(p.timer);
     66   if(browser&&browser.exitCode===null){const exited=new Promise(r=>browser.once('exit',r));browser.kill('SIGKILL');await exited;}
     67   await new Promise(r=>server.close(r));
     68   await rm(profile,{recursive:true,force:true,maxRetries:5,retryDelay:100});
     69 }