fix(panel): M64 r1 P2-1 连续/批量按键被异步注入合并 — 改单 worker 串行队列

/api/key 改异步后每个请求起一个 detached 线程,N 次 press 全部落在同一段里;launcher 用
EVIOCGKEY 10ms 状态采样读键,重叠 press 只产生 1 个 DOWN 边沿 → 滑动调音量 5 步变 1 步、
batch 顺序语义失效。

- 新增 src/mp_keyq.{c,h}:常驻 worker + FIFO 队列。请求线程只 open + 入队后立即返回
  (8080 仍不被阻塞);worker 按提交顺序逐个「按下→等待→抬起」,两次按键之间留
  MP_KEYQ_GAP_MS(20ms)=2× 采样周期,保证每次按下都被采样到。
- 批次 = 一个作业:批次内顺序、批次间顺序都保证;payload 上限 MP_KEYQ_JOB_MAX=32。
- 有界等待:队列总时长预算 2000ms(含执行中作业),超预算/队列满立即回 503 BUSY,
  无界等待(单核即挂死)被排除。
- mp_key.c:inject_key_seq() 统一 open+clamp+入队(-1 设备 / -2 队列忙),
  api_key_single / api_key_batch 都走它;设备打不开仍同步报 500(原语义不变)。
- 测试 test_mp_keyq.c(宿主,不碰 /dev/input、不发 /api/key)反向控制:修复前的并发注入
  5 连击被 10ms 采样器数成 1 次按下;修复后同一 5 连击数成 5 次且顺序一致;批次 3 个同键
  tap + 紧随一个单键 = 4 次按下不丢步;超预算立即被拒、释放后可继续提交。

安全三条(P1-1/P1-2/P1-3)本轮一行未动。
This commit is contained in:
AskaEth committed 2026-11-12 08:50:10 +08:00
1 parent 7b32d775c0
commit 008e177307
6 files changed
+409 -40

No files matched your search

