diff --git a/patch_v2/meowpanel/README.md b/patch_v2/meowpanel/README.md index f3b2a67..ff94c87 100644 --- a/patch_v2/meowpanel/README.md +++ b/patch_v2/meowpanel/README.md @@ -143,7 +143,7 @@ http://.local:8080/ | GET | `/api/apps/:pkg/export` | `nyai export` 下载流 | ✅ | | DELETE | `/api/apps/:pkg` | `nyai remove [--purge]` | ✅ | | POST | `/api/key/:code` | 按键注入(tap/long;`ms` clamp 0..1000;电源 116 已下线) | ✅(物理键红线内) | -| POST | `/api/key` | 批量/顺序注入 | ✅ | +| POST | `/api/key` | 批量/顺序注入(一个批次=一个串行作业) | ✅ | | GET | `/api/logs` | `MeowOS/logs` 文件列表(每文件含 `group` 来源分组) | ✅(无卡→503) | | GET | `/api/logs/crash` | **崩溃汇总**:解析最新 `crash_.log`(meow_crash 落盘格式)→ 每实体崩溃数 + 最近崩溃时间/信号 + dmesg 计数 | ✅(无卡→503;无 crash 日志→空汇总) | | GET | `/api/logs/read?file=&lines=` | 读日志(上限 1000 行;`..`/绝对→403;敏感名→403) | ✅ | @@ -238,8 +238,12 @@ http://.local:8080/ (一条 `{"press":"long","ms":8000}` 即强制关机)。电源/关机只走 `/api/system/shutdown`(前端有确认弹窗)。 - `tap`(默认,抬起间隔 80ms)、`long`(按住 ms,缺省 1000);`hold` → 501。 **`ms` 全局 clamp 到 0..1000**(`mp_key_clamp_ms`),单键与批量同一上限。 -- 注入在**独立线程**里做按下→等待→抬起(`inject_key_async`):修复前是在请求线程 `usleep(ms)`, - 一个键请求就把单线程 8080 服务整段阻塞。设备 fd 仍由请求线程同步 open,打不开 → 500。 +- 注入在**串行队列**里执行(`mp_keyq.c` 单 worker + FIFO):请求线程只 open 设备 + 入队后立即返回, + 一个键请求不会阻塞单线程 8080 服务;worker 按提交顺序逐个「按下→等待→抬起」,两次按键之间留 + `MP_KEYQ_GAP_MS`(20ms) 间隔——launcher 读键是 10ms 的 EVIOCGKEY 状态采样,release 后立刻 press + 会被采样合并(连续 tap 退化成 1 次)。批次 = 一个作业,批次内顺序与批次间顺序都保证。 + 队列总时长预算 2000ms:超预算/队列满立即 **503 `BUSY`**(不做无界等待,单核无界等=挂死)。 + 设备 fd 仍由请求线程同步 open,打不开 → 500。 - 局限:若 `/dev/input/event0/event2` 在设备上不可写(权限/路径差异),注入失败 → 500;注入为**即时性注入**, 不在 launcher 内维护长按状态机(与实体按键的 `longpressed` 语义近似,见 README 已知边界)。 diff --git a/patch_v2/meowpanel/src/mp.h b/patch_v2/meowpanel/src/mp.h index ddbdd9a..b28e308 100644 --- a/patch_v2/meowpanel/src/mp.h +++ b/patch_v2/meowpanel/src/mp.h @@ -323,7 +323,8 @@ int files_list(int sock, const char *relpath); char *read_text_file_limited(const char *abs, long long *size, int *truncated); int keycode_to_device(int code, const char **dev); int inject_key_event(const char *dev, int code, int down); -int inject_key_async(int code, int press_ms); /* M64 P1-3:释放放独立线程,不阻塞 panel */ +int inject_key_async(int code, int press_ms); /* 入串行队列后立即返回(0/-1 设备/-2 队列忙) */ +int inject_key_seq(const int *codes, const int *press_ms, int n); /* 同上,一批按键一个作业 */ int read_small_file(const char *path, char *buf, int cap); int apps_scan_dir(const char *appdir, const char *name, sbuf_t *out, int want_detail); void req_free(req_t *r); diff --git a/patch_v2/meowpanel/src/mp_key.c b/patch_v2/meowpanel/src/mp_key.c index f6bbb0a..491b55b 100644 --- a/patch_v2/meowpanel/src/mp_key.c +++ b/patch_v2/meowpanel/src/mp_key.c @@ -1,7 +1,7 @@ /* mp_key.c — 远程控制域:按键注入(写 /dev/input/event* EV_KEY,物理键红线内)。 */ #include "mp.h" #include "mp_argsafe.h" /* M64 P1-3:press_ms 上限 */ -#include /* 按键释放放独立线程:不再用 usleep 阻塞单线程 panel */ +#include "mp_keyq.h" /* M64 r1 P2-1:串行注入队列(不阻塞 8080,也不合并连续按键) */ #define PWR_KEYDEV "/dev/input/event0" #define ADC_KEYDEV "/dev/input/event2" @@ -49,39 +49,47 @@ int inject_key_event(const char *dev, int code, int down) return ok; } -typedef struct { int fd; int code; int press_ms; } keyjob_t; - -/* 按下→等→松开在独立线程里做。修复前是请求线程里 usleep(press_ms)(还无上限): - * 一个键请求就把单线程 8080 服务整段阻塞住。press_ms 已 clamp 到 ≤1000ms, - * 线程是 drop 型(detach,自带 job 内存与 fd),请求线程立即返回。 - * 设备 fd 仍由请求线程同步 open(失败立刻如实报 500,保住原有错误语义)。 */ -static void *keyjob_run(void *arg) +/* 队列 worker 调用的注入体:按下→等→抬起,一个键完整走完才轮到下一个。 + * fd 由本函数负责 close(mp_keyq.h 契约)。 */ +static void ev_sink(int fd, int code, int press_ms) { - keyjob_t *j = arg; - if (write_key_fd(j->fd, j->code, 1) == 0) { - usleep((useconds_t)(j->press_ms > 0 ? j->press_ms : 80) * 1000); - write_key_fd(j->fd, j->code, 0); + if (write_key_fd(fd, code, 1) == 0) { + usleep((useconds_t)(press_ms > 0 ? press_ms : 80) * 1000); + write_key_fd(fd, code, 0); } - close(j->fd); - free(j); - return NULL; + close(fd); +} + +/* 把一个按键序列作为**一个作业**提交到串行队列: + * - 作业内按键顺序执行,作业之间按提交顺序执行(P2-1:连续 tap 不再合并); + * - 请求线程只 open + 入队,不等待注入完成(P1-3:8080 不被阻塞)。 + * 设备 fd 仍在请求线程同步 open(失败立刻如实报 500,保住原有错误语义)。 + * 返回 0=已入队;-1=设备打不开/键码不支持;-2=队列忙(调用方回 503,别重试到无界等待)。 */ +int inject_key_seq(const int *codes, const int *press_ms, int n) +{ + int fds[MP_KEYQ_JOB_MAX], ms[MP_KEYQ_JOB_MAX]; + int opened = 0, rc = -1; + if (!codes || !press_ms || n <= 0 || n > MP_KEYQ_JOB_MAX) return -1; + for (int i = 0; i < n; i++) { + const char *dev = NULL; + if (keycode_to_device(codes[i], &dev) < 0) goto fail; + int fd = open(dev, O_WRONLY | O_NONBLOCK); + if (fd < 0) goto fail; + fds[opened] = fd; + ms[opened] = mp_key_clamp_ms(press_ms[i]); + opened++; + } + rc = mp_keyq_submit(fds, codes, ms, opened, ev_sink); + if (rc != MP_KEYQ_OK) goto fail; + return 0; +fail: + for (int i = 0; i < opened; i++) close(fds[i]); + return rc == MP_KEYQ_ERR_BUSY ? -2 : -1; } int inject_key_async(int code, int press_ms) { - const char *dev = NULL; - if (keycode_to_device(code, &dev) < 0) return -1; - int fd = open(dev, O_WRONLY | O_NONBLOCK); - if (fd < 0) return -1; - keyjob_t *j = malloc(sizeof *j); - if (!j) { close(fd); return -1; } - j->fd = fd; - j->code = code; - j->press_ms = mp_key_clamp_ms(press_ms); - pthread_t t; - if (pthread_create(&t, NULL, keyjob_run, j) != 0) { close(fd); free(j); return -1; } - pthread_detach(t); - return 0; + return inject_key_seq(&code, &press_ms, 1); } int api_key_single(int sock, int code, const req_t *r) @@ -116,7 +124,10 @@ int api_key_single(int sock, int code, const req_t *r) int dispatchable = (keycode_to_device(code, &tmpdev) >= 0); if (!dispatchable) return send_err(sock, 422, "UNSUPPORTED", "不支持的键码(仅音量/播放/上下曲)"); - if (inject_key_async(code, press_ms) < 0) + int irc = inject_key_async(code, press_ms); + if (irc == -2) + return send_err(sock, 503, "BUSY", "按键队列忙(连续按键过快或按键总时长超预算),请稍后重试"); + if (irc < 0) return send_err(sock, 500, "INTERNAL", "按键注入失败(无法打开输入设备)"); sbuf_t s = {0}; sA(&s, "{\"code\":%d,\"press\":\"%s\",\"dispatched\":true}", code, press); @@ -132,8 +143,9 @@ int api_key_batch(int sock, const req_t *r) if (!root) return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON"); jv_t *keys = jv_obj_get(root, "keys"); if (!keys || keys->type != J_ARR) { jv_free(root); return send_err(sock, 400, "BAD_REQUEST", "缺 keys 数组"); } - sbuf_t s = {0}; - int first = 1; + /* P2-1:整个批次作为**一个作业**提交 → 批次内顺序与提交顺序都保证(修复前每个键起一个 + * 线程并发注入,launcher 的 EVIOCGKEY 采样会把重叠 press 合并成一次)。 */ + int codes[MP_KEYQ_JOB_MAX], ms[MP_KEYQ_JOB_MAX], n = 0, overflow = 0; for (const jv_t *k = keys->child; k; k = k->next) { if (k->type != J_OBJ) continue; jv_t *c = jv_obj_get(k, "code"); @@ -147,14 +159,27 @@ int api_key_batch(int sock, const req_t *r) press_ms = mp_key_clamp_ms(press_ms); /* M64 P1-3:与单键同一上限 */ const char *tmpdev = NULL; if (keycode_to_device(code, &tmpdev) < 0) continue; - if (inject_key_async(code, press_ms) < 0) continue; - if (!first) sA(&s, ","); - first = 0; - sA(&s, "%d", code); + if (n >= MP_KEYQ_JOB_MAX) { overflow = 1; break; } + codes[n] = code; + ms[n] = press_ms; + n++; } jv_free(root); + if (overflow) return send_err(sock, 400, "BAD_REQUEST", "单次批量按键最多 32 个"); + sbuf_t s = {0}; + if (n > 0) { + int irc = inject_key_seq(codes, ms, n); + if (irc == -2) + return send_err(sock, 503, "BUSY", "按键队列忙(连续按键过快或按键总时长超预算),请稍后重试"); + if (irc < 0) + return send_err(sock, 500, "INTERNAL", "按键注入失败(无法打开输入设备)"); + for (int i = 0; i < n; i++) { + if (i) sA(&s, ","); + sA(&s, "%d", codes[i]); + } + } sbuf_t o = {0}; - if (first) sA(&o, "{\"dispatched\":[]}"); + if (!s.b) sA(&o, "{\"dispatched\":[]}"); else sA(&o, "{\"dispatched\":[%s]}", s.b); int ret = send_json(sock, 200, o.b ? o.b : ""); sbuf_free(&s); sbuf_free(&o); diff --git a/patch_v2/meowpanel/src/mp_keyq.c b/patch_v2/meowpanel/src/mp_keyq.c new file mode 100644 index 0000000..c60b0cc --- /dev/null +++ b/patch_v2/meowpanel/src/mp_keyq.c @@ -0,0 +1,102 @@ +/* mp_keyq.c — 按键注入串行队列(单 worker + FIFO,见 mp_keyq.h 背景)。 + * 请求线程只入队;worker 串行执行,保证 launcher 的 EVIOCGKEY 状态采样能看到每一次 + * press/release 边沿(连续 tap 不再被合并成一次)。 */ +#include "mp_keyq.h" + +#include +#include +#include + +typedef struct { + int n; + int dur; /* 本作业总时长(ms),用于队列预算 */ + mp_keyq_sink_t sink; + int fd[MP_KEYQ_JOB_MAX]; + int code[MP_KEYQ_JOB_MAX]; + int ms[MP_KEYQ_JOB_MAX]; +} keyjob_t; + +static pthread_mutex_t g_mu = PTHREAD_MUTEX_INITIALIZER; +static pthread_cond_t g_cv = PTHREAD_COND_INITIALIZER; +static keyjob_t g_q[MP_KEYQ_LEN]; +static int g_head, g_tail, g_count; +static int g_busy_ms; /* 队列中(含执行中)作业的时长合计 */ +static int g_worker_up; +static int g_done; + +/* 与 mp_key.c 的 tap 节奏一致:ms<=0 按 80ms 计;每个键后面还有 MP_KEYQ_GAP_MS 的间隔, + * 让 launcher 的 10ms 状态采样能看到「抬起→再按下」的边沿(否则连续 tap 仍被合并)。 */ +static int job_dur(const int *ms, int n) +{ + int d = 0; + for (int i = 0; i < n; i++) d += (ms[i] > 0 ? ms[i] : 80) + MP_KEYQ_GAP_MS; + return d; +} + +static void *keyq_worker(void *arg) +{ + (void)arg; + for (;;) { + pthread_mutex_lock(&g_mu); + while (!g_count) pthread_cond_wait(&g_cv, &g_mu); + keyjob_t j = g_q[g_head]; + g_head = (g_head + 1) % MP_KEYQ_LEN; + g_count--; + pthread_mutex_unlock(&g_mu); + + for (int i = 0; i < j.n; i++) { + j.sink(j.fd[i], j.code[i], j.ms[i]); + usleep(MP_KEYQ_GAP_MS * 1000); /* 抬起到下一次按下之间留出采样窗口 */ + } + + pthread_mutex_lock(&g_mu); + g_busy_ms -= j.dur; + g_done++; + pthread_mutex_unlock(&g_mu); + } + return NULL; +} + +int mp_keyq_submit(const int *fds, const int *codes, const int *press_ms, int n, + mp_keyq_sink_t sink) +{ + if (!fds || !codes || !press_ms || !sink || n <= 0 || n > MP_KEYQ_JOB_MAX) + return MP_KEYQ_ERR_BUSY; + int dur = job_dur(press_ms, n); + + pthread_mutex_lock(&g_mu); + if (!g_worker_up) { + pthread_t t; + if (pthread_create(&t, NULL, keyq_worker, NULL) != 0) { + pthread_mutex_unlock(&g_mu); + return MP_KEYQ_ERR_BUSY; + } + pthread_detach(t); + g_worker_up = 1; + } + if (g_count >= MP_KEYQ_LEN || g_busy_ms + dur > MP_KEYQ_BUDGET_MS) { + pthread_mutex_unlock(&g_mu); + return MP_KEYQ_ERR_BUSY; + } + keyjob_t *j = &g_q[g_tail]; + j->n = n; + j->dur = dur; + j->sink = sink; + memcpy(j->fd, fds, sizeof j->fd[0] * (size_t)n); + memcpy(j->code, codes, sizeof j->code[0] * (size_t)n); + memcpy(j->ms, press_ms, sizeof j->ms[0] * (size_t)n); + g_tail = (g_tail + 1) % MP_KEYQ_LEN; + g_count++; + g_busy_ms += dur; + pthread_cond_signal(&g_cv); + pthread_mutex_unlock(&g_mu); + return MP_KEYQ_OK; +} + +int mp_keyq_done_count(void) +{ + pthread_mutex_lock(&g_mu); + int d = g_done; + pthread_mutex_unlock(&g_mu); + return d; +} diff --git a/patch_v2/meowpanel/src/mp_keyq.h b/patch_v2/meowpanel/src/mp_keyq.h new file mode 100644 index 0000000..ffca52c --- /dev/null +++ b/patch_v2/meowpanel/src/mp_keyq.h @@ -0,0 +1,50 @@ +/* mp_keyq.h — 按键注入串行队列(M64 r1 P2-1)。 + * + * 背景:M64 P1-3 把「按下→等待→抬起」移出请求线程(避免阻塞单线程 8080),最初的实现是 + * 每个请求起一个 detached 线程 —— 于是**连续 tap 会并发重叠**:launcher 侧读键是 + * EVIOCGKEY 状态采样(keys.c kp_poll / main_menu.c 每轮采样一次),N 个重叠的 press 只 + * 产生 1 个 DOWN 边沿 → 滑动调音量 5 步退化成 1 步,batch 的「顺序」语义也失效。 + * + * 本模块用一个常驻 worker + FIFO 队列恢复串行语义: + * - 请求线程只做 open() + 入队(**从不阻塞、从不等待注入完成**),随后立即返回; + * - 一个作业内的按键按数组顺序串行执行,作业之间也按提交顺序串行; + * - 队列有总时长预算(MP_KEYQ_BUDGET_MS):超预算或队列满立即返回 MP_KEYQ_ERR_BUSY, + * 调用方回 503 明确错误,绝不做无界等待(单核设备上无界等就是挂死)。 + * + * fd 生命周期:入队成功后由 sink 负责(生产 sink 在执行完 press/release 后 close); + * 入队失败时 fd 仍属调用方,由调用方 close。本模块自己不碰 fd 的关闭。 + */ +#ifndef MP_KEYQ_H +#define MP_KEYQ_H + +#ifdef __cplusplus +extern "C" { +#endif + +#define MP_KEYQ_JOB_MAX 32 /* 单个作业(一次批量请求)最多按键数 */ +#define MP_KEYQ_LEN 32 /* 队列最多作业数(内存上限;通常先撞时间预算) */ +#define MP_KEYQ_BUDGET_MS 2000 /* 队列总时长预算(含正在执行的作业) */ +/* 两次按键之间的强制间隔。launcher 用 EVIOCGKEY 状态轮询读键(APPS.md:10ms 采样), + * 只有采样到「抬起」才会把下一次 press 认成新的按下;release 后立刻 press 会被采样合并 + * ——这正是 P2-1 的 5 步变 1 步。20ms = 2× 采样周期,保证每次按下都被看见。 */ +#define MP_KEYQ_GAP_MS 20 + +#define MP_KEYQ_OK 0 +#define MP_KEYQ_ERR_BUSY (-2) + +/* 注入回调:串行调用,永不同时进入。实现方负责 close(fd)。 */ +typedef void (*mp_keyq_sink_t)(int fd, int code, int press_ms); + +/* 提交一组按键(n = 1..MP_KEYQ_JOB_MAX)。fds/codes/press_ms 在返回前复制到队列内部。 + * 返回 MP_KEYQ_OK 或 MP_KEYQ_ERR_BUSY(队列满/超预算)。 */ +int mp_keyq_submit(const int *fds, const int *codes, const int *press_ms, int n, + mp_keyq_sink_t sink); + +/* 已完成作业数(测试/诊断用)。 */ +int mp_keyq_done_count(void); + +#ifdef __cplusplus +} +#endif + +#endif /* MP_KEYQ_H */ diff --git a/patch_v2/meowpanel/test_mp_keyq.c b/patch_v2/meowpanel/test_mp_keyq.c new file mode 100644 index 0000000..c2fcd2d --- /dev/null +++ b/patch_v2/meowpanel/test_mp_keyq.c @@ -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 +#include +#include +#include + +#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; +}