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 }