Release Vulpes OS 2.7-preview.24
This commit is contained in:
parent
49572b6b45
commit
97ef99f827
135 changed files with 7717 additions and 2524 deletions
|
|
@ -9,7 +9,8 @@ if [ "$(date +%s)" -lt "${BUILD_EPOCH:-0}" ]; then date -s "@$BUILD_EPOCH"; fi
|
|||
trap 'echo TUNDRA_NATIVE_SETUP_FAILED; while :; do /bin/busybox sleep 5; done' EXIT
|
||||
LOWER=/installed/home/droidian/vulpes-tundra-tests/runtime-reference-20260925/root
|
||||
if [ -n "${SYSTEM_IMAGE:-}" ]; then
|
||||
test "$(sha256sum "$SYSTEM_IMAGE" | cut -d' ' -f1)" = "$SYSTEM_IMAGE_SHA"
|
||||
test -f "$SYSTEM_IMAGE" && test ! -L "$SYSTEM_IMAGE"
|
||||
test "$(stat -c %s "$SYSTEM_IMAGE")" = "$SYSTEM_IMAGE_BYTES"
|
||||
mkdir -p /run/tundra-system
|
||||
mount -t ext4 -o loop,ro,noload "$SYSTEM_IMAGE" /run/tundra-system
|
||||
LOWER=/run/tundra-system
|
||||
|
|
|
|||
|
|
@ -45,7 +45,19 @@ done
|
|||
/sbin/dropbear -E -s -j -k -p 10.15.19.82:2222 -r /etc/tundra-hostkey -P /run/dropbear.pid
|
||||
echo TUNDRA_USB_SSH_READY
|
||||
# No LVM activation, fsck, journal replay, resizing, or writes to a block device.
|
||||
if [ -b /dev/mmcblk0p72 ] && [ "$(dd if=/dev/mmcblk0p72 bs=1048576 count=1 2>/dev/null | sha256sum | cut -d' ' -f1)" = "$EXPECTED_STORAGE_HASH" ]; then
|
||||
if [ "${RAW_USERDATA_EXT4:-0}" = 1 ]; then
|
||||
# Ext4 updates mutable mount counters and timestamps in the first MiB; use
|
||||
# the filesystem UUID at the fixed superblock offset as the stable identity.
|
||||
actual_uuid=$(dd if=/dev/mmcblk0p72 bs=1 skip=1128 count=16 2>/dev/null | od -An -tx1 | tr -d ' \n')
|
||||
if [ -b /dev/mmcblk0p72 ] && [ "$actual_uuid" = "$EXPECTED_STORAGE_UUID_HEX" ]; then
|
||||
mkdir -p /installed
|
||||
if [ "${TUNDRA_USERDATA_RW:-0}" = 1 ]; then
|
||||
mount -t ext4 -o rw /dev/mmcblk0p72 /installed
|
||||
else
|
||||
mount -t ext4 -o ro,noload /dev/mmcblk0p72 /installed
|
||||
fi
|
||||
fi
|
||||
elif [ -b /dev/mmcblk0p72 ] && [ "$(dd if=/dev/mmcblk0p72 bs=1048576 count=1 2>/dev/null | sha256sum | cut -d' ' -f1)" = "$EXPECTED_STORAGE_HASH" ]; then
|
||||
mkdir -p /dev/mapper /installed
|
||||
/sbin/dmsetup create tundra-installed-ro --readonly --table '0 104448000 linear /dev/mmcblk0p72 329728'
|
||||
/sbin/dmsetup mknodes
|
||||
|
|
|
|||
|
|
@ -5,8 +5,11 @@
|
|||
#define _start unused_stage0_start
|
||||
#include "init.c"
|
||||
#undef _start
|
||||
#ifndef TUNDRA_WATCHDOG_SECONDS
|
||||
#define TUNDRA_WATCHDOG_SECONDS 600
|
||||
#endif
|
||||
__attribute__((noreturn)) void _start(void) {
|
||||
long remaining[2] = {600, 0};
|
||||
long remaining[2] = {TUNDRA_WATCHDOG_SECONDS, 0};
|
||||
while (call(NR_nanosleep, (long)remaining, (long)remaining, 0, 0, 0) == -4) {}
|
||||
say("TUNDRA_TRIAL_DEADLINE_REBOOT\n");
|
||||
call(NR_reboot, 0xfee1dead, 672274793, 0x01234567, 0, 0);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
42
tundra/devices/sargo-p23-20261027/backup/manifest.json
Normal file
42
tundra/devices/sargo-p23-20261027/backup/manifest.json
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
{
|
||||
"recordedAt": "2026-09-24T18:33:08.938157+00:00",
|
||||
"device": "sargo",
|
||||
"rawPartitions": true,
|
||||
"restorationTested": false,
|
||||
"scope": "Boot, DTBO and vbmeta only; excludes userdata, vendor and system",
|
||||
"files": {
|
||||
"boot_a.img": {
|
||||
"source": "/dev/mmcblk0p13",
|
||||
"bytes": 67108864,
|
||||
"sha256": "df961004d4d455f62635c080a3f2543cdf893a683626a0fb4da9104f69f70699"
|
||||
},
|
||||
"boot_b.img": {
|
||||
"source": "/dev/mmcblk0p14",
|
||||
"bytes": 67108864,
|
||||
"sha256": "da4a4ba50191ed6fba8dda50c155c53e21c973ccf504f0a9aff0e2acffc48350"
|
||||
},
|
||||
"dtbo_a.img": {
|
||||
"source": "/dev/mmcblk0p44",
|
||||
"bytes": 8417280,
|
||||
"sha256": "0e43a57d6423e8d24cd944fcad0939ed81160e847a6d4203e2f7c16a86d69c03"
|
||||
},
|
||||
"dtbo_b.img": {
|
||||
"source": "/dev/mmcblk0p45",
|
||||
"bytes": 8417280,
|
||||
"sha256": "2453dbdb75d8e67e47ac531dc7a0b2cb36f00710b824e5d86019523f8d92cfe9"
|
||||
},
|
||||
"vbmeta_a.img": {
|
||||
"source": "/dev/mmcblk0p63",
|
||||
"bytes": 65536,
|
||||
"sha256": "add7e48f84f3e98061f33aabb4cc678b6a631af66834f0fae579324b7acc681a"
|
||||
},
|
||||
"vbmeta_b.img": {
|
||||
"source": "/dev/mmcblk0p64",
|
||||
"bytes": 65536,
|
||||
"sha256": "3e168134406526bc068e8f401231ce53b96f49ddf5a400a0a3ec25e118186cf2"
|
||||
}
|
||||
},
|
||||
"serialSha256": "8c024ab1ad147c85b56ae800d741120291d1e5aaf97b81f6a935c0294845f6fe",
|
||||
"activeSlot": "a",
|
||||
"qualificationSha256": "e9ad84d2659114786b409eb27b6fba9c71bf6e5580252cd7297425500235face"
|
||||
}
|
||||
14
tundra/devices/sargo-p23-20261027/device.json
Normal file
14
tundra/devices/sargo-p23-20261027/device.json
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"schema": 1,
|
||||
"device": "sargo",
|
||||
"activeSlot": "a",
|
||||
"serialSha256": "8c024ab1ad147c85b56ae800d741120291d1e5aaf97b81f6a935c0294845f6fe",
|
||||
"kernelSha256": "e214e5e363bcf4db6387668ba723299e75009bc128fb0088f706cfb311db0080",
|
||||
"private": true,
|
||||
"flashReady": false,
|
||||
"sourceBackupQualificationSha256": "8b45d37a992c2f338a678b1962edeb17b62822d8c05f4e72cf7cde6b1c831044",
|
||||
"files": {
|
||||
"backup/manifest.json": "620f0f6ae0a582d00c61c8d8bfb452eb52a0c251e54287a85ec298f757199170",
|
||||
"layout.json": "b4ec8d4ea3d8d5c23f51b6bd514984feb1a9aacfef563dccda2c59b0d667fd2b"
|
||||
}
|
||||
}
|
||||
17
tundra/devices/sargo-p23-20261027/layout.json
Normal file
17
tundra/devices/sargo-p23-20261027/layout.json
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"schema": 2,
|
||||
"device": "/dev/mmcblk0p72",
|
||||
"table": "direct",
|
||||
"size": "53648801280",
|
||||
"blkid": "/dev/mmcblk0p72: UUID=\"fed927a8-c694-4c9b-a8ec-48ad5a71139b\" TYPE=\"ext4\" PARTLABEL=\"userdata\" PARTUUID=\"dc37ac3d-ddc2-4350-a86d-5adaf76fcd78\"",
|
||||
"partitionLabel": "userdata",
|
||||
"filesystem": "ext4",
|
||||
"storage": "raw-userdata-ext4",
|
||||
"filesystemUuid": "fed927a8-c694-4c9b-a8ec-48ad5a71139b",
|
||||
"backingDevice": "259:40",
|
||||
"firstMiBSha256": "dc8f304baa35a09f82fdc4caa862b3dd71edfe05da34279c7a2d78cadb88cf4b",
|
||||
"activeSlot": "a",
|
||||
"serialSha256": "8c024ab1ad147c85b56ae800d741120291d1e5aaf97b81f6a935c0294845f6fe",
|
||||
"observed": "2026-10-27T22:xxZ via temporary rescue; read-only block inspection",
|
||||
"observedAt": "2026-10-27T22:xx:xxZ (temporary rescue; block reads only)"
|
||||
}
|
||||
54
tundra/probes/beatles-camera-broker.c
Normal file
54
tundra/probes/beatles-camera-broker.c
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Fixed-command local broker; camera access is confined to Android UID 1006.
|
||||
#define _GNU_SOURCE
|
||||
#include <sys/socket.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/un.h>
|
||||
#include <sys/wait.h>
|
||||
#include <grp.h>
|
||||
#include <signal.h>
|
||||
#include <errno.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#define SOCKET_DIR "/data/local/vulpes-beatles/camera-ipc"
|
||||
#define SOCKET_PATH SOCKET_DIR "/camera.sock"
|
||||
#define WORKER "/data/local/tmp/vulpes-beatles-camera-worker"
|
||||
|
||||
static volatile sig_atomic_t stopping;
|
||||
static void stop_now(int signal_number) { (void)signal_number; stopping = 1; }
|
||||
|
||||
int main(void) {
|
||||
if (setpgid(0,0) && errno != EACCES) return 1;
|
||||
struct sigaction action = {0}; action.sa_handler = stop_now;
|
||||
sigemptyset(&action.sa_mask); sigaction(SIGTERM,&action,NULL); sigaction(SIGINT,&action,NULL);
|
||||
umask(0);
|
||||
if (mkdir(SOCKET_DIR,0777) && errno != EEXIST) return 2;
|
||||
chmod(SOCKET_DIR,0777); unlink(SOCKET_PATH);
|
||||
int server=socket(AF_UNIX,SOCK_STREAM,0); if(server<0) return 3;
|
||||
struct sockaddr_un address={0}; address.sun_family=AF_UNIX;
|
||||
strncpy(address.sun_path,SOCKET_PATH,sizeof(address.sun_path)-1);
|
||||
if(bind(server,(struct sockaddr *)&address,sizeof(address)) || chmod(SOCKET_PATH,0666) || listen(server,2)) return 4;
|
||||
printf("camera broker ready uid=%d socket=%s\n",getuid(),SOCKET_PATH); fflush(stdout);
|
||||
while(!stopping) {
|
||||
int client=accept(server,NULL,NULL);
|
||||
if(client<0) { if(errno==EINTR) continue; break; }
|
||||
pid_t child=fork();
|
||||
if(child==0) {
|
||||
close(server);
|
||||
fprintf(stderr,"camera worker accepted pid=%d\n",getpid()); fflush(stderr);
|
||||
gid_t groups[]={1006};
|
||||
if(setgroups(1,groups) || setgid(1006) || setuid(1006)) _exit(120);
|
||||
if(dup2(client,STDIN_FILENO)<0 || dup2(client,STDOUT_FILENO)<0) _exit(121);
|
||||
if(client>STDOUT_FILENO) close(client);
|
||||
setenv("LD_LIBRARY_PATH","/system/lib:/data/local/tmp",1);
|
||||
execl(WORKER,"vulpes-beatles-camera-worker",(char *)NULL);
|
||||
_exit(127);
|
||||
}
|
||||
close(client);
|
||||
while(waitpid(-1,NULL,WNOHANG)>0) {}
|
||||
}
|
||||
close(server); unlink(SOCKET_PATH); return 0;
|
||||
}
|
||||
175
tundra/probes/beatles-camera-glconsumer-probe.cpp
Normal file
175
tundra/probes/beatles-camera-glconsumer-probe.cpp
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Bounded Android API 23 Camera1 diagnostic using an EGL/GLConsumer output path.
|
||||
#include <camera/Camera.h>
|
||||
#include <camera/CameraParameters.h>
|
||||
#include <binder/ProcessState.h>
|
||||
#include <gui/BufferQueue.h>
|
||||
#include <gui/GLConsumer.h>
|
||||
#include <gui/GraphicBufferAlloc.h>
|
||||
#include <EGL/egl.h>
|
||||
#include <GLES2/gl2.h>
|
||||
#include <GLES2/gl2ext.h>
|
||||
#include <utils/String16.h>
|
||||
#include <stdio.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#ifndef GL_TEXTURE_EXTERNAL_OES
|
||||
#define GL_TEXTURE_EXTERNAL_OES 0x8D65
|
||||
#endif
|
||||
|
||||
static GLuint compileShader(GLenum type, const char* source) {
|
||||
GLuint shader = glCreateShader(type);
|
||||
glShaderSource(shader, 1, &source, nullptr);
|
||||
glCompileShader(shader);
|
||||
GLint ok = 0;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &ok);
|
||||
if (!ok) {
|
||||
char log[1024];
|
||||
glGetShaderInfoLog(shader, sizeof(log), nullptr, log);
|
||||
fprintf(stderr, "shader_error=%s\n", log);
|
||||
return 0;
|
||||
}
|
||||
return shader;
|
||||
}
|
||||
|
||||
int main() {
|
||||
setvbuf(stderr, nullptr, _IONBF, 0);
|
||||
android::ProcessState::self()->startThreadPool();
|
||||
const int cameraCount = android::Camera::getNumberOfCameras();
|
||||
fprintf(stderr, "camera_count=%d uid=%d\n", cameraCount, getuid());
|
||||
if (cameraCount < 1) return 2;
|
||||
|
||||
android::sp<android::Camera> camera = android::Camera::connect(
|
||||
0, android::String16("org.vulpes.camera.glprobe"),
|
||||
android::Camera::USE_CALLING_UID);
|
||||
if (camera == nullptr) {
|
||||
fprintf(stderr, "camera_connect_failed\n");
|
||||
return 3;
|
||||
}
|
||||
android::CameraParameters parameters(camera->getParameters());
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
parameters.getPreviewSize(&width, &height);
|
||||
fprintf(stderr, "stock_preview_size=%dx%d format=%s\n", width, height,
|
||||
parameters.getPreviewFormat());
|
||||
if (width <= 0 || height <= 0 || width > 1920 || height > 1920) return 4;
|
||||
|
||||
EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
|
||||
EGLint major = 0, minor = 0;
|
||||
if (display == EGL_NO_DISPLAY || !eglInitialize(display, &major, &minor)) {
|
||||
fprintf(stderr, "egl_initialize_error=%x\n", eglGetError());
|
||||
return 5;
|
||||
}
|
||||
const EGLint configAttributes[] = {
|
||||
EGL_SURFACE_TYPE, EGL_PBUFFER_BIT,
|
||||
EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
|
||||
EGL_RED_SIZE, 8, EGL_GREEN_SIZE, 8, EGL_BLUE_SIZE, 8, EGL_NONE};
|
||||
EGLConfig config;
|
||||
EGLint configCount = 0;
|
||||
if (!eglChooseConfig(display, configAttributes, &config, 1, &configCount) ||
|
||||
configCount < 1) return 6;
|
||||
const EGLint surfaceAttributes[] = {
|
||||
EGL_WIDTH, width, EGL_HEIGHT, height, EGL_NONE};
|
||||
EGLSurface surface = eglCreatePbufferSurface(display, config, surfaceAttributes);
|
||||
const EGLint contextAttributes[] = {EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE};
|
||||
EGLContext context = eglCreateContext(display, config, EGL_NO_CONTEXT,
|
||||
contextAttributes);
|
||||
if (surface == EGL_NO_SURFACE || context == EGL_NO_CONTEXT ||
|
||||
!eglMakeCurrent(display, surface, surface, context)) {
|
||||
fprintf(stderr, "egl_context_error=%x\n", eglGetError());
|
||||
return 7;
|
||||
}
|
||||
|
||||
GLuint texture = 0;
|
||||
glGenTextures(1, &texture);
|
||||
glBindTexture(GL_TEXTURE_EXTERNAL_OES, texture);
|
||||
glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
|
||||
android::sp<android::IGraphicBufferProducer> producer;
|
||||
android::sp<android::IGraphicBufferConsumer> consumer;
|
||||
android::sp<android::GraphicBufferAlloc> allocator =
|
||||
new android::GraphicBufferAlloc();
|
||||
android::BufferQueue::createBufferQueue(&producer, &consumer, allocator);
|
||||
consumer->setDefaultBufferSize(width, height);
|
||||
android::sp<android::GLConsumer> glConsumer = new android::GLConsumer(
|
||||
consumer, texture, GL_TEXTURE_EXTERNAL_OES, true, false);
|
||||
|
||||
const int targetStatus = camera->setPreviewTarget(producer);
|
||||
fprintf(stderr, "set_preview_target=%d\n", targetStatus);
|
||||
const int startStatus = camera->startPreview();
|
||||
fprintf(stderr, "start_preview=%d\n", startStatus);
|
||||
if (targetStatus || startStatus) return 8;
|
||||
|
||||
const char* vertexSource =
|
||||
"attribute vec4 a; attribute vec2 t; varying vec2 v;"
|
||||
"void main(){gl_Position=a;v=t;}";
|
||||
const char* fragmentSource =
|
||||
"#extension GL_OES_EGL_image_external : require\n"
|
||||
"precision mediump float; uniform samplerExternalOES s; varying vec2 v;"
|
||||
"void main(){gl_FragColor=texture2D(s,v);}";
|
||||
GLuint program = glCreateProgram();
|
||||
GLuint vertex = compileShader(GL_VERTEX_SHADER, vertexSource);
|
||||
GLuint fragment = compileShader(GL_FRAGMENT_SHADER, fragmentSource);
|
||||
if (!vertex || !fragment) return 9;
|
||||
glAttachShader(program, vertex);
|
||||
glAttachShader(program, fragment);
|
||||
glBindAttribLocation(program, 0, "a");
|
||||
glBindAttribLocation(program, 1, "t");
|
||||
glLinkProgram(program);
|
||||
GLint linked = 0;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||
if (!linked) {
|
||||
char log[1024];
|
||||
glGetProgramInfoLog(program, sizeof(log), nullptr, log);
|
||||
fprintf(stderr, "program_link_error=%s\n", log);
|
||||
return 10;
|
||||
}
|
||||
const GLfloat vertices[] = {-1, -1, 1, -1, -1, 1, 1, 1};
|
||||
const GLfloat texcoords[] = {0, 1, 1, 1, 0, 0, 1, 0};
|
||||
glUseProgram(program);
|
||||
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, vertices);
|
||||
glEnableVertexAttribArray(0);
|
||||
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 0, texcoords);
|
||||
glEnableVertexAttribArray(1);
|
||||
glUniform1i(glGetUniformLocation(program, "s"), 0);
|
||||
|
||||
bool captured = false;
|
||||
for (int attempt = 0; attempt < 100 && !captured; ++attempt) {
|
||||
usleep(100000);
|
||||
const int updateStatus = glConsumer->updateTexImage();
|
||||
if (updateStatus != 0) continue;
|
||||
glViewport(0, 0, width, height);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
glFinish();
|
||||
unsigned char* pixels = new unsigned char[width * height * 3];
|
||||
glReadPixels(0, 0, width, height, GL_RGB, GL_UNSIGNED_BYTE, pixels);
|
||||
const GLenum glError = glGetError();
|
||||
if (glError == GL_NO_ERROR) {
|
||||
FILE* output = fopen("/data/local/tmp/vulpes-camera-glconsumer.ppm", "wb");
|
||||
if (output) {
|
||||
fprintf(output, "P6\n%d %d\n255\n", width, height);
|
||||
for (int y = height - 1; y >= 0; --y)
|
||||
fwrite(pixels + y * width * 3, 3, width, output);
|
||||
fclose(output);
|
||||
chmod("/data/local/tmp/vulpes-camera-glconsumer.ppm", 0644);
|
||||
captured = true;
|
||||
}
|
||||
}
|
||||
delete[] pixels;
|
||||
fprintf(stderr, "update=%d gl_error=%x captured=%d\n",
|
||||
updateStatus, glError, captured);
|
||||
}
|
||||
fprintf(stderr, "stopping_preview\n");
|
||||
camera->stopPreview();
|
||||
fprintf(stderr, "disconnecting_camera\n");
|
||||
camera->disconnect();
|
||||
fprintf(stderr, "captured=%d\n", captured);
|
||||
eglMakeCurrent(display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
|
||||
eglDestroyContext(display, context);
|
||||
eglDestroySurface(display, surface);
|
||||
eglTerminate(display);
|
||||
return captured ? 0 : 11;
|
||||
}
|
||||
213
tundra/probes/beatles-camera-gonkqueue-probe.cpp
Normal file
213
tundra/probes/beatles-camera-gonkqueue-probe.cpp
Normal file
|
|
@ -0,0 +1,213 @@
|
|||
// SPDX-License-Identifier: MPL-2.0
|
||||
// RAM-only diagnostic reproducing Gecko's Android 6 GonkBufferQueue path.
|
||||
#include <camera/Camera.h>
|
||||
#include <camera/CameraParameters.h>
|
||||
#include <system/camera.h>
|
||||
#include <binder/ProcessState.h>
|
||||
#include <gui/GraphicBufferAlloc.h>
|
||||
#include <gui/IConsumerListener.h>
|
||||
#include <ui/Fence.h>
|
||||
#include <ui/GraphicBuffer.h>
|
||||
#include <utils/String16.h>
|
||||
#include <utils/Mutex.h>
|
||||
#include <utils/Vector.h>
|
||||
#include <stdio.h>
|
||||
#include <unistd.h>
|
||||
#include <inttypes.h>
|
||||
#include <utils/Timers.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#define ANDROID_VERSION 23
|
||||
#include "GonkBufferQueueLL.h"
|
||||
#include "GonkBufferQueueProducer.h"
|
||||
|
||||
class CameraEvents : public android::CameraListener {
|
||||
public:
|
||||
void notify(int32_t type, int32_t ext1, int32_t ext2) override {
|
||||
fprintf(stderr, "camera_notify=%d,%d,%d\n", type, ext1, ext2);
|
||||
}
|
||||
void postData(int32_t type, const android::sp<android::IMemory>& memory,
|
||||
camera_frame_metadata_t*) override {
|
||||
if (memory == nullptr) return;
|
||||
const size_t size = memory->size();
|
||||
const unsigned char* bytes = static_cast<const unsigned char*>(memory->pointer());
|
||||
unsigned int low = 255, high = 0;
|
||||
uint64_t sum = 0;
|
||||
const size_t ySize = size < 640u * 480u ? size : 640u * 480u;
|
||||
for (size_t i = 0; i < ySize; ++i) {
|
||||
if (bytes[i] < low) low = bytes[i];
|
||||
if (bytes[i] > high) high = bytes[i];
|
||||
sum += bytes[i];
|
||||
}
|
||||
fprintf(stderr, "camera_data=%d size=%zu y=%u..%u mean=%.2f\n",
|
||||
type, size, low, high, ySize ? (double)sum / ySize : 0);
|
||||
static bool saved = false;
|
||||
if (type == CAMERA_MSG_COMPRESSED_IMAGE) {
|
||||
FILE* jpeg = fopen("/data/local/tmp/vulpes-camera-callback.jpg", "wb");
|
||||
if (jpeg) { fwrite(bytes, 1, size, jpeg); fclose(jpeg); chmod("/data/local/tmp/vulpes-camera-callback.jpg", 0644); }
|
||||
fprintf(stderr, "captured_jpeg=%zu\n", size);
|
||||
} else if (!saved) {
|
||||
FILE* output = fopen("/data/local/tmp/vulpes-camera-callback.yuv", "wb");
|
||||
if (output) { fwrite(bytes, 1, size, output); fclose(output); chmod("/data/local/tmp/vulpes-camera-callback.yuv", 0644); saved = true; }
|
||||
}
|
||||
}
|
||||
void postDataTimestamp(nsecs_t, int32_t type,
|
||||
const android::sp<android::IMemory>&) override {
|
||||
fprintf(stderr, "camera_timestamp_data=%d\n", type);
|
||||
}
|
||||
};
|
||||
|
||||
class QueueListener : public android::ConsumerListener {
|
||||
public:
|
||||
void onFrameAvailable(const android::BufferItem&) override {}
|
||||
void onFrameReplaced(const android::BufferItem&) override {}
|
||||
void onBuffersReleased() override {}
|
||||
void onSidebandStreamChanged() override {}
|
||||
};
|
||||
|
||||
int main() {
|
||||
setvbuf(stderr, nullptr, _IONBF, 0);
|
||||
android::ProcessState::self()->startThreadPool();
|
||||
const int count = android::Camera::getNumberOfCameras();
|
||||
fprintf(stderr, "camera_count=%d uid=%d\n", count, getuid());
|
||||
if (count < 1) return 2;
|
||||
|
||||
android::sp<android::Camera> camera = android::Camera::connect(
|
||||
// Match GonkCameraHwMgr.cpp exactly. CameraService/AppOps on this
|
||||
// firmware treats the client package as part of camera authorization.
|
||||
0, android::String16("gonk.camera"),
|
||||
android::Camera::USE_CALLING_UID);
|
||||
if (camera == nullptr) return 3;
|
||||
android::CameraParameters params(camera->getParameters());
|
||||
android::Vector<android::Size> previews, pictures;
|
||||
params.getSupportedPreviewSizes(previews);
|
||||
params.getSupportedPictureSizes(pictures);
|
||||
android::Size picture = {0, 0};
|
||||
for (size_t i = 0; i < pictures.size(); ++i)
|
||||
if (pictures[i].width * pictures[i].height > picture.width * picture.height)
|
||||
picture = pictures[i];
|
||||
android::Size preview = {0, 0};
|
||||
uint64_t bestDelta = UINT64_MAX;
|
||||
for (size_t i = 0; i < previews.size(); ++i) {
|
||||
const android::Size candidate = previews[i];
|
||||
if (picture.width && candidate.width * picture.height != picture.width * candidate.height)
|
||||
continue;
|
||||
const uint64_t area = (uint64_t)candidate.width * candidate.height;
|
||||
const uint64_t delta = area > 640u * 480u ? area - 640u * 480u : 640u * 480u - area;
|
||||
if (delta < bestDelta) { preview = candidate; bestDelta = delta; }
|
||||
}
|
||||
if (!preview.width && previews.size()) preview = previews[0];
|
||||
if (!preview.width || !picture.width) {
|
||||
fprintf(stderr, "no supported preview/picture size (preview=%zu picture=%zu)\n",
|
||||
previews.size(), pictures.size());
|
||||
camera->disconnect();
|
||||
return 4;
|
||||
}
|
||||
params.setPreviewSize(preview.width, preview.height);
|
||||
params.setPictureSize(picture.width, picture.height);
|
||||
// Gecko applies its recording hint after selecting still/preview sizes and
|
||||
// before starting the preview. Keep the probe's Camera1 state in step.
|
||||
params.set("recording-hint", "false");
|
||||
android::String8 configured = params.flatten();
|
||||
android::status_t status = camera->setParameters(configured);
|
||||
fprintf(stderr, "set_parameters=%d requested_preview=%dx%d picture=%dx%d\n",
|
||||
status, preview.width, preview.height, picture.width, picture.height);
|
||||
if (status != android::NO_ERROR) { camera->disconnect(); return 4; }
|
||||
params.unflatten(camera->getParameters());
|
||||
int width = 0, height = 0;
|
||||
params.getPreviewSize(&width, &height);
|
||||
int pictureWidth = 0, pictureHeight = 0;
|
||||
params.getPictureSize(&pictureWidth, &pictureHeight);
|
||||
fprintf(stderr, "preview=%dx%d picture=%dx%d format=%s\n", width, height,
|
||||
pictureWidth, pictureHeight, params.getPreviewFormat());
|
||||
|
||||
android::sp<android::IGraphicBufferProducer> producer;
|
||||
android::sp<android::IGonkGraphicBufferConsumer> consumer;
|
||||
android::sp<android::GraphicBufferAlloc> allocator =
|
||||
new android::GraphicBufferAlloc();
|
||||
android::GonkBufferQueue::createBufferQueue(&producer, &consumer, allocator);
|
||||
status = static_cast<android::GonkBufferQueueProducer*>(producer.get())
|
||||
->setSynchronousMode(false);
|
||||
fprintf(stderr, "async_producer=%d\n", status);
|
||||
status = consumer->setConsumerUsageBits(GRALLOC_USAGE_SW_READ_OFTEN);
|
||||
fprintf(stderr, "cpu_read_usage=%d\n", status);
|
||||
status = consumer->setMaxAcquiredBufferCount(4);
|
||||
fprintf(stderr, "max_acquired=%d\n", status);
|
||||
android::sp<QueueListener> listener = new QueueListener();
|
||||
android::sp<android::IConsumerListener> proxy =
|
||||
new android::GonkBufferQueue::ProxyConsumerListener(listener);
|
||||
status = consumer->consumerConnect(proxy, false);
|
||||
fprintf(stderr, "consumer_connect=%d\n", status);
|
||||
android::sp<CameraEvents> cameraEvents = new CameraEvents();
|
||||
camera->setListener(cameraEvents);
|
||||
status = camera->setPreviewTarget(producer);
|
||||
fprintf(stderr, "set_preview_target=%d\n", status);
|
||||
camera->setPreviewCallbackFlags(CAMERA_FRAME_CALLBACK_FLAG_CAMERA);
|
||||
fprintf(stderr, "preview_callback_flags=%d\n", CAMERA_FRAME_CALLBACK_FLAG_CAMERA);
|
||||
const android::status_t focusEvents = camera->sendCommand(
|
||||
CAMERA_CMD_ENABLE_FOCUS_MOVE_MSG, 1, 0);
|
||||
fprintf(stderr, "focus_move_events=%d\n", focusEvents);
|
||||
status = camera->startPreview();
|
||||
fprintf(stderr, "start_preview=%d\n", status);
|
||||
if (status) return 4;
|
||||
|
||||
unsigned acquired = 0;
|
||||
bool saved = false;
|
||||
const int64_t deadline = systemTime(SYSTEM_TIME_MONOTONIC) + 5000000000LL;
|
||||
while (systemTime(SYSTEM_TIME_MONOTONIC) < deadline) {
|
||||
android::IGonkGraphicBufferConsumer::BufferItem item;
|
||||
status = consumer->acquireBuffer(&item, 0);
|
||||
if (status != android::NO_ERROR) { usleep(20000); continue; }
|
||||
++acquired;
|
||||
android::sp<android::GraphicBuffer> buffer = item.mGraphicBuffer;
|
||||
if (item.mFence != nullptr) item.mFence->waitForever("vulpes-gonk-camera");
|
||||
void* pixels = nullptr;
|
||||
if (buffer != nullptr &&
|
||||
buffer->lock(GRALLOC_USAGE_SW_READ_OFTEN, &pixels) == android::NO_ERROR &&
|
||||
pixels != nullptr) {
|
||||
const size_t bytes = (size_t)buffer->getStride() * buffer->getHeight() * 3 / 2;
|
||||
const unsigned char* y = static_cast<const unsigned char*>(pixels);
|
||||
unsigned int minimum = 255, maximum = 0;
|
||||
uint64_t sum = 0;
|
||||
const size_t yBytes = (size_t)buffer->getStride() * buffer->getHeight();
|
||||
for (size_t i = 0; i < yBytes; ++i) {
|
||||
if (y[i] < minimum) minimum = y[i];
|
||||
if (y[i] > maximum) maximum = y[i];
|
||||
sum += y[i];
|
||||
}
|
||||
fprintf(stderr, "frame=%u number=%llu timestamp=%llu y=%u..%u mean=%.2f format=%d\n",
|
||||
acquired, (unsigned long long)item.mFrameNumber, (unsigned long long)item.mTimestamp, minimum, maximum,
|
||||
(double)sum / yBytes, buffer->format);
|
||||
if (!saved || acquired % 10 == 0) {
|
||||
FILE* output = fopen("/data/local/tmp/vulpes-camera-gonkqueue.nv21", "wb");
|
||||
if (output) {
|
||||
fwrite(pixels, 1, bytes, output);
|
||||
fclose(output);
|
||||
chmod("/data/local/tmp/vulpes-camera-gonkqueue.nv21", 0644);
|
||||
FILE* meta = fopen("/data/local/tmp/vulpes-camera-gonkqueue.txt", "w");
|
||||
if (meta) {
|
||||
fprintf(meta, "%u %u %u %d %zu %u %u %u %u\n", buffer->getWidth(),
|
||||
buffer->getHeight(), buffer->getStride(), buffer->format, bytes,
|
||||
acquired, minimum, maximum, (unsigned)(sum / yBytes));
|
||||
fclose(meta);
|
||||
chmod("/data/local/tmp/vulpes-camera-gonkqueue.txt", 0644);
|
||||
}
|
||||
saved = true;
|
||||
}
|
||||
}
|
||||
buffer->unlock();
|
||||
}
|
||||
consumer->releaseBuffer(item.mBuf, item.mFrameNumber, android::Fence::NO_FENCE);
|
||||
}
|
||||
fprintf(stderr, "acquired=%u saved=%d; taking_still\n", acquired, saved);
|
||||
status = camera->takePicture(CAMERA_MSG_SHUTTER | CAMERA_MSG_COMPRESSED_IMAGE);
|
||||
fprintf(stderr, "take_picture=%d\n", status);
|
||||
sleep(3);
|
||||
fprintf(stderr, "stopping_preview\n");
|
||||
camera->stopPreview();
|
||||
fprintf(stderr, "camera_stopped; disconnecting\n");
|
||||
camera->disconnect();
|
||||
consumer->consumerDisconnect();
|
||||
fprintf(stderr, "done\n");
|
||||
return saved ? 0 : 5;
|
||||
}
|
||||
99
tundra/probes/beatles-camera-permission-bridge.cpp
Normal file
99
tundra/probes/beatles-camera-permission-bridge.cpp
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Temporary, narrowly scoped API-23 permission controller for a RAM camera test.
|
||||
#include <binder/Binder.h>
|
||||
#include <binder/IServiceManager.h>
|
||||
#include <binder/Parcel.h>
|
||||
#include <binder/IPCThreadState.h>
|
||||
#include <binder/ProcessState.h>
|
||||
#include <camera/ICameraService.h>
|
||||
#include <grp.h>
|
||||
#include <utils/String16.h>
|
||||
#include <utils/String8.h>
|
||||
#include <utils/Vector.h>
|
||||
#include <unistd.h>
|
||||
#include <stdio.h>
|
||||
|
||||
namespace {
|
||||
const android::String16 kDescriptor("android.os.IPermissionController");
|
||||
const android::String16 kCameraPermission("android.permission.CAMERA");
|
||||
const android::String16 kSystemEventPermission("android.permission.CAMERA_SEND_SYSTEM_EVENTS");
|
||||
const android::String16 kProbePackage("gonk.camera");
|
||||
const android::String16 kSystemPackage("android");
|
||||
const int32_t kCameraUid = 1006;
|
||||
|
||||
class CameraPermissionBridge : public android::BBinder {
|
||||
public:
|
||||
const android::String16& getInterfaceDescriptor() const override {
|
||||
return kDescriptor;
|
||||
}
|
||||
|
||||
protected:
|
||||
android::status_t onTransact(uint32_t code, const android::Parcel& data,
|
||||
android::Parcel* reply, uint32_t flags) override {
|
||||
if (code == android::IBinder::FIRST_CALL_TRANSACTION) {
|
||||
if (!data.enforceInterface(kDescriptor)) return android::PERMISSION_DENIED;
|
||||
const android::String16 permission = data.readString16();
|
||||
const int32_t pid = data.readInt32();
|
||||
const int32_t uid = data.readInt32();
|
||||
(void)pid;
|
||||
const bool grant = (permission == kCameraPermission && uid == kCameraUid) ||
|
||||
(permission == kSystemEventPermission && uid == 1000);
|
||||
android::String8 permissionName(permission);
|
||||
fprintf(stderr, "permission check uid=%d name=%s grant=%d\n", uid,
|
||||
permissionName.string(), grant);
|
||||
reply->writeNoException();
|
||||
reply->writeInt32(grant ? 1 : 0);
|
||||
return android::NO_ERROR;
|
||||
}
|
||||
if (code == android::IBinder::FIRST_CALL_TRANSACTION + 1) {
|
||||
if (!data.enforceInterface(kDescriptor)) return android::PERMISSION_DENIED;
|
||||
const int32_t uid = data.readInt32();
|
||||
reply->writeNoException();
|
||||
const bool hasPackage = uid == kCameraUid || uid == 1000;
|
||||
reply->writeInt32(hasPackage ? 1 : 0);
|
||||
if (uid == kCameraUid) reply->writeString16(kProbePackage);
|
||||
else if (uid == 1000) reply->writeString16(kSystemPackage);
|
||||
return android::NO_ERROR;
|
||||
}
|
||||
if (code == android::IBinder::FIRST_CALL_TRANSACTION + 2) {
|
||||
if (!data.enforceInterface(kDescriptor)) return android::PERMISSION_DENIED;
|
||||
const android::String16 permission = data.readString16();
|
||||
reply->writeNoException();
|
||||
reply->writeInt32(permission == kCameraPermission ? 1 : 0);
|
||||
return android::NO_ERROR;
|
||||
}
|
||||
return android::BBinder::onTransact(code, data, reply, flags);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
int main(int argc, char**) {
|
||||
if (argc != 1) return 2;
|
||||
android::ProcessState::self()->startThreadPool();
|
||||
android::sp<android::IServiceManager> manager = android::defaultServiceManager();
|
||||
android::sp<CameraPermissionBridge> service = new CameraPermissionBridge();
|
||||
android::status_t result = manager->addService(android::String16("permission"), service);
|
||||
fprintf(stderr, "register permission bridge pid=%d uid=%d result=%d\n",
|
||||
getpid(), getuid(), result);
|
||||
if (result != android::NO_ERROR) return 3;
|
||||
gid_t systemGroup = 1000;
|
||||
if (setgroups(1, &systemGroup) || setgid(1000) || setuid(1000)) {
|
||||
perror("drop permission bridge to Android system UID");
|
||||
return 4;
|
||||
}
|
||||
fprintf(stderr, "permission bridge ready\n");
|
||||
android::sp<android::IBinder> cameraBinder =
|
||||
manager->checkService(android::String16("media.camera"));
|
||||
android::sp<android::ICameraService> cameraService =
|
||||
android::interface_cast<android::ICameraService>(cameraBinder);
|
||||
if (cameraService != nullptr) {
|
||||
const int32_t activeUsers[] = {0};
|
||||
cameraService->notifySystemEvent(android::ICameraService::USER_SWITCHED,
|
||||
activeUsers, 1);
|
||||
fprintf(stderr, "notified CameraService active user 0\n");
|
||||
} else {
|
||||
fprintf(stderr, "CameraService unavailable for active-user notification\n");
|
||||
}
|
||||
android::IPCThreadState::self()->joinThreadPool();
|
||||
return 0;
|
||||
}
|
||||
173
tundra/probes/beatles-camera-service-probe.cpp
Normal file
173
tundra/probes/beatles-camera-service-probe.cpp
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Bounded diagnostic for the Android Camera1 service on Nokia 2720 (API 23).
|
||||
#include <camera/Camera.h>
|
||||
#include <camera/CameraParameters.h>
|
||||
#include <hardware/gralloc.h>
|
||||
#include <binder/ProcessState.h>
|
||||
#include <gui/BufferQueue.h>
|
||||
#include <gui/CpuConsumer.h>
|
||||
#include <gui/GraphicBufferAlloc.h>
|
||||
#include <utils/String16.h>
|
||||
#include <utils/String8.h>
|
||||
#include <utils/Mutex.h>
|
||||
#include <stdio.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/stat.h>
|
||||
#include <thread>
|
||||
|
||||
class ProbeListener : public android::CameraListener {
|
||||
public:
|
||||
ProbeListener() : frames(0), bytes(0), jpegBytes(0), yMean(0), yMin(0), yMax(0) {}
|
||||
void notify(int32_t type, int32_t first, int32_t second) override {
|
||||
fprintf(stderr, "notify type=%d first=%d second=%d\n", type, first, second);
|
||||
}
|
||||
void postData(int32_t type, const android::sp<android::IMemory>& data,
|
||||
camera_frame_metadata_t*) override {
|
||||
if (data == nullptr) return;
|
||||
if (type == CAMERA_MSG_PREVIEW_FRAME) {
|
||||
android::Mutex::Autolock lock(mutex);
|
||||
++frames;
|
||||
bytes = data->size();
|
||||
const size_t yBytes = bytes < 640u * 480u ? bytes : 640u * 480u;
|
||||
const unsigned char* y = static_cast<const unsigned char*>(data->pointer());
|
||||
uint64_t sum = 0;
|
||||
yMin = 255;
|
||||
yMax = 0;
|
||||
for (size_t i = 0; i < yBytes; ++i) {
|
||||
if (y[i] < yMin) yMin = y[i];
|
||||
if (y[i] > yMax) yMax = y[i];
|
||||
sum += y[i];
|
||||
}
|
||||
yMean = yBytes ? (double)sum / yBytes : 0;
|
||||
if (frames == 20 && bytes) {
|
||||
FILE* out = fopen("/data/local/tmp/vulpes-camera-frame20.nv21", "wb");
|
||||
if (out) {
|
||||
fwrite(data->pointer(), 1, bytes, out);
|
||||
fclose(out);
|
||||
chmod("/data/local/tmp/vulpes-camera-frame20.nv21", 0644);
|
||||
}
|
||||
}
|
||||
} else if (type == CAMERA_MSG_COMPRESSED_IMAGE) {
|
||||
FILE* out = fopen("/data/local/tmp/vulpes-camera-photo.jpg", "wb");
|
||||
if (out) {
|
||||
jpegBytes = fwrite(data->pointer(), 1, data->size(), out);
|
||||
fclose(out);
|
||||
chmod("/data/local/tmp/vulpes-camera-photo.jpg", 0644);
|
||||
}
|
||||
fprintf(stderr, "jpeg_callback_bytes=%zu\n", jpegBytes);
|
||||
}
|
||||
}
|
||||
void postDataTimestamp(nsecs_t, int32_t,
|
||||
const android::sp<android::IMemory>&) override {}
|
||||
android::Mutex mutex;
|
||||
unsigned frames;
|
||||
size_t bytes;
|
||||
size_t jpegBytes;
|
||||
double yMean;
|
||||
unsigned yMin;
|
||||
unsigned yMax;
|
||||
};
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
if (argc > 2) return 2;
|
||||
setvbuf(stderr, nullptr, _IONBF, 0);
|
||||
const int cameraId = argc == 2 ? atoi(argv[1]) : 0;
|
||||
android::ProcessState::self()->startThreadPool();
|
||||
const int count = android::Camera::getNumberOfCameras();
|
||||
fprintf(stderr, "camera_count=%d uid=%d\n", count, getuid());
|
||||
if (cameraId < 0 || cameraId >= count) return 3;
|
||||
android::CameraInfo info;
|
||||
int status = android::Camera::getCameraInfo(cameraId, &info);
|
||||
fprintf(stderr, "camera_info_status=%d facing=%d orientation=%d\n",
|
||||
status, info.facing, info.orientation);
|
||||
android::sp<android::Camera> camera = android::Camera::connect(
|
||||
cameraId, android::String16("gonk.camera"),
|
||||
android::Camera::USE_CALLING_UID);
|
||||
if (camera == nullptr) {
|
||||
fprintf(stderr, "camera_connect_failed\n");
|
||||
return 4;
|
||||
}
|
||||
android::CameraParameters configured(camera->getParameters());
|
||||
// Stock KaiOS Camera reports app.camera.givenPreviewSize as 320x240
|
||||
// on the beatles device. Gecko then selects this 4:3 size against the
|
||||
// 1600x1200 still capture size. Keep the probe on that exact path.
|
||||
configured.setPreviewSize(320, 240);
|
||||
configured.setPictureSize(1600, 1200);
|
||||
configured.setPreviewFormat("yuv420sp");
|
||||
configured.set("recording-hint", "false");
|
||||
status = camera->setParameters(configured.flatten());
|
||||
fprintf(stderr, "set_full_parameters=%d\n", status);
|
||||
android::sp<ProbeListener> listener = new ProbeListener();
|
||||
camera->setListener(listener);
|
||||
camera->setPreviewCallbackFlags(CAMERA_FRAME_CALLBACK_FLAG_ENABLE_MASK);
|
||||
android::String8 parameters = camera->getParameters();
|
||||
fprintf(stderr, "camera_parameters=%s\n", parameters.string());
|
||||
fprintf(stderr, "before_bufferqueue_create\n");
|
||||
android::sp<android::IGraphicBufferProducer> producer;
|
||||
android::sp<android::IGraphicBufferConsumer> consumer;
|
||||
android::sp<android::GraphicBufferAlloc> allocator = new android::GraphicBufferAlloc();
|
||||
android::BufferQueue::createBufferQueue(&producer, &consumer, allocator);
|
||||
fprintf(stderr, "after_bufferqueue_create producer=%d consumer=%d\n", producer != nullptr, consumer != nullptr);
|
||||
fprintf(stderr, "before_cpuconsumer\n");
|
||||
android::sp<android::CpuConsumer> previewConsumer =
|
||||
new android::CpuConsumer(consumer, 4);
|
||||
fprintf(stderr, "after_cpuconsumer\n");
|
||||
status = consumer->setConsumerUsageBits(GRALLOC_USAGE_SW_READ_OFTEN |
|
||||
GRALLOC_USAGE_HW_TEXTURE | GRALLOC_USAGE_HW_RENDER);
|
||||
fprintf(stderr, "set_consumer_usage=%d bits=%08x\n", status,
|
||||
(unsigned)(GRALLOC_USAGE_SW_READ_OFTEN | GRALLOC_USAGE_HW_TEXTURE | GRALLOC_USAGE_HW_RENDER));
|
||||
fprintf(stderr, "before_consumer_setname\n");
|
||||
previewConsumer->setName(android::String8("Vulpes Camera Probe"));
|
||||
fprintf(stderr, "after_consumer_setname\n");
|
||||
fprintf(stderr, "before_consumer_size\n");
|
||||
previewConsumer->setDefaultBufferSize(320, 240);
|
||||
fprintf(stderr, "after_consumer_size\n");
|
||||
fprintf(stderr, "before_set_preview_target\n");
|
||||
status = camera->setPreviewTarget(producer);
|
||||
fprintf(stderr, "set_preview_target=%d\n", status);
|
||||
status = camera->startPreview();
|
||||
fprintf(stderr, "start_preview=%d\n", status);
|
||||
unsigned graphicFrames = 0, graphicSamples = 0;
|
||||
unsigned long long graphicSum = 0;
|
||||
unsigned graphicMin = 255, graphicMax = 0;
|
||||
for (int elapsed = 0; elapsed < 3; elapsed++) {
|
||||
android::CpuConsumer::LockedBuffer buffer;
|
||||
while (previewConsumer->lockNextBuffer(&buffer) == android::NO_ERROR) {
|
||||
++graphicFrames;
|
||||
fprintf(stderr, "graphic_frame=%u width=%u height=%u stride=%u format=%d\n",
|
||||
graphicFrames, buffer.width, buffer.height, buffer.stride,
|
||||
buffer.format);
|
||||
if (buffer.data && graphicFrames >= 5) {
|
||||
const size_t yBytes = (size_t)buffer.stride * buffer.height;
|
||||
if (graphicFrames == 5) {
|
||||
FILE* out = fopen("/data/local/tmp/vulpes-camera-graphic.nv21", "wb");
|
||||
if (out) { fwrite(buffer.data, 1, yBytes * 3 / 2, out); fclose(out); chmod("/data/local/tmp/vulpes-camera-graphic.nv21", 0644); }
|
||||
}
|
||||
for (size_t i = 0; i < yBytes; i += 4096) {
|
||||
unsigned value = buffer.data[i]; graphicSum += value; ++graphicSamples;
|
||||
if (value < graphicMin) graphicMin = value;
|
||||
if (value > graphicMax) graphicMax = value;
|
||||
}
|
||||
}
|
||||
previewConsumer->unlockBuffer(buffer);
|
||||
}
|
||||
usleep(1000000);
|
||||
}
|
||||
{
|
||||
android::Mutex::Autolock lock(listener->mutex);
|
||||
fprintf(stderr, "preview_frames=%u frame_bytes=%zu callback_y_mean=%.2f min=%u max=%u graphic_frames=%u graphic_samples=%u graphic_y_mean=%.2f min=%u max=%u\n",
|
||||
listener->frames, listener->bytes, listener->yMean, listener->yMin, listener->yMax,
|
||||
graphicFrames, graphicSamples,
|
||||
graphicSamples ? (double)graphicSum / graphicSamples : 0.0,
|
||||
graphicMin, graphicMax);
|
||||
}
|
||||
std::thread([camera]() {
|
||||
fprintf(stderr, "take_picture_begin\n");
|
||||
const int pictureStatus = camera->takePicture(CAMERA_MSG_COMPRESSED_IMAGE);
|
||||
fprintf(stderr, "take_picture=%d\n", pictureStatus);
|
||||
}).detach();
|
||||
sleep(4);
|
||||
camera->stopPreview();
|
||||
camera->disconnect();
|
||||
return 0;
|
||||
}
|
||||
214
tundra/probes/beatles-camera-worker.cpp
Normal file
214
tundra/probes/beatles-camera-worker.cpp
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
// SPDX-License-Identifier: MPL-2.0
|
||||
// CameraService worker for the Nokia 2720 Flip. The parent broker drops this
|
||||
// process to Android's camera UID before exec. Gaia talks to it over a local
|
||||
// socket; it never receives Binder or elevated credentials.
|
||||
#include <camera/Camera.h>
|
||||
#include <camera/CameraParameters.h>
|
||||
#include <binder/ProcessState.h>
|
||||
#include <gui/BufferQueue.h>
|
||||
#include <gui/CpuConsumer.h>
|
||||
#include <gui/GraphicBufferAlloc.h>
|
||||
#include <hardware/gralloc.h>
|
||||
#include <utils/Timers.h>
|
||||
#include <utils/Mutex.h>
|
||||
#include <utils/String8.h>
|
||||
#include <utils/String16.h>
|
||||
#include <condition_variable>
|
||||
#include <chrono>
|
||||
#include <atomic>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
#include <unistd.h>
|
||||
#include <sys/socket.h>
|
||||
|
||||
namespace {
|
||||
bool write_all(int fd, const void *data, size_t size) {
|
||||
const char *p = static_cast<const char *>(data);
|
||||
while (size) {
|
||||
ssize_t n = write(fd, p, size);
|
||||
if (n <= 0) return false;
|
||||
p += n; size -= static_cast<size_t>(n);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
bool read_line(int fd, std::string *line) {
|
||||
line->clear();
|
||||
char c;
|
||||
while (line->size() < 2048) {
|
||||
ssize_t n = read(fd, &c, 1);
|
||||
if (n <= 0) return false;
|
||||
if (c == '\n') return true;
|
||||
line->push_back(c);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
bool send_json(int fd, const std::string &json, bool frame=false,
|
||||
uint32_t sequence=0, const std::vector<uint8_t> *bytes=nullptr) {
|
||||
uint32_t length = static_cast<uint32_t>(json.size());
|
||||
if (frame) {
|
||||
uint32_t header[2] = {sequence, bytes ? static_cast<uint32_t>(bytes->size()) : 0};
|
||||
return write_all(fd, header, sizeof(header)) && (!bytes || write_all(fd,bytes->data(),bytes->size()));
|
||||
}
|
||||
return write_all(fd,&length,sizeof(length)) && write_all(fd,json.data(),json.size());
|
||||
}
|
||||
|
||||
class CameraWorker : public android::CameraListener {
|
||||
public:
|
||||
explicit CameraWorker(int fd) : fd_(fd), camera_id_(0), sequence_(0), stopping_(false) {}
|
||||
void notify(int32_t type, int32_t first, int32_t second) override {
|
||||
fprintf(stderr,"camera notify type=%d first=%d second=%d\n",type,first,second);
|
||||
}
|
||||
void postData(int32_t type, const android::sp<android::IMemory> &data,
|
||||
camera_frame_metadata_t *) override {
|
||||
if (data == nullptr) return;
|
||||
if (type == CAMERA_MSG_COMPRESSED_IMAGE) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
jpeg_.assign(static_cast<const uint8_t *>(data->pointer()),
|
||||
static_cast<const uint8_t *>(data->pointer()) + data->size());
|
||||
ready_.notify_all();
|
||||
return;
|
||||
}
|
||||
if (type != CAMERA_MSG_PREVIEW_FRAME) return;
|
||||
const size_t expected = 320u * 240u * 3u / 2u;
|
||||
if (data->size() < expected) return;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
frame_.assign(static_cast<const uint8_t *>(data->pointer()),
|
||||
static_cast<const uint8_t *>(data->pointer()) + expected);
|
||||
++sequence_;
|
||||
if(sequence_==1) fprintf(stderr,"camera first frame bytes=%zu uid=%d\n",frame_.size(),getuid());
|
||||
}
|
||||
ready_.notify_all();
|
||||
}
|
||||
void postDataTimestamp(nsecs_t, int32_t,
|
||||
const android::sp<android::IMemory> &) override {}
|
||||
|
||||
int run() {
|
||||
android::ProcessState::self()->startThreadPool();
|
||||
std::string line;
|
||||
while (read_line(fd_, &line)) {
|
||||
if (line == "start" || line == "start 0") {
|
||||
if (!start()) { send_json(fd_,"{\"ready\":false,\"error\":\"CAMERA_START_FAILED\"}"); continue; }
|
||||
if (!send_json(fd_,"{\"ready\":true,\"width\":320,\"height\":240,\"format\":\"nv21\",\"orientation\":270,\"flashModes\":[\"off\",\"auto\",\"on\",\"torch\"],\"pictureSizes\":[{\"width\":1600,\"height\":1200}],\"focusModes\":[\"fixed\"],\"maxFocusAreas\":0,\"zoomRatios\":[1] }")) return 1;
|
||||
} else if (line == "frame") {
|
||||
std::vector<uint8_t> frame;
|
||||
uint32_t seq;
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(mutex_);
|
||||
const uint32_t previous = delivered_;
|
||||
ready_.wait_for(lock,std::chrono::seconds(2),[&]{return sequence_>previous || stopping_;});
|
||||
if (sequence_<=previous || frame_.empty()) { if(!send_json(fd_,"",true,sequence_,nullptr)) return 1; continue; }
|
||||
seq=sequence_; delivered_=seq; frame=frame_;
|
||||
}
|
||||
if (!send_json(fd_,"",true,seq,&frame)) return 1;
|
||||
} else if (line == "stop") {
|
||||
stop(); send_json(fd_,"{\"ok\":true}"); return 0;
|
||||
} else if (line.find("\"action\":\"flash\"") != std::string::npos) {
|
||||
const size_t key=line.find("\"mode\":\"");
|
||||
const size_t begin=key==std::string::npos?key:key+8;
|
||||
const size_t end=begin==std::string::npos?begin:line.find('"',begin);
|
||||
if(begin==std::string::npos || end==std::string::npos) {
|
||||
send_json(fd_,"{\"ok\":false,\"error\":\"CAMERA_INVALID_FLASH_MODE\"}");continue;
|
||||
}
|
||||
const std::string mode=line.substr(begin,end-begin);
|
||||
android::CameraParameters params(camera_->getParameters());
|
||||
params.set(android::CameraParameters::KEY_FLASH_MODE,mode.c_str());
|
||||
const android::status_t status=camera_->setParameters(params.flatten());
|
||||
if(status==android::NO_ERROR) {
|
||||
camera_->setPreviewCallbackFlags(CAMERA_FRAME_CALLBACK_FLAG_CAMERA);
|
||||
camera_->startPreview();
|
||||
send_json(fd_,"{\"ok\":true,\"result\":true}");
|
||||
}
|
||||
else send_json(fd_,"{\"ok\":false,\"error\":\"CAMERA_FLASH_UNAVAILABLE\"}");
|
||||
} else if (line.find("\"action\":\"zoom\"") != std::string::npos) {
|
||||
// This module reports a single zoom ratio (1x), so expose a stable
|
||||
// no-op instead of asking Camera1 to select an unsupported index.
|
||||
send_json(fd_,"{\"ok\":true,\"result\":{\"zoom\":1}}");
|
||||
} else if (line.find("\"action\":\"picture\"") != std::string::npos) {
|
||||
{ std::lock_guard<std::mutex> lock(mutex_); jpeg_.clear(); }
|
||||
const android::status_t status=camera_->takePicture(CAMERA_MSG_COMPRESSED_IMAGE);
|
||||
std::vector<uint8_t> jpeg;
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(mutex_);
|
||||
ready_.wait_for(lock,std::chrono::seconds(12),[&]{return !jpeg_.empty() || stopping_;});
|
||||
jpeg=jpeg_;
|
||||
}
|
||||
camera_->setPreviewCallbackFlags(CAMERA_FRAME_CALLBACK_FLAG_CAMERA);
|
||||
camera_->startPreview();
|
||||
if(status!=android::NO_ERROR || jpeg.empty()) {
|
||||
send_json(fd_,"{\"ok\":false,\"error\":\"CAMERA_CAPTURE_FAILED\"}");continue;
|
||||
}
|
||||
char reply[128];
|
||||
std::snprintf(reply,sizeof(reply),"{\"ok\":true,\"jpegLength\":%zu}",jpeg.size());
|
||||
if(!send_json(fd_,reply) || !write_all(fd_,jpeg.data(),jpeg.size())) return 1;
|
||||
} else {
|
||||
send_json(fd_,"{\"ok\":false,\"error\":\"CAMERA_UNSUPPORTED_COMMAND\"}");
|
||||
}
|
||||
}
|
||||
stop(); return 0;
|
||||
}
|
||||
|
||||
private:
|
||||
bool start() {
|
||||
if (camera_.get() != nullptr) stop();
|
||||
fprintf(stderr,"camera count=%d uid=%d\n",android::Camera::getNumberOfCameras(),getuid());
|
||||
camera_ = android::Camera::connect(camera_id_,android::String16("gonk.camera"),android::Camera::USE_CALLING_UID);
|
||||
if (camera_.get() == nullptr) {fprintf(stderr,"camera connect failed\n");return false;}
|
||||
android::CameraParameters params(camera_->getParameters());
|
||||
params.setPreviewSize(320,240); params.setPictureSize(1600,1200);
|
||||
params.setPreviewFormat("yuv420sp"); params.set("recording-hint","false");
|
||||
android::status_t status=camera_->setParameters(params.flatten());
|
||||
fprintf(stderr,"camera set parameters=%d\n",status);
|
||||
if (status != android::NO_ERROR) { stop(); return false; }
|
||||
android::sp<android::IGraphicBufferProducer> producer;
|
||||
android::sp<android::IGraphicBufferConsumer> consumer;
|
||||
android::sp<android::GraphicBufferAlloc> allocator = new android::GraphicBufferAlloc();
|
||||
android::BufferQueue::createBufferQueue(&producer,&consumer,allocator);
|
||||
consumer->setConsumerUsageBits(GRALLOC_USAGE_SW_READ_OFTEN | GRALLOC_USAGE_HW_TEXTURE | GRALLOC_USAGE_HW_RENDER);
|
||||
consumer->setDefaultBufferSize(320,240);
|
||||
consumer_ = new android::CpuConsumer(consumer,4);
|
||||
consumer_->setName(android::String8("Vulpes Camera"));
|
||||
status=camera_->setPreviewTarget(producer);
|
||||
fprintf(stderr,"camera set target=%d\n",status);
|
||||
if (status != android::NO_ERROR) { stop(); return false; }
|
||||
camera_->setListener(this);
|
||||
// The broker consumes copied Camera1 preview frames rather than video metadata.
|
||||
camera_->setPreviewCallbackFlags(CAMERA_FRAME_CALLBACK_FLAG_CAMERA);
|
||||
status=camera_->startPreview(); fprintf(stderr,"camera start preview=%d\n",status);
|
||||
if (status != android::NO_ERROR) { stop(); return false; }
|
||||
stopping_=false; delivered_=sequence_;
|
||||
consumer_thread_=std::thread([this]{
|
||||
while(!stopping_) {
|
||||
android::CpuConsumer::LockedBuffer buffer;
|
||||
if(consumer_->lockNextBuffer(&buffer)==android::NO_ERROR) consumer_->unlockBuffer(buffer);
|
||||
else usleep(10000);
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
void stop() {
|
||||
{ std::lock_guard<std::mutex> lock(mutex_); stopping_=true; }
|
||||
ready_.notify_all();
|
||||
if (camera_.get() != nullptr) { camera_->stopPreview(); camera_->disconnect(); camera_.clear(); }
|
||||
if(consumer_thread_.joinable()) consumer_thread_.join();
|
||||
consumer_.clear();
|
||||
}
|
||||
int fd_; int camera_id_;
|
||||
android::sp<android::Camera> camera_;
|
||||
android::sp<android::CpuConsumer> consumer_;
|
||||
std::mutex mutex_; std::condition_variable ready_;
|
||||
std::thread consumer_thread_;
|
||||
std::vector<uint8_t> frame_, jpeg_; uint32_t sequence_, delivered_; std::atomic<bool> stopping_;
|
||||
};
|
||||
}
|
||||
|
||||
int main() {
|
||||
setvbuf(stderr,nullptr,_IONBF,0);
|
||||
android::sp<CameraWorker> worker = new CameraWorker(STDIN_FILENO);
|
||||
return worker->run();
|
||||
}
|
||||
|
|
@ -31,6 +31,7 @@
|
|||
#include "beatles-lid.h"
|
||||
static volatile sig_atomic_t interrupted;
|
||||
static int phone_controls;
|
||||
static int camera_broker_active;
|
||||
static void interrupt(int sig) { interrupted = sig; }
|
||||
static void inventory_camera_nodes(void) {
|
||||
static const int minors[] = {0, 1, 32, 33};
|
||||
|
|
@ -238,6 +239,11 @@ static void inside(int seconds) {
|
|||
require(mount("/proc", ROOT "/proc", NULL, MS_BIND, NULL), "bind proc");
|
||||
require(mount("/sys", ROOT "/sys", NULL, MS_BIND, NULL), "bind sys");
|
||||
require(mount(NULL, ROOT "/sys", NULL, MS_BIND | MS_REMOUNT | MS_RDONLY, NULL), "readonly sys");
|
||||
if (camera_broker_active) {
|
||||
if (mkdir(ROOT "/work/camera-ipc",0777) && errno!=EEXIST) _exit(1);
|
||||
require(mount(BASE "/camera-ipc",ROOT "/work/camera-ipc",NULL,MS_BIND,NULL),"camera IPC mount");
|
||||
require(chmod(ROOT "/work/camera-ipc",0777),"camera IPC permissions");
|
||||
}
|
||||
require(mount("tmpfs", ROOT "/dev", "tmpfs", MS_NOSUID, "size=1m,mode=0755"), "devices");
|
||||
node(ROOT "/dev/null",1,3,0666); node(ROOT "/dev/zero",1,5,0666);
|
||||
node(ROOT "/dev/random",1,8,0666); node(ROOT "/dev/urandom",1,9,0666);
|
||||
|
|
@ -261,6 +267,7 @@ static void inside(int seconds) {
|
|||
setenv("VULPES_TRIAL_MANUAL",seconds >= 900 ? "1" : "0",1);
|
||||
setenv("VULPES_TUNDRA_RADIO",phone_controls?"1":"0",1);
|
||||
setenv("VULPES_TUNDRA_WIFI",phone_controls?"1":"0",1);
|
||||
setenv("VULPES_TUNDRA_CAMERA",camera_broker_active?"1":"0",1);
|
||||
setenv("LANG","C",1); setenv("DISPLAY",":0",1);
|
||||
setenv("LD_LIBRARY_PATH","/usr/lib/arm-linux-gnueabihf:/lib/arm-linux-gnueabihf",1);
|
||||
execl("/bin/sh","sh","/work/display-test.sh",NULL);
|
||||
|
|
@ -272,6 +279,7 @@ int main(int argc, char **argv) {
|
|||
int seconds = !strcmp(argv[1],"--temporary-45s") ? 45 :
|
||||
!strcmp(argv[1],"--temporary-90s") ? 90 :
|
||||
(!strcmp(argv[1],"--temporary-180s") || !strcmp(argv[1],"--phone-180s")) ? 180 :
|
||||
!strcmp(argv[1],"--temporary-240s") ? 240 :
|
||||
(!strcmp(argv[1],"--interactive-900s") || !strcmp(argv[1],"--phone-900s")) ? 900 :
|
||||
(!strcmp(argv[1],"--interactive-1800s") || !strcmp(argv[1],"--phone-1800s")) ? 1800 : 0;
|
||||
if (!seconds) return 2;
|
||||
|
|
@ -309,6 +317,17 @@ int main(int argc, char **argv) {
|
|||
* previous run's image be mistaken for a frame from this session. */
|
||||
unlink(ROOT "/work/camera-frame.yuv");
|
||||
unlink(ROOT "/work/camera-photo.jpg");
|
||||
/* These names belong to our API-23 diagnostic; adb shell cannot remove
|
||||
* camera-UID files from Android's sticky /data/local/tmp directory. */
|
||||
unlink("/data/local/tmp/vulpes-camera-frame.nv21");
|
||||
unlink("/data/local/tmp/vulpes-camera-frame20.nv21");
|
||||
unlink("/data/local/tmp/vulpes-camera-graphic.nv21");
|
||||
unlink("/data/local/tmp/vulpes-camera-photo.jpg");
|
||||
unlink("/data/local/tmp/vulpes-camera-glconsumer.ppm");
|
||||
unlink("/data/local/tmp/vulpes-camera-gonkqueue.nv21");
|
||||
unlink("/data/local/tmp/vulpes-camera-gonkqueue.txt");
|
||||
unlink("/data/local/tmp/vulpes-camera-callback.yuv");
|
||||
unlink("/data/local/tmp/vulpes-camera-callback.jpg");
|
||||
sleep(2);
|
||||
if (!access(BASE "/camera-probe-once", F_OK)) {
|
||||
unlink(BASE "/camera-probe-once");
|
||||
|
|
@ -326,12 +345,80 @@ int main(int argc, char **argv) {
|
|||
}
|
||||
int result = android_service("/system/bin/stop");
|
||||
printf("stop b2g: %d\n",result);
|
||||
pid_t child = -1, radio = -1;
|
||||
pid_t child = -1, radio = -1, permission_bridge = -1, camera_broker = -1;
|
||||
struct lid_state lid;lid_init(&lid,fb);
|
||||
if (!result) {
|
||||
sleep(1);
|
||||
puts("Camera node inventory after stopping B2G");
|
||||
inventory_camera_nodes();
|
||||
int gl_camera_probe = !access(BASE "/camera-gl-probe-once", F_OK);
|
||||
int camera_service_probe = !access(BASE "/camera-service-probe-once", F_OK);
|
||||
if (gl_camera_probe || camera_service_probe) {
|
||||
unlink(BASE "/camera-service-probe-once");
|
||||
unlink(BASE "/camera-gl-probe-once");
|
||||
permission_bridge = fork();
|
||||
if (!permission_bridge) {
|
||||
int output = open(BASE "/camera-permission-bridge.log",
|
||||
O_CREAT | O_TRUNC | O_WRONLY, 0644);
|
||||
if (output >= 0) {
|
||||
fchmod(output, 0644);
|
||||
dup2(output, STDOUT_FILENO);
|
||||
dup2(output, STDERR_FILENO);
|
||||
close(output);
|
||||
}
|
||||
setenv("LD_LIBRARY_PATH", "/system/lib:/data/local/tmp", 1);
|
||||
execl("/data/local/tmp/vulpes-camera-permission-bridge",
|
||||
"vulpes-camera-permission-bridge", NULL);
|
||||
_exit(127);
|
||||
}
|
||||
if (permission_bridge > 0) sleep(2);
|
||||
pid_t probe = fork();
|
||||
if (!probe) {
|
||||
int output = open(gl_camera_probe ? BASE "/camera-gl-probe.log" :
|
||||
BASE "/camera-service-probe.log",
|
||||
O_CREAT | O_TRUNC | O_WRONLY, 0644);
|
||||
if (output >= 0) {
|
||||
fchmod(output, 0644);
|
||||
dup2(output, STDOUT_FILENO);
|
||||
dup2(output, STDERR_FILENO);
|
||||
close(output);
|
||||
}
|
||||
gid_t groups[] = {1006};
|
||||
if (setgroups(1, groups) || setgid(1006) || setuid(1006)) _exit(126);
|
||||
setenv("LD_LIBRARY_PATH", "/system/lib:/data/local/tmp", 1);
|
||||
if (gl_camera_probe)
|
||||
execl("/data/local/tmp/vulpes-camera-gl-probe-20261023",
|
||||
"vulpes-camera-gl-probe-20261023", NULL);
|
||||
else
|
||||
execl("/data/local/tmp/vulpes-camera-probe-20261023",
|
||||
"vulpes-camera-probe-20261023", "0", NULL);
|
||||
_exit(127);
|
||||
}
|
||||
if (probe > 0) {
|
||||
int probe_status = 0, finished = 0;
|
||||
for (int attempt = 0; attempt < (gl_camera_probe ? 200 : 100); attempt++) {
|
||||
if (waitpid(probe, &probe_status, WNOHANG) == probe) {
|
||||
finished = 1;
|
||||
break;
|
||||
}
|
||||
usleep(100000);
|
||||
}
|
||||
if (!finished) {
|
||||
kill(probe, SIGKILL);
|
||||
waitpid(probe, &probe_status, 0);
|
||||
printf("Camera%s probe timed out after %d seconds\n",
|
||||
gl_camera_probe ? " EGL/GLConsumer" : "1", gl_camera_probe ? 20 : 10);
|
||||
} else {
|
||||
printf("CameraService probe exit: %d\n",
|
||||
WIFEXITED(probe_status) ? WEXITSTATUS(probe_status) : -1);
|
||||
}
|
||||
}
|
||||
if (permission_bridge > 0) {
|
||||
kill(permission_bridge, SIGTERM);
|
||||
waitpid(permission_bridge, NULL, 0);
|
||||
permission_bridge = -1;
|
||||
}
|
||||
}
|
||||
int preview_dump_probe = !access(BASE "/qcamera-preview-dump-once", F_OK);
|
||||
if (!access(BASE "/qcamera-probe-once", F_OK) || preview_dump_probe) {
|
||||
unlink(BASE "/qcamera-probe-once");
|
||||
|
|
@ -393,6 +480,47 @@ int main(int argc, char **argv) {
|
|||
if (prop > 0) waitpid(prop, NULL, 0);
|
||||
if (!umount(dump_path)) rmdir(dump_path);
|
||||
}
|
||||
camera_broker_active = !access(BASE "/camera-broker-once",F_OK);
|
||||
if (camera_broker_active) {
|
||||
unlink(BASE "/camera-broker-once");
|
||||
if (mkdir(BASE "/camera-ipc",0777) && errno!=EEXIST) perror("camera IPC directory");
|
||||
chmod(BASE "/camera-ipc",0777);
|
||||
unlink(BASE "/camera-ipc/camera.sock");
|
||||
permission_bridge=fork();
|
||||
if(!permission_bridge) {
|
||||
int output=open(BASE "/camera-permission-bridge.log",O_CREAT|O_TRUNC|O_WRONLY,0644);
|
||||
if(output>=0){fchmod(output,0644);dup2(output,STDOUT_FILENO);dup2(output,STDERR_FILENO);close(output);}
|
||||
setenv("LD_LIBRARY_PATH","/system/lib:/data/local/tmp",1);
|
||||
execl("/data/local/tmp/vulpes-camera-permission-bridge","vulpes-camera-permission-bridge",NULL);
|
||||
_exit(127);
|
||||
}
|
||||
if(permission_bridge>0) {
|
||||
int ready=0;
|
||||
for(int i=0;i<50;i++) {
|
||||
FILE *log=fopen(BASE "/camera-permission-bridge.log","r");
|
||||
if(log){char line[256];while(fgets(line,sizeof(line),log))if(strstr(line,"permission bridge ready"))ready=1;fclose(log);}
|
||||
if(ready)break;
|
||||
usleep(100000);
|
||||
}
|
||||
}
|
||||
camera_broker=fork();
|
||||
if(!camera_broker) {
|
||||
int output=open(BASE "/camera-broker.log",O_CREAT|O_TRUNC|O_WRONLY,0644);
|
||||
if(output>=0){fchmod(output,0644);dup2(output,STDOUT_FILENO);dup2(output,STDERR_FILENO);close(output);}
|
||||
setenv("LD_LIBRARY_PATH","/system/lib:/data/local/tmp",1);
|
||||
execl("/data/local/tmp/vulpes-beatles-camera-broker","vulpes-beatles-camera-broker",NULL);
|
||||
_exit(127);
|
||||
}
|
||||
if(camera_broker>0) {
|
||||
int ready=0;
|
||||
for(int i=0;i<50;i++) {
|
||||
struct stat info;
|
||||
if(!stat(BASE "/camera-ipc/camera.sock",&info) && S_ISSOCK(info.st_mode)){ready=1;break;}
|
||||
usleep(100000);
|
||||
}
|
||||
if(!ready){fprintf(stderr,"Camera broker did not create its socket\n");camera_broker_active=0;}
|
||||
}
|
||||
}
|
||||
if (seconds >= 180) {
|
||||
radio = fork();
|
||||
if (!radio) radio_inside();
|
||||
|
|
@ -450,6 +578,18 @@ int main(int argc, char **argv) {
|
|||
}
|
||||
if(radio>0) {kill(-radio,SIGKILL);waitpid(radio,NULL,0);}
|
||||
}
|
||||
if (camera_broker > 0) {
|
||||
kill(-camera_broker,SIGTERM);
|
||||
for(int i=0;i<30;i++) {
|
||||
if(waitpid(camera_broker,NULL,WNOHANG)==camera_broker){camera_broker=-1;break;}
|
||||
usleep(100000);
|
||||
}
|
||||
if(camera_broker>0){kill(-camera_broker,SIGKILL);waitpid(camera_broker,NULL,0);}
|
||||
}
|
||||
if (permission_bridge > 0) {
|
||||
kill(permission_bridge, SIGTERM);
|
||||
waitpid(permission_bridge, NULL, 0);
|
||||
}
|
||||
clean_runtime();
|
||||
lid_restore(&lid);
|
||||
if (ioctl(fb,FBIOPUT_VSCREENINFO,&original)) perror("restore framebuffer mode");
|
||||
|
|
|
|||
72
tundra/runtime/beatles-camera-client.py
Normal file
72
tundra/runtime/beatles-camera-client.py
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
#!/usr/bin/python3.11
|
||||
# SPDX-License-Identifier: MPL-2.0
|
||||
"""Narrow stdio-to-socket adapter for the Android CameraService broker."""
|
||||
import os
|
||||
import json
|
||||
import socket
|
||||
import sys
|
||||
|
||||
SOCKET = '/work/camera-ipc/camera.sock'
|
||||
|
||||
def read_exact(stream, size):
|
||||
data = bytearray()
|
||||
while len(data) < size:
|
||||
chunk = stream.recv(size - len(data))
|
||||
if not chunk:
|
||||
raise EOFError('Camera broker closed the session')
|
||||
data.extend(chunk)
|
||||
return bytes(data)
|
||||
|
||||
def forward(sock, target, size):
|
||||
header = read_exact(sock, size)
|
||||
target.write(header); target.flush()
|
||||
return header
|
||||
|
||||
def packet(sock, output):
|
||||
header = read_exact(sock, 4)
|
||||
size = int.from_bytes(header, 'little')
|
||||
if size > 65536:
|
||||
raise ValueError('Camera broker sent an oversized response')
|
||||
payload = read_exact(sock, size)
|
||||
output.write(header + payload); output.flush()
|
||||
return json.loads(payload.decode('utf-8'))
|
||||
|
||||
def main():
|
||||
client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
client.connect(SOCKET)
|
||||
output = os.fdopen(os.dup(1), 'wb', buffering=0)
|
||||
try:
|
||||
client.sendall(b'start\n')
|
||||
header = forward(client, output, 4)
|
||||
length = int.from_bytes(header, 'little')
|
||||
if not 0 < length <= 65536:
|
||||
return 2
|
||||
forward(client, output, length)
|
||||
while True:
|
||||
command = sys.stdin.buffer.readline(2049)
|
||||
if not command or len(command) > 2048:
|
||||
break
|
||||
client.sendall(command)
|
||||
if command.strip() == b'frame':
|
||||
header = forward(client, output, 8)
|
||||
length = int.from_bytes(header[4:], 'little')
|
||||
if length > 320 * 240 * 3 // 2:
|
||||
return 3
|
||||
if length:
|
||||
forward(client, output, length)
|
||||
elif command.strip() == b'stop':
|
||||
packet(client, output)
|
||||
break
|
||||
else:
|
||||
response = packet(client, output)
|
||||
jpeg = response.get('jpegLength', 0)
|
||||
if isinstance(jpeg, int) and 0 < jpeg <= 32 * 1024 * 1024:
|
||||
forward(client, output, jpeg)
|
||||
except (BrokenPipeError, ConnectionError, EOFError):
|
||||
return 0
|
||||
finally:
|
||||
client.close(); output.close()
|
||||
return 0
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
|
|
@ -86,6 +86,7 @@ def control(p):
|
|||
wake_recovery.deadline = None
|
||||
maximum = int((BACKLIGHT/'max_brightness').read_text())
|
||||
(BACKLIGHT/'brightness').write_text(str(max(1, round(maximum * level)) if enabled else 0))
|
||||
set_session_idle(not enabled)
|
||||
elif p.get('operation') == 'vibrate':
|
||||
vibration.start(p.get('pattern'))
|
||||
elif p.get('operation') == 'volume':
|
||||
|
|
@ -103,6 +104,29 @@ def control(p):
|
|||
raise ValueError('NOT_SUPPORTED')
|
||||
return True
|
||||
|
||||
def set_session_idle(idle):
|
||||
"""Tell logind that Gaia has switched the display between active and idle."""
|
||||
try:
|
||||
result = subprocess.run(['/usr/bin/loginctl', 'list-sessions', '--no-legend', '--no-pager'],
|
||||
capture_output=True, text=True, timeout=3, check=True)
|
||||
sessions = []
|
||||
for line in result.stdout.splitlines():
|
||||
fields = line.split()
|
||||
if len(fields) >= 2 and fields[1] == '32011' and fields[0].isalnum():
|
||||
sessions.append(fields[0])
|
||||
if not sessions:
|
||||
raise RuntimeError('No Tundra logind sessions found')
|
||||
value = 'true' if idle else 'false'
|
||||
for session in sessions:
|
||||
subprocess.run(['/usr/bin/busctl', '--system', 'call', 'org.freedesktop.login1',
|
||||
'/org/freedesktop/login1/session/'+session,
|
||||
'org.freedesktop.login1.Session', 'SetIdleHint', 'b', value],
|
||||
capture_output=True, text=True, timeout=3, check=True)
|
||||
return True
|
||||
except (OSError, RuntimeError, subprocess.SubprocessError) as error:
|
||||
print('TUNDRA_LOGIND_IDLE_FAILED '+str(error), file=sys.stderr, flush=True)
|
||||
return False
|
||||
|
||||
def serve():
|
||||
SOCKET.parent.mkdir(parents=True, exist_ok=True)
|
||||
SOCKET.unlink(missing_ok=True)
|
||||
|
|
|
|||
|
|
@ -28,9 +28,8 @@ class Listener(C.Structure):
|
|||
|
||||
|
||||
class Camera:
|
||||
width, height = 640, 480
|
||||
|
||||
def __init__(self, index):
|
||||
def __init__(self, index, width=640, height=480):
|
||||
self.width, self.height = width, height
|
||||
self.camera = None
|
||||
self.display = self.surface = self.context = None
|
||||
self.frame = None
|
||||
|
|
@ -223,6 +222,8 @@ class Camera:
|
|||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--camera', type=int, choices=(0, 1), default=0)
|
||||
parser.add_argument('--width', type=int, choices=(320, 640), default=640)
|
||||
parser.add_argument('--height', type=int, choices=(240, 480), default=480)
|
||||
parser.add_argument('--probe', action='store_true', help='Count frames for five seconds, retain no images')
|
||||
args = parser.parse_args()
|
||||
output = os.fdopen(os.dup(1), 'wb', buffering=0)
|
||||
|
|
@ -235,7 +236,9 @@ def main():
|
|||
os.dup2(2, 1) # HAL callbacks can print; keep stdout protocol uncontaminated.
|
||||
camera = None
|
||||
try:
|
||||
camera = Camera(args.camera)
|
||||
if (args.width, args.height) not in ((320,240),(640,480)):
|
||||
raise ValueError('CAMERA_UNSUPPORTED_PREVIEW_SIZE')
|
||||
camera = Camera(args.camera, args.width, args.height)
|
||||
started = last_request = time.monotonic()
|
||||
if not args.probe:
|
||||
metadata = json.dumps(camera.metadata()).encode()
|
||||
|
|
|
|||
32
tundra/runtime/enable-cpuidle.py
Normal file
32
tundra/runtime/enable-cpuidle.py
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: MPL-2.0
|
||||
"""Enable the Sargo Qualcomm low-power states when the kernel exposes them."""
|
||||
from pathlib import Path
|
||||
import time
|
||||
|
||||
|
||||
PARAMETER = Path('/sys/module/lpm_levels/parameters/sleep_disabled')
|
||||
|
||||
|
||||
def main():
|
||||
deadline = time.monotonic() + 8
|
||||
while time.monotonic() < deadline and not PARAMETER.exists():
|
||||
time.sleep(.1)
|
||||
if not PARAMETER.exists():
|
||||
print('Qualcomm lpm_levels parameter unavailable; leaving kernel defaults')
|
||||
return
|
||||
current = PARAMETER.read_text().strip()
|
||||
if current == 'N':
|
||||
print('Qualcomm low-power states are already enabled')
|
||||
return
|
||||
if current != 'Y':
|
||||
print('Unknown sleep_disabled value; leaving kernel setting unchanged:', current)
|
||||
return
|
||||
PARAMETER.write_text('N')
|
||||
if PARAMETER.read_text().strip() != 'N':
|
||||
raise RuntimeError('Could not enable Qualcomm CPU low-power states')
|
||||
print('Enabled Qualcomm CPU low-power states')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
|
@ -15,7 +15,33 @@ def battery_state(root=Path('/sys/class/power_supply/battery')):
|
|||
status = (root/'status').read_text().strip()
|
||||
if not 0 <= level <= 100 or status not in ('Charging', 'Discharging', 'Full', 'Not charging'):
|
||||
return None
|
||||
return {'level': level/100, 'charging': status in ('Charging', 'Full'), 'status': status}
|
||||
charging = status in ('Charging', 'Full')
|
||||
state = {'level': level/100, 'charging': charging, 'status': status}
|
||||
# Linux power-supply values use matching micro-units, so their ratio
|
||||
# gives hours. Keep the estimate absent when the driver has no usable
|
||||
# current/energy telemetry; Gaia will show that honestly.
|
||||
try:
|
||||
energy = int((root/'energy_now').read_text().strip())
|
||||
power = abs(int((root/'power_now').read_text().strip()))
|
||||
if charging:
|
||||
energy = int((root/'energy_full').read_text().strip()) - energy
|
||||
if power > 0 and energy > 0:
|
||||
seconds = energy * 3600 / power
|
||||
if 0 < seconds < 10 * 24 * 3600:
|
||||
state['chargingTime' if charging else 'dischargingTime'] = seconds
|
||||
except (OSError, ValueError):
|
||||
try:
|
||||
charge = int((root/'charge_counter').read_text().strip())
|
||||
current = abs(int((root/'current_now').read_text().strip()))
|
||||
if charging:
|
||||
charge = int((root/'charge_full').read_text().strip()) - charge
|
||||
if current > 0 and charge > 0:
|
||||
seconds = charge * 3600 / current
|
||||
if 0 < seconds < 10 * 24 * 3600:
|
||||
state['chargingTime' if charging else 'dischargingTime'] = seconds
|
||||
except (OSError, ValueError):
|
||||
pass
|
||||
return state
|
||||
except (OSError, ValueError):
|
||||
return None
|
||||
|
||||
|
|
|
|||
|
|
@ -65,6 +65,18 @@ Description=Checkpoint Tundra clock once per minute
|
|||
OnBootSec=60
|
||||
OnUnitActiveSec=60
|
||||
""")
|
||||
write('etc/systemd/system/tundra-cpuidle.service', '''[Unit]
|
||||
Description=Enable Qualcomm CPU low-power states for Tundra
|
||||
After=systemd-modules-load.service tundra-hal.service
|
||||
Before=tundra-display.service
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStartPre=/usr/bin/python3 /etc/tundra/wait-hal.py
|
||||
ExecStart=/usr/bin/python3 /etc/tundra/enable-cpuidle.py
|
||||
RemainAfterExit=yes
|
||||
''')
|
||||
shutil.copy2(Path(__file__).with_name('enable-cpuidle.py'),
|
||||
root/'etc/tundra/enable-cpuidle.py')
|
||||
wants = root/'etc/systemd/system/tundra.target.wants'
|
||||
wants.mkdir(parents=True, exist_ok=True)
|
||||
for unit in ('tundra-clock-restore.service', 'tundra-clock-save.timer'):
|
||||
|
|
@ -75,7 +87,7 @@ OnUnitActiveSec=60
|
|||
write('etc/pam.d/other','auth required pam_deny.so\naccount required pam_deny.so\npassword required pam_deny.so\nsession required pam_deny.so\n')
|
||||
write('etc/pam.d/common-account','account required pam_unix.so\n')
|
||||
write('etc/pam.d/common-session-noninteractive','session required pam_unix.so\n')
|
||||
write('etc/systemd/logind.conf','[Login]\nHandlePowerKey=ignore\nIdleAction=ignore\n')
|
||||
write('etc/systemd/logind.conf','[Login]\nHandlePowerKey=ignore\nIdleAction=suspend\nIdleActionSec=30s\n')
|
||||
write('etc/systemd/journald.conf','[Journal]\nStorage=volatile\nRuntimeMaxUse=32M\n')
|
||||
write('etc/passwd','root:x:0:0:root:/root:/bin/bash\ndroidian:x:32011:32011:Vulpes trial:/home/droidian:/bin/bash\nsystem:x:1000:1000:Android HAL:/:/usr/sbin/nologin\n')
|
||||
write('etc/group','root:x:0:\ndroidian:x:32011:\nsystem:x:1000:droidian\ngraphics:x:1003:droidian\nandroid_input:x:1004:droidian\nvideo:x:44:droidian\nrender:x:105:droidian\ninput:x:104:droidian\nnetdev:x:988:\n')
|
||||
|
|
@ -111,8 +123,8 @@ TimeoutStopSec=15
|
|||
write('etc/systemd/system/tundra-display.service','''[Unit]
|
||||
Description=Gaia and Gecko on the Sargo display
|
||||
Requires=tundra-hal.service dbus.service systemd-logind.service
|
||||
After=tundra-hal.service dbus.service systemd-logind.service systemd-user-sessions.service
|
||||
Wants=systemd-user-sessions.service
|
||||
Wants=tundra-cpuidle.service
|
||||
After=tundra-hal.service dbus.service systemd-logind.service tundra-cpuidle.service
|
||||
[Service]
|
||||
User=droidian
|
||||
PAMName=tundra
|
||||
|
|
@ -122,6 +134,7 @@ Environment=XDG_SESSION_TYPE=wayland
|
|||
Environment=XDG_CURRENT_DESKTOP=Vulpes
|
||||
Environment=LANG=C.UTF-8
|
||||
Environment=VULPES_TUNDRA=1
|
||||
Environment=VULPES_DEVICE=sargo
|
||||
Environment=VULPES_TUNDRA_RADIO=1
|
||||
Environment=WLR_BACKENDS=hwcomposer,libinput
|
||||
Environment=EGL_PLATFORM=hwcomposer
|
||||
|
|
@ -144,11 +157,15 @@ TimeoutStopSec=15
|
|||
display.write_text(display.read_text().replace('--own-compositor --hybris-egl',
|
||||
'--own-compositor --hybris-egl --diagnostics'))
|
||||
write('etc/tundra/wait-hal.py','''import subprocess,time
|
||||
for _ in range(120):
|
||||
p=subprocess.run(['/usr/bin/lxc-attach','-n','tundra-hal','--','/system/bin/getprop','init.svc.vendor.hwcomposer-2-2'],capture_output=True,text=True)
|
||||
deadline=time.monotonic()+8
|
||||
while time.monotonic()<deadline:
|
||||
try:
|
||||
p=subprocess.run(['/usr/bin/lxc-attach','-n','tundra-hal','--','/system/bin/getprop','init.svc.vendor.hwcomposer-2-2'],capture_output=True,text=True,timeout=.5)
|
||||
except subprocess.TimeoutExpired:
|
||||
continue
|
||||
if p.stdout.strip()=='running':break
|
||||
time.sleep(.5)
|
||||
else:raise RuntimeError('Display HAL did not start')
|
||||
time.sleep(.25)
|
||||
else:raise RuntimeError('Display HAL did not start within 8 seconds')
|
||||
''')
|
||||
# Cellular HAL and oFono must see the same rmnet interfaces. Display-only
|
||||
# diagnostics retain a separate network namespace. No Android network manager runs.
|
||||
|
|
@ -161,7 +178,6 @@ lxc.apparmor.profile = unconfined
|
|||
lxc.autodev = 0
|
||||
lxc.cap.drop = mac_admin mac_override sys_boot
|
||||
lxc.cgroup.devices.deny = b *:* rwm
|
||||
lxc.cgroup2.devices.deny = b *:* rwm
|
||||
lxc.cgroup2.memory.max = 536870912
|
||||
lxc.mount.entry = tmpfs dev tmpfs nosuid 0 0
|
||||
lxc.mount.entry = /dev/__properties__ dev/__properties__ none bind,create=dir 0 0
|
||||
|
|
@ -173,6 +189,8 @@ lxc.mount.entry = /mnt mnt none rbind 0 0
|
|||
''')
|
||||
for name in ('hal_policy.py','setup-hal.py','lxc-guard.py','hardware-status.py','prepare-network.py','wifi-control.py','radio-control.py','mobile-settings.py','sms-inbox.py','call-audio.py','camera-preview.py','buttons.py'):
|
||||
shutil.copy2(Path(__file__).parent/name,root/'etc/tundra'/name)
|
||||
shutil.copy2(Path(__file__).resolve().parents[1]/'tools/inspect-hardware.py',
|
||||
root/'etc/tundra/inspect-hardware.py')
|
||||
write('etc/systemd/system/tundra-hardware.service', '''[Unit]
|
||||
Description=Tundra read-only battery and network state
|
||||
After=systemd-udev-trigger.service
|
||||
|
|
|
|||
|
|
@ -175,6 +175,39 @@ def active_wifi_uuid():
|
|||
except ValueError:return None
|
||||
|
||||
|
||||
def device_state():
|
||||
"""Return NetworkManager's live link state, even when its AP vanished."""
|
||||
result = nm('-g', 'GENERAL.STATE', 'device', 'show', DEVICE, timeout=5)
|
||||
if result.returncode:
|
||||
raise WifiError('WIFI_UNAVAILABLE')
|
||||
match = re.match(r'\s*(\d+)', result.stdout)
|
||||
if not match:
|
||||
raise WifiError('WIFI_UNAVAILABLE')
|
||||
code = int(match.group(1))
|
||||
state = {20: 'unavailable', 30: 'disconnected', 40: 'connecting',
|
||||
50: 'connecting', 60: 'connecting', 70: 'connecting',
|
||||
80: 'connecting', 90: 'connecting', 100: 'connected',
|
||||
110: 'disconnecting', 120: 'failed'}.get(code, 'unknown')
|
||||
return state
|
||||
|
||||
|
||||
def current_network(profile_dir, observed=()):
|
||||
"""Resolve the active profile to a remembered SSID if scan results omit it."""
|
||||
if device_state() != 'connected':
|
||||
return None
|
||||
uid = active_wifi_uuid()
|
||||
if not uid or uid == HOTSPOT_UUID:
|
||||
return None
|
||||
for network in observed:
|
||||
if network.get('connected'):
|
||||
return network
|
||||
saved = next((row for row in saved_profiles(profile_dir) if row['id'] == uid), None)
|
||||
if not saved:
|
||||
return None
|
||||
return {'ssid': saved['ssid'], 'security': saved['security'],
|
||||
'relSignalStrength': 0, 'savedId': uid, 'connected': True}
|
||||
|
||||
|
||||
def hotspot_state(profile_dir):
|
||||
path=profile_dir/'vulpes-hotspot.nmconnection'
|
||||
result={'enabled':active_wifi_uuid()==HOTSPOT_UUID,'configured':False,'ssid':'Vulpes OS','security':'wpa2-psk','password':''}
|
||||
|
|
@ -269,11 +302,14 @@ def dispatch(request, profile_dir=PROFILE_DIR):
|
|||
for network in found:
|
||||
stored = next((row for row in saved if row['ssid'] == network['ssid'] and row['security'] == network['security']), None)
|
||||
network.update(known=stored is not None, savedId=stored['id'] if stored else None)
|
||||
state = device_state() if enabled else 'disabled'
|
||||
active = current_network(profile_dir, found) if state == 'connected' else None
|
||||
info={}
|
||||
if any(n['connected'] for n in found):
|
||||
if active:
|
||||
try:info=connection_info()
|
||||
except (WifiError,OSError,subprocess.TimeoutExpired):pass
|
||||
return {'enabled': enabled, 'networks': found, 'connectionInformation':info}
|
||||
return {'enabled': enabled, 'state':state, 'network':active,
|
||||
'networks': found, 'connectionInformation':info}
|
||||
if operation == 'disconnect':
|
||||
if nm('device', 'disconnect', DEVICE).returncode:
|
||||
raise WifiError('WIFI_DISCONNECT_FAILED')
|
||||
|
|
@ -304,7 +340,10 @@ def dispatch(request, profile_dir=PROFILE_DIR):
|
|||
private_write(target, text)
|
||||
if nm('connection', 'load', str(target)).returncode:
|
||||
raise WifiError('WIFI_UNAVAILABLE')
|
||||
if nm('connection', 'up', 'uuid', uid).returncode:
|
||||
# NetworkManager may retain a failed activation after the AP goes out
|
||||
# of range. Explicitly drop that stale activation before reconnecting.
|
||||
nm('connection', 'down', 'uuid', uid)
|
||||
if nm('connection', 'up', 'uuid', uid, 'ifname', DEVICE).returncode:
|
||||
raise WifiError('WIFI_CONNECT_FAILED')
|
||||
connected = True
|
||||
return {'connected': True}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,15 @@ class Buttons(unittest.TestCase):
|
|||
for p in [{'operation':'screen','enabled':True,'level':-1},{'operation':'volume','level':float('nan')},{'operation':'exec','command':'true'}]:
|
||||
with self.assertRaises(ValueError):b.control(p)
|
||||
|
||||
def test_display_state_updates_all_logind_sessions(self):
|
||||
sessions='1 32011 droidian - 1017 manager - no -\nc2 32011 droidian - 1082 background - no -\nc3 32011 droidian seat0 1119 user tty7 no -\n'
|
||||
with patch.object(b.subprocess,'run') as run:
|
||||
run.side_effect=[type('Result',(),{'stdout':sessions})(),None,None,None]
|
||||
self.assertTrue(b.set_session_idle(True))
|
||||
calls=[call.args[0] for call in run.call_args_list[1:]]
|
||||
self.assertEqual(len(calls),3)
|
||||
self.assertTrue(all(call[-2:]==['b','true'] for call in calls))
|
||||
|
||||
def test_vibration_pattern_progresses_without_blocking_and_can_be_cancelled(self):
|
||||
with tempfile.TemporaryDirectory() as d,patch.object(b,'VIBRATOR',Path(d)),patch.object(b.time,'monotonic',return_value=10):
|
||||
(Path(d)/'trigger').write_text('[transient]')
|
||||
|
|
|
|||
|
|
@ -113,11 +113,12 @@ on boot
|
|||
self.assertIn(line.split(':')[-1], shells)
|
||||
config = (root/'etc/tundra/lxc.conf').read_text()
|
||||
self.assertIn('lxc.cgroup.devices.deny = b *:* rwm', config)
|
||||
self.assertIn('lxc.cgroup2.devices.deny = b *:* rwm', config)
|
||||
self.assertNotIn('lxc.cgroup2.devices.deny', config)
|
||||
self.assertNotIn('host', config)
|
||||
self.assertIn('lxc.cgroup2.memory.max = 536870912', config)
|
||||
service=(root/'etc/systemd/system/tundra-display.service').read_text()
|
||||
self.assertIn('ExecStartPre=+chvt 7', service)
|
||||
self.assertIn('Environment=VULPES_DEVICE=sargo', service)
|
||||
self.assertNotIn('ExecStartPost=', service)
|
||||
# systemd-user's PAM stack includes these on the reference root.
|
||||
for name in ('common-account', 'common-session-noninteractive'):
|
||||
|
|
|
|||
|
|
@ -38,6 +38,40 @@ class WifiTests(unittest.TestCase):
|
|||
self.assertEqual(wifi.connection_info(),{})
|
||||
run.assert_not_called()
|
||||
|
||||
def test_status_reports_live_disconnection_instead_of_stale_connected_state(self):
|
||||
def nm(*args, **kwargs):
|
||||
if args[:2] == ('radio', 'wifi'):
|
||||
return SimpleNamespace(returncode=0, stdout='enabled\n')
|
||||
if args[:2] == ('-g', 'GENERAL.STATE'):
|
||||
return SimpleNamespace(returncode=0, stdout='30 (disconnected)\n')
|
||||
raise AssertionError(args)
|
||||
with tempfile.TemporaryDirectory() as folder, patch.object(wifi, 'nm', side_effect=nm):
|
||||
result = wifi.dispatch({'operation':'status'}, Path(folder))
|
||||
self.assertEqual(result['state'], 'disconnected')
|
||||
self.assertIsNone(result['network'])
|
||||
|
||||
def test_status_recovers_active_saved_network_even_if_scan_has_no_ap_row(self):
|
||||
with tempfile.TemporaryDirectory() as folder:
|
||||
profile_dir = Path(folder)
|
||||
uid = 'd31f8c9d-a1f9-4df1-876a-491fe814cf2a'
|
||||
profile_dir.mkdir(exist_ok=True)
|
||||
(profile_dir / ('vulpes-' + uid + '.nmconnection')).write_text(
|
||||
wifi.profile_text(AP, 'testpassword', uid))
|
||||
def nm(*args, **kwargs):
|
||||
if args[:2] == ('radio', 'wifi'):
|
||||
return SimpleNamespace(returncode=0, stdout='enabled\n')
|
||||
if args[:2] == ('-g', 'GENERAL.STATE'):
|
||||
return SimpleNamespace(returncode=0, stdout='100 (connected)\n')
|
||||
if args[:2] == ('-g', 'GENERAL.CON-UUID'):
|
||||
return SimpleNamespace(returncode=0, stdout=uid+'\n')
|
||||
raise AssertionError(args)
|
||||
with patch.object(wifi, 'nm', side_effect=nm), \
|
||||
patch.object(wifi, 'connection_info', return_value={'ipAddress':'192.0.2.2'}):
|
||||
result = wifi.dispatch({'operation':'status'}, profile_dir)
|
||||
self.assertEqual(result['state'], 'connected')
|
||||
self.assertEqual(result['network']['ssid'], AP['ssid'])
|
||||
self.assertEqual(result['network']['savedId'], uid)
|
||||
|
||||
def test_secret_persists_only_after_success(self):
|
||||
for fail in (False, True):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
|
|
|
|||
49
tundra/tools/battery-idle-log.py
Normal file
49
tundra/tools/battery-idle-log.py
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: MPL-2.0
|
||||
"""Low-overhead, read-only battery and thermal sampler for a RAM trial."""
|
||||
import json
|
||||
from pathlib import Path
|
||||
import time
|
||||
|
||||
|
||||
def values(directory, names):
|
||||
result = {}
|
||||
for name in names:
|
||||
try:
|
||||
result[name] = (directory / name).read_text().strip()
|
||||
except OSError:
|
||||
pass
|
||||
return result
|
||||
|
||||
|
||||
def sample():
|
||||
supplies = {}
|
||||
for path in sorted(Path('/sys/class/power_supply').iterdir()):
|
||||
supplies[path.name] = values(path, (
|
||||
'type', 'status', 'online', 'capacity', 'current_now',
|
||||
'voltage_now', 'power_now', 'charge_now', 'energy_now', 'temp'))
|
||||
thermal = {}
|
||||
for path in sorted(Path('/sys/class/thermal').glob('thermal_zone*')):
|
||||
item = values(path, ('type', 'temp'))
|
||||
if item:
|
||||
thermal[path.name] = item
|
||||
idle = {}
|
||||
for path in sorted(Path('/sys/devices/system/cpu').glob('cpu[0-9]*/cpuidle/state*')):
|
||||
item = values(path, ('name', 'time', 'usage', 'disable'))
|
||||
if item:
|
||||
idle[str(path.relative_to('/sys/devices/system/cpu'))] = item
|
||||
return {'timeUnix': time.time(), 'timeMonotonic': time.monotonic(),
|
||||
'powerSupplies': supplies, 'thermalZones': thermal, 'cpuIdle': idle}
|
||||
|
||||
|
||||
def main():
|
||||
output = Path('/run/tundra/battery-idle.jsonl')
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
with output.open('a', buffering=1) as stream:
|
||||
while True:
|
||||
stream.write(json.dumps(sample(), separators=(',', ':')) + '\n')
|
||||
time.sleep(30)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
64
tundra/tools/build-beatles-camera.py
Normal file
64
tundra/tools/build-beatles-camera.py
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: MPL-2.0
|
||||
"""Build the Nokia CameraService worker and its fixed-command broker."""
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
ENGINE = ROOT / 'tundra/out/beatles/gecko-156.0.1'
|
||||
BUILD = ENGINE / 'camera-broker-build'
|
||||
NDK = BUILD / 'android-ndk-r27d'
|
||||
HEADERS = BUILD / 'android-headers-api23/android-headers-23/23'
|
||||
AOSP = BUILD / 'aosp/include'
|
||||
LIBS = ENGINE / 'camera-probe-runtime'
|
||||
OUT = BUILD
|
||||
|
||||
def digest(path):
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
|
||||
def build(name, source, command):
|
||||
target = OUT / name
|
||||
stamp = target.with_suffix('.build.json')
|
||||
record = {'source': digest(source), 'command': command}
|
||||
if target.is_file() and stamp.is_file():
|
||||
try:
|
||||
if json.loads(stamp.read_text()) == record:
|
||||
print('Using cached camera tool:', name)
|
||||
return target
|
||||
except (OSError, ValueError):
|
||||
pass
|
||||
subprocess.run(command, check=True)
|
||||
target.chmod(0o755)
|
||||
stamp.write_text(json.dumps(record, sort_keys=True) + '\n')
|
||||
return target
|
||||
|
||||
def main():
|
||||
required = [NDK / 'toolchains/llvm/prebuilt/linux-x86_64/bin/clang++',
|
||||
NDK / 'toolchains/llvm/prebuilt/linux-x86_64/bin/clang',
|
||||
HEADERS / 'linux/binder.h', AOSP / 'camera/Camera.h',
|
||||
LIBS / 'libc++_shared.so', LIBS / 'beatles-camera-permission-bridge']
|
||||
missing = [str(path) for path in required if not path.exists()]
|
||||
if missing:
|
||||
raise SystemExit('Private API-23 Camera1 build inputs missing:\n ' + '\n '.join(missing))
|
||||
sysroot = NDK / 'toolchains/llvm/prebuilt/linux-x86_64/sysroot'
|
||||
cxx = str(NDK / 'toolchains/llvm/prebuilt/linux-x86_64/bin/clang++')
|
||||
cc = str(NDK / 'toolchains/llvm/prebuilt/linux-x86_64/bin/clang')
|
||||
common = ['--target=armv7a-linux-androideabi23', '--sysroot=' + str(sysroot)]
|
||||
worker_source = ROOT / 'tundra/probes/beatles-camera-worker.cpp'
|
||||
worker_command = [cxx, *common, '-fno-rtti', '-I', str(HEADERS), '-I', str(AOSP),
|
||||
str(worker_source), '-L', str(LIBS), '-Wl,-rpath,/data/local/tmp',
|
||||
'-Wl,--allow-shlib-undefined', '-lcamera_client', '-lbinder', '-lgui',
|
||||
'-lutils', '-lcutils', '-llog', '-lui', '-lhardware', '-lmedia',
|
||||
'-lcamera_metadata', '-lsync', '-lbase', '-lc++_shared', '-o']
|
||||
build('beatles-camera-worker', worker_source,
|
||||
worker_command + [str(OUT / 'beatles-camera-worker')])
|
||||
broker_source = ROOT / 'tundra/probes/beatles-camera-broker.c'
|
||||
broker_command = [cc, *common, '-Wall', '-Wextra', '-Werror', str(broker_source), '-o']
|
||||
build('beatles-camera-broker', broker_source,
|
||||
broker_command + [str(OUT / 'beatles-camera-broker')])
|
||||
print('Android Camera1 worker and broker are ready.')
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
|
@ -18,6 +18,7 @@ from cryptography.hazmat.primitives import serialization
|
|||
from ramdisk_reference import read_newc, select_helpers
|
||||
from storage_layout import mount_table
|
||||
from shared_ui import compress_archive
|
||||
from kernel_cmdline import vulpes_cmdline
|
||||
from tundra import ROOT, cpio, checked_source, digest, save, linker
|
||||
|
||||
|
||||
|
|
@ -33,7 +34,9 @@ def main():
|
|||
parser.add_argument('--layout',type=Path,required=True)
|
||||
parser.add_argument('--device-workspace', type=Path,
|
||||
help='Private per-phone workspace; never use the development inventory')
|
||||
parser.add_argument('--native',action='store_true',help='Switch to the staged native root with a ten-minute watchdog')
|
||||
parser.add_argument('--native',action='store_true',help='Switch to the staged native root with a timed watchdog')
|
||||
parser.add_argument('--watchdog-seconds', type=int, default=600,
|
||||
help='RAM trial auto-reboot deadline (60–3600 seconds; default: 600)')
|
||||
parser.add_argument('--system-image',type=Path,help='system.json for a self-contained image staged at /tundra/images/<sha256>.ext4')
|
||||
parser.add_argument('--differential-ui', action='store_true', help='Bundle only UI files differing from the verified immutable system image')
|
||||
parser.add_argument('--hold-hal',action='store_true',help='Boot basic.target first so HAL startup can be traced over USB')
|
||||
|
|
@ -45,6 +48,8 @@ def main():
|
|||
parser.add_argument('--state', type=Path, help='Prepared Tundra data-image manifest; enables persistent writes in that file')
|
||||
parser.add_argument('--no-deadline', action='store_true', help='Keep the persistent development boot running; requires --state')
|
||||
args=parser.parse_args()
|
||||
if not 60 <= args.watchdog_seconds <= 3600:
|
||||
parser.error('--watchdog-seconds must be between 60 and 3600')
|
||||
if args.hold_hal and not args.native:parser.error('--hold-hal requires --native')
|
||||
if args.system_image and not args.native:parser.error('--system-image requires --native')
|
||||
if args.differential_ui and not args.system_image:parser.error('--differential-ui requires --system-image')
|
||||
|
|
@ -79,6 +84,9 @@ def main():
|
|||
else:
|
||||
plan,_=load_boot().checked_plan()
|
||||
storage_table = mount_table(layout)
|
||||
if state and storage_table is None and not (
|
||||
layout.get('schema') == 2 and layout.get('storage') == 'raw-userdata-ext4'):
|
||||
raise ValueError('Persistent trials require inventoried ext4 userdata')
|
||||
out=args.output.resolve();out.mkdir(parents=True,exist_ok=False)
|
||||
source=Path(plan['backupDirectory'])/'boot_a.img'
|
||||
packager=checked_source('aosp-mkbootimg')
|
||||
|
|
@ -86,6 +94,8 @@ def main():
|
|||
result=subprocess.check_output(['python3',str(packager/'unpack_bootimg.py'),'--boot_img',str(source),'--out',str(unpack),'--format=mkbootimg'],text=True)
|
||||
options=shlex.split(result);fields=dict(zip(options[::2],options[1::2]))
|
||||
if fields['--header_version']!='0':raise ValueError('Expected header v0')
|
||||
source_cmdline=fields['--cmdline']
|
||||
fields['--cmdline'],removed_kernel_parameters=vulpes_cmdline(source_cmdline)
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
helpers=select_helpers(read_newc((unpack/'ramdisk').read_bytes()),Path(temp),
|
||||
extra=('sbin/blkid','sbin/e2fsck') if state else ())
|
||||
|
|
@ -113,15 +123,23 @@ def main():
|
|||
(out/'known_hosts').write_text('[10.15.19.82]:2222 '+hostpublic+'\n[127.0.0.1]:2222 '+hostpublic+'\n')
|
||||
init=(ROOT/'boot/rescue-init.sh').read_bytes().replace(b'# No LVM activation',
|
||||
('EXPECTED_STORAGE_HASH='+shlex.quote(layout['firstMiBSha256'])+'\n# No LVM activation').encode())
|
||||
init=init.replace(b"'0 104448000 linear /dev/mmcblk0p72 329728'",
|
||||
shlex.quote(storage_table).encode())
|
||||
sources=[Path(__file__),ROOT/'boot/rescue-init.sh',ROOT/'tools/ramdisk_reference.py',
|
||||
if storage_table is None:
|
||||
uuid_hex=layout['filesystemUuid'].replace('-','')
|
||||
init=init.replace(b'# No LVM activation',
|
||||
('RAW_USERDATA_EXT4=1\nEXPECTED_STORAGE_UUID_HEX='+uuid_hex+
|
||||
'\n# No LVM activation').encode())
|
||||
else:
|
||||
init=init.replace(b"'0 104448000 linear /dev/mmcblk0p72 329728'",
|
||||
shlex.quote(storage_table).encode())
|
||||
sources=[Path(__file__),ROOT/'boot/rescue-init.sh',ROOT/'tools/kernel_cmdline.py',ROOT/'tools/ramdisk_reference.py',
|
||||
ROOT/'tools/storage_layout.py']
|
||||
if args.device_workspace:
|
||||
sources += [ROOT/'tools/sargo_workspace.py', ROOT/'tools/boot_backup.py']
|
||||
if state:
|
||||
# Keep the existing layout intact; only /tundra/data/<UUID>.ext4 is used
|
||||
# for Tundra state. Opening its containing filesystem RW is necessary.
|
||||
if storage_table is None:
|
||||
init=init.replace(b'RAW_USERDATA_EXT4=1',b'RAW_USERDATA_EXT4=1\nTUNDRA_USERDATA_RW=1')
|
||||
init=init.replace(b'--readonly --table', b'--table')
|
||||
init=init.replace(b'-o ro,noload /dev/mapper/tundra-installed-ro', b'-o rw /dev/mapper/tundra-installed-ro')
|
||||
init=init.replace(b'# Temporary RAM-only boot. Installed data is never mounted writable.', b'# Private persistent boot. Only the dedicated Tundra data file receives system writes.')
|
||||
|
|
@ -130,7 +148,7 @@ def main():
|
|||
shared_ui = {}
|
||||
if args.native:
|
||||
binary=out/'watchdog'
|
||||
subprocess.run(['clang','--target=aarch64-linux-gnu','--ld-path='+str(linker()),'-Os','-ffreestanding','-fno-builtin','-fno-stack-protector','-fno-pie','-fno-unwind-tables','-fno-asynchronous-unwind-tables','-Wall','-Wextra','-Werror','-nostdlib','-static','-Wl,--build-id=none','-Wl,-e,_start',str(ROOT/'boot/watchdog.c'),'-o',str(binary)],check=True)
|
||||
subprocess.run(['clang','--target=aarch64-linux-gnu','--ld-path='+str(linker()),'-Os','-ffreestanding','-fno-builtin','-fno-stack-protector','-fno-pie','-fno-unwind-tables','-fno-asynchronous-unwind-tables','-Wall','-Wextra','-Werror','-nostdlib','-static','-Wl,--build-id=none','-Wl,-e,_start','-DTUNDRA_WATCHDOG_SECONDS='+str(args.watchdog_seconds),str(ROOT/'boot/watchdog.c'),'-o',str(binary)],check=True)
|
||||
init=init.replace(b"(sleep 480; echo 'Tundra trial deadline reached'; reboot -f) &",b'/sbin/tundra-watchdog &')
|
||||
if args.no_deadline:
|
||||
init=init.replace(b'/sbin/tundra-watchdog &', b'# Persistent development boot: no timed reboot.')
|
||||
|
|
@ -174,6 +192,8 @@ def main():
|
|||
archive.add(config_dir, arcname=directory, filter=owned)
|
||||
files['native-config.tar']=(0o600,compress_archive(buffer.getvalue()))
|
||||
sources += list(sorted((ROOT/'runtime').glob('*.py')))
|
||||
if args.runtime_config:
|
||||
sources.append(ROOT/'tools/inspect-hardware.py')
|
||||
files['sbin/tundra-watchdog']=(0o755,binary.read_bytes())
|
||||
native_init=(ROOT/'boot/native-root.sh').read_bytes().replace(b'# BUILD_TIME_FLOOR', ('BUILD_EPOCH='+str(built_at)).encode())
|
||||
if args.runtime_config:
|
||||
|
|
@ -181,7 +201,8 @@ def main():
|
|||
if system:
|
||||
sha = system['imageSha256']
|
||||
native_init = native_init.replace(b'# A kernel fallback',
|
||||
('SYSTEM_IMAGE=/installed/tundra/images/'+sha+'.ext4\nSYSTEM_IMAGE_SHA='+sha+'\n# A kernel fallback').encode())
|
||||
('SYSTEM_IMAGE=/installed/tundra/images/'+sha+'.ext4\nSYSTEM_IMAGE_SHA='+sha+
|
||||
'\nSYSTEM_IMAGE_BYTES='+str(system['bytes'])+'\n# A kernel fallback').encode())
|
||||
# Bundle UI changes with the boot trial rather than retransferring
|
||||
# the entire immutable system image for each CSS/JS adjustment.
|
||||
from shared_ui import build_payload, differential_payload
|
||||
|
|
@ -240,9 +261,10 @@ def main():
|
|||
shutil.copy2(path,target)
|
||||
save(out/'build.json',{'scope':'Private persistent Sargo development boot; unqualified' if state else ('Private temporary native-root trial; unqualified' if args.native else 'Private temporary SSH boot; installed root read-only; no graphical session'),
|
||||
'builtAtUnix':built_at,'runtimeConfigOverlay':args.runtime_config,'networkTrial':args.network,'hardwareTrial':args.hardware,'interactiveDiagnostics':args.diagnostics,'image':image.name,'imageSha256':digest(image),'sourceBootSha256':digest(source),
|
||||
'removedKernelParameters':removed_kernel_parameters,
|
||||
'serialSha256':plan['serialSha256'],'kernelSha256':kernel_hash,
|
||||
'deviceWorkspaceSha256':plan.get('workspaceSha256'),
|
||||
'ramdiskSha256':digest(ramdisk),'roundtripPassed':True,'flashReady':False,'localDebugger':args.local_debugger,'watchdogSeconds':None if args.no_deadline else (600 if args.native else 480),'persistentState':state,'storageWrites':bool(state),'nativeRootRequested':args.native,'halHeldForDiagnostics':args.hold_hal,
|
||||
'ramdiskSha256':digest(ramdisk),'roundtripPassed':True,'flashReady':False,'localDebugger':args.local_debugger,'watchdogSeconds':None if args.no_deadline else (args.watchdog_seconds if args.native else 480),'persistentState':state,'storageWrites':bool(state),'nativeRootRequested':args.native,'halHeldForDiagnostics':args.hold_hal,
|
||||
'layout':layout,'layoutSha256':digest(args.layout),'systemImage':system,'sharedUiHashes':shared_ui,'sourceSnapshot':True,'sources':{str(p.relative_to(ROOT)):digest(p) for p in sources},
|
||||
'helpers':{n:__import__('hashlib').sha256(b).hexdigest() for n,b in helpers.items()},
|
||||
'sharedUiFilesAlreadyInSystem':omitted_ui if args.differential_ui else 0,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import argparse
|
|||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shlex
|
||||
import shutil
|
||||
import subprocess
|
||||
import uuid
|
||||
from persistent_state import FEATURES, digest
|
||||
|
|
@ -15,6 +17,8 @@ def main():
|
|||
parser.add_argument('--output', type=Path, required=True)
|
||||
parser.add_argument('--system-image', type=Path, required=True)
|
||||
parser.add_argument('--size-mib', type=int, default=2048)
|
||||
parser.add_argument('--profile-dir', type=Path,
|
||||
help='Copy an existing droidian-trial browser profile into a fresh P23 state')
|
||||
args = parser.parse_args()
|
||||
if args.size_mib < 512 or args.size_mib > 8192:
|
||||
parser.error('Choose 512–8192 MiB')
|
||||
|
|
@ -31,6 +35,23 @@ def main():
|
|||
for directory in ('upper','work'):
|
||||
(seed/directory).mkdir(mode=0o755 if directory == 'upper' else 0o700)
|
||||
(seed/'upper').chmod(0o755)
|
||||
migrated = []
|
||||
opt_layer = None
|
||||
if args.profile_dir:
|
||||
profile = args.profile_dir.resolve(strict=True)
|
||||
if not profile.is_dir() or profile.is_symlink():
|
||||
raise ValueError('Profile source must be a real directory')
|
||||
opt_layer = seed/'upper/opt'
|
||||
vulpes_layer = opt_layer/'vulpes'
|
||||
profiles_layer = vulpes_layer/'profiles'
|
||||
profiles_layer.mkdir(parents=True)
|
||||
opt_layer.chmod(0o755)
|
||||
vulpes_layer.chmod(0o755)
|
||||
profiles_layer.chmod(0o755)
|
||||
target = profiles_layer/'droidian-trial'
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copytree(profile, target, symlinks=True)
|
||||
migrated = [vulpes_layer, profiles_layer, target, *target.rglob('*')]
|
||||
identifier = str(uuid.uuid4())
|
||||
image = out/(identifier+'.ext4')
|
||||
with image.open('xb') as stream:
|
||||
|
|
@ -42,12 +63,23 @@ def main():
|
|||
for field in ('uid','gid'):
|
||||
subprocess.run(['debugfs','-w','-R',f'set_inode_field {name} {field} 0',str(image)],
|
||||
check=True,stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)
|
||||
if migrated:
|
||||
# Gaia runs as the droidian account (UID/GID 32011) on Sargo.
|
||||
commands = out/'profile-ownership.debugfs'
|
||||
commands.write_text(''.join(
|
||||
f'set_inode_field {shlex.quote("/upper/" + str(path.relative_to(seed/"upper")))} {field} 32011\n'
|
||||
for path in migrated for field in ('uid','gid')) +
|
||||
'set_inode_field /upper/opt uid 0\nset_inode_field /upper/opt gid 0\n')
|
||||
subprocess.run(['debugfs','-w','-f',str(commands),str(image)],check=True,
|
||||
stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)
|
||||
commands.unlink()
|
||||
check = subprocess.run(['e2fsck','-f','-n',str(image)],text=True,capture_output=True)
|
||||
(out/'filesystem-check.txt').write_text(check.stdout+check.stderr)
|
||||
if check.returncode:raise RuntimeError('Data image failed filesystem verification')
|
||||
manifest = {'schema':1,'device':'sargo','uuid':identifier,'image':image.name,
|
||||
'imageSha256':digest(image),'bytes':image.stat().st_size,
|
||||
'systemImageSha256':system['imageSha256'],'filesystemCheckPassed':True,
|
||||
'migratedProfile':bool(args.profile_dir),
|
||||
'scope':'Private persistent overlay; containing filesystem retained; no partition formatting'}
|
||||
(out/'state.json').write_text(json.dumps(manifest,indent=2)+'\n')
|
||||
print(out/'state.json')
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
# SPDX-License-Identifier: MPL-2.0
|
||||
"""Read readiness, never dial, send messages, enter PINs or open camera nodes."""
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import time
|
||||
|
|
@ -38,6 +39,150 @@ def selected(values, names):
|
|||
if key in values and isinstance(values[key], dict)}
|
||||
|
||||
|
||||
def process_cpu_snapshot():
|
||||
"""Return process names and CPU ticks without command lines or arguments."""
|
||||
result = {}
|
||||
for entry in Path('/proc').iterdir():
|
||||
if not entry.name.isdigit():
|
||||
continue
|
||||
try:
|
||||
raw = (entry / 'stat').read_text()
|
||||
# comm can contain spaces and parentheses; fields after the last )
|
||||
# start at field 3 (state), so utime/stime are offsets 11 and 12.
|
||||
fields = raw[raw.rfind(')') + 2:].split()
|
||||
name = raw[raw.find('(') + 1:raw.rfind(')')]
|
||||
result[int(entry.name)] = (name,
|
||||
int(fields[11]) + int(fields[12]))
|
||||
except (OSError, ValueError, IndexError):
|
||||
continue
|
||||
return result
|
||||
|
||||
|
||||
def thread_cpu_snapshot():
|
||||
"""Return thread names and CPU ticks; never collect command-line data."""
|
||||
result = {}
|
||||
for task in Path('/proc').glob('[0-9]*/task/[0-9]*'):
|
||||
try:
|
||||
raw = (task/'stat').read_text()
|
||||
fields = raw[raw.rfind(')') + 2:].split()
|
||||
name = raw[raw.find('(') + 1:raw.rfind(')')]
|
||||
result[(int(task.parent.parent.name), int(task.name))] = (
|
||||
name, int(fields[11]) + int(fields[12]))
|
||||
except (OSError, ValueError, IndexError):
|
||||
continue
|
||||
return result
|
||||
|
||||
|
||||
def cpu_idle_snapshot():
|
||||
"""Read per-CPU idle-state residency counters where the kernel exports them."""
|
||||
result = {}
|
||||
for cpu in sorted(Path('/sys/devices/system/cpu').glob('cpu[0-9]*')):
|
||||
if not cpu.name[3:].isdigit():
|
||||
continue
|
||||
states = []
|
||||
for state in sorted((cpu/'cpuidle').glob('state[0-9]*')):
|
||||
try:
|
||||
states.append({key:(state/key).read_text().strip() for key in
|
||||
('name','desc','usage','time','disable')
|
||||
if (state/key).is_file()})
|
||||
except OSError:
|
||||
continue
|
||||
if states:
|
||||
result[cpu.name] = states
|
||||
return result
|
||||
|
||||
|
||||
def cpu_frequency_snapshot():
|
||||
result = {}
|
||||
for cpu in sorted(Path('/sys/devices/system/cpu').glob('cpu[0-9]*')):
|
||||
if not cpu.name[3:].isdigit():
|
||||
continue
|
||||
path = cpu/'cpufreq'
|
||||
values = {}
|
||||
for key in ('scaling_governor','scaling_cur_freq','cpuinfo_cur_freq',
|
||||
'scaling_min_freq','scaling_max_freq'):
|
||||
try:
|
||||
values[key] = (path/key).read_text().strip()
|
||||
except OSError:
|
||||
pass
|
||||
if values:
|
||||
result[cpu.name] = values
|
||||
return result
|
||||
|
||||
|
||||
def thermal_readings():
|
||||
readings = {}
|
||||
for zone in Path('/sys/class/thermal').glob('thermal_zone*'):
|
||||
try:
|
||||
label = (zone / 'type').read_text().strip()
|
||||
value = int((zone / 'temp').read_text().strip())
|
||||
# Linux thermal sysfs normally reports millidegrees Celsius.
|
||||
readings[label or zone.name] = round(value / 1000, 1)
|
||||
except (OSError, ValueError):
|
||||
continue
|
||||
return readings
|
||||
|
||||
|
||||
def power_supplies():
|
||||
result = {}
|
||||
for supply in Path('/sys/class/power_supply').glob('*'):
|
||||
data = {}
|
||||
for name in ('type', 'status', 'capacity', 'voltage_now',
|
||||
'current_now', 'power_now', 'temp', 'online'):
|
||||
try:
|
||||
data[name] = (supply / name).read_text().strip()
|
||||
except OSError:
|
||||
pass
|
||||
if data:
|
||||
result[supply.name] = data
|
||||
return result
|
||||
|
||||
|
||||
def wakeup_snapshot():
|
||||
"""Read kernel wakeup counters; source labels are kernel driver names."""
|
||||
for path in (Path('/sys/kernel/debug/wakeup_sources'),
|
||||
Path('/sys/kernel/tracing/wakeup_sources')):
|
||||
try:
|
||||
lines = path.read_text().splitlines()
|
||||
except OSError:
|
||||
continue
|
||||
if len(lines) < 2:
|
||||
continue
|
||||
headers = lines[0].split()
|
||||
active_index = next((i for i, value in enumerate(headers)
|
||||
if value.lower() == 'active_count'), None)
|
||||
event_index = next((i for i, value in enumerate(headers)
|
||||
if value.lower() == 'event_count'), None)
|
||||
if active_index is None or event_index is None:
|
||||
return None
|
||||
result = {}
|
||||
for line in lines[1:]:
|
||||
fields = line.split()
|
||||
try:
|
||||
if len(fields) <= max(active_index, event_index):
|
||||
continue
|
||||
result[fields[0]] = {
|
||||
'activeCount':int(fields[active_index]),
|
||||
'eventCount':int(fields[event_index]),
|
||||
}
|
||||
except (ValueError, IndexError):
|
||||
continue
|
||||
return result
|
||||
return None
|
||||
|
||||
|
||||
def counter_delta(before, after, keys):
|
||||
"""Return positive counter deltas, tolerating counters reset at reboot."""
|
||||
result = {}
|
||||
for name, values in (after or {}).items():
|
||||
prior = (before or {}).get(name, {})
|
||||
delta = {key:max(0, int(values[key])-int(prior.get(key, values[key])))
|
||||
for key in keys if key in values}
|
||||
if any(delta.values()):
|
||||
result[name] = delta
|
||||
return result
|
||||
|
||||
|
||||
def inspect():
|
||||
native = 'tundra.rescue=1' in Path('/proc/cmdline').read_text()
|
||||
report = {'recordedAtUnix': time.time(), 'environment': 'tundra' if native else 'reference',
|
||||
|
|
@ -80,12 +225,114 @@ def inspect():
|
|||
'applicationInstalled': Path('/usr/bin/droidian-camera').exists(),
|
||||
'captureTested': False,
|
||||
}
|
||||
report['battery'] = {}
|
||||
for name in ('capacity', 'status'):
|
||||
report['battery'] = power_supplies()
|
||||
report['thermalCelsius'] = thermal_readings()
|
||||
backlights = {}
|
||||
for path in Path('/sys/class/backlight').glob('*/brightness'):
|
||||
try:
|
||||
report['battery'][name] = Path('/sys/class/power_supply/battery', name).read_text().strip()
|
||||
backlights[path.parent.name] = (path.read_text().strip(),
|
||||
(path.parent / 'max_brightness').read_text().strip())
|
||||
except OSError:
|
||||
continue
|
||||
report['backlight'] = backlights
|
||||
before = process_cpu_snapshot()
|
||||
threads_before = thread_cpu_snapshot()
|
||||
idle_before = cpu_idle_snapshot()
|
||||
wakeups_before = wakeup_snapshot()
|
||||
battery_before = power_supplies()
|
||||
started = time.monotonic()
|
||||
time.sleep(5)
|
||||
elapsed = time.monotonic() - started
|
||||
after = process_cpu_snapshot()
|
||||
threads_after = thread_cpu_snapshot()
|
||||
ticks_per_second = os.sysconf('SC_CLK_TCK')
|
||||
cpu = []
|
||||
for pid, (name, ticks) in after.items():
|
||||
if pid not in before or before[pid][0] != name:
|
||||
continue
|
||||
delta = max(0, ticks - before[pid][1])
|
||||
percent = 100 * delta / ticks_per_second / elapsed
|
||||
if percent >= 1:
|
||||
cpu.append({'process': name, 'percentOfOneCore': round(percent, 1)})
|
||||
report['cpuSample'] = {'durationSeconds': round(elapsed, 2),
|
||||
'busyProcesses': sorted(cpu,
|
||||
key=lambda item: item['percentOfOneCore'],
|
||||
reverse=True)[:12]}
|
||||
busy_threads = []
|
||||
for key, (name, ticks) in threads_after.items():
|
||||
prior = threads_before.get(key)
|
||||
if prior is None or prior[0] != name:
|
||||
continue
|
||||
percent = 100 * max(0, ticks-prior[1]) / ticks_per_second / elapsed
|
||||
if percent >= 1:
|
||||
busy_threads.append({'processId':key[0], 'threadId':key[1],
|
||||
'thread':name,
|
||||
'percentOfOneCore':round(percent,1)})
|
||||
report['cpuSample']['busyThreads'] = sorted(
|
||||
busy_threads, key=lambda item:item['percentOfOneCore'], reverse=True)[:20]
|
||||
idle_after = cpu_idle_snapshot()
|
||||
residency = {}
|
||||
for cpu_name, states in idle_after.items():
|
||||
previous = idle_before.get(cpu_name, [])
|
||||
before_by_name = {state.get('name'):state for state in previous}
|
||||
deltas = []
|
||||
for state in states:
|
||||
prior = before_by_name.get(state.get('name'), {})
|
||||
item = {'name':state.get('name')}
|
||||
for key in ('usage','time'):
|
||||
if key in state:
|
||||
try:
|
||||
item[key+'Delta'] = max(0,int(state[key])-int(prior.get(key,state[key])))
|
||||
except ValueError:
|
||||
pass
|
||||
if 'disable' in state:
|
||||
item['disabled'] = state['disable']
|
||||
deltas.append(item)
|
||||
if deltas:
|
||||
residency[cpu_name] = deltas
|
||||
report['cpuIdle'] = {'available':bool(residency),
|
||||
'statesDuringSample':residency,
|
||||
'frequencyAfterSample':cpu_frequency_snapshot(),
|
||||
'kernelCommandLineFlags':[
|
||||
token for token in Path('/proc/cmdline').read_text().split()
|
||||
if token.startswith(('lpm_levels.','cpuidle.','nohz='))],
|
||||
'lowPowerParameter':None,
|
||||
'systemSuspend':{}}
|
||||
for key, path in {
|
||||
'sleepDisabled':'/sys/module/lpm_levels/parameters/sleep_disabled',
|
||||
'cpuIdleEnabled':'/sys/module/lpm_levels/parameters/enable_lpm',
|
||||
'powerState':'/sys/power/state',
|
||||
'memorySleep':'/sys/power/mem_sleep',
|
||||
}.items():
|
||||
try:
|
||||
value = Path(path).read_text().strip()
|
||||
if key in ('powerState','memorySleep'):
|
||||
report['cpuIdle']['systemSuspend'][key] = value
|
||||
else:
|
||||
report['cpuIdle']['lowPowerParameter'] = {
|
||||
**(report['cpuIdle']['lowPowerParameter'] or {}), key:value}
|
||||
except OSError:
|
||||
pass
|
||||
suspend_stats = {}
|
||||
for path in Path('/sys/power/suspend_stats').glob('*'):
|
||||
if path.is_file():
|
||||
try:
|
||||
suspend_stats[path.name] = path.read_text().strip()
|
||||
except OSError:
|
||||
pass
|
||||
report['cpuIdle']['systemSuspend']['statistics'] = suspend_stats or None
|
||||
wakeups_after = wakeup_snapshot()
|
||||
report['wakeupSources'] = {
|
||||
'available':wakeups_after is not None,
|
||||
'eventDeltas':counter_delta(wakeups_before,wakeups_after,('eventCount',)),
|
||||
}
|
||||
report['batteryDelta'] = {}
|
||||
for supply, values in power_supplies().items():
|
||||
prior = battery_before.get(supply,{})
|
||||
for key in ('current_now','voltage_now','power_now','capacity','status'):
|
||||
if key in values:
|
||||
report['batteryDelta'].setdefault(supply,{})[key] = {
|
||||
'before':prior.get(key), 'after':values[key]}
|
||||
return report
|
||||
|
||||
|
||||
|
|
|
|||
22
tundra/tools/kernel_cmdline.py
Normal file
22
tundra/tools/kernel_cmdline.py
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: MPL-2.0
|
||||
"""Small helpers for keeping inherited Android boot flags out of Vulpes."""
|
||||
|
||||
|
||||
def remove_parameters(cmdline, names):
|
||||
"""Return (cleaned_cmdline, removed_tokens) for exact kernel parameter names."""
|
||||
names = set(names)
|
||||
kept = []
|
||||
removed = []
|
||||
for token in cmdline.split():
|
||||
if token.split('=', 1)[0] in names:
|
||||
removed.append(token)
|
||||
else:
|
||||
kept.append(token)
|
||||
return ' '.join(kept), removed
|
||||
|
||||
|
||||
def vulpes_cmdline(cmdline):
|
||||
# This Qualcomm cpuidle parameter disables every selectable low-power
|
||||
# level. It is inherited from Android and must not disable Vulpes CPU idle.
|
||||
return remove_parameters(cmdline, {'lpm_levels.sleep_disabled'})
|
||||
|
|
@ -52,6 +52,7 @@ def main():
|
|||
files['opt/firefox/vulpes/tests/BeatlesAudit.sys.mjs']=ROOT/'tundra/devices/beatles/gaia-audit.sys.mjs'
|
||||
files['opt/firefox/vulpes/tests/ProbeChild.sys.mjs']=ROOT/'tests/ProbeChild.sys.mjs'
|
||||
files['opt/vulpes/assets/webapps/settings.gaiamobile.org/js/modules/wifi_utils.js']=ROOT/'assets/webapps/settings.gaiamobile.org/js/modules/wifi_utils.js'
|
||||
files['opt/vulpes/tundra/runtime/beatles-camera-client.py']=ROOT/'tundra/runtime/beatles-camera-client.py'
|
||||
add_beatles_t9(files)
|
||||
for name in ('gaia-session.py','cover-display.py','radio_control.py','radio_status.py','call_audio.py','stream_volume.py','hardware_control.py','wifi_control.py','sms_pdu.py'):
|
||||
files['work/'+name]=ROOT/'tundra/devices/beatles'/name
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import io
|
|||
import tarfile
|
||||
from ramdisk_reference import read_newc
|
||||
from shared_ui import build_payload, compress_archive
|
||||
from kernel_cmdline import vulpes_cmdline
|
||||
from tundra import ROOT, checked_source, cpio, digest, save
|
||||
|
||||
|
||||
|
|
@ -46,6 +47,8 @@ def main():
|
|||
options=shlex.split(text);fields=dict(zip(options[::2],options[1::2]))
|
||||
if fields['--header_version']!='0' or digest(temp/'base/kernel')!=build['kernelSha256']:
|
||||
raise ValueError('Unexpected boot format or kernel')
|
||||
source_cmdline=fields['--cmdline']
|
||||
fields['--cmdline'],removed_kernel_parameters=vulpes_cmdline(source_cmdline)
|
||||
original=read_newc((temp/'base/ramdisk').read_bytes())
|
||||
if 'shared-ui.tar' not in original or 'standalone-mount.sh' not in original:
|
||||
raise ValueError('Missing standalone UI overlay')
|
||||
|
|
@ -112,12 +115,13 @@ def main():
|
|||
for name in hashes:
|
||||
target=out/'sources'/name;target.parent.mkdir(parents=True,exist_ok=True)
|
||||
shutil.copy2(ROOT.parent/name,target)
|
||||
for path in (Path(__file__).resolve(),ROOT/'tools/shared_ui.py',*native_sources.values(),*([ROOT/'runtime/prepare-native.py'] if args.settings_adapters else [])):
|
||||
for path in (Path(__file__).resolve(),ROOT/'tools/kernel_cmdline.py',ROOT/'tools/shared_ui.py',*native_sources.values(),*([ROOT/'runtime/prepare-native.py'] if args.settings_adapters else [])):
|
||||
target=out/'sources'/path.relative_to(ROOT.parent)
|
||||
target.parent.mkdir(parents=True,exist_ok=True);shutil.copy2(path,target)
|
||||
build['sources'][str(path.relative_to(ROOT))]=digest(path)
|
||||
shutil.copy2(base/'client-key',out/'client-key')
|
||||
build.update(image='boot.img',imageSha256=digest(out/'boot.img'),ramdiskSha256=digest(ramdisk),
|
||||
removedKernelParameters=removed_kernel_parameters,
|
||||
sharedUiHashes=hashes,builtAtUnix=int(time.time()),flashReady=False,
|
||||
baseImageSha256=args.expected_sha256,uiOnlyRefresh=not args.settings_adapters,changedRamdiskEntries=changed,
|
||||
scope='Private RAM Settings update; kernel and storage configuration preserved')
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
# SPDX-License-Identifier: MPL-2.0
|
||||
"""Synchronize the shared host and run a bounded private Nokia Gaia trial."""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
|
|
@ -15,6 +16,26 @@ OUT=ROOT/'tundra/out/beatles/gecko-156.0.1'
|
|||
REMOTE='/data/local/vulpes-beatles'
|
||||
|
||||
|
||||
def compile_if_changed(command, output, sources, signature):
|
||||
"""Avoid relinking the same ARM tools on every bounded trial."""
|
||||
stamp = output.with_name(output.name + '.build.json')
|
||||
inputs = []
|
||||
for path in sources:
|
||||
stat = path.stat()
|
||||
digest = hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
inputs.append({'path':str(path), 'size':stat.st_size, 'sha256':digest})
|
||||
record = {'command':command,'signature':signature, 'inputs':inputs}
|
||||
if output.is_file() and stamp.is_file():
|
||||
try:
|
||||
if json.loads(stamp.read_text()) == record:
|
||||
print('Using cached ARM tool:', output.name, flush=True)
|
||||
return
|
||||
except (OSError, ValueError):
|
||||
pass
|
||||
subprocess.run(command, check=True)
|
||||
stamp.write_text(json.dumps(record, sort_keys=True)+'\n')
|
||||
|
||||
|
||||
def main():
|
||||
parser=argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--interactive',action='store_true',help='Manual session, no automatic keypad actions; KaiOS returns within 30 minutes')
|
||||
|
|
@ -23,17 +44,46 @@ def main():
|
|||
parser.add_argument('--phone',action='store_true',help='Enable Nokia radio, Wi-Fi, SMS/call and hardware control brokers')
|
||||
parser.add_argument('--camera-probe',action='store_true',help='Run the bounded Qualcomm HAL preview probe before Gaia starts')
|
||||
parser.add_argument('--camera-preview-binary',type=Path,help='Use a private QCamera preview-dump binary for a bounded RAM-only trial')
|
||||
parser.add_argument('--camera-service-probe',action='store_true',help='Test Android Camera1/CameraService as the camera system UID in a bounded RAM-only trial')
|
||||
parser.add_argument('--camera-service-probe-binary',type=Path,help='Private Android API 23 Camera1 diagnostic binary')
|
||||
parser.add_argument('--camera-gl-probe',action='store_true',help='Test Camera1 through EGL/GLConsumer in a bounded RAM-only trial')
|
||||
parser.add_argument('--camera-gl-probe-binary',type=Path,help='Private Android API 23 EGL/GLConsumer diagnostic binary')
|
||||
parser.add_argument('--camera-permission-bridge-binary',type=Path,help='Temporary camera-only Android API 23 permission controller')
|
||||
parser.add_argument('--camera-permission-bridge-runtime',type=Path,help='Android API 23 C++ runtime required by the permission bridge')
|
||||
parser.add_argument('--camera-broker',action='store_true',help='Run the fixed-command CameraService bridge under UID 1006 during Gaia qualification')
|
||||
parser.add_argument('--no-camera-broker',action='store_true',help='Disable the CameraService bridge for a non-camera session')
|
||||
parser.add_argument('--camera-broker-worker',type=Path,help='Private Android API 23 persistent Camera1 worker binary')
|
||||
parser.add_argument('--camera-broker-binary',type=Path,help='Private Android API 23 local camera broker binary')
|
||||
parser.add_argument('--display-session-binary',type=Path,help='Use a prebuilt compatible session supervisor')
|
||||
args=parser.parse_args()
|
||||
session_option=(('--phone-' if args.phone else '--interactive-')+str(args.minutes*60)+'s') if args.interactive else ('--phone-180s' if args.phone else '--temporary-90s' if (args.camera_probe or args.camera_preview_binary) else '--temporary-180s')
|
||||
camera_diagnostics=(args.camera_probe, args.camera_preview_binary is not None,
|
||||
args.camera_service_probe, args.camera_gl_probe)
|
||||
camera_broker=args.camera_broker or (not args.no_camera_broker and not any(camera_diagnostics))
|
||||
camera_modes=sum(camera_diagnostics)+(args.camera_broker is True)
|
||||
if camera_modes > 1:
|
||||
parser.error('Choose one camera probe per RAM trial; all use the same sensor')
|
||||
session_option=(('--phone-' if args.phone else '--interactive-')+str(args.minutes*60)+'s') if args.interactive else ('--phone-180s' if args.phone else '--temporary-90s' if (args.camera_probe or args.camera_preview_binary or args.camera_service_probe or args.camera_gl_probe) else '--temporary-240s')
|
||||
rows=subprocess.check_output(['adb','devices','-l'],text=True).splitlines()
|
||||
devices=[r.split()[0] for r in rows if 'model:Nokia_2720_Flip' in r.split() and r.split()[1]=='device']
|
||||
if len(devices)!=1:raise RuntimeError('Exactly one connected Nokia required')
|
||||
adb=['adb','-s',devices[0]]
|
||||
if subprocess.check_output(adb+['shell','getprop ro.product.codename'],text=True).strip()!='beatles':
|
||||
raise RuntimeError('Unexpected device')
|
||||
# A previous interrupted run must never make an old frame or log appear
|
||||
# to belong to this trial. These paths are private diagnostics only.
|
||||
stale = [REMOTE+'/camera-service-probe-once', REMOTE+'/camera-service-probe.log',
|
||||
REMOTE+'/camera-gl-probe-once', REMOTE+'/camera-gl-probe.log',
|
||||
REMOTE+'/camera-permission-bridge.log', REMOTE+'/qcamera-probe-once',
|
||||
REMOTE+'/camera-broker-once',REMOTE+'/camera-broker.log',REMOTE+'/camera-ipc/camera.sock',
|
||||
REMOTE+'/qcamera-probe.log', REMOTE+'/qcamera-preview-dump-once',
|
||||
REMOTE+'/qcamera-preview-dump', REMOTE+'/rootfs/work/camera-frame.yuv',
|
||||
REMOTE+'/rootfs/work/camera-photo.jpg']
|
||||
cleanup=subprocess.run(adb+['shell','rm -f '+' '.join(stale)],capture_output=True,text=True)
|
||||
if cleanup.returncode:
|
||||
print('Could not clear every prior camera artifact:',cleanup.stderr.strip(),flush=True)
|
||||
if not args.interactive:
|
||||
lid=subprocess.check_output(adb+['shell','cat /sys/devices/soc.0/hall_sensor1.73/hall_status'],text=True).strip()
|
||||
if lid=='1' and not (args.camera_probe or args.camera_preview_binary):
|
||||
if lid=='1' and not (args.camera_probe or args.camera_preview_binary or args.camera_service_probe or args.camera_gl_probe):
|
||||
raise RuntimeError('Open the Nokia flip before running keypad qualification; no session started')
|
||||
ini=subprocess.check_output(adb+['shell','cat '+REMOTE+'/rootfs/opt/firefox/application.ini'],text=True)
|
||||
if 'Version=156.0.1' not in ini.splitlines():raise RuntimeError('Stage Gecko 156.0.1 before launching')
|
||||
|
|
@ -45,12 +95,25 @@ def main():
|
|||
else:
|
||||
subprocess.run(adb+['shell','rm -f '+REMOTE+'/rootfs/work/cover-animation.rgb565'],check=True,capture_output=True)
|
||||
sysroot=OUT/'sysroot/rootfs'
|
||||
compiler=[str(OUT/'mozbuild/clang/bin/clang'),'--target=arm-linux-gnueabihf',
|
||||
'--sysroot='+str(sysroot),'--gcc-install-dir='+str(sysroot/'usr/lib/gcc/arm-linux-gnueabihf/12'),
|
||||
'-fuse-ld=lld','-march=armv7-a','-mfpu=neon','-mfloat-abi=hard','-O2','-Wall','-Wextra','-Werror']
|
||||
subprocess.run(compiler+['-static',str(ROOT/'tundra/probes/beatles-control-client.c'),'-o',str(OUT/'control-client')],check=True)
|
||||
subprocess.run(compiler+['-static',str(ROOT/'tundra/probes/beatles-display-session.c'),'-o',str(OUT/'display-session')],check=True)
|
||||
subprocess.run(compiler+[str(ROOT/'tundra/probes/beatles-keypad.c'),'-lX11','-lXtst','-o',str(OUT/'keypad')],check=True)
|
||||
display_session=OUT/'display-session'
|
||||
if args.display_session_binary:
|
||||
if not args.display_session_binary.is_file():raise RuntimeError('Prebuilt session supervisor not found')
|
||||
display_session=args.display_session_binary
|
||||
else:
|
||||
compiler=[str(OUT/'mozbuild/clang/bin/clang'),'--target=arm-linux-gnueabihf',
|
||||
'--sysroot='+str(sysroot),'--gcc-install-dir='+str(sysroot/'usr/lib/gcc/arm-linux-gnueabihf/12'),
|
||||
'-fuse-ld=lld','-march=armv7-a','-mfpu=neon','-mfloat-abi=hard','-O2','-Wall','-Wextra','-Werror']
|
||||
compiler_stat=Path(compiler[0]).stat()
|
||||
runtime_lock=sysroot.parent/'runtime-lock.json'
|
||||
signature={'command':compiler,'compilerSize':compiler_stat.st_size,
|
||||
'compilerMtimeNs':compiler_stat.st_mtime_ns,
|
||||
'runtimeLockSha256':hashlib.sha256(runtime_lock.read_bytes()).hexdigest()}
|
||||
compile_if_changed(compiler+['-static',str(ROOT/'tundra/probes/beatles-control-client.c'),'-o',str(OUT/'control-client')],
|
||||
OUT/'control-client',[ROOT/'tundra/probes/beatles-control-client.c'],signature)
|
||||
compile_if_changed(compiler+['-static',str(ROOT/'tundra/probes/beatles-display-session.c'),'-o',str(OUT/'display-session')],
|
||||
OUT/'display-session',[ROOT/'tundra/probes/beatles-display-session.c'],signature)
|
||||
compile_if_changed(compiler+[str(ROOT/'tundra/probes/beatles-keypad.c'),'-lX11','-lXtst','-o',str(OUT/'keypad')],
|
||||
OUT/'keypad',[ROOT/'tundra/probes/beatles-keypad.c'],signature)
|
||||
(OUT/'gaia-test.sh').write_text('exec /usr/bin/python3.11 /work/gaia-session.py\n')
|
||||
dirs=' '.join(REMOTE+'/rootfs/'+d for d in ('opt/firefox/vulpes/tests','opt/firefox/defaults/pref','work','proc','sys','dev','tmp','run'))
|
||||
subprocess.run(adb+['shell','mkdir -p '+dirs+'; chmod 1777 '+REMOTE+'/rootfs/tmp; rm -f '+REMOTE+'/display-session.done '+REMOTE+'/rootfs/work/gaia-audit.json'],capture_output=True,check=True)
|
||||
|
|
@ -65,6 +128,57 @@ def main():
|
|||
subprocess.run(adb+['shell','rm -f '+REMOTE+'/qcamera-preview-dump '+REMOTE+'/qcamera-preview-dump-once '+REMOTE+'/qcamera-probe.log '+REMOTE+'/rootfs/work/camera-frame.yuv '+REMOTE+'/rootfs/work/camera-photo.jpg'],check=True,capture_output=True)
|
||||
subprocess.run(adb+['push',str(args.camera_preview_binary),REMOTE+'/qcamera-preview-dump'],check=True,capture_output=True)
|
||||
subprocess.run(adb+['shell','chmod 755 '+REMOTE+'/qcamera-preview-dump; touch '+REMOTE+'/qcamera-preview-dump-once'],check=True,capture_output=True)
|
||||
if args.camera_service_probe:
|
||||
if not args.camera_service_probe_binary or not args.camera_service_probe_binary.is_file():
|
||||
raise RuntimeError('--camera-service-probe requires --camera-service-probe-binary')
|
||||
if not args.camera_permission_bridge_binary or not args.camera_permission_bridge_binary.is_file():
|
||||
raise RuntimeError('--camera-service-probe requires --camera-permission-bridge-binary')
|
||||
if not args.camera_permission_bridge_runtime or not args.camera_permission_bridge_runtime.is_file():
|
||||
raise RuntimeError('--camera-service-probe requires --camera-permission-bridge-runtime')
|
||||
probe='/data/local/tmp/vulpes-camera-probe-20261023'
|
||||
subprocess.run(adb+['push',str(args.camera_service_probe_binary),probe],check=True,capture_output=True)
|
||||
bridge='/data/local/tmp/vulpes-camera-permission-bridge'
|
||||
runtime='/data/local/tmp/libc++_shared.so'
|
||||
subprocess.run(adb+['push',str(args.camera_permission_bridge_binary),bridge],check=True,capture_output=True)
|
||||
subprocess.run(adb+['push',str(args.camera_permission_bridge_runtime),runtime],check=True,capture_output=True)
|
||||
subprocess.run(adb+['shell','chmod 755 '+probe+' '+bridge+' '+runtime+'; rm -f '+REMOTE+'/camera-permission-bridge.log; touch '+REMOTE+'/camera-service-probe-once'],check=True,capture_output=True)
|
||||
if args.camera_gl_probe:
|
||||
if not args.camera_gl_probe_binary or not args.camera_gl_probe_binary.is_file():
|
||||
raise RuntimeError('--camera-gl-probe requires --camera-gl-probe-binary')
|
||||
if not args.camera_permission_bridge_binary or not args.camera_permission_bridge_binary.is_file():
|
||||
raise RuntimeError('--camera-gl-probe requires --camera-permission-bridge-binary')
|
||||
if not args.camera_permission_bridge_runtime or not args.camera_permission_bridge_runtime.is_file():
|
||||
raise RuntimeError('--camera-gl-probe requires --camera-permission-bridge-runtime')
|
||||
probe='/data/local/tmp/vulpes-camera-gl-probe-20261023'
|
||||
subprocess.run(adb+['push',str(args.camera_gl_probe_binary),probe],check=True,capture_output=True)
|
||||
bridge='/data/local/tmp/vulpes-camera-permission-bridge'
|
||||
runtime='/data/local/tmp/libc++_shared.so'
|
||||
subprocess.run(adb+['push',str(args.camera_permission_bridge_binary),bridge],check=True,capture_output=True)
|
||||
subprocess.run(adb+['push',str(args.camera_permission_bridge_runtime),runtime],check=True,capture_output=True)
|
||||
subprocess.run(adb+['shell','chmod 755 '+probe+' '+bridge+' '+runtime+'; rm -f '+REMOTE+'/camera-permission-bridge.log; touch '+REMOTE+'/camera-gl-probe-once'],check=True,capture_output=True)
|
||||
if camera_broker:
|
||||
subprocess.run([sys.executable,str(ROOT/'tundra/tools/build-beatles-camera.py')],check=True)
|
||||
broker_build=OUT/'camera-broker-build'
|
||||
worker_path=args.camera_broker_worker or broker_build/'beatles-camera-worker'
|
||||
broker_path=args.camera_broker_binary or broker_build/'beatles-camera-broker'
|
||||
permission_bridge_path=args.camera_permission_bridge_binary or OUT/'camera-probe-runtime/beatles-camera-permission-bridge'
|
||||
permission_runtime_path=args.camera_permission_bridge_runtime or OUT/'camera-probe-runtime/libc++_shared.so'
|
||||
if not worker_path.is_file():
|
||||
raise RuntimeError('CameraService worker binary not found: '+str(worker_path))
|
||||
if not broker_path.is_file():
|
||||
raise RuntimeError('CameraService broker binary not found: '+str(broker_path))
|
||||
if not permission_bridge_path.is_file():
|
||||
raise RuntimeError('Camera permission bridge binary not found: '+str(permission_bridge_path))
|
||||
if not permission_runtime_path.is_file():
|
||||
raise RuntimeError('Camera permission bridge runtime not found: '+str(permission_runtime_path))
|
||||
broker_worker='/data/local/tmp/vulpes-beatles-camera-worker'
|
||||
broker='/data/local/tmp/vulpes-beatles-camera-broker'
|
||||
bridge='/data/local/tmp/vulpes-camera-permission-bridge'
|
||||
runtime='/data/local/tmp/libc++_shared.so'
|
||||
for source,target in ((worker_path,broker_worker),(broker_path,broker),
|
||||
(permission_bridge_path,bridge),(permission_runtime_path,runtime)):
|
||||
subprocess.run(adb+['push',str(source),target],check=True,capture_output=True)
|
||||
subprocess.run(adb+['shell','chmod 755 '+broker_worker+' '+broker+' '+bridge+' '+runtime+'; mkdir -p '+REMOTE+'/camera-ipc; rm -f '+REMOTE+'/camera-ipc/camera.sock '+REMOTE+'/camera-permission-bridge.log '+REMOTE+'/camera-broker.log; touch '+REMOTE+'/camera-broker-once'],check=True,capture_output=True)
|
||||
subprocess.run(adb+['push',str(ROOT/'tundra/devices/beatles/t9'),REMOTE+'/rootfs/opt/vulpes/tundra/devices/beatles/'],check=True,capture_output=True,timeout=120)
|
||||
# Seed the isolated Gaia media store with small, known-good playback samples.
|
||||
media=REMOTE+'/rootfs/work/gaia-profile/vulpes-media'
|
||||
|
|
@ -86,7 +200,9 @@ def main():
|
|||
for directory in ('host','services','adapters'):
|
||||
subprocess.run(adb+['push',str(ROOT/directory),REMOTE+'/rootfs/opt/firefox/vulpes/'],check=True,capture_output=True,timeout=120)
|
||||
subprocess.run(adb+['push',str(ROOT/'host'),REMOTE+'/rootfs/opt/vulpes/'],check=True,capture_output=True,timeout=120)
|
||||
files=((OUT/'control-client','rootfs/work/control-client'),(OUT/'display-session','display-session'),(OUT/'keypad','rootfs/work/keypad'),
|
||||
subprocess.run(adb+['shell','mkdir -p '+REMOTE+'/rootfs/opt/vulpes/tundra/runtime'],check=True,capture_output=True)
|
||||
subprocess.run(adb+['push',str(ROOT/'tundra/runtime/beatles-camera-client.py'),REMOTE+'/rootfs/opt/vulpes/tundra/runtime/beatles-camera-client.py'],check=True,capture_output=True)
|
||||
files=((OUT/'control-client','rootfs/work/control-client'),(display_session,'display-session'),(OUT/'keypad','rootfs/work/keypad'),
|
||||
(OUT/'gaia-test.sh','rootfs/work/display-test.sh'),
|
||||
(ROOT/'tundra/devices/beatles/gaia-session.py','rootfs/work/gaia-session.py'),
|
||||
(ROOT/'tundra/devices/beatles/cover-display.py','rootfs/work/cover-display.py'),
|
||||
|
|
@ -113,6 +229,9 @@ def main():
|
|||
# Its restoration marker must not be mistaken for this trial.
|
||||
subprocess.run(adb+['shell','rm -f '+REMOTE+'/display-session.done'],check=True,capture_output=True)
|
||||
script='''(function(){var Cc=Components.classes,Ci=Components.interfaces;
|
||||
var b=Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch);
|
||||
b.setBoolPref("devtools.debugger.remote-enabled",true);
|
||||
Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefService).savePrefFile(null);
|
||||
var f=Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);f.initWithPath(%s);
|
||||
var p=Cc["@mozilla.org/process/util;1"].createInstance(Ci.nsIProcess);p.init(f);
|
||||
p.runAsync([%s],1,null,false);return "started";})()''' % (json.dumps(REMOTE+'/display-session'),json.dumps(session_option))
|
||||
|
|
@ -141,7 +260,7 @@ def main():
|
|||
time.sleep(1)
|
||||
subprocess.run(adb+['shell','touch '+REMOTE+'/stop-session'],check=True,capture_output=True)
|
||||
raise RuntimeError('Manual session startup timed out; requested KaiOS restoration')
|
||||
for second in range(205):
|
||||
for second in range(265):
|
||||
state=subprocess.check_output(adb+['shell','cat '+REMOTE+'/display-session.done'],text=True).strip()
|
||||
if state=='restored':break
|
||||
if second and second%30==0:print('Trial running:',second,'seconds',flush=True)
|
||||
|
|
@ -149,12 +268,19 @@ def main():
|
|||
else:raise RuntimeError('No restoration marker; inspect the watchdog before retrying')
|
||||
report=OUT/('gaia-trial-'+time.strftime('%Y%m%d-%H%M%S'))
|
||||
report.mkdir()
|
||||
for relative in ('display-session.log','qcamera-probe.log','framebuffer.ppm','rootfs/work/camera-frame.yuv','rootfs/work/camera-photo.jpg','rootfs/work/Xorg.log','rootfs/work/xorg-console.log',
|
||||
for relative in ('display-session.log','qcamera-probe.log','camera-service-probe.log','camera-gl-probe.log','camera-permission-bridge.log','camera-broker.log','framebuffer.ppm','rootfs/work/camera-frame.yuv','rootfs/work/camera-photo.jpg','rootfs/work/Xorg.log','rootfs/work/xorg-console.log',
|
||||
'rootfs/run/tundra-hardware/status.json','rootfs/work/radio-status.log',
|
||||
'rootfs/work/cover-display.log','rootfs/work/cover-preview.png','rootfs/work/cover-refresh.json','rootfs/work/lid-state.json',
|
||||
'rootfs/work/keypad.log','rootfs/work/gecko.log','rootfs/work/gaia-server.log',
|
||||
'rootfs/work/gaia-audit.json','rootfs/work/gaia-native.png','rootfs/work/memory-samples.jsonl'):
|
||||
subprocess.run(adb+['pull',REMOTE+'/'+relative,str(report/Path(relative).name)],capture_output=True)
|
||||
if args.camera_gl_probe:
|
||||
for remote, name in (('/data/local/tmp/vulpes-camera-glconsumer.ppm','camera-glconsumer.ppm'),
|
||||
('/data/local/tmp/vulpes-camera-gonkqueue.nv21','camera-gonkqueue.nv21'),
|
||||
('/data/local/tmp/vulpes-camera-gonkqueue.txt','camera-gonkqueue.txt'),
|
||||
('/data/local/tmp/vulpes-camera-callback.yuv','camera-callback.yuv'),
|
||||
('/data/local/tmp/vulpes-camera-callback.jpg','camera-callback.jpg')):
|
||||
subprocess.run(adb+['pull',remote,str(report/name)],capture_output=True)
|
||||
service=subprocess.check_output(adb+['shell','getprop init.svc.b2g'],text=True).strip()
|
||||
print('KaiOS service:',service,'Report:',report,flush=True)
|
||||
# Keep both outputs from the bounded Qualcomm probe. Its still-snapshot
|
||||
|
|
@ -170,6 +296,10 @@ def main():
|
|||
if service!='running':raise RuntimeError('KaiOS did not restart')
|
||||
if args.camera_preview_binary:
|
||||
subprocess.run(adb+['shell','rm -f '+REMOTE+'/qcamera-preview-dump '+REMOTE+'/qcamera-preview-dump-once'],capture_output=True)
|
||||
if args.camera_service_probe:
|
||||
subprocess.run(adb+['shell','rm -f /data/local/tmp/vulpes-camera-probe-20261023 /data/local/tmp/vulpes-camera-permission-bridge /data/local/tmp/libc++_shared.so /data/local/tmp/vulpes-camera-frame.nv21'],capture_output=True)
|
||||
if args.camera_gl_probe:
|
||||
subprocess.run(adb+['shell','rm -f /data/local/tmp/vulpes-camera-gl-probe-20261023 /data/local/tmp/vulpes-camera-permission-bridge /data/local/tmp/libc++_shared.so'],capture_output=True)
|
||||
if not (report/'gaia-audit.json').is_file():raise RuntimeError('Gaia audit did not complete')
|
||||
audit=json.loads((report/'gaia-audit.json').read_text())
|
||||
summary={key:value for key,value in audit.items() if key not in ('icons','layout')}
|
||||
|
|
|
|||
|
|
@ -51,6 +51,8 @@ def selected_files():
|
|||
*sorted((ROOT.parent/'services').rglob('*.mjs')),
|
||||
*sorted((ROOT/'mms-codec').glob('*')),
|
||||
*sorted((ROOT.parent/'assets/webapps/marketplace.gaiamobile.org').rglob('*')),
|
||||
ROOT.parent/'assets/webapps/system.gaiamobile.org/style/system/system.css',
|
||||
ROOT.parent/'assets/webapps/system.gaiamobile.org/style/system/initlogo.css',
|
||||
ROOT.parent/'tools/installed_apps.py',
|
||||
*sorted((ROOT.parent/'gaia/shared/elements/vulpes-fonts').rglob('*')),
|
||||
*sorted((ROOT.parent/'overrides').rglob('*')),
|
||||
|
|
@ -64,6 +66,8 @@ def selected_files():
|
|||
ROOT.parent/'assets/webapps/settings.gaiamobile.org/js/panels/app_permissions_list/panel.js',
|
||||
ROOT.parent/'assets/webapps/settings.gaiamobile.org/js/panels/screen_lock_passcode/panel.js',
|
||||
ROOT.parent/'assets/webapps/verticalhome.gaiamobile.org/gaia_build_defer_index.js',
|
||||
ROOT.parent/'assets/webapps/verticalhome.gaiamobile.org/js/init.json',
|
||||
ROOT.parent/'gaia/apps/verticalhome/js/sources/application.js',
|
||||
ROOT.parent/'assets/webapps/wallpaper.gaiamobile.org/style/wallpaper.css',
|
||||
ROOT.parent/'assets/webapps/ringtones.gaiamobile.org/gaia_build_defer_pick.js',
|
||||
ROOT.parent/'assets/webapps/ringtones.gaiamobile.org/gaia_build_defer_manage.js',
|
||||
|
|
|
|||
|
|
@ -9,6 +9,18 @@ def mount_table(layout):
|
|||
raise ValueError('Expected Sargo userdata, not another partition')
|
||||
if not re.fullmatch(r'[0-9a-f]{64}', layout.get('firstMiBSha256', '')):
|
||||
raise ValueError('Missing userdata identity checksum')
|
||||
if layout.get('schema') == 2 and layout.get('table') == 'direct':
|
||||
if (layout.get('partitionLabel') != 'userdata' or
|
||||
layout.get('filesystem') != 'ext4' or
|
||||
layout.get('storage') != 'raw-userdata-ext4' or
|
||||
not re.fullmatch(r'[0-9]+:[0-9]+', layout.get('backingDevice', '')) or
|
||||
not re.fullmatch(r'[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}',
|
||||
layout.get('filesystemUuid', ''))):
|
||||
raise ValueError('Direct mapping requires inventoried raw ext4 userdata')
|
||||
size = int(layout.get('size', 0))
|
||||
if size <= 0 or size % 512:
|
||||
raise ValueError('Invalid direct userdata geometry')
|
||||
return None
|
||||
match = re.fullmatch(r'0 ([0-9]+) linear ([0-9]+:[0-9]+) ([0-9]+)',
|
||||
layout.get('table', ''))
|
||||
if not match:
|
||||
|
|
|
|||
|
|
@ -36,11 +36,34 @@ def main():
|
|||
spec=importlib.util.spec_from_file_location('package_gaia',ROOT/'tundra/tools/package-beatles-gaia.py')
|
||||
package=importlib.util.module_from_spec(spec);spec.loader.exec_module(package)
|
||||
files=package.shared_files()
|
||||
current={str(p.relative_to(ROOT)):hashlib.sha256(p.read_bytes()).hexdigest() for p in files}
|
||||
out=ROOT/'tundra/out/beatles/gecko-156.0.1'
|
||||
cache_path=out/'.gaia-sync-hash-cache.json'
|
||||
try:
|
||||
cache=json.loads(cache_path.read_text()).get('files',{})
|
||||
except (OSError,ValueError,AttributeError):
|
||||
cache={}
|
||||
current={}
|
||||
next_cache={}
|
||||
reused=0
|
||||
for path in files:
|
||||
name=str(path.relative_to(ROOT))
|
||||
stat=path.stat()
|
||||
fingerprint={'device':stat.st_dev,'inode':stat.st_ino,'size':stat.st_size,
|
||||
'mtime_ns':stat.st_mtime_ns,'ctime_ns':stat.st_ctime_ns}
|
||||
previous=cache.get(name,{})
|
||||
digest=previous.get('sha256') if all(previous.get(k)==v for k,v in fingerprint.items()) else None
|
||||
if not digest:
|
||||
digest=hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
else:
|
||||
reused+=1
|
||||
current[name]=digest
|
||||
next_cache[name]={**fingerprint,'sha256':digest}
|
||||
temporary=cache_path.with_suffix('.tmp')
|
||||
temporary.write_text(json.dumps({'files':next_cache},separators=(',',':'))+'\n')
|
||||
temporary.replace(cache_path)
|
||||
raw=shell('cat '+REMOTE+'/work/gaia-sync.json 2>/dev/null || true')
|
||||
previous=json.loads(raw).get('files',{}) if raw else {}
|
||||
changed,removed=changes(current,previous)
|
||||
out=ROOT/'tundra/out/beatles/gecko-156.0.1'
|
||||
manifest=out/'gaia-sync.json'
|
||||
manifest.write_text(json.dumps({'files':current},indent=2)+'\n')
|
||||
if changed or removed:
|
||||
|
|
@ -57,6 +80,6 @@ def main():
|
|||
shell('rm -f '+' '.join(shlex.quote(REMOTE+'/opt/vulpes/'+p) for p in removed[start:start+100]))
|
||||
subprocess.run(adb+['push',str(manifest),REMOTE+'/work/gaia-sync.json.new'],check=True,capture_output=True)
|
||||
shell('mv '+REMOTE+'/work/gaia-sync.json.new '+REMOTE+'/work/gaia-sync.json')
|
||||
print(json.dumps({'files':len(current),'updated':len(changed),'removed':len(removed),'profilePreserved':True}))
|
||||
print(json.dumps({'files':len(current),'hashesReused':reused,'updated':len(changed),'removed':len(removed),'profilePreserved':True}))
|
||||
|
||||
if __name__=='__main__':main()
|
||||
|
|
|
|||
Loading…
Reference in a new issue