+187
View File
@@ -0,0 +1,187 @@
/* test_mp_keyq.c — M64 r1 P2-1 宿主回归:连续 tap 不被合并 + 顺序保证 + 有界预算。
*
* 纯宿主测试:不碰 /dev/input、不发 /api/key、不启面板服务。注入 sink 是记录器,
* 另有一个「电平采样器」线程模拟 launcher 的读键方式(EVIOCGKEY 状态采样:只有从 0→1
* 的跳变才算一次按键,见 meow_launcherd/keys.c kp_poll + main_menu.c 每轮采样)。
*
* 编译运行(在 patch_v2/meowpanel 下):
* cc -O1 -Wall -Wextra -I src -pthread test_mp_keyq.c src/mp_keyq.c -o ~/tmp/test_mp_keyq
* ~/tmp/test_mp_keyq
*
* 反向控制:同一个「5 连击」分别用
* ① 修复前的实现方式(每个按键一个 detached 线程并发 press/sleep/release)→ 采样器只看到 1 次按下;
* ② 修复后的队列(mp_keyq_submit 一个作业 / 5 个单键作业)→ 采样器看到 5 次按下。
*/
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include "mp_keyq.h"
static int g_fail;
#define CHECK(cond, ...) do { \
if (!(cond)) { printf(" FAIL: "); printf(__VA_ARGS__); printf("\n"); g_fail = 1; } \
else { printf(" PASS: "); printf(__VA_ARGS__); printf("\n"); } \
} while (0)
/* ---- 被注入的「设备」:单一电平(key down/up) ---- */
static pthread_mutex_t g_dev_mu = PTHREAD_MUTEX_INITIALIZER;
static int g_level; /* 0=抬起 1=按下 */
static void dev_set(int v)
{
pthread_mutex_lock(&g_dev_mu);
g_level = v;
pthread_mutex_unlock(&g_dev_mu);
}
static int dev_get(void)
{
pthread_mutex_lock(&g_dev_mu);
int v = g_level;
pthread_mutex_unlock(&g_dev_mu);
return v;
}
/* ---- 记录器 sink:按调用顺序记录,并驱动设备电平 ---- */
#define REC_MAX 64
static int g_rec_code[REC_MAX];
static int g_rec_n;
static void rec_sink(int fd, int code, int press_ms)
{
(void)fd;
dev_set(1);
usleep((useconds_t)(press_ms > 0 ? press_ms : 80) * 1000);
dev_set(0);
/* 记录顺序与时序(在 release 之后记,避免与记录数组本身的并发) */
pthread_mutex_lock(&g_dev_mu);
if (g_rec_n < REC_MAX) g_rec_code[g_rec_n] = code;
g_rec_n++;
pthread_mutex_unlock(&g_dev_mu);
}
/* ---- 电平采样器:2ms 一次,只数 0→1 的跳变(模拟 launcher kp_poll) ---- */
static volatile int g_samp_stop, g_samp_edges;
static void *sampler(void *arg)
{
(void)arg;
int prev = 0;
while (!g_samp_stop) {
int cur = dev_get();
if (cur && !prev) g_samp_edges++;
prev = cur;
usleep(2 * 1000);
}
return NULL;
}
static void rec_reset(void)
{
pthread_mutex_lock(&g_dev_mu);
g_rec_n = 0;
memset(g_rec_code, 0, sizeof g_rec_code);
pthread_mutex_unlock(&g_dev_mu);
g_samp_edges = 0;
dev_set(0);
}
/* 等队列把 n 个作业执行完(有界等待,测试用)。 */
static int wait_done(int base, int n, int timeout_ms)
{
for (int w = 0; w < timeout_ms; w += 5) {
if (mp_keyq_done_count() - base >= n) return 1;
usleep(5 * 1000);
}
return 0;
}
/* 修复前的做法:每个按键一个 detached 线程,press→sleep→release 并发跑。 */
static void *legacy_thread(void *arg)
{
int ms = (int)(long)arg;
dev_set(1);
usleep((useconds_t)ms * 1000);
dev_set(0);
return NULL;
}
int main(void)
{
pthread_t st;
pthread_create(&st, NULL, sampler, NULL);
usleep(10 * 1000); /* 让采样器先跑起来 */
printf("== 反向控制:修复前的并发注入,5 连击只看到 1 次按下 ==\n");
rec_reset();
{
pthread_t t[5];
for (int i = 0; i < 5; i++) pthread_create(&t[i], NULL, legacy_thread, (void *)(long)40);
usleep(120 * 1000); /* 5 个线程同时 press、40ms 后各自 release */
for (int i = 0; i < 5; i++) pthread_join(t[i], NULL);
}
usleep(20 * 1000);
CHECK(g_samp_edges >= 1 && g_samp_edges < 5,
"修复前:5 个重叠 press 被采样成 %d 次按下(< 5,正是滑动只走 1 步的原因)", g_samp_edges);
printf("== 修复后:5 个单键作业串行 → 5 次按下,顺序一致 ==\n");
rec_reset();
int base = mp_keyq_done_count();
int codes5[5] = { 114, 115, 163, 164, 165 };
for (int i = 0; i < 5; i++) {
int fd = i;
int rc = mp_keyq_submit(&fd, &codes5[i], (int[]){ 40 }, 1, rec_sink);
CHECK(rc == MP_KEYQ_OK, "第 %d 个 tap 入队成功(请求线程不等待注入完成)", i + 1);
}
CHECK(wait_done(base, 5, 2000), "5 个作业在预算内执行完");
usleep(20 * 1000);
CHECK(g_samp_edges == 5, "采样器看到 5 次按下(修复前只有 1 次)");
{
int ok = 1;
for (int i = 0; i < 5; i++) if (g_rec_code[i] != codes5[i]) ok = 0;
CHECK(ok && g_rec_n == 5, "记录顺序 = 提交顺序(114,115,163,164,165)");
}
printf("== 修复后:批次内部顺序 + 与下一个请求不重叠(不丢步) ==\n");
rec_reset();
base = mp_keyq_done_count();
{
int fds[3] = { 0, 1, 2 }, codes[3] = { 115, 115, 115 }, ms[3] = { 30, 30, 30 };
int rc = mp_keyq_submit(fds, codes, ms, 3, rec_sink);
CHECK(rc == MP_KEYQ_OK, "批次(3 个同键 tap)作为一个作业入队");
int fd4 = 3, code4 = 114;
rc = mp_keyq_submit(&fd4, &code4, (int[]){ 30 }, 1, rec_sink);
CHECK(rc == MP_KEYQ_OK, "紧随其后的单个请求入队成功(不丢)");
}
CHECK(wait_done(base, 2, 2000), "批次 + 单键在预算内执行完");
usleep(20 * 1000);
CHECK(g_samp_edges == 4, "采样器看到 4 次按下(3 个同键 tap 未被合并)");
CHECK(g_rec_code[0] == 115 && g_rec_code[1] == 115 && g_rec_code[2] == 115 && g_rec_code[3] == 114,
"批次内 3 次 115 顺序执行,之后才是下一个请求的 114");
printf("== 有界预算:超预算立即拒绝,不做无界等待 ==\n");
{
int fd = 9, code = 115;
int base2 = mp_keyq_done_count();
/* 每个作业实际时长 = press_ms + MP_KEYQ_GAP_MS(20),预算 2000ms */
int rc1 = mp_keyq_submit(&fd, &code, (int[]){ 800 }, 1, rec_sink);
CHECK(rc1 == MP_KEYQ_OK, "800ms 作业入队(820ms 预算占用)");
int rc2 = mp_keyq_submit(&fd, &code, (int[]){ 800 }, 1, rec_sink);
CHECK(rc2 == MP_KEYQ_OK, "第二个 800ms 作业入队(累计 1640ms)");
int rc3 = mp_keyq_submit(&fd, &code, (int[]){ 400 }, 1, rec_sink);
CHECK(rc3 == MP_KEYQ_ERR_BUSY, "第三个作业超 2000ms 预算 → 立刻 MP_KEYQ_ERR_BUSY(503,不排队等)");
int rc4 = mp_keyq_submit(&fd, &code, (int[]){ 0 }, MP_KEYQ_JOB_MAX + 1, rec_sink);
CHECK(rc4 == MP_KEYQ_ERR_BUSY, "作业超过 MP_KEYQ_JOB_MAX → 拒绝");
CHECK(wait_done(base2, 2, 4000), "两个 800ms 作业在预算内执行完");
int rc5 = mp_keyq_submit(&fd, &code, (int[]){ 80 }, 1, rec_sink);
CHECK(rc5 == MP_KEYQ_OK, "预算释放后可继续提交(拒绝不是永久性的)");
}
g_samp_stop = 1;
pthread_join(st, NULL);
printf(g_fail ? "\nRESULT: FAIL\n" : "\nRESULT: ALL PASS\n");
return g_fail ? 1 : 0;
}