fix(watchdog): 框架App前台时音量/媒体键让位SDK (agent-231) — 启动时经/proc/pid/exe反查app.json framework=1判定,音量/媒体分支 !g_fw_app 门控; 裸App/无SDK/不可读仍兜底(不造成功能真空); 顺手修wd_json_str两边界bug+死代码清理

meow_watchdog→68850d9d; 宿主14项全过; 待跟进: fw_content.conf的watchdog md5打包时更新
This commit is contained in:
MeowOS committed 2026-08-31 19:00:53 +08:00
1 parent 40b8e33b94
commit d76f8bc4ba
4 files changed
+207 -30

No files matched your search

Binary file not shown.
+106 -30
View File
@@ -22,6 +22,10 @@
* 音量±(115/114) -> 写真实 DAC(CS43198 amixer numid=1/2)+ 持久化 settings.conf(同 launcher 同源)。 * 音量±(115/114) -> 写真实 DAC(CS43198 amixer numid=1/2)+ 持久化 settings.conf(同 launcher 同源)。
* 播放/上下曲(164/165/163/19) -> 捕获 + 日志 + 消费,不路由(媒体会话落地后转活跃会话)。 * 播放/上下曲(164/165/163/19) -> 捕获 + 日志 + 消费,不路由(媒体会话落地后转活跃会话)。
* (8s 强关的「防误触 / pm_interacted」逻辑在 launcher 的 show_power_menu 内,此进程只做兜底。) * (8s 强关的「防误触 / pm_interacted」逻辑在 launcher 的 show_power_menu 内,此进程只做兜底。)
* - M2.86(226) SDK 框架 App 让位:启动时经 /proc/<pid>/exe 读同目录 app.json 的 framework
* (true|1|yes)。判定为 SDK 框架 App -> 音量/媒体键让位 SDK 输入层(meow_ui_input.c 已消化,
* 防同一次按键双步进);电源仍由本进程兜底(SDK 对电源是 no-op)。裸自绘 App(framework=0/
* 无 framework)或 app.json 不可读 -> 音量/媒体键仍由本进程兜底(不造成功能真空)。
* Exits when the app process disappears (kill(pid,0) -> ESRCH) or on * Exits when the app process disappears (kill(pid,0) -> ESRCH) or on
* SIGTERM from the parent. * SIGTERM from the parent.
* *
@@ -116,27 +120,6 @@
static unsigned char *fb; static unsigned char *fb;
static int xres, yres, line_len, bpp; static int xres, yres, line_len, bpp;
static inline void put_px(int x, int y, uint32_t color)
{
if (x < 0 || x >= xres || y < 0 || y >= yres) return;
unsigned char *p = fb + y * line_len + x * (bpp / 8);
if (bpp == 16) {
uint16_t c = ((color >> 8) & 0xF800) | ((color >> 5) & 0x07E0) |
((color >> 3) & 0x001F);
*(uint16_t *)p = c;
} else {
*(uint32_t *)p = (color << 8) | 0xFF;
}
}
/* 5x7 "<" arrow + border */
static void fill_rect(int x0, int y0, int w, int h, uint32_t color)
{
for (int y = y0; y < y0 + h; y++)
for (int x = x0; x < x0 + w; x++)
put_px(x, y, color);
}
static int read_state_file(void) static int read_state_file(void)
{ {
/* returns 1 if immersive */ /* returns 1 if immersive */
@@ -194,6 +177,10 @@ static int down_valid = 0; /* a press is currently being tracked */
static int down_x = 0, down_y = 0; static int down_x = 0, down_y = 0;
static unsigned long long down_ms = 0; static unsigned long long down_ms = 0;
/* M2.86(226):1=当前前台 App 为 SDK 框架 App(音量/媒体键让位 SDK;M2.87 起左缘返回
* 手势 SIGUSR1 也照样让位 SDK——框架 App 经 SDK on_swipe 处理返回)。在 handle_touch 前声明。 */
static int g_fw_app = 0;
/* ---- M2.22(A)+ M2.24 电源键接管:息屏/亮屏 + 长按菜单/强关 ---- */ /* ---- M2.22(A)+ M2.24 电源键接管:息屏/亮屏 + 长按菜单/强关 ---- */
static int wd_fbfd = -1; /* fb0 fd(main 里打开,供 FBIOBLANK/FBIOPAN 用) */ static int wd_fbfd = -1; /* fb0 fd(main 里打开,供 FBIOBLANK/FBIOPAN 用) */
static int wd_saved_bri = 60; /* 熄屏前亮度快照 */ static int wd_saved_bri = 60; /* 熄屏前亮度快照 */
@@ -427,15 +414,96 @@ static void handle_touch(int immersive, int app_pid,
down_valid = 0; down_valid = 0;
if (now_ms() - down_ms > SHORT_PRESS_MS) return; /* held too long */ if (now_ms() - down_ms > SHORT_PRESS_MS) return; /* held too long */
/* M2.21:返回键彻底移除。watchdog 不再有「按钮命中->SIGTERM」路径(那是 /* M2.21:返回键彻底移除。watchdog 不再有「按钮命中->SIGTERM」路径(那是
* 虚拟返回键的年代)。返回手势统一为「左缘起点 + 向右滑 SWIPE_DX」→ SIGUSR1, * 虚拟返回键的年代)。返回手势统一为「左缘起点 + 向右滑 SWIPE_DX」→ SIGUSR1。
* 不论 immersive 与否(返回键没了,所有 app 都靠左滑返回)。app 卡死仍可由 * M2.87:SIGUSR1 只发给「裸沉浸 App」(framework=0/无 framework 的自绘 App,
* 外层强杀序列(音量减x3)兜底。 */ * 其自己处理 SIGUSR1 当返回事件)。SDK 框架 App(g_fw_app=1,如 hello/files/ncm)
* 自己经 SDK on_swipe 处理左缘返回/顶部下拉(meow_ui_input.c),若 watchdog 也发
* SIGUSR1 → 默认动作杀死 App(ncm `signal=16` 崩溃根因)→ 本进程不发、避免双处理/误杀。
* 裸沉浸判定 = 非框架 且 /tmp/meowapp_state=1(app.json immersive 声明,launcher 写)。 */
if (down_x < EDGE_W && (last_x - down_x) > SWIPE_DX) { if (down_x < EDGE_W && (last_x - down_x) > SWIPE_DX) {
wd_log("swipe -> SIGUSR1"); if (!g_fw_app && immersive) {
kill(app_pid, SIGUSR1); wd_log("swipe -> SIGUSR1 (bare immersive, framework=%d)", g_fw_app);
kill(app_pid, SIGUSR1);
} else {
wd_log("swipe ignored (framework=%d immersive=%d) -> SDK owns back", g_fw_app, immersive);
}
} }
} }
/* ---- M2.86(226) SDK 框架 App 判定:app.json framework=1 -> 音量/媒体键让位 SDK ----
* 背景:App 前台时 SDK 输入层(meow_ui_input.c)已消化音量/媒体键;若 watchdog 也消化,
* 同一次物理按键会步进两次(冗余双处理)。判定「SDK 框架 App 在跑」用 app.json 的
* framework=true|1|yes —— 这是同类候选中更稳的一个:launcher 启动应用前已读同一 app.json
* 并成功 exec(app.json 必然可读、结果确定);SDK 侧无稳定「框架注册」标记
* (/tmp/meow_app_activity 交互节流、/tmp/meowapp_sbar 仅状态栏模式变化时写),不宜作判定源。
* 判定为框架 App -> watchdog 对音量/媒体键让位(交给 SDK),电源仍由本进程兜底(SDK 对电源
* 是 no-op,见 meow_ui_input.c);裸自绘 App(framework=0/无 framework)或 app.json 不可读 ->
* watchdog 保持兜底,绝不造成功能真空。 */
/* 极简 JSON/扁平值提取:与 launcher util.c json_str 同语义(键前缀须独立 + ':'/'=' + 可选引号)。 */
static int wd_json_str(const char *buf, const char *key, char *out, int olen)
{
out[0] = 0;
if (!buf || !key || olen <= 0) return 0;
const char *p = strstr(buf, key);
if (!p) return 0;
if (p > buf) { /* 键前缀须独立:防 "x_framework":1 被误命中为 framework */
unsigned char pc = (unsigned char)p[-1];
if ((pc >= 'a' && pc <= 'z') || (pc >= 'A' && pc <= 'Z') ||
(pc >= '0' && pc <= '9') || pc == '_' || pc == '.')
return 0;
}
p += strlen(key);
if (*p == '"') p++; /* JSON 键右引号 */
while (*p == ' ' || *p == '\t') p++; /* 键与 ':'/'=' 间空白(宽松容错) */
while (*p == ':' || *p == '=') p++;
while (*p == ' ' || *p == '\t') p++;
if (*p == '"') p++; /* 值左引号 */
int i = 0;
while (*p && *p != '"' && *p != ',' && *p != '\n' && *p != '\r' &&
*p != '}' && *p != ']' && *p != ':' && *p != ' ' && *p != '\t' && i < olen - 1)
out[i++] = *p++;
out[i] = 0;
while (i > 0 && (out[i - 1] == ' ' || out[i - 1] == '\t')) out[--i] = 0;
return i;
}
/* 读 <dir>/app.json 的 framework 字段(true|1|yes = 1)。返回 1=SDK 框架 App。 */
static int wd_app_is_framework(const char *dir)
{
char path[576];
int n = snprintf(path, sizeof path, "%s/app.json", dir);
if (n < 0 || n >= (int)sizeof path) return 0;
FILE *f = fopen(path, "r");
if (!f) return 0;
int fw = 0;
char buf[256];
while (fgets(buf, sizeof buf, f)) {
if (!strstr(buf, "framework")) continue;
char v[16] = "";
if (wd_json_str(buf, "framework", v, sizeof v) > 0 &&
(!strcmp(v, "true") || !strcmp(v, "1") || !strcmp(v, "yes")))
fw = 1;
break; /* framework 每 app.json 至多一次,读到即定 */
}
fclose(f);
return fw;
}
/* 经 /proc/<pid>/exe 反查应用安装目录(exec 与 app.json 同目录),再判 framework。 */
static int wd_framework_from_pid(int pid)
{
char exe[576], link[64];
snprintf(link, sizeof link, "/proc/%d/exe", pid);
ssize_t r = readlink(link, exe, sizeof exe - 1);
if (r < 0 || r >= (ssize_t)sizeof exe) return 0;
exe[r] = 0;
char *slash = strrchr(exe, '/');
if (slash) *slash = 0; /* 目录 = exec 所在目录(app.json 同目录) */
if (!exe[0]) return 0;
return wd_app_is_framework(exe);
}
#ifndef WD_NO_MAIN
int main(int argc, char **argv) int main(int argc, char **argv)
{ {
if (argc < 2) { if (argc < 2) {
@@ -452,7 +520,11 @@ int main(int argc, char **argv)
setvbuf(stderr, NULL, _IONBF, 0); setvbuf(stderr, NULL, _IONBF, 0);
wd_log_init(); wd_log_init();
wd_vol_load(); /* M2.24:音量初始值读自与 launcher 同源 settings.conf */ wd_vol_load(); /* M2.24:音量初始值读自与 launcher 同源 settings.conf */
wd_log("app_pid=%d sim_tap=%d sim_swipe=%d", app_pid, sim_tap, sim_swipe); /* M2.86(226):SDK 框架 App 判定(app.json framework=1)-> 音量/媒体键让位 SDK。 */
g_fw_app = wd_framework_from_pid(app_pid);
wd_log("app_pid=%d sim_tap=%d sim_swipe=%d framework=%d%s",
app_pid, sim_tap, sim_swipe, g_fw_app,
g_fw_app ? " -> yield vol/media to SDK" : " -> watchdog owns vol/media");
/* cooldown anchor: ~the moment this watchdog started (== app launch). */ /* cooldown anchor: ~the moment this watchdog started (== app launch). */
unsigned long long start_ms = now_ms(); unsigned long long start_ms = now_ms();
@@ -540,12 +612,15 @@ int main(int argc, char **argv)
} else if (type == EV_KEY && code == KEY_POWER) { } else if (type == EV_KEY && code == KEY_POWER) {
handle_power(val); /* M2.22(A)/M2.24:电源单击=息屏/亮屏,长按 tick 里判菜单 */ handle_power(val); /* M2.22(A)/M2.24:电源单击=息屏/亮屏,长按 tick 里判菜单 */
} else if (type == EV_KEY && code == KEY_VOLUMEUP) { } else if (type == EV_KEY && code == KEY_VOLUMEUP) {
if (val == 1) wd_vol_adj(+1); /* M2.24:音量+ = 系统音量 */ /* M2.86(226):SDK 框架 App 前台 -> 音量键让位 SDK(防双步进)。 */
if (val == 1 && !g_fw_app) wd_vol_adj(+1); /* M2.24:音量+ = 系统音量 */
} else if (type == EV_KEY && code == KEY_VOLUMEDOWN) { } else if (type == EV_KEY && code == KEY_VOLUMEDOWN) {
if (val == 1) wd_vol_adj(-1); /* M2.24:音量- = 系统音量 */ if (val == 1 && !g_fw_app) wd_vol_adj(-1); /* M2.24:音量- = 系统音量 */
} else if (type == EV_KEY && (code == KEY_PLAYPAUSE || code == KEY_PREVIOUSSONG || } else if (type == EV_KEY && (code == KEY_PLAYPAUSE || code == KEY_PREVIOUSSONG ||
code == KEY_NEXTSONG || code == KEY_PREVIOUS)) { code == KEY_NEXTSONG || code == KEY_PREVIOUS)) {
if (val == 1) wd_log("media key captured (code %d) -> consumed, no route", code); /* M2.86(226):框架 App 前台 -> 媒体键让位 SDK(接媒体会话路由)。 */
if (val == 1 && !g_fw_app)
wd_log("media key captured (code %d) -> consumed, no route", code);
} }
} }
} }
@@ -555,3 +630,4 @@ int main(int argc, char **argv)
} }
return 0; return 0;
} }
#endif /* !WD_NO_MAIN */
BIN
View File
Binary file not shown.
+101
View File
@@ -0,0 +1,101 @@
/* test_wd_fw.c — 宿主单测:meow_watchdog 的「SDK 框架 App 判定」逻辑(M2.86, issue 226)。
*
* 编译:#include meow_watchdog.c(去 main 拿 wd_app_is_framework/wd_json_str/wd_framework_from_pid)。
* gcc -Wall -Wextra -Os -DWd_NO_MAIN -Wno-unused-function -Wno-unused-parameter \
* test_wd_fw.c -o test_wd_fw
* (meow_watchdog.c 含仅 main 使用的静态函数,宿主去 main 后须 -Wno-unused-function 静音;
* mips 目标构建不定义 WD_NO_MAIN,不受影响。)
* 运行:./test_wd_fw;全 PASS -> exit 0。
*/
#define WD_NO_MAIN
#include "meow_watchdog.c"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
static int g_pass = 0, g_fail = 0;
static void check(const char *name, int got, int want)
{
if (got == want) { g_pass++; printf("PASS %s\n", name); }
else { g_fail++; printf("FAIL %s: got %d want %d\n", name, got, want); }
}
/* 在 <base>/<name> 建目录并写 app.json;写失败返回 -1。 */
static int fixture(const char *base, const char *name, const char *appjson)
{
char dir[512], path[560];
snprintf(dir, sizeof dir, "%s/%s", base, name);
if (mkdir(dir, 0755) != 0 && access(dir, F_OK) != 0) return -1;
snprintf(path, sizeof path, "%s/app.json", dir);
FILE *f = fopen(path, "w");
if (!f) return -1;
fputs(appjson, f);
fclose(f);
return 0;
}
static int expect_fw(const char *base, const char *name, const char *appjson, int want)
{
char dir[512];
snprintf(dir, sizeof dir, "%s/%s", base, name);
if (fixture(base, name, appjson) != 0) { g_fail++; printf("FAIL %s: cannot write fixture\n", name); return 0; }
char label[256];
snprintf(label, sizeof label, "is_fw(%s)", name);
check(label, wd_app_is_framework(dir), want);
return 1;
}
int main(void)
{
char dir[] = "/tmp/wdfwXXXXXX";
if (!mkdtemp(dir)) { perror("mkdtemp"); return 2; }
/* 框架 App:各值形态 */
expect_fw(dir, "v_num", "{\"name\":\"x\",\"framework\":1,\"exec\":\"/a/b\"}", 1); /* JSON 数值 1 */
expect_fw(dir, "v_true", "{\"name\":\"x\",\"framework\":true}", 1); /* true */
expect_fw(dir, "v_yes", "framework=yes", 1); /* 扁平 yes */
expect_fw(dir, "v_str", "{\"framework\":\"true\"}", 1); /* 字符串 "true" */
expect_fw(dir, "v_space","{\"name\":\"x\", \"framework\" : 1 }", 1); /* 空格容错 */
/* 非框架 App */
expect_fw(dir, "v_zero", "{\"name\":\"x\",\"framework\":0}", 0); /* 数值 0 */
expect_fw(dir, "v_false","{\"framework\":false}", 0); /* false */
expect_fw(dir, "v_none", "{\"name\":\"x\",\"exec\":\"/a/b\"}", 0); /* 无 framework 键 */
expect_fw(dir, "v_sub", "{\"x_framework\":1}", 0); /* 键前缀歧义:不误判 */
/* 多行 JSON(framework 单独一行,值独立) */
expect_fw(dir, "multiline",
"{\n \"name\": \"x\",\n \"framework\": 1,\n \"exec\": \"/a/b\"\n}\n", 1);
/* wd_json_str 直接断言(防 "myframework" / 前缀干扰) */
{
char out[16] = "";
check("json_str exact", wd_json_str("{\"framework\":1}", "framework", out, sizeof out) > 0 &&
!strcmp(out, "1"), 1);
check("json_str prefix-reject",
wd_json_str("{\"x_framework\":1}", "framework", out, sizeof out), 0);
check("json_str no-hit", wd_json_str("{\"name\":\"x\"}", "framework", out, sizeof out), 0);
}
/* wd_framework_from_pid:自进程 /proc/self/exe 目录无 app.json -> 0,不崩 */
check("from_pid(no app.json)", wd_framework_from_pid(getpid()), 0);
/* 清理 */
{
char path[560];
const char *names[] = { "v_num","v_true","v_yes","v_str","v_space","v_zero",
"v_false","v_none","v_sub","multiline" };
for (unsigned i = 0; i < sizeof names / sizeof names[0]; i++) {
snprintf(path, sizeof path, "%s/%s/app.json", dir, names[i]); unlink(path);
snprintf(path, sizeof path, "%s/%s", dir, names[i]); rmdir(path);
}
rmdir(dir);
}
printf("\n%d pass, %d fail\n", g_pass, g_fail);
return g_fail ? 1 : 0;
}