diff --git a/app/src/main/assets/runtime-trial-page.js b/app/src/main/assets/runtime-trial-page.js index bfdcc8e97..f4cbe86ea 100644 --- a/app/src/main/assets/runtime-trial-page.js +++ b/app/src/main/assets/runtime-trial-page.js @@ -1,16 +1,25 @@ (function(){ - var nonce='__DSHA_TRIAL_NONCE__',expectedHome=__DSHA_TRIAL_HOME__,ready=false,opened=false,message=false,sent=false,acknowledged=false,failed=false,activeSocket=null; + var nonce='__DSHA_TRIAL_NONCE__',expectedHome=__DSHA_TRIAL_HOME__,ready=false,opened=false,message=false,sent=false,done=false,failed=false,activeSocket=null; + // 诊断字段:网页到底走到哪一步(启动遮罩/boot 标记、mux socket 状态与关闭码)。 + // 只在 4 秒后按 2 秒节流上报进度,成功与致命失败的上报路径保持不变。 + var booting=false,socketState='none',socketCode=0,socketClean=false,started=Date.now(),lastPost=0,PROGRESS_AFTER=4000,PROGRESS_EVERY=2000; var Original=window.WebSocket; function Socket(url,protocols){var socket=protocols===undefined?new Original(url):new Original(url,protocols);try{var address=new URL(url,location.href);if(address.host===location.host&&address.pathname==='/api/remote.mux'){ - activeSocket=socket;opened=false;message=false; - socket.addEventListener('open',function(){if(activeSocket===socket)opened=true}); - socket.addEventListener('close',function(){if(activeSocket===socket){opened=false;message=false}}); - socket.addEventListener('message',function(event){if(activeSocket!==socket)return;try{var frame=JSON.parse(event.data),value=frame.value; - if(frame.type==='item'&&typeof frame.streamId==='string'&&value&&value.type==='ready'&&typeof value.clientId==='string'&&value.clientId.length&&value.host&&value.host.home===expectedHome)message=true; - }catch(e){}}); - }}catch(e){}return socket} + activeSocket=socket;opened=false;message=false;socketState='connecting';socketCode=0;socketClean=false; + socket.addEventListener('open',function(){if(activeSocket===socket){opened=true;socketState='open'}}); + socket.addEventListener('close',function(event){if(activeSocket!==socket)return;opened=false;message=false;socketState='closed';socketCode=event&&typeof event.code==='number'?event.code:-1;socketClean=!!(event&&event.wasClean)}); + socket.addEventListener('error',function(){if(activeSocket===socket)socketState='error'}); + socket.addEventListener('message',function(event){if(activeSocket!==socket)return;try{var frame=JSON.parse(event.data),value=frame.value; + if(frame.type==='item'&&typeof frame.streamId==='string'&&value&&value.type==='ready'&&typeof value.clientId==='string'&&value.clientId.length&&value.host&&value.host.home===expectedHome)message=true; + }catch(e){}}); + }}catch(e){}return socket} Socket.prototype=Original.prototype;Object.setPrototypeOf(Socket,Original);window.WebSocket=Socket; - function post(failure){if(sent||acknowledged)return;sent=true;fetch('/dsha-runtime-trial/'+nonce,{method:'POST',credentials:'same-origin',headers:{'content-type':'application/json'},body:JSON.stringify({nonce:nonce,ready:ready,transport:opened&&message,failure:failure})}).then(function(r){sent=false;if(r.ok){acknowledged=true;clearInterval(timer)}},function(){sent=false})} - window.addEventListener('dsha-startup',function(event){try{var value=JSON.parse(event.detail);if(value.type==='ready')ready=true;if(value.type==='issue'&&value.fatal){failed=true;post(true)}}catch(e){}}); - var timer=setInterval(function(){if(failed){post(true);return}var root=document.getElementById('root'),boot=document.querySelector('[data-dsh-boot]');if(!boot&&root&&root.children.length)ready=true;if(ready&&opened&&message)post(false)},500); + function payload(kind){return {nonce:nonce,ready:ready,transport:opened&&message,failure:kind==='fail',kind:kind,boot:booting,socket:{state:socketState,code:socketCode,clean:socketClean}}} + function post(kind){if(sent||done)return;var now=Date.now(); + if(kind==='progress'&&(now-startedctx.webServer.register({kind:'exact',path:endpoint,handler:async(req,res)=>{ const rejection=ctx.connection.requestRejection(req);if(rejection!==undefined){res.writeHead(rejection);res.end();return} if(req.method==='POST'){ let body='';for await(const chunk of req){body+=chunk;if(body.length>4096){res.writeHead(413);res.end();return}} - try{const value=JSON.parse(body);if(value.nonce!==nonce)throw Error('nonce');if(value.failure)failure='TRIAL_RENDERER_FAILED';else renderer=value.ready===true&&value.transport===true;}catch{res.writeHead(400);res.end();return} + try{const value=JSON.parse(body);if(value.nonce!==nonce)throw Error('nonce');if(value.failure)failure='TRIAL_RENDERER_FAILED';else if(value.ready===true&&value.transport===true)renderer=true;reportPage(value)}catch{res.writeHead(400);res.end();return} } res.setHeader('content-type','application/json');res.setHeader('cache-control','no-store');res.end(JSON.stringify({nonce,pid:process.pid,port:ctx.webServer.port,dataRead:read===payload,dataWrite:true,storageReopened:true,storageFreshReopened:true,sessionReopened:true,hash,renderer,failure})); }})); diff --git a/tools/test-runtime-trial.mjs b/tools/test-runtime-trial.mjs index 28d8cde58..6857f31aa 100644 --- a/tools/test-runtime-trial.mjs +++ b/tools/test-runtime-trial.mjs @@ -154,12 +154,16 @@ try{ const home='/root/.dsha-runtime-trial-'+ 'a'.repeat(32)+'/isolated-user-home'; const readyFrame={type:'item',streamId:'test-stream',value:{type:'ready',clientId:'test-client',host:{home}}}; const source=(await fs.readFile(path.join(repository,'app/src/main/assets/runtime-trial-page.js'),'utf8')).replace('__DSHA_TRIAL_NONCE__','a'.repeat(32)).replace('__DSHA_TRIAL_HOME__',JSON.stringify(home)); - function browser(responses){let tick,cleared=0,requests=0;const sockets=[]; + function browser(responses){let tick,cleared=0,requests=0;const sockets=[],bodies=[];let clock=Date.now(); function Socket(){this.events={};this.addEventListener=(name,fn)=>{this.events[name]=fn};sockets.push(this)} - const context={window:{WebSocket:Socket,addEventListener(){}},URL,Object,JSON,location:{href:'http://127.0.0.1:3080/',host:'127.0.0.1:3080'}, + const context={window:{WebSocket:Socket,addEventListener(){}},URL,Object,JSON,Date:{now:()=>clock},location:{href:'http://127.0.0.1:3080/',host:'127.0.0.1:3080'}, document:{getElementById:()=>({children:[{}]}),querySelector:()=>null},setInterval(fn){tick=fn;return 7},clearInterval(id){assert.equal(id,7);cleared++}, - fetch:async()=>{requests++;const response=responses.shift();if(response instanceof Error)throw response;return response||{ok:true}}}; - vm.runInNewContext(source,context);return{connect(url='ws://127.0.0.1:3080/api/remote.mux',frame=readyFrame){new context.window.WebSocket(url);const socket=sockets.at(-1);socket.events.open?.();socket.events.message?.({data:JSON.stringify(frame)});},async tick(){tick();await new Promise(resolve=>setImmediate(resolve));},get requests(){return requests},get cleared(){return cleared}}; + fetch:async(url,init)=>{requests++;bodies.push(JSON.parse(init.body));const response=responses.shift();if(response instanceof Error)throw response;return response||{ok:true}}}; + vm.runInNewContext(source,context); + return{connect(url='ws://127.0.0.1:3080/api/remote.mux',frame=readyFrame){new context.window.WebSocket(url);const socket=sockets.at(-1);socket.events.open?.();socket.events.message?.({data:JSON.stringify(frame)});return socket}, + openOnly(url='ws://127.0.0.1:3080/api/remote.mux'){new context.window.WebSocket(url);const socket=sockets.at(-1);socket.events.open?.();return socket}, + async tick(){tick();await new Promise(resolve=>setImmediate(resolve))},advance(ms){clock+=ms}, + get requests(){return requests},get cleared(){return cleared},get bodies(){return bodies}}; } for(const response of [{ok:false},new Error('synthetic_network_failure')])await test('synthetic_renderer_retry_'+(response instanceof Error?'network':'http'),async()=>{ const page=browser([response,{ok:true}]);page.connect();await page.tick();assert.equal(page.requests,1);assert.equal(page.cleared,0); @@ -168,6 +172,25 @@ try{ await test('synthetic_foreign_socket_cannot_acknowledge_trial',async()=>{const page=browser([]);page.connect('ws://127.0.0.1:9999/socket');await page.tick();assert.equal(page.requests,0);assert.equal(page.cleared,0);}); await test('synthetic_error_or_unrelated_frame_is_not_a_backend_handshake',async()=>{for(const frame of [{type:'error',streamId:'bad',error:{}},{type:'item',streamId:'another',value:{message:'not ready'}}]){const page=browser([]);page.connect(undefined,frame);await page.tick();assert.equal(page.requests,0)}}); await test('synthetic_wrong_trial_home_cannot_acknowledge',async()=>{const page=browser([]);page.connect(undefined,{...readyFrame,value:{...readyFrame.value,host:{home:'/other-generation'}}});await page.tick();assert.equal(page.requests,0)}); + // 未完成握手时只上报诊断进度,且不能清掉重试定时器或冒充成功。 + await test('synthetic_progress_report_exposes_page_state',async()=>{ + const page=browser([]);page.openOnly();await page.tick();assert.equal(page.requests,0); + page.advance(4500);await page.tick();assert.equal(page.requests,1); + const body=page.bodies[0];assert.equal(body.kind,'progress');assert.equal(body.ready,true);assert.equal(body.transport,false); + assert.equal(body.failure,false);assert.equal(body.boot,false);assert.equal(body.socket.state,'open');assert.equal(body.socket.code,0); + assert.equal(page.cleared,0); + }); + await test('synthetic_socket_close_code_is_reported',async()=>{ + const page=browser([]);const socket=page.openOnly();socket.events.close?.({code:1006,wasClean:false}); + page.advance(4500);await page.tick();assert.equal(page.requests,1); + const body=page.bodies[0];assert.equal(body.transport,false);assert.equal(body.socket.state,'closed'); + assert.equal(body.socket.code,1006);assert.equal(body.socket.clean,false); + }); + await test('synthetic_progress_report_is_throttled',async()=>{ + const page=browser([]);page.openOnly();page.advance(4500);await page.tick();assert.equal(page.requests,1); + page.advance(600);await page.tick();assert.equal(page.requests,1); + page.advance(2500);await page.tick();assert.equal(page.requests,2); + }); }catch{process.exitCode=1;} finally{ const report={version:1,platform:process.platform,node:process.version,fixture:'synthetic, private temporary directory',androidRendererVerified:false,results};