Files
MeowOS-For-R3P2/patch_v2/meowpanel/test_mp_keyq.c
T
AskaEth 008e177307 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)本轮一行未动。
2026-11-12 08:50:10 +08:00

188 lines
7.2 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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;
}