Release Vulpes OS 2.7-preview.22
This commit is contained in:
parent
200d1fb235
commit
a138f97877
294 changed files with 515786 additions and 187 deletions
71
tundra/probes/armv7-gecko-launch.c
Normal file
71
tundra/probes/armv7-gecko-launch.c
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
/* SPDX-License-Identifier: MPL-2.0 */
|
||||
/* Temporary ARM EABI runtime probe. Private mount namespace; Gecko runs as shell. */
|
||||
#ifndef ROOT
|
||||
#define ROOT "/data/local/tmp/vulpes-gecko-armhf/rootfs"
|
||||
#endif
|
||||
#ifndef GECKO
|
||||
#define GECKO "/usr/lib/firefox-esr/firefox-esr"
|
||||
#endif
|
||||
static long call(long n, long a, long b, long c, long d, long e) {
|
||||
register long r0 __asm__("r0") = a;
|
||||
register long r1 __asm__("r1") = b;
|
||||
register long r2 __asm__("r2") = c;
|
||||
register long r3 __asm__("r3") = d;
|
||||
register long r4 __asm__("r4") = e;
|
||||
register long r7 __asm__("r7") = n;
|
||||
__asm__ volatile("svc 0" : "+r"(r0) : "r"(r1), "r"(r2), "r"(r3), "r"(r4), "r"(r7) : "memory", "cc");
|
||||
return r0;
|
||||
}
|
||||
static void output(const char *s) {
|
||||
long len=0; while(s[len])len++;
|
||||
call(4,2,(long)s,len,0,0);
|
||||
}
|
||||
static void check(long result, const char *operation) {
|
||||
if(result>=0)return;
|
||||
output(operation);output(" failed, errno=");
|
||||
char buf[16];long i=15;buf[i]=0;result=-result;
|
||||
do {buf[--i]='0'+result%10;result/=10;}while(result);
|
||||
output(buf+i);output("\n");call(1,1,0,0,0,0);for(;;){}
|
||||
}
|
||||
static void device(const char *path, long minor) {
|
||||
check(call(14,(long)path,8192|0666,256|minor,0,0),"mknod");
|
||||
check(call(15,(long)path,0666,0,0,0),"device permissions");
|
||||
}
|
||||
__attribute__((noreturn)) void run(long *stack) {
|
||||
long argc=stack[0]; char **argv=(char **)(stack+1);
|
||||
int render=argc==2 && argv[1][0]=='r';
|
||||
if(argc!=2 || (!render && argv[1][0]!='v')) {
|
||||
output("Use version or render\n");call(1,2,0,0,0,0);for(;;){}
|
||||
}
|
||||
long fd=call(5,(long)"/proc/self/oom_score_adj",1,0,0,0);
|
||||
check(fd,"oom_score_adj open");check(call(4,fd,(long)"1000",4,0,0),"oom_score_adj write");call(6,fd,0,0,0,0);
|
||||
check(call(337,0x20000|0x40000000,0,0,0,0),"unshare mount and network namespaces");
|
||||
check(call(21,0,(long)"/",0,16384|262144,0),"private mount propagation");
|
||||
check(call(21,(long)"/proc",(long)ROOT "/proc",0,4096,0),"bind proc");
|
||||
/* /data is nodev: character devices need a private tmpfs mount. */
|
||||
check(call(21,(long)"tmpfs",(long)ROOT "/dev",(long)"tmpfs",2,(long)"size=1m,mode=0755"),"private devices");
|
||||
device(ROOT "/dev/null",3);device(ROOT "/dev/zero",5);
|
||||
device(ROOT "/dev/random",8);device(ROOT "/dev/urandom",9);
|
||||
check(call(39,(long)ROOT "/dev/shm",01777,0,0,0),"shared memory directory");
|
||||
check(call(21,(long)"tmpfs",(long)ROOT "/dev/shm",(long)"tmpfs",2|4,(long)"size=64m,mode=1777"),"private shared memory");
|
||||
check(call(61,(long)ROOT,0,0,0,0),"chroot");
|
||||
check(call(12,(long)"/",0,0,0,0),"chdir");
|
||||
check(call(206,0,0,0,0,0),"drop supplementary groups");
|
||||
check(call(214,2000,0,0,0,0),"setgid");
|
||||
check(call(213,2000,0,0,0,0),"setuid");
|
||||
check(call(172,38,1,0,0,0),"no new privileges");
|
||||
char *env[]={"HOME=/work", "TMPDIR=/tmp", "MOZ_HEADLESS=1",
|
||||
"MOZ_HEADLESS_WIDTH=240", "MOZ_HEADLESS_HEIGHT=320", "LANG=C",
|
||||
"FONTCONFIG_PATH=/etc/fonts", "MOZ_CRASHREPORTER_DISABLE=1",
|
||||
"LD_LIBRARY_PATH=/opt/firefox:/usr/lib/firefox-esr:/lib/arm-linux-gnueabihf:/usr/lib/arm-linux-gnueabihf",0};
|
||||
char *versionArgs[]={GECKO,"--version",0};
|
||||
char *renderArgs[]={GECKO,"--headless","--no-remote",
|
||||
"--profile","/work/profile","--window-size","240,320",
|
||||
"--screenshot","/work/gecko.png","file:///work/test.html",0};
|
||||
output("VULPES ARMv7 Gecko probe: isolated mounts/network, uid=2000\n");
|
||||
check(call(11,(long)versionArgs[0],(long)(render?renderArgs:versionArgs),(long)env,0,0),"exec Gecko");
|
||||
call(1,1,0,0,0,0);for(;;){}
|
||||
}
|
||||
__attribute__((naked,noreturn)) void _start(void) {
|
||||
__asm__ volatile("mov r0, sp\nb run");
|
||||
}
|
||||
48
tundra/probes/armv7-smoke.c
Normal file
48
tundra/probes/armv7-smoke.c
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
/* SPDX-License-Identifier: MPL-2.0 */
|
||||
/* Linux EABI smoke test: no libc, filesystem writes, or privileged operations. */
|
||||
static long syscall3(long number, long a, long b, long c) {
|
||||
register long r0 __asm__("r0") = a;
|
||||
register long r1 __asm__("r1") = b;
|
||||
register long r2 __asm__("r2") = c;
|
||||
register long r7 __asm__("r7") = number;
|
||||
__asm__ volatile("svc 0" : "+r"(r0) : "r"(r1), "r"(r2), "r"(r7) : "memory", "cc");
|
||||
return r0;
|
||||
}
|
||||
|
||||
static void print(const char *text) {
|
||||
long length = 0;
|
||||
while (text[length]) ++length;
|
||||
while (length > 0) {
|
||||
long count = syscall3(4, 1, (long)text, length);
|
||||
if (count <= 0) return;
|
||||
text += count;
|
||||
length -= count;
|
||||
}
|
||||
}
|
||||
|
||||
static void number(unsigned long value) {
|
||||
char buffer[16];
|
||||
int end = 15;
|
||||
buffer[end] = 0;
|
||||
do {
|
||||
buffer[--end] = '0' + value % 10;
|
||||
value /= 10;
|
||||
} while (value);
|
||||
print(buffer + end);
|
||||
}
|
||||
|
||||
__attribute__((noreturn)) void _start(void) {
|
||||
struct { char system[65], node[65], release[65], version[65], machine[65], domain[65]; } uts;
|
||||
long status = syscall3(122, (long)&uts, 0, 0);
|
||||
print("VULPES_ARMV7_PROBE=1\n");
|
||||
print("This is a native diagnostic, not Gecko or a replacement OS.\nuid=");
|
||||
number((unsigned long)syscall3(24, 0, 0, 0));
|
||||
print("\n");
|
||||
if (status == 0) {
|
||||
print("kernel="); print(uts.release);
|
||||
print("\narchitecture="); print(uts.machine); print("\n");
|
||||
}
|
||||
print(status == 0 ? "result=PASS\n" : "result=FAIL\n");
|
||||
syscall3(1, status == 0 ? 0 : 1, 0, 0);
|
||||
for (;;) {}
|
||||
}
|
||||
63
tundra/probes/beatles-control-client.c
Normal file
63
tundra/probes/beatles-control-client.c
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
/* SPDX-License-Identifier: MPL-2.0 */
|
||||
/* Unprivileged one-request relay. The root broker still validates every command. */
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
#include <errno.h>
|
||||
#include <signal.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#define REQUEST_LIMIT 8192
|
||||
#define RESPONSE_LIMIT 65536
|
||||
|
||||
static int write_all(int fd, const char *data, size_t size) {
|
||||
while (size) {
|
||||
ssize_t count=write(fd,data,size);
|
||||
if (count<0 && errno==EINTR) continue;
|
||||
if (count<=0) return -1;
|
||||
data+=count;size-=(size_t)count;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main(int argc,char **argv) {
|
||||
const char *path;
|
||||
if (argc!=2) return 2;
|
||||
if (!strcmp(argv[1],"--radio")) path="/work/phone-control.sock";
|
||||
else if (!strcmp(argv[1],"--wifi")) path="/work/wifi-control.sock";
|
||||
else return 2;
|
||||
signal(SIGPIPE,SIG_IGN);
|
||||
/* Bound the whole exchange, including a truncated stdin or an unresponsive broker. */
|
||||
alarm(40);
|
||||
char request[REQUEST_LIMIT+1];size_t used=0;
|
||||
for (;;) {
|
||||
ssize_t count=read(STDIN_FILENO,request+used,sizeof(request)-used);
|
||||
if (count<0 && errno==EINTR) continue;
|
||||
if (count<0) return 1;
|
||||
if (!count) break;
|
||||
used+=(size_t)count;
|
||||
if (used>REQUEST_LIMIT) return 2;
|
||||
}
|
||||
if (!used || memchr(request,'\n',used) || memchr(request,'\r',used)) return 2;
|
||||
int fd=socket(AF_UNIX,SOCK_STREAM,0);
|
||||
if (fd<0) return 1;
|
||||
struct sockaddr_un address={.sun_family=AF_UNIX};
|
||||
memcpy(address.sun_path,path,strlen(path)+1);
|
||||
if (connect(fd,(struct sockaddr *)&address,sizeof(address)) ||
|
||||
write_all(fd,request,used) || write_all(fd,"\n",1)) {close(fd);return 1;}
|
||||
char response[RESPONSE_LIMIT+1];used=0;
|
||||
for (;;) {
|
||||
ssize_t count=read(fd,response+used,sizeof(response)-used);
|
||||
if (count<0 && errno==EINTR) continue;
|
||||
if (count<=0) {close(fd);return 1;}
|
||||
used+=(size_t)count;
|
||||
if (used>RESPONSE_LIMIT) {close(fd);return 1;}
|
||||
char *end=memchr(response,'\n',used);
|
||||
if (end) {
|
||||
int result=write_all(STDOUT_FILENO,response,(size_t)(end-response)+1);
|
||||
close(fd);return result?1:0;
|
||||
}
|
||||
}
|
||||
}
|
||||
463
tundra/probes/beatles-display-session.c
Normal file
463
tundra/probes/beatles-display-session.c
Normal file
|
|
@ -0,0 +1,463 @@
|
|||
/* SPDX-License-Identifier: MPL-2.0 */
|
||||
#define _GNU_SOURCE
|
||||
#include <dirent.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <grp.h>
|
||||
#include <linux/fb.h>
|
||||
#include <linux/videodev2.h>
|
||||
#include <net/if.h>
|
||||
#include <sched.h>
|
||||
#include <signal.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/file.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/mount.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/prctl.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/sysmacros.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/wait.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#define BASE "/data/local/vulpes-beatles"
|
||||
#define ROOT BASE "/rootfs"
|
||||
#include "beatles-lid.h"
|
||||
static volatile sig_atomic_t interrupted;
|
||||
static int phone_controls;
|
||||
static void interrupt(int sig) { interrupted = sig; }
|
||||
static void inventory_camera_nodes(void) {
|
||||
static const int minors[] = {0, 1, 32, 33};
|
||||
for (unsigned i = 0; i < sizeof(minors)/sizeof(minors[0]); i++) {
|
||||
char path[32];
|
||||
snprintf(path, sizeof(path), "/dev/video%d", minors[i]);
|
||||
int fd = open(path, O_RDWR | O_NONBLOCK | O_CLOEXEC);
|
||||
if (fd < 0) { printf("Camera probe %s: open failed (%d)\n", path, errno); continue; }
|
||||
struct v4l2_capability caps = {0};
|
||||
if (ioctl(fd, VIDIOC_QUERYCAP, &caps)) {
|
||||
printf("Camera probe %s: VIDIOC_QUERYCAP failed (%d)\n", path, errno);
|
||||
close(fd); continue;
|
||||
}
|
||||
uint32_t device_caps = (caps.capabilities & V4L2_CAP_DEVICE_CAPS) ? caps.device_caps : caps.capabilities;
|
||||
printf("Camera probe %s: driver=%s card=%s caps=%08x\n", path, caps.driver, caps.card, device_caps);
|
||||
for (unsigned type_index = 0; type_index < 2; type_index++) {
|
||||
uint32_t type = type_index ? V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE : V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
||||
if (!(device_caps & (type_index ? V4L2_CAP_VIDEO_CAPTURE_MPLANE : V4L2_CAP_VIDEO_CAPTURE))) continue;
|
||||
for (uint32_t index = 0; index < 16; index++) {
|
||||
struct v4l2_fmtdesc format = {.index = index, .type = type};
|
||||
if (ioctl(fd, VIDIOC_ENUM_FMT, &format)) break;
|
||||
printf("Camera probe %s: capture format=%c%c%c%c flags=%08x\n", path,
|
||||
format.pixelformat & 0xff, (format.pixelformat >> 8) & 0xff,
|
||||
(format.pixelformat >> 16) & 0xff, (format.pixelformat >> 24) & 0xff, format.flags);
|
||||
}
|
||||
}
|
||||
close(fd);
|
||||
}
|
||||
}
|
||||
static int copy_camera_dump(const char *directory, const char *extension,
|
||||
const char *destination, const char *label) {
|
||||
DIR *camera_dir = opendir(directory);
|
||||
if (!camera_dir) return 0;
|
||||
struct dirent *entry;
|
||||
size_t extension_length = strlen(extension);
|
||||
int copied = 0;
|
||||
while ((entry = readdir(camera_dir))) {
|
||||
size_t name_length = strlen(entry->d_name);
|
||||
if (name_length <= extension_length ||
|
||||
strcmp(entry->d_name + name_length - extension_length, extension)) continue;
|
||||
char source[256];
|
||||
snprintf(source, sizeof(source), "%s/%s", directory, entry->d_name);
|
||||
int in = open(source, O_RDONLY | O_CLOEXEC);
|
||||
int out = open(destination, O_CREAT | O_TRUNC | O_WRONLY | O_CLOEXEC, 0644);
|
||||
if (in >= 0 && out >= 0) {
|
||||
char buffer[65536]; ssize_t bytes;
|
||||
while ((bytes = read(in, buffer, sizeof(buffer))) > 0) {
|
||||
ssize_t offset = 0;
|
||||
while (offset < bytes) {
|
||||
ssize_t written = write(out, buffer + offset, (size_t)(bytes - offset));
|
||||
if (written <= 0) break;
|
||||
offset += written;
|
||||
}
|
||||
if (offset != bytes) break;
|
||||
}
|
||||
struct stat copied_stat;
|
||||
if (!fstat(out, &copied_stat) && copied_stat.st_size > 0) {
|
||||
printf("QCamera captured %s %s (%ld bytes)\n", label, entry->d_name,
|
||||
(long)copied_stat.st_size);
|
||||
copied = 1;
|
||||
}
|
||||
fchmod(out, 0644); close(out);
|
||||
} else {
|
||||
if (out >= 0) close(out);
|
||||
if (in >= 0) close(in);
|
||||
}
|
||||
if (in >= 0) close(in);
|
||||
if (copied) break;
|
||||
}
|
||||
closedir(camera_dir);
|
||||
return copied;
|
||||
}
|
||||
static void require(int result, const char *what) {
|
||||
if (result < 0) { perror(what); _exit(1); }
|
||||
}
|
||||
static void node(const char *path, int major, int minor, mode_t mode) {
|
||||
require(mknod(path, S_IFCHR | mode, makedev(major, minor)), path);
|
||||
require(chmod(path, mode), "chmod device");
|
||||
}
|
||||
static int android_service(const char *operation) {
|
||||
pid_t child = fork();
|
||||
if (!child) { execl(operation, operation, "b2g", NULL); _exit(127); }
|
||||
int status;
|
||||
return child > 0 && waitpid(child, &status, 0) == child && WIFEXITED(status)
|
||||
? WEXITSTATUS(status) : -1;
|
||||
}
|
||||
/* nsIProcess may pass open descriptors from B2G. Keeping a RIL socket alive
|
||||
* after stopping B2G prevents the vendor daemon from accepting our reader. */
|
||||
static int close_inherited(int *sockets) {
|
||||
DIR *dir = opendir("/proc/self/fd");
|
||||
if (!dir) return -1;
|
||||
int count = 0;
|
||||
struct dirent *entry;
|
||||
while ((entry = readdir(dir))) {
|
||||
char *end, path[64], target[128];
|
||||
long fd = strtol(entry->d_name, &end, 10);
|
||||
if (*end || fd < 3 || fd == dirfd(dir)) continue;
|
||||
snprintf(path,sizeof(path),"/proc/self/fd/%ld",fd);
|
||||
ssize_t n = readlink(path,target,sizeof(target)-1);
|
||||
if (n > 0) {target[n] = 0; if (!strncmp(target,"socket:[",8)) (*sockets)++;}
|
||||
if (!close((int)fd)) count++;
|
||||
}
|
||||
closedir(dir);
|
||||
return count;
|
||||
}
|
||||
static void clean_runtime(void) {
|
||||
DIR *dir = opendir("/proc");
|
||||
if (!dir) return;
|
||||
struct dirent *entry;
|
||||
while ((entry = readdir(dir))) {
|
||||
char *end, path[128], target[512];
|
||||
long pid = strtol(entry->d_name, &end, 10);
|
||||
if (*end || pid <= 1 || pid == getpid()) continue;
|
||||
snprintf(path, sizeof(path), "/proc/%ld/root", pid);
|
||||
ssize_t n = readlink(path, target, sizeof(target) - 1);
|
||||
if (n > 0) {
|
||||
target[n] = 0;
|
||||
if (!strcmp(target, ROOT)) kill((pid_t)pid, SIGKILL);
|
||||
}
|
||||
}
|
||||
closedir(dir);
|
||||
}
|
||||
static void capture_framebuffer(int fd) {
|
||||
struct fb_var_screeninfo v;
|
||||
struct fb_fix_screeninfo f;
|
||||
if (ioctl(fd,FBIOGET_VSCREENINFO,&v) || ioctl(fd,FBIOGET_FSCREENINFO,&f)) return;
|
||||
if (v.xres != 240 || v.yres != 320 || v.bits_per_pixel != 16) return;
|
||||
size_t length=(size_t)(v.yoffset+v.yres)*f.line_length;
|
||||
if (length>f.smem_len || (v.xoffset+v.xres)*2>f.line_length) return;
|
||||
unsigned char *pixels=mmap(NULL,length,PROT_READ,MAP_SHARED,fd,0);
|
||||
if (pixels==MAP_FAILED) { perror("framebuffer capture mmap");return; }
|
||||
FILE *image=fopen(BASE "/framebuffer.ppm","wb");
|
||||
if (image) {
|
||||
fchmod(fileno(image),0644);fprintf(image,"P6\n%u %u\n255\n",v.xres,v.yres);
|
||||
unsigned char row[240*3];
|
||||
for (unsigned y=0;y<v.yres;y++) {
|
||||
for (unsigned x=0;x<v.xres;x++) {
|
||||
uint16_t pixel;
|
||||
memcpy(&pixel,pixels+(y+v.yoffset)*f.line_length+(x+v.xoffset)*2,2);
|
||||
const struct fb_bitfield channels[]={v.red,v.green,v.blue};
|
||||
for (unsigned c=0;c<3;c++) {
|
||||
unsigned mask=(1u<<channels[c].length)-1;
|
||||
row[x*3+c]=mask?((pixel>>channels[c].offset)&mask)*255/mask:0;
|
||||
}
|
||||
}
|
||||
fwrite(row,1,sizeof(row),image);
|
||||
}
|
||||
fclose(image);puts("Framebuffer captured after Gaia screenshot");
|
||||
}
|
||||
munmap(pixels,length);
|
||||
}
|
||||
/* Hardware brokers own the vendor sockets; Gecko never receives their mounts. */
|
||||
static void radio_inside(void) {
|
||||
require(setsid(), "radio setsid");
|
||||
require(unshare(CLONE_NEWNS), "radio mount namespace");
|
||||
require(mount(NULL,"/",NULL,MS_REC | MS_PRIVATE,NULL),"radio private mounts");
|
||||
require(mount("tmpfs",ROOT "/dev","tmpfs",MS_NOSUID,"size=1m,mode=0755"),"radio devices");
|
||||
node(ROOT "/dev/null",1,3,0666);node(ROOT "/dev/urandom",1,9,0666);
|
||||
require(mount("/sys",ROOT "/sys",NULL,MS_BIND,NULL),"radio sys");
|
||||
require(mount(NULL,ROOT "/sys",NULL,MS_BIND | MS_REMOUNT | MS_RDONLY,NULL),"radio readonly sys");
|
||||
if (mkdir(ROOT "/work/radio-sockets",0700) && errno != EEXIST) _exit(1);
|
||||
require(mount("/dev/socket",ROOT "/work/radio-sockets",NULL,MS_BIND,NULL),"radio socket mount");
|
||||
if(phone_controls) {
|
||||
/* Keep /sys read-only. Expose only these controls to the root broker,
|
||||
* never to the browser's separate mount namespace. */
|
||||
if(mkdir(ROOT "/work/hardware-controls",0700) && errno!=EEXIST)_exit(1);
|
||||
require(chmod(ROOT "/work/hardware-controls",0700),"hardware permissions");
|
||||
const char *sources[]={"/sys/class/leds/lcd-backlight/brightness",
|
||||
"/sys/class/leds/lcd-backlight/max_brightness","/sys/class/timed_output/vibrator/enable"};
|
||||
const char *targets[]={ROOT "/work/hardware-controls/backlight",
|
||||
ROOT "/work/hardware-controls/backlight-max",ROOT "/work/hardware-controls/vibrator"};
|
||||
for(unsigned i=0;i<3;i++) {
|
||||
int control=open(targets[i],O_CREAT|O_WRONLY|O_CLOEXEC,0600);
|
||||
require(control,"hardware control file");close(control);
|
||||
require(mount(sources[i],targets[i],NULL,MS_BIND,NULL),"hardware control mount");
|
||||
}
|
||||
if(mkdir(ROOT "/dev/socket",0700) && errno!=EEXIST)_exit(1);
|
||||
const char *property_sources[]={"/dev/__properties__","/dev/socket/property_service"};
|
||||
const char *property_targets[]={ROOT "/dev/__properties__",ROOT "/dev/socket/property_service"};
|
||||
for(unsigned i=0;i<2;i++) {
|
||||
int target=open(property_targets[i],O_CREAT|O_WRONLY|O_CLOEXEC,0600);
|
||||
require(target,"property endpoint");close(target);
|
||||
require(mount(property_sources[i],property_targets[i],NULL,MS_BIND,NULL),"property endpoint mount");
|
||||
}
|
||||
struct stat binder;
|
||||
require(stat("/dev/binder",&binder),"binder device");
|
||||
node(ROOT "/dev/binder",major(binder.st_rdev),minor(binder.st_rdev),0600);
|
||||
if(mkdir(ROOT "/system",0755) && errno!=EEXIST)_exit(1);
|
||||
require(mount("/system",ROOT "/system",NULL,MS_BIND,NULL),"radio system");
|
||||
require(mount(NULL,ROOT "/system",NULL,MS_BIND|MS_REMOUNT|MS_RDONLY,NULL),"radio readonly system");
|
||||
}
|
||||
require(chroot(ROOT),"radio chroot");require(chdir("/"),"radio chdir");
|
||||
clearenv();setenv("PATH","/usr/bin:/bin",1);setenv("HOME","/work",1);
|
||||
setenv("VULPES_RIL_SOCKET_DIR","/work/radio-sockets",1);
|
||||
setenv("LD_LIBRARY_PATH","/usr/lib/arm-linux-gnueabihf:/lib/arm-linux-gnueabihf",1);
|
||||
int log=open("/work/radio-status.log",O_CREAT | O_TRUNC | O_WRONLY,0644);
|
||||
require(log,"radio log");require(fchmod(log,0644),"radio log mode");dup2(log,1);dup2(log,2);close(log);
|
||||
execl("/usr/bin/python3.11","python3.11",phone_controls?"/work/radio_control.py":"/work/radio_status.py",NULL);
|
||||
_exit(127);
|
||||
}
|
||||
static void inside(int seconds) {
|
||||
require(setsid(), "worker setsid");
|
||||
require(unshare(CLONE_NEWNS | (phone_controls ? 0 : CLONE_NEWNET)), "unshare");
|
||||
require(mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL), "private mounts");
|
||||
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");
|
||||
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);
|
||||
node(ROOT "/dev/tty",5,0,0600); node(ROOT "/dev/tty0",4,0,0600);
|
||||
node(ROOT "/dev/tty1",4,1,0600); node(ROOT "/dev/fb0",29,0,0600);
|
||||
node(ROOT "/dev/fb1",29,1,0600);
|
||||
require(mkdir(ROOT "/dev/input",0755), "input directory");
|
||||
node(ROOT "/dev/input/event0",13,64,0600);
|
||||
node(ROOT "/dev/input/event2",13,66,0600);
|
||||
node(ROOT "/dev/input/event3",13,67,0600);
|
||||
require(mkdir(ROOT "/dev/shm",01777), "shm directory");
|
||||
require(mount("tmpfs", ROOT "/dev/shm", "tmpfs", MS_NOSUID | MS_NODEV,
|
||||
"size=64m,mode=1777"), "shared memory");
|
||||
int sock = socket(AF_INET,SOCK_DGRAM,0);
|
||||
require(sock,"loopback socket");
|
||||
struct ifreq request = {0}; strcpy(request.ifr_name,"lo");
|
||||
request.ifr_flags = IFF_UP | IFF_RUNNING;
|
||||
require(ioctl(sock,SIOCSIFFLAGS,&request),"loopback up"); close(sock);
|
||||
require(chroot(ROOT),"chroot"); require(chdir("/"),"chdir");
|
||||
clearenv(); setenv("PATH","/usr/bin:/bin",1); setenv("HOME","/work",1);
|
||||
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("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);
|
||||
perror("session script"); _exit(1);
|
||||
}
|
||||
int main(int argc, char **argv) {
|
||||
if (getuid() || argc != 2) return 2;
|
||||
phone_controls=!strcmp(argv[1],"--phone-1800s") || !strcmp(argv[1],"--phone-900s") || !strcmp(argv[1],"--phone-180s");
|
||||
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],"--interactive-900s") || !strcmp(argv[1],"--phone-900s")) ? 900 :
|
||||
(!strcmp(argv[1],"--interactive-1800s") || !strcmp(argv[1],"--phone-1800s")) ? 1800 : 0;
|
||||
if (!seconds) return 2;
|
||||
int inherited_sockets = 0;
|
||||
int inherited = close_inherited(&inherited_sockets);
|
||||
if (inherited < 0) return 6;
|
||||
int lock = open(BASE "/session.lock",O_CREAT | O_RDWR,0600);
|
||||
if (lock < 0 || flock(lock,LOCK_EX | LOCK_NB)) return 3;
|
||||
int fb = open("/dev/graphics/fb0",O_RDWR | O_CLOEXEC);
|
||||
struct fb_var_screeninfo original;
|
||||
if (fb < 0 || ioctl(fb,FBIOGET_VSCREENINFO,&original)) return 4;
|
||||
pid_t detached = fork();
|
||||
if (detached < 0) return 5;
|
||||
if (detached) return 0;
|
||||
require(setsid(),"watchdog setsid");
|
||||
const char *groups[] = {"/acct/cgroup.procs", "/dev/cpuctl/tasks"};
|
||||
char pidtext[32]; int length=snprintf(pidtext,sizeof(pidtext),"%d",getpid());
|
||||
for(unsigned i=0;i<sizeof(groups)/sizeof(groups[0]);i++) {
|
||||
int group=open(groups[i],O_WRONLY);
|
||||
require(group,"watchdog cgroup");
|
||||
require(write(group,pidtext,length),"detach watchdog cgroup"); close(group);
|
||||
}
|
||||
signal(SIGTERM,interrupt); signal(SIGINT,interrupt); signal(SIGHUP,SIG_IGN);
|
||||
int log = open(BASE "/display-session.log",O_CREAT | O_TRUNC | O_WRONLY,0644);
|
||||
require(log,"log"); fchmod(log,0644); dup2(log,1); dup2(log,2); close(log);
|
||||
setvbuf(stdout,NULL,_IONBF,0); setvbuf(stderr,NULL,_IONBF,0);
|
||||
printf("Closed inherited descriptors: %d, sockets: %d\n",inherited,inherited_sockets);
|
||||
inventory_camera_nodes();
|
||||
printf("Temporary display session: automatic KaiOS return in %d seconds\n",seconds);
|
||||
unlink(BASE "/stop-session");
|
||||
unlink(BASE "/framebuffer.ppm");
|
||||
unlink(ROOT "/work/gaia-native.png");
|
||||
unlink(ROOT "/work/gaia-audit.json");
|
||||
/* Camera output is also stored in the RAM-backed Gaia root. Never let a
|
||||
* previous run's image be mistaken for a frame from this session. */
|
||||
unlink(ROOT "/work/camera-frame.yuv");
|
||||
unlink(ROOT "/work/camera-photo.jpg");
|
||||
sleep(2);
|
||||
if (!access(BASE "/camera-probe-once", F_OK)) {
|
||||
unlink(BASE "/camera-probe-once");
|
||||
pid_t probe = fork();
|
||||
if (!probe) { execl("/data/local/tmp/vulpes-camera-stream", "vulpes-camera-stream", NULL); _exit(127); }
|
||||
if (probe > 0) {
|
||||
int probe_status = 0, finished = 0;
|
||||
for (int attempt = 0; attempt < 100; attempt++) {
|
||||
if (waitpid(probe, &probe_status, WNOHANG) == probe) { finished = 1; break; }
|
||||
usleep(100000);
|
||||
}
|
||||
if (!finished) { kill(probe, SIGKILL); waitpid(probe, &probe_status, 0); puts("camera probe timed out after 10 seconds"); }
|
||||
else printf("camera probe exit: %d\n", WIFEXITED(probe_status) ? WEXITSTATUS(probe_status) : -1);
|
||||
}
|
||||
}
|
||||
int result = android_service("/system/bin/stop");
|
||||
printf("stop b2g: %d\n",result);
|
||||
pid_t child = -1, radio = -1;
|
||||
struct lid_state lid;lid_init(&lid,fb);
|
||||
if (!result) {
|
||||
sleep(1);
|
||||
puts("Camera node inventory after stopping B2G");
|
||||
inventory_camera_nodes();
|
||||
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");
|
||||
unlink(BASE "/qcamera-preview-dump-once");
|
||||
const char *dump_path = "/data/misc/camera";
|
||||
if (mkdir("/data/misc", 0755) && errno != EEXIST) perror("camera misc directory");
|
||||
if (mkdir(dump_path, 0770) && errno != EEXIST) perror("camera dump directory");
|
||||
char snapshot_dir[256];
|
||||
snprintf(snapshot_dir, sizeof(snapshot_dir), "%s/test", dump_path);
|
||||
if (mount("tmpfs", dump_path, "tmpfs", MS_NOSUID, "size=24m,mode=0770,uid=1006,gid=1006")) perror("camera dump tmpfs");
|
||||
else {
|
||||
puts("QCamera snapshot output uses bounded volatile storage");
|
||||
if (mkdir(snapshot_dir, 0770) && errno != EEXIST) perror("camera snapshot directory");
|
||||
if (chown(snapshot_dir, 1006, 1006)) perror("camera snapshot ownership");
|
||||
}
|
||||
pid_t prop = fork();
|
||||
if (!prop) { execl("/system/bin/setprop", "setprop", "persist.camera.dumpimg", "1", NULL); _exit(127); }
|
||||
if (prop > 0) waitpid(prop, NULL, 0);
|
||||
int input[2];
|
||||
if (pipe(input) == 0) {
|
||||
pid_t probe = fork();
|
||||
if (!probe) {
|
||||
close(input[1]);
|
||||
dup2(input[0], STDIN_FILENO);
|
||||
close(input[0]);
|
||||
int output = open(BASE "/qcamera-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);
|
||||
const char *qcamera = preview_dump_probe ? BASE "/qcamera-preview-dump" : "/system/bin/mm-qcamera-app";
|
||||
execl(qcamera, qcamera, NULL);
|
||||
_exit(127);
|
||||
}
|
||||
close(input[0]);
|
||||
if (probe > 0) {
|
||||
const char *camera_input = preview_dump_probe ? "0\nA\n" : "0\na\nA\n";
|
||||
(void)write(input[1], camera_input, strlen(camera_input));
|
||||
/* Give the sensor's auto-exposure and white balance time to settle. */
|
||||
sleep(10);
|
||||
(void)write(input[1], "P\n", 2);
|
||||
sleep(12);
|
||||
(void)write(input[1], "B\nZ\n", 4);
|
||||
close(input[1]);
|
||||
int probe_status = 0, finished = 0;
|
||||
for (int attempt = 0; attempt < 250; attempt++) {
|
||||
if (waitpid(probe, &probe_status, WNOHANG) == probe) { finished = 1; break; }
|
||||
usleep(100000);
|
||||
}
|
||||
if (!finished) { kill(probe, SIGKILL); waitpid(probe, &probe_status, 0); puts("QCamera menu probe timed out"); }
|
||||
else printf("QCamera menu exit: %d\n", WIFEXITED(probe_status) ? WEXITSTATUS(probe_status) : -1);
|
||||
} else close(input[1]);
|
||||
} else perror("QCamera probe pipe");
|
||||
int frame_copied = copy_camera_dump(dump_path, ".yuv", ROOT "/work/camera-frame.yuv", "YUV frame");
|
||||
if (!frame_copied) frame_copied = copy_camera_dump(snapshot_dir, ".yuv", ROOT "/work/camera-frame.yuv", "snapshot YUV");
|
||||
int jpeg_copied = copy_camera_dump(snapshot_dir, ".jpg", ROOT "/work/camera-photo.jpg", "JPEG photo");
|
||||
if (!frame_copied && !jpeg_copied) puts("QCamera acquired preview buffers but did not export a frame or photo");
|
||||
prop = fork();
|
||||
if (!prop) { execl("/system/bin/setprop", "setprop", "persist.camera.dumpimg", "0", NULL); _exit(127); }
|
||||
if (prop > 0) waitpid(prop, NULL, 0);
|
||||
if (!umount(dump_path)) rmdir(dump_path);
|
||||
}
|
||||
if (seconds >= 180) {
|
||||
radio = fork();
|
||||
if (!radio) radio_inside();
|
||||
}
|
||||
child = fork();
|
||||
if (!child) inside(seconds);
|
||||
struct timespec begin, now;
|
||||
clock_gettime(CLOCK_MONOTONIC,&begin);
|
||||
int frames=0,captured=0;
|
||||
double sample_start=begin.tv_sec+begin.tv_nsec/1e9,pan_seconds=0;
|
||||
int sample_frames=0,pan_attempts=0;
|
||||
for (int tick=0; child > 0 && !interrupted; tick++) {
|
||||
clock_gettime(CLOCK_MONOTONIC,&now);
|
||||
if (now.tv_sec-begin.tv_sec >= seconds) break;
|
||||
int status;
|
||||
if (waitpid(child,&status,WNOHANG)==child) {
|
||||
printf("session ended: %d\n",status); break;
|
||||
}
|
||||
if (!access(BASE "/stop-session",F_OK)) break;
|
||||
if (!captured && !access(ROOT "/work/gaia-native.png",F_OK)) {
|
||||
capture_framebuffer(fb);captured=1;
|
||||
}
|
||||
if(seconds>=180)lid_update(&lid,now.tv_sec+now.tv_nsec/1e9);
|
||||
struct fb_var_screeninfo current;
|
||||
if (lid.opened != 0 && !ioctl(fb,FBIOGET_VSCREENINFO,¤t)) {
|
||||
struct timespec pan_start,pan_end;
|
||||
clock_gettime(CLOCK_MONOTONIC,&pan_start);
|
||||
int pan=ioctl(fb,FBIOPAN_DISPLAY,¤t);
|
||||
clock_gettime(CLOCK_MONOTONIC,&pan_end);
|
||||
pan_seconds+=pan_end.tv_sec-pan_start.tv_sec+(pan_end.tv_nsec-pan_start.tv_nsec)/1e9;
|
||||
pan_attempts++;
|
||||
if (pan && tick==20) perror("display refresh");
|
||||
if (!pan) {frames++;sample_frames++;}
|
||||
}
|
||||
double sample_now=now.tv_sec+now.tv_nsec/1e9;
|
||||
if(sample_now-sample_start>=10) {
|
||||
printf("Display sample: submissions/s=%.2f pan_ms=%.2f failed=%d lid=%d\n",
|
||||
sample_frames/(sample_now-sample_start),pan_attempts?1000*pan_seconds/pan_attempts:0,
|
||||
pan_attempts-sample_frames,lid.opened);
|
||||
sample_start=sample_now;pan_seconds=0;sample_frames=0;pan_attempts=0;
|
||||
}
|
||||
usleep(lid.opened==0?20000:5000);
|
||||
}
|
||||
clock_gettime(CLOCK_MONOTONIC,&now);
|
||||
printf("Framebuffer submissions: %d in %.2f seconds\n",frames,
|
||||
now.tv_sec-begin.tv_sec+(now.tv_nsec-begin.tv_nsec)/1e9);
|
||||
}
|
||||
if (child > 0) { kill(-child,SIGTERM); sleep(1); kill(-child,SIGKILL); }
|
||||
if (radio > 0) {
|
||||
kill(-radio,SIGTERM);
|
||||
// Allow both modem slots to restore before returning to KaiOS.
|
||||
for(int i=0;i<600;i++) {
|
||||
if(waitpid(radio,NULL,WNOHANG)==radio) { radio=-1;break; }
|
||||
usleep(100000);
|
||||
}
|
||||
if(radio>0) {kill(-radio,SIGKILL);waitpid(radio,NULL,0);}
|
||||
}
|
||||
clean_runtime();
|
||||
lid_restore(&lid);
|
||||
if (ioctl(fb,FBIOPUT_VSCREENINFO,&original)) perror("restore framebuffer mode");
|
||||
if (ioctl(fb,FBIOPAN_DISPLAY,&original)) perror("restore framebuffer pan");
|
||||
close(fb);
|
||||
unlink("/data/local/debugger-socket");
|
||||
printf("start b2g: %d\n",android_service("/system/bin/start"));
|
||||
int done=open(BASE "/display-session.done",O_CREAT | O_TRUNC | O_WRONLY,0644);
|
||||
if(done>=0){fchmod(done,0644);write(done,"restored\n",9);close(done);}
|
||||
return 0;
|
||||
}
|
||||
43
tundra/probes/beatles-hal-inventory.c
Normal file
43
tundra/probes/beatles-hal-inventory.c
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
/* SPDX-License-Identifier: MPL-2.0 */
|
||||
/* Read-only Android 6 ARM32 HAL enumeration in an isolated bionic process.
|
||||
* ABI: AOSP android-6.0.1_r1 hardware/libhardware/include/hardware/
|
||||
* hardware.h, sensors.h. No sensor activation or camera device open. */
|
||||
typedef unsigned int u32;
|
||||
typedef unsigned short u16;
|
||||
extern void *dlopen(const char *,int);
|
||||
extern void *dlsym(void *,const char *);
|
||||
extern int printf(const char *,...);
|
||||
extern int putchar(int);
|
||||
extern int fflush(void *);
|
||||
extern unsigned alarm(unsigned);
|
||||
extern void _exit(int) __attribute__((noreturn));
|
||||
struct module {u32 tag;u16 version,hal;const char *id,*name,*author;void *methods,*dso;u32 reserved[25];};
|
||||
struct sensor {const char *name,*vendor;int version,handle,type;float range,resolution,power;int min_delay;u32 fifo_reserved,fifo_max;const char *string_type,*permission;int max_delay;u32 flags;void *reserved[2];};
|
||||
struct sensors {struct module common;int (*list)(struct sensors *,const struct sensor **);};
|
||||
_Static_assert(sizeof(struct module)==128,"Android 6 ARM32 module ABI");
|
||||
_Static_assert(sizeof(struct sensor)==68,"Android 6 ARM32 sensor ABI");
|
||||
static void string(const char *value) {
|
||||
putchar('"');
|
||||
if(value)for(unsigned i=0;value[i] && i<160;i++) {
|
||||
unsigned char c=value[i];
|
||||
if(c=='"'||c=='\\')putchar('\\');
|
||||
if(c<32)printf("\\u%04x",c);else putchar(c);
|
||||
}
|
||||
putchar('"');
|
||||
}
|
||||
__attribute__((constructor)) static void inventory(void) {
|
||||
alarm(8);
|
||||
void *library=dlopen("/system/lib/hw/sensors.msm8909.so",2);
|
||||
struct sensors *module=library?dlsym(library,"HMI"):0;
|
||||
if(!module || !module->list){printf("{\"error\":\"SENSOR_HAL_UNAVAILABLE\"}\n");fflush(0);_exit(1);}
|
||||
const struct sensor *list=0;
|
||||
int count=module->list(module,&list);
|
||||
if(count<0 || count>128 || (count && !list)){printf("{\"error\":\"INVALID_SENSOR_LIST\"}\n");fflush(0);_exit(2);}
|
||||
printf("{\"moduleVersion\":%u,\"count\":%d,\"sensors\":[",module->common.version,count);
|
||||
for(int i=0;i<count;i++) {
|
||||
if(i)putchar(',');
|
||||
printf("{\"type\":%d,\"name\":",list[i].type);string(list[i].name);
|
||||
printf(",\"vendor\":");string(list[i].vendor);putchar('}');
|
||||
}
|
||||
printf("]}\n");fflush(0);_exit(0);
|
||||
}
|
||||
9
tundra/probes/beatles-hall.h
Normal file
9
tundra/probes/beatles-hall.h
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
/* SPDX-License-Identifier: MPL-2.0 */
|
||||
#ifndef VULPES_BEATLES_HALL_H
|
||||
#define VULPES_BEATLES_HALL_H
|
||||
/* Nokia hall_status reports the magnet: 1 = closed, 0 = open.
|
||||
* Keep this conversion shared by panel power and keypad handling. */
|
||||
static inline int beatles_hall_open(int raw) {
|
||||
return raw == 0 ? 1 : raw == 1 ? 0 : -1;
|
||||
}
|
||||
#endif
|
||||
185
tundra/probes/beatles-keypad.c
Normal file
185
tundra/probes/beatles-keypad.c
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
/* SPDX-License-Identifier: MPL-2.0 */
|
||||
#include <X11/Xlib.h>
|
||||
#include <X11/keysym.h>
|
||||
#include <X11/XF86keysym.h>
|
||||
#include <X11/extensions/XTest.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <linux/input.h>
|
||||
#include <poll.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <unistd.h>
|
||||
#include "beatles-hall.h"
|
||||
|
||||
static int allow_key(unsigned code, int value, int hall) {
|
||||
return !value || beatles_hall_open(hall)!=0 ||
|
||||
code==KEY_VOLUMEUP || code==KEY_VOLUMEDOWN;
|
||||
}
|
||||
|
||||
static KeySym symbol(unsigned code) {
|
||||
if(code>=KEY_1 && code<=KEY_9)return XK_1+code-KEY_1;
|
||||
switch(code) {
|
||||
case KEY_0:return XK_0;
|
||||
case KEY_VOLUMEUP:return XF86XK_AudioRaiseVolume;
|
||||
case KEY_VOLUMEDOWN:return XF86XK_AudioLowerVolume;
|
||||
case KEY_UP:return XK_Up; case KEY_DOWN:return XK_Down;
|
||||
case KEY_LEFT:return XK_Left; case KEY_RIGHT:return XK_Right;
|
||||
case KEY_OK:case KEY_ENTER:return XK_Return;
|
||||
case KEY_BACK:return XK_Escape; case KEY_BACKSPACE:return XK_BackSpace;
|
||||
case KEY_MENU:return XK_Menu; case KEY_SEND:return XK_F1;
|
||||
case KEY_POWER:return XK_F4; /* qpnp_pon.kl: ENDCALL */
|
||||
case KEY_CAMERA:return XK_F2; /* matrix_keypad.kl: NOTIFICATION */
|
||||
case KEY_NUMERIC_STAR:return XK_8;
|
||||
case KEY_NUMERIC_POUND:return XK_3;
|
||||
default:return NoSymbol;
|
||||
}
|
||||
}
|
||||
/* Exercise the evdev reader as well as X11. These diagnostic commands are
|
||||
* used only while our reader owns the keypad; they do not bypass its mapping. */
|
||||
static unsigned native_code(const char *key) {
|
||||
if(!strcmp(key,"ArrowRight"))return KEY_RIGHT;
|
||||
if(!strcmp(key,"ArrowLeft"))return KEY_LEFT;
|
||||
if(!strcmp(key,"ArrowUp"))return KEY_UP;
|
||||
if(!strcmp(key,"ArrowDown"))return KEY_DOWN;
|
||||
if(!strcmp(key,"Enter"))return KEY_OK;
|
||||
if(!strcmp(key,"Escape"))return KEY_BACK;
|
||||
if(!strcmp(key,"EndCall"))return KEY_POWER;
|
||||
if(!strcmp(key,"Notifications"))return KEY_CAMERA;
|
||||
if(!strcmp(key,"AudioVolumeUp"))return KEY_VOLUMEUP;
|
||||
if(!strcmp(key,"AudioVolumeDown"))return KEY_VOLUMEDOWN;
|
||||
if(!strcmp(key,"Backspace"))return KEY_BACKSPACE;
|
||||
if(!key[0] || key[1])return 0;
|
||||
if(key[0]>='1' && key[0]<='9')return KEY_1+key[0]-'1';
|
||||
if(key[0]=='0')return KEY_0;
|
||||
if(key[0]=='*')return KEY_NUMERIC_STAR;
|
||||
if(key[0]=='#')return KEY_NUMERIC_POUND;
|
||||
return 0;
|
||||
}
|
||||
static int inject(int fd,unsigned code) {
|
||||
struct input_event events[4]={
|
||||
{.type=EV_KEY,.code=code,.value=1}, {.type=EV_SYN,.code=SYN_REPORT},
|
||||
{.type=EV_KEY,.code=code,.value=0}, {.type=EV_SYN,.code=SYN_REPORT},
|
||||
};
|
||||
ssize_t written=write(fd,events,sizeof(events));
|
||||
if(written!=(ssize_t)sizeof(events)){perror("keypad inject");return 1;}
|
||||
return 0;
|
||||
}
|
||||
static int inject_keys(const char *keys,int sequence) {
|
||||
size_t length=strlen(keys);
|
||||
if((sequence && (!length || length>40 || strspn(keys,"0123456789*#")!=length)) ||
|
||||
(!sequence && !native_code(keys)))return 2;
|
||||
unsigned code=sequence?0:native_code(keys);
|
||||
const char *path=code==KEY_VOLUMEUP ? "/dev/input/event3" :
|
||||
(code==KEY_POWER || code==KEY_VOLUMEDOWN) ? "/dev/input/event2" : "/dev/input/event0";
|
||||
int fd=open(path,O_WRONLY|O_CLOEXEC);
|
||||
if(fd<0){perror("keypad injection open");return 2;}
|
||||
if(!ioctl(fd,EVIOCGRAB,1)) {
|
||||
ioctl(fd,EVIOCGRAB,0);close(fd);
|
||||
fputs("Keypad reader must own the device before injection\n",stderr);return 3;
|
||||
}
|
||||
if(errno!=EBUSY){perror("keypad ownership");close(fd);return 3;}
|
||||
int result=0;
|
||||
for(size_t i=0;i<(sequence?length:1);i++) {
|
||||
char single[2]={keys[i],0};
|
||||
if(inject(fd,native_code(sequence?single:keys))){result=1;break;}
|
||||
usleep(130000);
|
||||
}
|
||||
close(fd);return result;
|
||||
}
|
||||
int main(int argc,char **argv) {
|
||||
if(argc==3 && !strcmp(argv[1],"--inject-key"))return inject_keys(argv[2],0);
|
||||
if(argc==3 && !strcmp(argv[1],"--inject-keys"))return inject_keys(argv[2],1);
|
||||
Display *display=NULL;
|
||||
for(unsigned attempt=0;attempt<100 && !display;attempt++) {
|
||||
display=XOpenDisplay(NULL);
|
||||
if(!display)usleep(100000);
|
||||
}
|
||||
if(!display){fputs("Keypad: X server not ready after 10 seconds\n",stderr);return 1;}
|
||||
if(argc==3 && !strcmp(argv[1],"--type-keys")) {
|
||||
size_t length=strlen(argv[2]);
|
||||
if(!length || length>40 || strspn(argv[2],"0123456789*#")!=length)return 2;
|
||||
for(size_t i=0;i<length;i++) {
|
||||
char key=argv[2][i];int shifted=key=='*'||key=='#';
|
||||
KeyCode code=XKeysymToKeycode(display,key=='*'?XK_8:key=='#'?XK_3:XK_0+key-'0');
|
||||
if(shifted)XTestFakeKeyEvent(display,XKeysymToKeycode(display,XK_Shift_L),True,CurrentTime);
|
||||
XTestFakeKeyEvent(display,code,True,CurrentTime);
|
||||
XTestFakeKeyEvent(display,code,False,CurrentTime);
|
||||
if(shifted)XTestFakeKeyEvent(display,XKeysymToKeycode(display,XK_Shift_L),False,CurrentTime);
|
||||
XFlush(display);usleep(130000);
|
||||
}
|
||||
XSync(display,False);XCloseDisplay(display);return 0;
|
||||
}
|
||||
if(argc==3 && !strcmp(argv[1],"--send-key")) {
|
||||
KeySym key=!strcmp(argv[2],"ArrowRight")?XK_Right:
|
||||
!strcmp(argv[2],"ArrowLeft")?XK_Left:!strcmp(argv[2],"ArrowUp")?XK_Up:
|
||||
!strcmp(argv[2],"ArrowDown")?XK_Down:!strcmp(argv[2],"Backspace")?XK_BackSpace:
|
||||
!strcmp(argv[2],"Enter")?XK_Return:!strcmp(argv[2],"Escape")?XK_Escape:
|
||||
!strcmp(argv[2],"2")?XK_2:NoSymbol;
|
||||
if(key==NoSymbol){XCloseDisplay(display);return 2;}
|
||||
KeyCode code=XKeysymToKeycode(display,key);
|
||||
XTestFakeKeyEvent(display,code,True,CurrentTime);
|
||||
XTestFakeKeyEvent(display,code,False,CurrentTime);
|
||||
XSync(display,False);XCloseDisplay(display);return 0;
|
||||
}
|
||||
if(argc==2 && !strcmp(argv[1],"--self-test")) {
|
||||
unsigned keys[]={KEY_UP,KEY_OK,KEY_BACK};
|
||||
for(unsigned i=0;i<sizeof(keys)/sizeof(keys[0]);i++) {
|
||||
KeyCode code=XKeysymToKeycode(display,symbol(keys[i]));
|
||||
XTestFakeKeyEvent(display,code,True,CurrentTime);
|
||||
XTestFakeKeyEvent(display,code,False,CurrentTime);
|
||||
}
|
||||
XSync(display,False);XCloseDisplay(display);return 0;
|
||||
}
|
||||
int fd=open("/dev/input/event0",O_RDONLY|O_NONBLOCK);
|
||||
if(fd<0){perror("keypad open");return 2;}
|
||||
if(ioctl(fd,EVIOCGRAB,1)){perror("keypad grab");return 3;}
|
||||
int power=open("/dev/input/event2",O_RDONLY|O_NONBLOCK);
|
||||
char name[80]={0};
|
||||
if(power<0 || ioctl(power,EVIOCGNAME(sizeof(name)),name)<0 || strcmp(name,"qpnp_pon") || ioctl(power,EVIOCGRAB,1)) {
|
||||
fprintf(stderr,"Nokia end-call input unavailable: %s (name=%s)\n",strerror(errno),name);
|
||||
if(power>=0)close(power);
|
||||
ioctl(fd,EVIOCGRAB,0);close(fd);XCloseDisplay(display);return 3;
|
||||
}
|
||||
int side=open("/dev/input/event3",O_RDONLY|O_NONBLOCK);
|
||||
memset(name,0,sizeof(name));
|
||||
if(side<0 || ioctl(side,EVIOCGNAME(sizeof(name)),name)<0 || strcmp(name,"gpio-keys") || ioctl(side,EVIOCGRAB,1)) {
|
||||
fprintf(stderr,"Nokia volume input unavailable: %s (name=%s)\n",strerror(errno),name);
|
||||
if(side>=0)close(side);
|
||||
ioctl(power,EVIOCGRAB,0);close(power);ioctl(fd,EVIOCGRAB,0);close(fd);XCloseDisplay(display);return 3;
|
||||
}
|
||||
puts("Nokia keypad bridge ready");fflush(stdout);
|
||||
struct pollfd pollers[3]={{.fd=fd,.events=POLLIN},{.fd=power,.events=POLLIN},{.fd=side,.events=POLLIN}};
|
||||
for(;;) {
|
||||
if(poll(pollers,3,1000)<0)break;
|
||||
for(unsigned source=0;source<3;source++) {
|
||||
if(!(pollers[source].revents&POLLIN))continue;
|
||||
struct input_event event;
|
||||
while(read(pollers[source].fd,&event,sizeof(event))==sizeof(event)) {
|
||||
if(source==1 && event.code!=KEY_POWER && event.code!=KEY_VOLUMEDOWN)continue;
|
||||
if(source==2 && event.code!=KEY_VOLUMEUP && event.code!=KEY_VOLUMEDOWN)continue;
|
||||
/* Long holds must not hang up and then immediately dismiss the screen. */
|
||||
if((event.code==KEY_POWER || event.code==KEY_VOLUMEUP || event.code==KEY_VOLUMEDOWN) && event.value==2)continue;
|
||||
if(event.type!=EV_KEY)continue;
|
||||
/* Always release keys, but ignore presses from the closed inner keypad. */
|
||||
if(event.value) {
|
||||
FILE *hall=fopen("/sys/devices/soc.0/hall_sensor1.73/hall_status","r");
|
||||
int opened=-1;
|
||||
if(hall){if(fscanf(hall,"%d",&opened)!=1)opened=-1;fclose(hall);}
|
||||
if(!allow_key(event.code,event.value,opened))continue;
|
||||
}
|
||||
KeySym key=symbol(event.code);
|
||||
if(key==NoSymbol)continue;
|
||||
int shifted=event.code==KEY_NUMERIC_STAR||event.code==KEY_NUMERIC_POUND;
|
||||
if(shifted && event.value)XTestFakeKeyEvent(display,XKeysymToKeycode(display,XK_Shift_L),True,CurrentTime);
|
||||
XTestFakeKeyEvent(display,XKeysymToKeycode(display,key),event.value!=0,CurrentTime);
|
||||
if(shifted && !event.value)XTestFakeKeyEvent(display,XKeysymToKeycode(display,XK_Shift_L),False,CurrentTime);
|
||||
XFlush(display);
|
||||
}
|
||||
}
|
||||
}
|
||||
ioctl(side,EVIOCGRAB,0);close(side);
|
||||
ioctl(power,EVIOCGRAB,0);close(power);
|
||||
ioctl(fd,EVIOCGRAB,0);close(fd);XCloseDisplay(display);return 0;
|
||||
}
|
||||
90
tundra/probes/beatles-lid.h
Normal file
90
tundra/probes/beatles-lid.h
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
/* SPDX-License-Identifier: MPL-2.0 */
|
||||
/* Panel policy belongs to the Nokia adapter, outside the browser sandbox. */
|
||||
#include "beatles-hall.h"
|
||||
struct lid_state {
|
||||
int inner, outer, opened, outer_on, ready, candidate;
|
||||
int keys_brightness;
|
||||
double last_poll, closed_at, candidate_at;
|
||||
};
|
||||
#ifndef BEATLES_KEYS_BRIGHTNESS
|
||||
#define BEATLES_KEYS_BRIGHTNESS "/sys/class/leds/button-backlight/brightness"
|
||||
#endif
|
||||
static int lid_keys_read(void) {
|
||||
FILE *f=fopen(BEATLES_KEYS_BRIGHTNESS,"r");
|
||||
if(!f)return -1;
|
||||
int value=-1;if(fscanf(f,"%d",&value)!=1)value=-1;
|
||||
fclose(f);return value;
|
||||
}
|
||||
static void lid_keys_write(int value) {
|
||||
if(value<0)return;
|
||||
FILE *f=fopen(BEATLES_KEYS_BRIGHTNESS,"w");
|
||||
if(!f){perror("keypad brightness");return;}
|
||||
if(fprintf(f,"%d\n",value)<0)perror("keypad brightness write");
|
||||
if(fclose(f))perror("keypad brightness close");
|
||||
}
|
||||
static int lid_read_open(void) {
|
||||
FILE *f=fopen("/sys/devices/soc.0/hall_sensor1.73/hall_status","r");
|
||||
if(!f)return -1;
|
||||
int value=-1;
|
||||
if(fscanf(f,"%d",&value)!=1)value=-1;
|
||||
fclose(f);return beatles_hall_open(value);
|
||||
}
|
||||
static void lid_publish(const struct lid_state *lid) {
|
||||
FILE *f=fopen(ROOT "/work/lid-state.tmp","w");
|
||||
if(!f)return;
|
||||
fchmod(fileno(f),0644);
|
||||
fprintf(f,"{\"opened\":%s,\"externalOn\":%s}\n",lid->opened?"true":"false",lid->outer_on?"true":"false");
|
||||
fclose(f);rename(ROOT "/work/lid-state.tmp",ROOT "/work/lid-state.json");
|
||||
}
|
||||
static void lid_init(struct lid_state *lid,int inner) {
|
||||
memset(lid,0,sizeof(*lid));lid->inner=inner;lid->opened=-1;lid->candidate=-1;
|
||||
lid->keys_brightness=lid_keys_read();
|
||||
lid->outer=open("/dev/graphics/fb1",O_RDWR|O_CLOEXEC);
|
||||
unlink(ROOT "/work/cover-ready");unlink(ROOT "/work/lid-state.json");
|
||||
}
|
||||
static void lid_apply(struct lid_state *lid,int opened,int ready,double now) {
|
||||
if(opened!=0 && opened!=1)return;
|
||||
/* Xorg can wake fb0 during initialization after our first closed-lid poll.
|
||||
* cover-ready is published after Xorg/keypad startup: reapply the panel
|
||||
* selection then, before presenting the first external frame. */
|
||||
int changed=opened!=lid->opened || (ready && !lid->ready && !opened);
|
||||
if(changed) {
|
||||
/* Finish blanking the cover before waking the inner display. */
|
||||
if(opened && lid->outer>=0) {
|
||||
if(ioctl(lid->outer,FBIOBLANK,FB_BLANK_POWERDOWN)){perror("lid outer blank");return;}
|
||||
lid->outer_on=0;
|
||||
}
|
||||
if(ioctl(lid->inner,FBIOBLANK,opened?FB_BLANK_UNBLANK:FB_BLANK_POWERDOWN)) {
|
||||
perror("lid inner blank");return;
|
||||
}
|
||||
lid_keys_write(opened?lid->keys_brightness:0);
|
||||
lid->opened=opened;lid->closed_at=opened?0:now;
|
||||
printf("Clamshell: %s; internal screen %s\n",opened?"open":"closed",opened?"on":"off");
|
||||
}
|
||||
if(ready && !lid->ready && !opened)lid->closed_at=now;
|
||||
lid->ready=ready;
|
||||
int outer=lid->outer>=0 && !opened && now-lid->closed_at<15 && ready;
|
||||
int outer_changed=outer!=lid->outer_on;
|
||||
if(outer_changed || (changed && !opened)) {
|
||||
if(lid->outer>=0 && ioctl(lid->outer,FBIOBLANK,outer?FB_BLANK_UNBLANK:FB_BLANK_POWERDOWN))perror("lid outer blank");
|
||||
else lid->outer_on=outer;
|
||||
printf("External screen: %s\n",lid->outer_on?"on":"off");
|
||||
}
|
||||
if(changed || outer_changed)lid_publish(lid);
|
||||
}
|
||||
static void lid_update(struct lid_state *lid,double now) {
|
||||
if(now-lid->last_poll<.1)return;
|
||||
lid->last_poll=now;
|
||||
int opened=lid_read_open();
|
||||
if(opened<0)return;
|
||||
if(opened!=lid->candidate) {
|
||||
lid->candidate=opened;lid->candidate_at=now;return;
|
||||
}
|
||||
if(now-lid->candidate_at<.15)return;
|
||||
lid_apply(lid,opened,!access(ROOT "/work/cover-ready",F_OK),now);
|
||||
}
|
||||
static void lid_restore(struct lid_state *lid) {
|
||||
if(lid->outer>=0) {ioctl(lid->outer,FBIOBLANK,FB_BLANK_POWERDOWN);close(lid->outer);}
|
||||
if(lid->inner>=0 && ioctl(lid->inner,FBIOBLANK,FB_BLANK_UNBLANK))perror("restore internal panel");
|
||||
lid_keys_write(lid->keys_brightness);
|
||||
}
|
||||
25
tundra/probes/beatles-panel-info.c
Normal file
25
tundra/probes/beatles-panel-info.c
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
/* SPDX-License-Identifier: MPL-2.0 */
|
||||
#include <fcntl.h>
|
||||
#include <linux/fb.h>
|
||||
#include <linux/input.h>
|
||||
#include <stdio.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
int main(void) {
|
||||
FILE *out=fopen("/data/local/tmp/vulpes-panel-info.json","w");
|
||||
if(!out)return 1;
|
||||
fchmod(fileno(out),0644);
|
||||
fprintf(out,"{\"panels\":[");
|
||||
for(int i=0;i<2;i++) {
|
||||
char path[64];snprintf(path,sizeof(path),"/dev/graphics/fb%d",i);
|
||||
int fd=open(path,O_RDONLY);struct fb_var_screeninfo v;struct fb_fix_screeninfo f;
|
||||
if(fd<0 || ioctl(fd,FBIOGET_VSCREENINFO,&v) || ioctl(fd,FBIOGET_FSCREENINFO,&f))return 2;
|
||||
fprintf(out,"%s{\"width\":%u,\"height\":%u,\"virtualHeight\":%u,\"bpp\":%u,\"stride\":%u,\"bytes\":%u}",i?",":"",v.xres,v.yres,v.yres_virtual,v.bits_per_pixel,f.line_length,f.smem_len);
|
||||
close(fd);
|
||||
}
|
||||
unsigned char keys[(KEY_MAX+8)/8]={0};int fd=open("/dev/input/event1",O_RDONLY);
|
||||
if(fd<0 || ioctl(fd,EVIOCGKEY(sizeof(keys)),keys)<0)return 3;
|
||||
fprintf(out,"],\"hallKey249\":%s}\n",(keys[249/8]&(1<<(249%8)))?"true":"false");
|
||||
close(fd);fclose(out);return 0;
|
||||
}
|
||||
55
tundra/probes/beatles-x11-test.c
Normal file
55
tundra/probes/beatles-x11-test.c
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
/* SPDX-License-Identifier: MPL-2.0 */
|
||||
#include <X11/Xlib.h>
|
||||
#include <X11/Xutil.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
int main(void) {
|
||||
Display *display=XOpenDisplay(NULL);
|
||||
if(!display){fputs("Cannot connect to Xorg\n",stderr);return 1;}
|
||||
int screen=DefaultScreen(display);
|
||||
Window window=XCreateSimpleWindow(display,RootWindow(display,screen),0,0,240,320,
|
||||
0,0,0x0010);
|
||||
XSetWindowAttributes attributes={.override_redirect=True};
|
||||
XChangeWindowAttributes(display,window,CWOverrideRedirect,&attributes);
|
||||
XSelectInput(display,window,ExposureMask|KeyPressMask|KeyReleaseMask);
|
||||
XMapRaised(display,window); XSetInputFocus(display,window,RevertToParent,CurrentTime);
|
||||
GC gc=XCreateGC(display,window,0,NULL);
|
||||
char keytext[128]="Press a physical key";
|
||||
time_t start=time(NULL);
|
||||
int count=0;
|
||||
while(time(NULL)-start<25) {
|
||||
while(XPending(display)) {
|
||||
XEvent event; XNextEvent(display,&event);
|
||||
if(event.type==KeyPress||event.type==KeyRelease) {
|
||||
KeySym key=XLookupKeysym(&event.xkey,0);
|
||||
snprintf(keytext,sizeof(keytext),"%s: %s",event.type==KeyPress?"Down":"Up",
|
||||
XKeysymToString(key)?XKeysymToString(key):"unknown");
|
||||
printf("keycode=%u keysym=%lu %s\n",event.xkey.keycode,key,keytext);fflush(stdout);
|
||||
}
|
||||
}
|
||||
XSetForeground(display,gc,0x0010);XFillRectangle(display,window,gc,0,0,240,320);
|
||||
XSetForeground(display,gc,0xffff);
|
||||
const char *lines[]={"VULPES DISPLAY TEST","Linux / Xorg / ARMv7","240 x 320 / physical keypad",keytext,"KaiOS returns automatically"};
|
||||
for(unsigned i=0;i<sizeof(lines)/sizeof(lines[0]);i++)
|
||||
XDrawString(display,window,gc,10,35+i*35,lines[i],strlen(lines[i]));
|
||||
XSetForeground(display,gc,0x07ff); XFillRectangle(display,window,gc,10+(count++%160),235,50,25);
|
||||
XFlush(display);usleep(100000);
|
||||
}
|
||||
XImage *image=XGetImage(display,window,0,0,240,320,AllPlanes,ZPixmap);
|
||||
if(image){FILE *file=fopen("/work/x11-test.ppm","wb");if(file){
|
||||
fprintf(file,"P6\n240 320\n255\n");
|
||||
for(int y=0;y<320;y++) {
|
||||
for(int x=0;x<240;x++) {
|
||||
unsigned long pixel=XGetPixel(image,x,y);
|
||||
unsigned char rgb[]={((pixel>>11)&31)*255/31,((pixel>>5)&63)*255/63,(pixel&31)*255/31};
|
||||
fwrite(rgb,1,3,file);
|
||||
}
|
||||
}
|
||||
fclose(file);
|
||||
}
|
||||
XDestroyImage(image);}
|
||||
XCloseDisplay(display);return 0;
|
||||
}
|
||||
Loading…
Reference in a new issue