diff --git a/patch_v2/meow_launcherd/app_launcher.c b/patch_v2/meow_launcherd/app_launcher.c index 3fd7db8..dd1951d 100644 --- a/patch_v2/meow_launcherd/app_launcher.c +++ b/patch_v2/meow_launcherd/app_launcher.c @@ -2,6 +2,7 @@ /* 本文件由拆分工具从 meow_launcherd.c 按职责搬移;逻辑与原文 0-diff(仅 static 可见性按跨模块需求调整)。*/ #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 收敛点) */ static struct meow_acct g_accts[MEOW_MAX_ACTS]; /* 主循环单写者 */ @@ -1420,14 +1421,19 @@ static int run_app_notif_overlay(struct app *a, pid_t pid) * 避免「标记先消失 → App 抢在恢复帧前 present 新帧 → 又被旧帧盖回」的一帧回闪。 */ pm_capture_app_frame(); meow_ui_flush(); - unlink(NOTIF_ACTIVE); - unlink(POWMENU_ACTIVE); if (!keep_running) { + /* §310 输入穿透定案(与 lock-overlay 同源):App 被 SIGSTOP 冻结过 → 在标记仍存在时 + * 先 SIGCONT,让 SDK 输入线程排空冻结期缓冲的 evdev 事件 + 对齐 EVIOCGKEY,再等 + * OVERLAY_DRAIN_WINDOW_MS 后 unlink——防关面板瞬间把积累触摸回放成 App 内点击。 */ kill(pid, SIGCONT); - logmsg("notif-overlay: closed -> SIGCONT app"); + usleep(OVERLAY_DRAIN_WINDOW_MS * 1000); + logmsg("notif-overlay: closed -> SIGCONT app, drain %dms, then unlink mark", + OVERLAY_DRAIN_WINDOW_MS); } else { logmsg("notif-overlay: closed -> app never stopped (media keep-alive), no SIGCONT"); } + unlink(NOTIF_ACTIVE); + unlink(POWMENU_ACTIVE); return 0; } @@ -1463,18 +1469,43 @@ static int run_app_lock_overlay(struct app *a, pid_t pid) meow_theme_reload(); pm_capture_app_frame(); + /* 置位叠加期 App pid:run_lock_screen 每圈探测该 App 是否在锁屏期崩溃(§310 残影), + * 崩溃即中止锁屏返回 LCK_OVERLAY_RET_DIED,走下方统一退出路径。 */ + g_lock_overlay_pid = (int)pid; int lr = run_lock_screen(); + g_lock_overlay_pid = 0; + + if (lr == LCK_OVERLAY_RET_DIED) { + /* 叠加期 App 已死:不 SIGCONT(死进程)、不恢复死帧;unlink 标记交回 waitpid 循环回收, + * run_menu 统一重绘主屏(含状态栏)。launcher 不崩。 */ + unlink(NOTIF_ACTIVE); + unlink(POWMENU_ACTIVE); + logmsg("lock-overlay: app pid %d died during lock -> unified exit path", pid); + return 1; + } pm_capture_app_frame(); meow_ui_flush(); - unlink(NOTIF_ACTIVE); - unlink(POWMENU_ACTIVE); if (!keep_running) { + /* §310 输入穿透定案(overlay_gate.h):App 被 SIGSTOP 冻结过 → 在 /tmp/meow_notif_active + * 仍未 unlink 时**先 SIGCONT**,让 SDK 输入线程排空冻结期缓冲的 evdev 事件副本 + 对齐 + * EVIOCGKEY,再等 OVERLAY_DRAIN_WINDOW_MS(> SDK 200ms 节流窗)后 unlink——防解锁瞬间 + * 把积累的触摸回放成 App 内点击(锁屏穿透→「启动奇怪界面」→ 状态栏消失的触发源)。 */ kill(pid, SIGCONT); - logmsg("lock-overlay: closed (lr=%d) -> SIGCONT app", lr); + usleep(OVERLAY_DRAIN_WINDOW_MS * 1000); + logmsg("lock-overlay: closed (lr=%d) -> SIGCONT app, drain %dms, then unlink mark", + lr, OVERLAY_DRAIN_WINDOW_MS); } else { logmsg("lock-overlay: closed (lr=%d) -> app never stopped (media keep-alive), no SIGCONT", lr); } + unlink(NOTIF_ACTIVE); + unlink(POWMENU_ACTIVE); + /* §310 状态栏恢复:解锁后强制 g_sb_mode_last=-1 + 立即重叠加顶条(不等 app 循环 1s 慢刷), + * 消除「解锁回 App 无状态栏」的残留态。overlay_force_sb_resync() = 定案恒真。 */ + if (overlay_force_sb_resync()) { + g_sb_mode_last = -1; + if (!a->fullscreen) app_overlay_statusbar(); + } return 0; } @@ -1956,8 +1987,17 @@ static int launch_app_chain(struct app *a, const char *chain) meow_panel_on(); meow_auto_slept = 0; fb_pan_buf0_force(); - /* M2.x 锁屏:App 前台唤醒后若需锁屏 → SIGSTOP 叠加锁屏页;解锁/SIGCONT 回应用 */ - if (lck_wake_should_lock()) run_app_lock_overlay(a, pid); + /* M2.x 锁屏:App 前台唤醒后若需锁屏 → SIGSTOP 叠加锁屏页;解锁/SIGCONT 回应用。 + * §310:叠加期 App 崩溃(LR==LCK_OVERLAY_RET_DIED)→ 回收僵尸走统一退出路径 + * (launcher 不崩、run_menu 重绘主屏),同 run_app_notif_overlay 的 died 分支。 */ + if (lck_wake_should_lock()) { + int lr2 = run_app_lock_overlay(a, pid); + if (lr2 != 0) { + reap_wait(pid, &status); + ams_status_valid = 1; + break; + } + } } } if (screen_off_timeout > 0 && meow_screen_on && !meow_auto_slept && !fg_keep_awake()) { @@ -1980,8 +2020,16 @@ static int launch_app_chain(struct app *a, const char *chain) meow_panel_on(); meow_auto_slept = 0; fb_pan_buf0_force(); - /* M2.x 锁屏:App 前台唤醒后若需锁屏 → SIGSTOP 叠加锁屏页;解锁/SIGCONT 回应用 */ - if (lck_wake_should_lock()) run_app_lock_overlay(a, pid); + /* M2.x 锁屏:App 前台唤醒后若需锁屏 → SIGSTOP 叠加锁屏页;解锁/SIGCONT 回应用。 + * §310:叠加期 App 崩溃 → 回收僵尸走统一退出路径(launcher 不崩、run_menu 重绘主屏)。 */ + if (lck_wake_should_lock()) { + int lr2 = run_app_lock_overlay(a, pid); + if (lr2 != 0) { + reap_wait(pid, &status); + ams_status_valid = 1; + break; + } + } } } if (vold && !app_prev_vold) { diff --git a/patch_v2/meow_launcherd/lock_screen.h b/patch_v2/meow_launcherd/lock_screen.h index 0a52230..dc0e0d9 100644 --- a/patch_v2/meow_launcherd/lock_screen.h +++ b/patch_v2/meow_launcherd/lock_screen.h @@ -88,6 +88,20 @@ void lck_manual_lock_clear(void); /* 删标志(幂等) */ * 取代 lck_lock_required(1) 作为唤醒门——主动锁强制锁屏,覆盖「关闭唤醒锁」的手动锁场景。 */ int lck_wake_should_lock(void); +/* ── 锁屏叠加期 App 存活探针(§310 残影修复,纯判定,宿主可单测;header-only inline)---- + * 锁屏运行在 App 前台叠加会话中(overlay_pid>0,run_app_lock_overlay 置位)且该进程已成僵尸 + * (调用方 proc_is_zombie 判定后传入 proc_zombie=1)→ 应中止锁屏走统一退出(返回 1)。 + * 菜单直接锁屏(overlay_pid=0,无 App 可死)恒 0;此谓词不实际读 /proc。 */ +static inline int lck_overlay_should_abort(int overlay_pid, int proc_zombie) +{ + return (overlay_pid > 0 && proc_zombie) ? 1 : 0; +} + +/* run_lock_screen() 返回值:1=已解锁,0=锁屏期间电源键息屏。叠加期 App 崩溃时返回 + * LCK_OVERLAY_RET_DIED(-1)——调用方 run_app_lock_overlay 据此外走「统一退出路径」 + * (不再 SIGCONT 死进程/不恢复死帧,交回 waitpid 循环回收,run_menu 重绘主屏)。 */ +#define LCK_OVERLAY_RET_DIED (-1) + #ifdef __cplusplus } #endif diff --git a/patch_v2/meow_launcherd/lock_screen_ui.c b/patch_v2/meow_launcherd/lock_screen_ui.c index dcfc061..4484968 100644 --- a/patch_v2/meow_launcherd/lock_screen_ui.c +++ b/patch_v2/meow_launcherd/lock_screen_ui.c @@ -12,6 +12,12 @@ */ #include "meow_launcher_internal.h" +/* §310 残影:锁屏处于 App 前台叠加会话时置位当前前台 App pid(run_app_lock_overlay 写入)。 + * run_lock_screen 每圈据此探测 App 是否已死(组合 App 冻结缓冲 evdev 会在解锁瞬间回放成点击 / + * 叠加期 OOM 杀让 fb0 留残帧——探到死即中止锁屏,交回 waitpid 循环回收 + run_menu 重绘主屏)。 + * 菜单直接锁屏(无 App)时保持 0 = 不探测。 */ +int g_lock_overlay_pid = 0; + /* ---- 键盘几何(480×720):3 列×4 行圆角按钮 ---- */ #define LK_X0 32 /* 首列左缘 */ #define LK_COLW 130 /* 列宽 */ @@ -156,6 +162,17 @@ int run_lock_screen(void) goto done; } + /* §310 残影:锁屏叠加期 App 崩溃(僵尸)→ 中止锁屏走统一退出路径(交回 waitpid 循环 + * 回收 + run_menu 重绘主屏含状态栏)。仅当 run_app_lock_overlay 置位 g_lock_overlay_pid + * 才探(菜单直接锁屏无 App 可死,恒不中止)。 */ + if (g_lock_overlay_pid > 0 && + lck_overlay_should_abort(g_lock_overlay_pid, proc_is_zombie(g_lock_overlay_pid))) { + logmsg("lock: overlay app pid %d died during lock -> abort (unified exit path)", + g_lock_overlay_pid); + g_lock_overlay_pid = 0; + goto died; + } + /* 锁定到期自动解禁(清计数+戳),仅重画状态行 */ if (lock_until > 0 && now >= lock_until) { lck_lock_remaining(); @@ -260,6 +277,12 @@ done: kp_close(&kp_keys); logmsg("lock: exit unlocked=%d screen_on=%d", unlocked, meow_screen_on); return unlocked; +died: + if (tfd >= 0) close(tfd); + kp_close(&kp_power); + kp_close(&kp_keys); + logmsg("lock: exit overlay-app-died -> LCK_OVERLAY_RET_DIED"); + return LCK_OVERLAY_RET_DIED; } /* 电源菜单「锁定」:**主动锁屏**(用户定案 2026-09-02 强化)。 diff --git a/patch_v2/meow_launcherd/meow_launcher_internal.h b/patch_v2/meow_launcherd/meow_launcher_internal.h index 70844bf..af2d02a 100644 --- a/patch_v2/meow_launcherd/meow_launcher_internal.h +++ b/patch_v2/meow_launcherd/meow_launcher_internal.h @@ -827,10 +827,14 @@ void settings_sync_cache_listener(const char *key); void settings_volume_adj(int delta); int settings_volume_get(void); int show_power_menu(void); -/* M2.x 锁屏:阻塞式锁屏页(渲染+输入);返回 1=已解锁(回主屏),0=锁屏期间电源键息屏(屏已关)。 +/* M2.x 锁屏:阻塞式锁屏页(渲染+输入);返回 1=已解锁(回主屏),0=锁屏期间电源键息屏(屏已关), + * -1(LCK_OVERLAY_RET_DIED)=锁屏叠加期 App 崩溃(run_app_lock_overlay 走统一退出路径)。 * meow_lock_screen_engage() 供电源菜单「锁定」与唤醒路径用;未配凭据时 no-op。 */ int run_lock_screen(void); int meow_lock_screen_engage(void); +/* §310 残影:锁屏处于 App 前台叠加会话时置位当前 App pid(run_app_lock_overlay 写入, + * run_lock_screen 每圈据此 proc_is_zombie 探测;菜单直接锁屏时保持 0=不探)。 */ +extern int g_lock_overlay_pid; /* 设置页「锁屏 PIN」数字输入模态(系统级 PIN 设置/修改输入):满 4 位返回 1 并拷贝到 out,电源=取消返回 0。 */ int lck_input_pin_modal(const char *title, char *out, int outcap); /* 设置页「锁屏 PIN」独立子页面(subpage.c 脚手架;阻塞到返回)。 */ diff --git a/patch_v2/meow_launcherd/meow_launcherd b/patch_v2/meow_launcherd/meow_launcherd index 958abf0..9ca1a98 100755 Binary files a/patch_v2/meow_launcherd/meow_launcherd and b/patch_v2/meow_launcherd/meow_launcherd differ diff --git a/patch_v2/meow_launcherd/overlay_gate.h b/patch_v2/meow_launcherd/overlay_gate.h new file mode 100644 index 0000000..025fc29 --- /dev/null +++ b/patch_v2/meow_launcherd/overlay_gate.h @@ -0,0 +1,45 @@ +/* overlay_gate.h — SIGSTOP 叠加会话(通知中心 §10.5 / 锁屏 M2.x)关闭与死亡回退的**纯决策**。 + * + * 宿主可单测(无信号/无设备/无文件系统依赖,全部 static inline);app_launcher.c 的两处叠加 + * 关闭路径(run_app_notif_overlay / run_app_lock_overlay)调用,避免把「排空窗口」「中止条件」 + * 这类时序/判定散落内联难验证。三个 bug 的定案语义都收敛到这里: + * + * ① 输入穿透(S1 根因):App 被 SIGSTOP 冻结过 → 关闭时必须在 /tmp/meow_notif_active 仍存在 + * 时先 SIGCONT,让 SDK 输入线程(meow_ui_input.c overlay_input_suppressed)排空冻结期缓冲的 + * evdev 事件副本 + 对齐 EVIOCGKEY,再等 OVERLAY_DRAIN_WINDOW_MS 后 unlink 标记。 + * 若反序(unlink->SIGCONT,改动前写法),App 在无隔离态恢复 → 解冻瞬间把积累的触摸回放成 + * App 内点击(锁屏穿透 /「启动奇怪界面」/ 解锁后状态栏消失的触发源)。 + * ② 状态栏恢复(S2):解锁后强制 g_sb_mode_last=-1 让 app 循环立即重叠加,不等 1s 慢刷。 + * ③ 死亡重绘(S3):锁屏叠加中 App 崩溃(/proc//stat==Z)→ 中止锁屏走统一退出路径, + * 由 waitpid 循环回收 + run_menu dirty=1 重绘主屏(含状态栏)。 + */ +#ifndef MEOW_OVERLAY_GATE_H +#define MEOW_OVERLAY_GATE_H + +/* SDK overlay_input_suppressed() 的标记存在性节流窗是 200ms;排空窗须 > 该值(留 100ms 余量), + * 保证 App 恢复后 SDK 输入线程确实重新 stat 到标记再整批丢弃缓冲事件。 */ +#define OVERLAY_DRAIN_WINDOW_MS 300 + +/* ① 排空是否必要:App 被 SIGSTOP 冻结过(非保活)才需要「SIGCONT 保持隔离 → 排窗 → unlink」; + * 媒体保活(keep_running,从未 STOP)直接「恢复帧上屏 → unlink」(自抑随标记消失自然解除)。 */ +static inline int overlay_drain_required(int keep_running) +{ + return !keep_running ? 1 : 0; +} + +/* ③ 锁屏叠加中 App 崩溃是否触发「统一退出路径」(= 中止锁屏、交回 waitpid 循环回收、run_menu 重绘)。 + * proc_zombie=1(/proc//stat==Z)→ 走退出(返回 1);否则正常恢复(返回 0)。 */ +static inline int lock_overlay_app_died_exit(int proc_zombie) +{ + return proc_zombie ? 1 : 0; +} + +/* ② 解锁后是否需要强制状态栏重同步(返回 1):始终需要——把 g_sb_mode_last 拨回 -1, + * 使 run_menu / app 循环的「模式变化→立即重叠加」分支命中(-1 != m 恒真除非 m==-1, + * 但 m==-1 表示无模式文件=展开态,同样被 app_overlay_statusbar 兜底到展开整条)。 */ +static inline int overlay_force_sb_resync(void) +{ + return 1; +} + +#endif /* MEOW_OVERLAY_GATE_H */ diff --git a/patch_v2/meow_launcherd/test_lock_screen.c b/patch_v2/meow_launcherd/test_lock_screen.c index f0093d6..4f08b17 100644 --- a/patch_v2/meow_launcherd/test_lock_screen.c +++ b/patch_v2/meow_launcherd/test_lock_screen.c @@ -75,6 +75,14 @@ int main(void) meow_settings_save(); CHECK(lck_wake_should_lock() == 1, "wake_should_lock=1 (wake=1)"); + /* ---- §310 残影:锁屏叠加期 App 崩溃中止探针(纯判定)---- + * 锁屏在 App 前台叠加会话(overlay_pid>0)且该 App 已成僵尸 → 中止锁屏(返回 1); + * 菜单直接锁屏(overlay_pid=0)恒不中止;叠加期 App 仍活(proc_zombie=0)也不中止。 */ + CHECK(lck_overlay_should_abort(0, 0) == 0, "overlay abort=0 (no overlay pid, not zombie)"); + CHECK(lck_overlay_should_abort(1234, 0) == 0, "overlay abort=0 (overlay pid, app alive)"); + CHECK(lck_overlay_should_abort(0, 1) == 0, "overlay abort=0 (no overlay pid, zombie ignored)"); + CHECK(lck_overlay_should_abort(1234, 1) == 1, "overlay abort=1 (overlay pid + zombie -> unified exit)"); + /* ---- 面板一次性解锁恢复口(bypass 标志)---- */ CHECK(lck_unlock_once_consume() == 0, "unlock_once_consume=0 when flag absent"); { diff --git a/patch_v2/meow_launcherd/test_overlay_gate.c b/patch_v2/meow_launcherd/test_overlay_gate.c new file mode 100644 index 0000000..42d3d50 --- /dev/null +++ b/patch_v2/meow_launcherd/test_overlay_gate.c @@ -0,0 +1,50 @@ +/* test_overlay_gate.c — §310 三连 bug 的**纯决策**宿主单测(overlay_gate.h + lock_screen.h 谓词)。 + * + * 覆盖三个症状对应的关闭/恢复决策,不依赖信号/设备/文件系统: + * S1 锁屏期触摸不穿透 → overlay_drain_required():App 被 SIGSTOP 过(非保活)必须在 + * unlink /tmp/meow_notif_active 前先 SIGCONT + 排窗,让 SDK 输入线程 + * 排空冻结期缓冲的 evdev 事件副本;OVERLAY_DRAIN_WINDOW_MS > SDK 200ms 节流。 + * S2 解锁状态栏恢复 → overlay_force_sb_resync():解锁后必须强制 g_sb_mode_last=-1 重叠加, + * 不等 app 循环 1s 慢刷(消除「解锁回 App 无状态栏」残留)。 + * S3 App 死亡触发主屏重绘 → lock_overlay_app_died_exit():锁屏叠加期 App 崩溃(僵尸)必须走 + * 统一退出路径(交回 waitpid 循环回收 + run_menu 重绘主屏含状态栏)。 + * + * 编译运行(在 patch_v2/meow_launcherd 下,无其它依赖): + * gcc -Wall -Wextra -o test_overlay_gate test_overlay_gate.c && ./test_overlay_gate + * 全 PASS -> exit 0。 + */ +#include +#include "overlay_gate.h" +#include "lock_screen.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) +{ + /* ---- S1 输入穿透:SIGSTOP 过的 App 关闭时必须先排空再 unlink ---- */ + CHECK(overlay_drain_required(0) == 1, "S1 drain required (app was SIGSTOP/frozen)"); + CHECK(overlay_drain_required(1) == 0, "S1 no drain needed (media keep-alive, never stopped)"); + /* 排空窗必须大于 SDK overlay_input_suppressed 的 200ms 标记节流窗,保证恢复后确实重新 stat 到 + * 标记再整批丢弃缓冲事件;此处断言契约不被踩线。 */ + CHECK(OVERLAY_DRAIN_WINDOW_MS > 200, "S1 drain window > SDK 200ms throttle"); + + /* ---- S2 解锁状态栏恢复:必须强制重同步 ---- */ + CHECK(overlay_force_sb_resync() == 1, "S2 force status bar re-sync after unlock"); + + /* ---- S3 App 死亡触发主屏重绘:崩溃必须走统一退出路径 ---- */ + CHECK(lock_overlay_app_died_exit(1) == 1, "S3 app died -> unified exit path (redraw home)"); + CHECK(lock_overlay_app_died_exit(0) == 0, "S3 app alive -> normal restore"); + + /* ---- §310 锁屏叠加期中止谓词(lock_screen.h 纯判定)---- */ + CHECK(lck_overlay_should_abort(0, 0) == 0, "S3 abort=0 (menu lock, no overlay pid)"); + CHECK(lck_overlay_should_abort(0, 1) == 0, "S3 abort=0 (no overlay pid, zombie ignored)"); + CHECK(lck_overlay_should_abort(4321, 0) == 0, "S3 abort=0 (overlay pid, app alive)"); + CHECK(lck_overlay_should_abort(4321, 1) == 1, "S3 abort=1 (overlay pid + zombie -> abort lock)"); + + printf("\n%s (%d passed, %d failed)\n", g_fail ? "SOME TESTS FAILED" : "ALL PASS", g_pass, g_fail); + return g_fail ? 1 : 0; +}