Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 20 additions & 11 deletions app/src/main/assets/runtime-trial-page.js
Original file line number Diff line number Diff line change
@@ -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-started<PROGRESS_AFTER||now-lastPost<PROGRESS_EVERY))return;
lastPost=now;sent=true;
fetch('/dsha-runtime-trial/'+nonce,{method:'POST',credentials:'same-origin',headers:{'content-type':'application/json'},body:JSON.stringify(payload(kind))}).then(function(r){sent=false;if(r.ok&&kind!=='progress'){done=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('fail')}}catch(e){}});
var timer=setInterval(function(){var root=document.getElementById('root'),boot=document.querySelector('[data-dsh-boot]');booting=!!boot;if(!boot&&root&&root.children.length)ready=true;
if(failed){post('fail');return}if(ready&&opened&&message){post('ack');return}post('progress')},500);
})();
21 changes: 19 additions & 2 deletions app/src/main/assets/runtime-trial-plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,13 +104,30 @@ async function initialize(ctx){
try{await writer.flush()}finally{await writer.close()}
const reader=await ctx.sessionPersistence.open(sample.id,'read');
try{const state=await reader.read();if(reader.header.id!==sample.id||reader.header.version!==4||state.events.length!==0)throw Error('Trial session reopen mismatch')}finally{await reader.close()}
const hash=createHash('sha256').update(read).digest('hex');let renderer=false,failure='';
const hash=createHash('sha256').update(read).digest('hex');let renderer=false,failure='',pageState='';
// 网页侧进度上报只用于诊断:写进行输出(宿主保存在失败记录的 output 里),
// 不回退已经确认的 renderer,也不能把进度上报当成握手成功。
function reportPage(value){
if(!value||typeof value!=='object')return;
try{
const socket=value.socket&&typeof value.socket==='object'?value.socket:{};
const code=typeof socket.code==='number'&&Number.isFinite(socket.code)?Math.trunc(socket.code):-1;
const text=JSON.stringify({
kind:typeof value.kind==='string'?value.kind.slice(0,16):'',
ready:value.ready===true,transport:value.transport===true,failure:value.failure===true,boot:value.boot===true,
socket:{state:typeof socket.state==='string'?socket.state.slice(0,12):'',code:code,clean:socket.clean===true},
});
if(text===pageState)return;
pageState=text;
process.stdout.write('DSHA_TRIAL_PAGE_STATE '+text+'\n');
}catch{}
}
const endpoint='/dsha-runtime-trial/'+nonce;
ctx.effect(()=>ctx.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}));
}}));
Expand Down
31 changes: 27 additions & 4 deletions tools/test-runtime-trial.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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};
Expand Down