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:
1 parent
7b32d775c0
commit
008e177307
6 files changed
+409
-40
No files matched your search
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user