fix(launcher): §310 追加两修——未冻结App锁屏输入穿透(独占触控) + 主界面应用名截断

真机 M6.3 反馈(bcef8e0 三连修复未好全,追加两问题):

P1 锁屏 PIN 输入穿透仍在(媒体保活 App 实时读 evdev)
  根因:媒体保活 App(播放中)从不 SIGSTOP(保 PCM),SDK 输入线程实时读 evdev,不走缓冲。
  bcef8e0 的「SIGCONT 排空」只作用于冻结 App;对未冻结 App,隔离全靠 /tmp/meow_notif_active
  让 SDK 排空——若 App 的 meow_ui.a 缺该排空(或节流窗内漏网),锁屏期触摸依旧抵达 App。
  修法:run_lock_screen() 与 lck_input_pin_modal() 打开 DEV_TOUCH 后 EVIOCGRAB 独占触控设备
  (真机 goodix 实测 EVIOCGRAB 返回 0)——抓取期事件只投递抓取者、不为其它 fd 缓冲/回放,
  故**任何**前台进程(冻结或未冻结)锁屏期都收不到触摸(真全模态)。失败时记日志降级
  (仍靠标记+SIGSTOP 隔离)。overlay_gate.h 增 overlay_keepalive_needs_grab 判据。

P2 主界面应用名只显示 5 字(「流媒体客户」式,半字残形)
  根因:app_launcher.c `strncpy(a->name, name, 16)` 仍把显示名硬截 16 字节——M6.3 只扩了
  name[49]+json_str 整序列,漏改此二处。UTF-8 下 16B = 5 汉字 + 「端」首字节 + 无 NUL,
  显示成「流媒体客户」+ 残形;main_menu.c menu_name_fit 按字节回退又停在半截字符、加 "..." 溢出。
  修法:新增 meow_utf8.h(meow_utf8_char_len / meow_utf8_copy_bounded,纯决策可单测)——
  app_launcher.c 两处 strncpy(16) 改 app_name_copy(=copy_bounded, 拷满 sizeof name=49);
  menu_name_fit 改整字符前进 + 预留省略号宽度(mfont 用「…」/点阵用 "..."),总宽 ≤ maxw,
  绝不劈开多字节字符。

新增 test_utf8_name.c(14 断言:整字符长/整序列有界拷贝,含 cap=16/17/12 边界不劈字)+
  test_overlay_gate.c 扩展(S1b 未冻结 App 靠 EVIOCGRAB 隔离 4 断言)。launcher 全量宿主回归
  + SDK 叠加/状态栏/输入回归全 PASS;mipsel 交叉 -Wall -Wextra 0 warning。
md5 56dac860841c3884b4322707246e22af(/usr/bin 只读,待固件打包 M6.4;fw_content.conf 未动)。
This commit is contained in:
AskaEth committed 2026-09-02 08:13:55 +08:00
1 parent 67aa5fc7a8
commit c730333776
8 files changed
+224 -14

No files matched your search

