Release Vulpes OS 2.7-preview.24

This commit is contained in:
OverSu 2026-11-04 12:52:20 +01:00
commit 97ef99f827
135 changed files with 7717 additions and 2524 deletions

View file

@ -6,7 +6,7 @@
"abi": "armeabi-v7a",
"soc": "MSM8905",
"board": "msm8909",
"taVariant": null,
"taVariant": "TA-1175",
"observedFirmware": {
"kaios": "2.5.2.2",
"gecko": "48.0a2",
@ -128,7 +128,7 @@
"Predictive T9 lexicons are generated from Gaia keyboard wordlists; 12,000 frequent words per locale, loaded on demand. User-dictionary learning is not implemented"
],
"privateBootBuild": {
"path": "tundra/out/beatles/boot-trial-20261018-r5",
"path": "tundra/out/beatles/boot-trial-20261022-r7",
"signed": false,
"bootTested": false,
"standalone": false,

View file

@ -1,4 +1,5 @@
import {tundraRadio} from 'resource://vulpes/host/Tundra.sys.mjs';
import {Runtime} from 'resource://vulpes/host/Runtime.sys.mjs';
import {setTimeout} from 'resource://gre/modules/Timer.sys.mjs';
const sleep=ms=>new Promise(resolve=>setTimeout(resolve,ms));
@ -164,6 +165,62 @@ async function run(win) {
await key('Escape');
}
}
// Exercise the real Tundra worker with the exact 320x240 preview mode
// used by KaiOS on beatles. Keep the sample in memory and release Camera1
// immediately; this is a smoke check, not a retained photo or recording.
const cameraActor={};
let cameraSession;
try {
const metadata=await Runtime.platform.nativeCamera.request(cameraActor,{action:'start',camera:'back'});
cameraSession=metadata.session;
const frame=await Runtime.platform.nativeCamera.request(cameraActor,{action:'frame',session:cameraSession});
const data=new Uint8Array(frame.data);
const pixels=metadata.width*metadata.height;
let sum=0,min=255,max=0;
for(let i=0;i<Math.min(pixels,data.length);i++) { const value=data[i];sum+=value;min=Math.min(min,value);max=Math.max(max,value); }
report.cameraPreview={width:metadata.width,height:metadata.height,sequence:frame.sequence,
bytes:data.length,yMean:data.length?Number((sum/Math.min(pixels,data.length)).toFixed(2)):null,yMin:data.length?min:null,yMax:data.length?max:null,
passed:data.length===pixels*3/2 && max>32};
const picture=await Runtime.platform.nativeCamera.request(cameraActor,{action:'picture',session:cameraSession,size:{width:1600,height:1200}});
const jpeg=new Uint8Array(picture.data);
report.cameraPicture={bytes:jpeg.length,jpeg:jpeg[0]===0xff&&jpeg[1]===0xd8,passed:jpeg.length>4096&&jpeg[0]===0xff&&jpeg[1]===0xd8};
} catch(error) {
report.cameraPreview={passed:false,error:error.message || String(error)};
} finally {
if(cameraSession!==undefined) {
try { await Runtime.platform.nativeCamera.request(cameraActor,{action:'stop',session:cameraSession}); }
catch(error) { report.cameraPreview={...report.cameraPreview,stopError:error.message || String(error)}; }
}
}
report.cameraApps=Runtime.apps.filter(app=>/camera|photo/i.test(app.id+' '+app.name+' '+app.origin)).map(app=>({id:app.id,name:app.name,origin:app.origin,manifestURL:app.manifestURL}));
try {
// Match the URL-based launcher used by the other Gaia app audits. The
// app registry's internal ID is not stable across Gaia manifests.
await evaluate('system',`return await VulpesCompat.call('apps.launch',{manifestURL:'http://camera.localhost:8765/manifest.webapp'});`);
for(let i=0;i<120&&!find(system,'camera');i++)await sleep(250);
if(find(system,'camera')) {
for(let i=0;i<120;i++) {
const state=await evaluate('camera',"return {ready:document.readyState==='complete',app:typeof app!=='undefined'&&!!app.camera,busy:typeof app!=='undefined'&&!!app.camera?.isBusy};");
if(state.ready&&state.app&&(!state.busy||i>=119))break;
await sleep(250);
}
await sleep(2500);
report.cameraApp=await evaluate('camera',"var v=document.querySelector('.js-video'),c=document.querySelector('canvas[data-vulpes-native-preview]'),a=typeof app==='undefined'?null:app,cam=a?.camera,stream=cam?.mozCamera,canvasLuma=null;try{if(c){const t=document.createElement('canvas');t.width=80;t.height=60;const x=t.getContext('2d',{willReadFrequently:true});x.drawImage(c,0,0,80,60);const p=x.getImageData(0,0,80,60).data;let sum=0,min=255,max=0;for(let i=0;i<p.length;i+=4){const y=(77*p[i]+150*p[i+1]+29*p[i+2])>>8;sum+=y;min=Math.min(min,y);max=Math.max(max,y);}canvasLuma={mean:Number((sum/4800).toFixed(1)),min,max};}}catch(e){canvasLuma={error:e.message||String(e)};}return {loaded:document.readyState==='complete',readyState:document.readyState,url:location.href,title:document.title,viewfinder:!!v,previewCanvas:!!c,canvasSize:c?[c.width,c.height]:null,canvasLuma,videoHidden:v?getComputedStyle(v).display==='none':false,documentHidden:document.hidden,visibility:document.visibilityState,errorText:document.querySelector('.overlay[data-type=\"request-fail\"] #overlay-text')?.textContent,bodyText:document.body?.innerText.slice(0,240),appGlobal:!!a,cameraObject:!!cam,cameraBusy:cam?.isBusy,cameraReady:!!stream&&!cam.isBusy,mozCamera:!!stream,previewFrames:stream?.vulpesTiming?.frames||0,previewClosed:stream?.vulpesClosed,mode:cam?.mode||null,cameraList:cam?.cameraList};");
if(report.cameraApp.cameraReady) {
try {
await evaluate('camera',"await app.camera.mozCamera.vulpesCameraControl({action:'flash',mode:'torch'});return true;");
await sleep(1500);
report.cameraApp.torchCanvas=await evaluate('camera',"const c=document.querySelector('canvas[data-vulpes-native-preview]'),t=document.createElement('canvas');t.width=80;t.height=60;const x=t.getContext('2d',{willReadFrequently:true});x.drawImage(c,0,0,80,60);const p=x.getImageData(0,0,80,60).data;let sum=0,min=255,max=0;for(let i=0;i<p.length;i+=4){const y=(77*p[i]+150*p[i+1]+29*p[i+2])>>8;sum+=y;min=Math.min(min,y);max=Math.max(max,y);}return {mean:Number((sum/4800).toFixed(1)),min,max};");
} catch(error) { report.cameraApp.torchCanvas={error:error.message||String(error)}; }
finally {
try { await evaluate('camera',"await app.camera.mozCamera.vulpesCameraControl({action:'flash',mode:'off'});return true;"); }
catch(error) { report.cameraApp.torchOffError=error.message||String(error); }
}
try { report.cameraApp.picture=await evaluate('camera',"const b=await app.camera.mozCamera.takePicture(),d=new Uint8Array(await b.arrayBuffer());let bitmap=null;try{bitmap=await createImageBitmap(b);const c=document.createElement('canvas');c.width=80;c.height=60;const x=c.getContext('2d',{willReadFrequently:true});x.drawImage(bitmap,0,0,c.width,c.height);const p=x.getImageData(0,0,c.width,c.height).data;let sum=0,min=255,max=0;for(let i=0;i<p.length;i+=4){const y=(77*p[i]+150*p[i+1]+29*p[i+2])>>8;sum+=y;min=Math.min(min,y);max=Math.max(max,y);}return {bytes:b.size,type:b.type,jpeg:d[0]===255&&d[1]===216,width:bitmap.width,height:bitmap.height,lumaMean:Number((sum/(p.length/4)).toFixed(1)),lumaMin:min,lumaMax:max};}finally{bitmap?.close();}"); }
catch(error) { report.cameraApp.picture={error:error.message||String(error)}; }
}
} else report.cameraApp={loaded:false,launch:'sent',apps:report.cameraApps};
} catch(error) { report.cameraApp={loaded:false,error:error.message||String(error),apps:report.cameraApps}; }
}catch(error){report.error=String(error);report.stack=error.stack;}
await save();
}