+13 -2
View File
@@ -3,6 +3,7 @@
#include "meow_launcher_internal.h"
#include "meow_ams_start.h" /* MSG_AMS_START pending/拉起决策(meow_ams_start.c) */
#include "overlay_gate.h" /* SIGSTOP 叠加会话关闭顺序/死亡回退纯决策(§310 三连 bug 收敛点) */
#include "meow_utf8.h" /* UTF-8 整序列有界拷贝(显示名不劈多字节字符,问题2) */
static struct meow_acct g_accts[MEOW_MAX_ACTS]; /* 主循环单写者 */
@@ -11,6 +12,16 @@ int g_naccts = 0;
volatile int g_app_session_active = 0; /* §10.5:1=launch_app waitpid 前台会话中(util.c 读) */
/* 显示名 UTF-8 整序列有界拷贝(dst 容量 cap 含 NUL)。
* M6.3「name[49]+json_str 整序列」只把 json 读入局部 name[49] 与 a->name[49] 放大了,**漏改
* strncpy(a->name, name, 16)**——仍把显示名硬截 16 字节,UTF-8 下「流媒体客户端」(18B) 变
* 「流媒体客户」+ 半个「端」(16B = 5 汉字 + 首字节),真机主界面应用名截成 5 字(用户 M6.3 实测)。
* 本助手(= meow_utf8_copy_bounded)snprintf 到 cap 后回退掉被截断的半截多字节字符。 */
static void app_name_copy(char *dst, int cap, const char *src)
{
meow_utf8_copy_bounded(dst, cap, src);
}
/* ---- amsd 上报 / 控制挂点(AMSD_DESIGN §4):best-effort,amsd 不在不影响 launcher ---- */
#define MEOW_AMS_SOCK_DEFAULT "/usr/data/meow_ams.sock"
@@ -309,7 +320,7 @@ static void meow_acct_sync(const char *app_id, const struct app *ap)
a->background = ap->background; /* M4 */
a->background_audio = ap->background_audio;
strncpy(a->icon, ap->icon, 128);
strncpy(a->name, ap->name, 16);
app_name_copy(a->name, (int)sizeof a->name, ap->name);
strncpy(a->exec, ap->exec, 128);
a->ok = ap->ok;
a->nroute = ap->nroute > MEOW_MAX_ROUTES ? MEOW_MAX_ROUTES : ap->nroute;
@@ -813,7 +824,7 @@ void meow_parse_app_json(FILE *f, struct app *a)
}
}
}
strncpy(a->name, name, 16);
app_name_copy(a->name, (int)sizeof a->name, name);
strncpy(a->exec, exec, 128);
strncpy(a->package, package, sizeof a->package - 1);
a->framework = fram ? '1' : 0;
+33 -3
View File
@@ -126,6 +126,17 @@ int run_lock_screen(void)
meow_touch mt; mt_reset(&mt);
int tfd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK);
/* §310 输入穿透(媒体保活 App 不被 SIGSTOP,实时读 evdev):锁屏页独占触控设备——
* EVIOCGRAB 使本 fd 之外的其它进程(保活 App 的 SDK 输入线程 / watchdog 手势)在该设备
* 上收不到事件(事件仅投递抓取者),且抓取期事件不会为未抓取 fd 缓冲/回放。由此无论 App
* 冻结与否,锁屏期**任何** App 都收不到触摸(真全模态)。失败(驱动不支持)则记日志降级,
* 仍靠 keep_running/SIGSTOP+标记隔离。关闭 tfd 时抓取自动解除(仍显式释放一次)。 */
if (tfd >= 0) {
int grab = 1;
if (ioctl(tfd, EVIOCGRAB, &grab) < 0)
logmsg("lock: EVIOCGRAB touch fail: %s (graceful degrade, marker+SIGSTOP isola only)",
strerror(errno));
}
struct keypoll kp_power, kp_keys;
kp_open(&kp_power, DEV_POWER);
kp_open(&kp_keys, DEV_KEYS);
@@ -272,13 +283,21 @@ int run_lock_screen(void)
usleep(POLL_MS * 1000);
}
done:
if (tfd >= 0) close(tfd);
if (tfd >= 0) {
int grab = 0; /* 显式释放 EVIOCGRAB(关闭 tfd 也会自动解除,双保险) */
ioctl(tfd, EVIOCGRAB, &grab);
close(tfd);
}
kp_close(&kp_power);
kp_close(&kp_keys);
logmsg("lock: exit unlocked=%d screen_on=%d", unlocked, meow_screen_on);
return unlocked;
died:
if (tfd >= 0) close(tfd);
if (tfd >= 0) {
int grab = 0;
ioctl(tfd, EVIOCGRAB, &grab);
close(tfd);
}
kp_close(&kp_power);
kp_close(&kp_keys);
logmsg("lock: exit overlay-app-died -> LCK_OVERLAY_RET_DIED");
@@ -316,6 +335,13 @@ int lck_input_pin_modal(const char *title, char *out, int outcap)
fb_pan_buf0_force();
meow_touch mt; mt_reset(&mt);
int tfd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK);
/* §310 输入穿透:设置页 PIN 模态同样独占触控设备(后台保活 App 不冻结实时读 evdev,防其
* 同坐标控件被 PIN 键盘点击双重触发)。失败降级(仍靠标记+SIGSTOP 隔离)。 */
if (tfd >= 0) {
int grab = 1;
if (ioctl(tfd, EVIOCGRAB, &grab) < 0)
logmsg("pin-modal: EVIOCGRAB touch fail: %s (graceful degrade)", strerror(errno));
}
struct keypoll kp_power, kp_keys;
kp_open(&kp_power, DEV_POWER);
kp_open(&kp_keys, DEV_KEYS);
@@ -412,7 +438,11 @@ int lck_input_pin_modal(const char *title, char *out, int outcap)
usleep(POLL_MS * 1000);
}
done2:
if (tfd >= 0) close(tfd);
if (tfd >= 0) {
int grab = 0; /* 显式释放 EVIOCGRAB(关闭 tfd 亦自动解除,双保险) */
ioctl(tfd, EVIOCGRAB, &grab);
close(tfd);
}
kp_close(&kp_power);
kp_close(&kp_keys);
return result;
+31 -9
View File
@@ -2,6 +2,7 @@
/* 本文件由拆分工具从 meow_launcherd.c 按职责搬移;逻辑与原文 0-diff(仅 static 可见性按跨模块需求调整)。*/
#include "meow_launcher_internal.h"
#include "meow_ams_start.h" /* MSG_AMS_START 拉起决策(meow_ams_start.c):run_menu 消费用 */
#include "meow_utf8.h" /* UTF-8 整序列字符步进(menu_name_fit 截断不劈多字节字符,问题2) */
/* set by run_menu: the player's panel was blanked (screen timeout) when the
@@ -393,20 +394,41 @@ static void menu_first_glyph(const char *nm, char out[4])
}
}
/* 名称截断(按可用像素宽;超出加 '…')。 */
/* 名称截断(按可用像素宽):UTF-8 整序列 + 「…」收尾,总宽 ≤ maxw。
* 旧实现:①按字节回退、边界判定错(do..while 读的是刚清零的字节,恒停在一个续字节上)→ 停在
* 半截多字节字符,显示成半字/方框;②先截满再加 "..." 未为省略号留宽 → 追加后溢出 maxw 被裁切。
* 修法:按字符起始字节前进,绝不劈开多字节字符;预留省略号宽度(mfont_ready 用「…」/回退点阵用
* "..."),截断后总宽 ≤ maxw。 */
static void menu_name_fit(const char *name, int maxw, int size, char *out, int olen)
{
char buf[64];
snprintf(buf, sizeof buf, "%s", name);
if (ui_width(buf, size) <= maxw) { snprintf(out, olen, "%s", buf); return; }
int n = (int)strlen(buf);
while (n > 0 && ui_width(buf, size) > maxw) {
/* 回退一个 UTF-8 字符边界 */
do { buf[--n] = 0; } while (n > 0 && ((unsigned char)buf[n] & 0xC0) == 0x80);
if (!name) name = "";
if (maxw < 0) maxw = 0;
const char *ell = mfont_ready ? "…" : "...";
int ell_w = ui_width(ell, size);
int budget = maxw - ell_w;
if (budget < 0) budget = 0;
if (ui_width(name, size) <= budget) { snprintf(out, olen, "%s", name); return; }
/* 整字符前进(meow_utf8_char_len 定步长):cut 恒停在一个完整字符之后,绝不劈开多字节字符;
* 连同下一整字符会超预算即停。 */
size_t stop = strlen(name);
if (stop > (size_t)olen - 1) stop = (size_t)olen - 1;
size_t cut = 0;
while (cut < stop && name[cut]) {
int step = meow_utf8_char_len((unsigned char)name[cut]);
size_t clen = (cut + (size_t)step <= stop) ? (size_t)step : (stop - cut);
if (clen == 0) break;
char tmp[64];
if (clen >= sizeof tmp) clen = sizeof tmp - 1;
memcpy(tmp, name + cut, clen);
tmp[clen] = 0;
if (ui_width(tmp, size) > budget) break;
cut += clen;
}
snprintf(out, olen, "%s...", buf);
if (cut == 0) { snprintf(out, olen, "%s", ell); return; }
snprintf(out, olen, "%.*s%s", (int)cut, name, ell);
}
/* 图标绘制:primary 圆角垫块始终垫底(MD3 图标 tile),首字母叠加其上,最后有图叠加
* meow_ui_img_cover(居中裁方)。
* 修复:原实现 `if (icon) { meow_ui_img_cover; return; }` 在图标为透明背景 logo(不透明像素
Binary file not shown.
+57
View File
@@ -0,0 +1,57 @@
/* meow_utf8.h — UTF-8 整序列有界拷贝/字符步进纯决策(宿主可单测;无 ui_width/mfont 依赖)。
*
* 背景(问题2,M6.3 真机):主界面应用名「流媒体客户端」被截成 5 字 + 半个字。两处根因:
* ① app_launcher.c `strncpy(a->name, name, 16)` 仍把显示名硬截 16 字节(UTF-8 下=5 汉字+ 半个
* 「端」)——M6.3 只扩了 name[49]+json_str 整序列,漏改此处;
* ② main_menu.c `menu_name_fit` 按字节回退、边界判定错(do..while 读的是刚清零的字节,恒停续字节),
* 停在半截多字节字符;再加 "..." 未为省略号留宽 → 溢出 maxw 被裁切。
* 本头把 UTF-8 整序列处理抽成纯函数(meow_utf8_char_len / meow_utf8_copy_bounded / 截断决策),
* 两处复用并可宿主单测(test_utf8_name.c)。
*/
#ifndef MEOW_UTF8_H
#define MEOW_UTF8_H
#include <stddef.h>
#include <stdio.h> /* snprintf(有界拷贝用) */
#ifdef __cplusplus
extern "C" {
#endif
/* 由首字节 c0 求整字符 UTF-8 字节长(1..4)。非法首字节(0x80..0xBF 续字节)返回 1(按异常字节跳过,
* 绝不把孤立续字节与后字并成大字符)。纯按前导位,不看后续字节是否齐全(那由 copy_bounded 处理)。 */
static inline int meow_utf8_char_len(unsigned char c0)
{
if (c0 < 0x80) return 1;
if (c0 < 0xC0) return 1; /* 孤立续字节:兜底 1 */
if (c0 < 0xE0) return 2;
if (c0 < 0xF0) return 3;
return 4;
}
/* 有界拷贝:把 src 拷入 dst(容量 cap,含 NUL),**绝不劈开多字节字符**。snprintf 截断若落在字符
* 中间(或 src 本身尾部是半截字符),回退到整字符边界:删去最末不完整字符。返回拷贝后字节长(不含
* NUL)。源可为 NULL。持久化/展示名均用——保证 UTF-8 整序列,杜绝「5 汉字+半个字」式残字。 */
static inline int meow_utf8_copy_bounded(char *dst, int cap, const char *src)
{
if (!dst || cap <= 0) return 0;
if (cap == 1) { dst[0] = 0; return 0; }
if (!src) src = "";
snprintf(dst, (size_t)cap, "%s", src);
int n = (int)strlen(dst);
if (n <= 0) return 0;
/* 定位最末一个「起始字节」(非续字节):从尾部回退续字节。 */
int last_start = n - 1;
while (last_start > 0 && ((unsigned char)dst[last_start] & 0xC0) == 0x80)
last_start--;
/* 末字符:起始字节=last_start,实际字节数=have;字符应需 need。have<need → 不完整 → 删。 */
int need = meow_utf8_char_len((unsigned char)dst[last_start]);
int have = n - last_start;
if (have < need) { dst[last_start] = 0; n = last_start; }
return n;
}
#ifdef __cplusplus
}
#endif
#endif /* MEOW_UTF8_H */
+11
View File
@@ -42,4 +42,15 @@ static inline int overlay_force_sb_resync(void)
return 1;
}
/* 未冻结(媒体保活,keep_running)App 是否**需要 EVIOCGRAB 独占隔离**(返回 1)。
* §310 问题1真根:媒体保活 App 从不 SIGSTOP(保 PCM),SDK 输入线程**实时读 evdev**(不走缓冲),
* 标记(/tmp/meow_notif_active)让 SDK 排空只在有该新 SDK 的 App 上生效;launcher 侧可**无条件**
* 用 EVIOCGRAB 独占触控设备,使包括保活 App 在内的**任何**前台进程在锁屏期收不到触摸(且抓取期
* 事件不为未抓取 fd 缓冲/回放)。keep_alive=1 → 隔离靠 grab(overlay_drain_required==0,无
* SIGCONT 排空语义);冻结 App(keep_running=0)→ 隔离靠 SIGSTOP+SIGCONT 排空(drain_required==1)。 */
static inline int overlay_keepalive_needs_grab(int keep_running)
{
return keep_running ? 1 : 0;
}
#endif /* MEOW_OVERLAY_GATE_H */
@@ -8,6 +8,9 @@
* 不等 app 循环 1s 慢刷(消除「解锁回 App 无状态栏」残留)。
* S3 App 死亡触发主屏重绘 → lock_overlay_app_died_exit():锁屏叠加期 App 崩溃(僵尸)必须走
* 统一退出路径(交回 waitpid 循环回收 + run_menu 重绘主屏含状态栏)。
* S1b 未冻结 App 输入被排 → overlay_keepalive_needs_grab():媒体保活 App(保持运行、SDK 输入
* 线程实时读 evdev)在锁屏期必须靠 launcher EVIOCGRAB 独占触控设备 +
* 标记让 SDK 排空——而非冻结 App 的 SIGCONT 排空(drain_required=0)。
*
* 编译运行(在 patch_v2/meow_launcherd 下,无其它依赖):
* gcc -Wall -Wextra -o test_overlay_gate test_overlay_gate.c && ./test_overlay_gate
@@ -32,6 +35,17 @@ int main(void)
* 标记再整批丢弃缓冲事件;此处断言契约不被踩线。 */
CHECK(OVERLAY_DRAIN_WINDOW_MS > 200, "S1 drain window > SDK 200ms throttle");
/* ---- S1b 未冻结(媒体保活)App 输入被排:靠 EVIOCGRAB 独占隔离,非 SIGCONT 排空 ----
* 保活 App 从不 SIGSTOP(保 PCM),SDK 输入线程实时读 evdev;launcher 只能靠 EVIOCGRAB 独占
* 触控设备(真机 goodix 已证 EVIOCGRAB 返回 0)使任何前台进程锁屏期收不到触摸。 */
CHECK(overlay_keepalive_needs_grab(1) == 1, "S1b keep-alive app needs EVIOCGRAB isolation");
CHECK(overlay_keepalive_needs_grab(0) == 0, "S1b frozen app -> SIGCONT drain, not grab");
/* 保活 App 隔离与冻结 App 排空互斥:一条路径而非两条。 */
CHECK(overlay_drain_required(1) == 0 && overlay_keepalive_needs_grab(1) == 1,
"S1b keep-alive: grab path (drain==0)");
CHECK(overlay_drain_required(0) == 1 && overlay_keepalive_needs_grab(0) == 0,
"S1b frozen: drain path (grab==0)");
/* ---- S2 解锁状态栏恢复:必须强制重同步 ---- */
CHECK(overlay_force_sb_resync() == 1, "S2 force status bar re-sync after unlock");
+65
View File
@@ -0,0 +1,65 @@
/* test_utf8_name.c — 问题2(主界面应用名截断)宿主单测:UTF-8 整序列有界拷贝 / 字符步进。
*
* 背景(M6.3 真机):主界面应用名「流媒体客户端」被截成 5 字 + 半个字。真因 = app_launcher.c
* `strncpy(a->name, name, 16)` 仍把显示名硬截 16 字节(UTF-8 下 16B = 5 汉字 + 「端」首字节),且
* main_menu.c `menu_name_fit` 按字节回退停在半截字符、加 "..." 又溢出 maxw。本测试验证 meow_utf8.h
* 的整序列有界拷贝:绝不劈开多字节字符、给出完整汉字序列(修复 strncpy(16) 与 menu_name_fit)。
*
* 编译运行(在 patch_v2/meow_launcherd 下):
* gcc -Wall -Wextra -o test_utf8_name test_utf8_name.c && ./test_utf8_name
* 全 PASS -> exit 0。
*/
#include <stdio.h>
#include <string.h>
#include "meow_utf8.h"
static int g_pass = 0, g_fail = 0;
#define CHECK(cond, msg) do { \
if (cond) { g_pass++; printf("PASS %s\n", msg); } \
else { g_fail++; printf("FAIL %s\n", msg); } \
} while (0)
int main(void)
{
/* meow_utf8_char_len:由首字节求整字符长。 */
CHECK(meow_utf8_char_len('A') == 1, "char_len ASCII = 1");
CHECK(meow_utf8_char_len((unsigned char)0xC3) == 2, "char_len 2-byte lead = 2");
CHECK(meow_utf8_char_len((unsigned char)0xE6) == 3, "char_len 3-byte lead = 3");
CHECK(meow_utf8_char_len((unsigned char)0xF0) == 4, "char_len 4-byte lead = 4");
CHECK(meow_utf8_char_len((unsigned char)0x80) == 1, "char_len lone continuation = 1 (no merge)");
/* meow_utf8_copy_bounded:不劈多字节字符。 */
char buf[64];
/* 源「流媒体客户端」= 18B(6×3B)。cap=49 → 全量(修复 strncpy(16) 后的正确行为)。 */
meow_utf8_copy_bounded(buf, (int)sizeof buf, "流媒体客户端");
CHECK(strcmp(buf, "流媒体客户端") == 0, "copy cap=49 keeps full '流媒体客户端'");
CHECK(strlen(buf) == 18, "copy cap=49 len=18 (6 chars)");
/* cap=16(旧 bug 的容量):旧 strncpy(16) 给 5 汉字+半个字+无NUL;现应给 5 整汉字+ NUL。 */
meow_utf8_copy_bounded(buf, 16, "流媒体客户端");
CHECK(strcmp(buf, "流媒体客户") == 0, "copy cap=16 -> 5 whole chars '流媒体客户'");
CHECK(strlen(buf) == 15, "copy cap=16 -> 15 bytes (5 CJK, no partial 6th)");
/* cap=17:尾部「端」首字节越界 → 整字符去掉,仍 5 整汉字,绝不半字残形。 */
meow_utf8_copy_bounded(buf, 17, "流媒体客户端");
CHECK(strcmp(buf, "流媒体客户") == 0, "copy cap=17 -> still 5 whole chars (no partial)");
/* cap=12:缓冲区 12B 含 NUL → 内容上限 11B,4 整汉字(12B)放不下 → 截到 3 整汉字(绝非半字)。
* 注:snprintf 语义 = cap 字节含 NUL,故内容 ≤ cap-1(旧 strncpy(dst,src,12) 拷 12B 无 NUL,
* 有越界读风险;此处改为安全 NUL 结尾,代价是内容少 1B——正确取舍)。 */
meow_utf8_copy_bounded(buf, 12, "流媒体客户端");
CHECK(strcmp(buf, "流媒体") == 0, "copy cap=12 -> 3 whole chars '流媒体' (no partial 4th)");
/* ASCII / 混合:不劈 ASCII。 */
meow_utf8_copy_bounded(buf, (int)sizeof buf, "Hello App");
CHECK(strcmp(buf, "Hello App") == 0, "copy ASCII unchanged");
/* NULL/空源安全性。 */
meow_utf8_copy_bounded(buf, (int)sizeof buf, NULL);
CHECK(buf[0] == 0, "copy NULL src -> empty");
meow_utf8_copy_bounded(buf, 1, "流媒体客户端");
CHECK(buf[0] == 0, "copy cap=1 -> empty (no write overflow)");
printf("\n%s (%d passed, %d failed)\n", g_fail ? "SOME TESTS FAILED" : "ALL PASS", g_pass, g_fail);
return g_fail ? 1 : 0;
}