View file

@ -50,8 +50,8 @@ for path in (ROOT/'assets').iterdir():
if path.is_file():os.chown(path,2000,2000)
def unprivileged():
Path('/proc/self/oom_score_adj').write_text('800')
def unprivileged(oom_score=800):
Path('/proc/self/oom_score_adj').write_text(str(oom_score))
# Android's paranoid-network kernel requires inet even for private loopback.
os.setgroups([3003])
os.setgid(2000)
@ -64,15 +64,15 @@ env.update(HOME='/work',TMPDIR='/tmp',DISPLAY=':0',MOZ_ENABLE_WAYLAND='0',
VULPES_FULLSCREEN='1',VULPES_KEYPAD='1',VULPES_NATIVE_TOUCH='1',
VULPES_DEVICE='beatles',VULPES_TUNDRA_STATUS='1',
MOZ_CRASHREPORTER_DISABLE='1',FONTCONFIG_PATH='/etc/fonts')
duration=1780 if os.environ.get("VULPES_TRIAL_MANUAL")=="1" else 160
duration=1780 if os.environ.get("VULPES_TRIAL_MANUAL")=="1" else 220
processes=[]
handles=[]
try:
def start(command,log,drop=False):
def start(command,log,drop=False,oom_score=800):
path=Path('/work/'+log)
output=path.open('w');os.chmod(path,0o644);handles.append(output)
proc=subprocess.Popen(command,env=env,stdout=output,stderr=subprocess.STDOUT,
preexec_fn=unprivileged if drop else None)
preexec_fn=(lambda: unprivileged(oom_score)) if drop else None)
processes.append(proc)
return proc
xorg=start(['/usr/lib/xorg/Xorg',':0','vt1','-config','/etc/X11/xorg.conf',
@ -101,7 +101,7 @@ try:
with socket.create_connection(('127.0.0.1',8765),timeout=.2):break
except OSError:time.sleep(.1)
gecko=start(['/opt/firefox/firefox','-no-remote','-purgecaches','-profile',str(PROFILE),
'-chrome','chrome://vulpes/content/shell.xhtml'],'gecko.log',True)
'-chrome','chrome://vulpes/content/shell.xhtml'],'gecko.log',True,oom_score=200)
begin=time.monotonic()
last_key_sequence=0
last_memory_sample=0