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
@@ -143,7 +143,7 @@ http://<host>.local:8080/
|
|||||||
| GET | `/api/apps/:pkg/export` | `nyai export` 下载流 | ✅ |
|
| GET | `/api/apps/:pkg/export` | `nyai export` 下载流 | ✅ |
|
||||||
| DELETE | `/api/apps/:pkg` | `nyai remove [--purge]` | ✅ |
|
| DELETE | `/api/apps/:pkg` | `nyai remove [--purge]` | ✅ |
|
||||||
| POST | `/api/key/:code` | 按键注入(tap/long;`ms` clamp 0..1000;电源 116 已下线) | ✅(物理键红线内) |
|
| 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` | `MeowOS/logs` 文件列表(每文件含 `group` 来源分组) | ✅(无卡→503) |
|
||||||
| GET | `/api/logs/crash` | **崩溃汇总**:解析最新 `crash_<bootts>.log`(meow_crash 落盘格式)→ 每实体崩溃数 + 最近崩溃时间/信号 + dmesg 计数 | ✅(无卡→503;无 crash 日志→空汇总) |
|
| GET | `/api/logs/crash` | **崩溃汇总**:解析最新 `crash_<bootts>.log`(meow_crash 落盘格式)→ 每实体崩溃数 + 最近崩溃时间/信号 + dmesg 计数 | ✅(无卡→503;无 crash 日志→空汇总) |
|
||||||
| GET | `/api/logs/read?file=&lines=` | 读日志(上限 1000 行;`..`/绝对→403;敏感名→403) | ✅ |
|
| GET | `/api/logs/read?file=&lines=` | 读日志(上限 1000 行;`..`/绝对→403;敏感名→403) | ✅ |
|
||||||
@@ -238,8 +238,12 @@ http://<host>.local:8080/
|
|||||||
(一条 `{"press":"long","ms":8000}` 即强制关机)。电源/关机只走 `/api/system/shutdown`(前端有确认弹窗)。
|
(一条 `{"press":"long","ms":8000}` 即强制关机)。电源/关机只走 `/api/system/shutdown`(前端有确认弹窗)。
|
||||||
- `tap`(默认,抬起间隔 80ms)、`long`(按住 ms,缺省 1000);`hold` → 501。
|
- `tap`(默认,抬起间隔 80ms)、`long`(按住 ms,缺省 1000);`hold` → 501。
|
||||||
**`ms` 全局 clamp 到 0..1000**(`mp_key_clamp_ms`),单键与批量同一上限。
|
**`ms` 全局 clamp 到 0..1000**(`mp_key_clamp_ms`),单键与批量同一上限。
|
||||||
- 注入在**独立线程**里做按下→等待→抬起(`inject_key_async`):修复前是在请求线程 `usleep(ms)`,
|
- 注入在**串行队列**里执行(`mp_keyq.c` 单 worker + FIFO):请求线程只 open 设备 + 入队后立即返回,
|
||||||
一个键请求就把单线程 8080 服务整段阻塞。设备 fd 仍由请求线程同步 open,打不开 → 500。
|
一个键请求不会阻塞单线程 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;注入为**即时性注入**,
|
- 局限:若 `/dev/input/event0/event2` 在设备上不可写(权限/路径差异),注入失败 → 500;注入为**即时性注入**,
|
||||||
不在 launcher 内维护长按状态机(与实体按键的 `longpressed` 语义近似,见 README 已知边界)。
|
不在 launcher 内维护长按状态机(与实体按键的 `longpressed` 语义近似,见 README 已知边界)。
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
char *read_text_file_limited(const char *abs, long long *size, int *truncated);
|
||||||
int keycode_to_device(int code, const char **dev);
|
int keycode_to_device(int code, const char **dev);
|
||||||
int inject_key_event(const char *dev, int code, int down);
|
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 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);
|
int apps_scan_dir(const char *appdir, const char *name, sbuf_t *out, int want_detail);
|
||||||
void req_free(req_t *r);
|
void req_free(req_t *r);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/* mp_key.c — 远程控制域:按键注入(写 /dev/input/event* EV_KEY,物理键红线内)。 */
|
/* mp_key.c — 远程控制域:按键注入(写 /dev/input/event* EV_KEY,物理键红线内)。 */
|
||||||
#include "mp.h"
|
#include "mp.h"
|
||||||
#include "mp_argsafe.h" /* M64 P1-3:press_ms 上限 */
|
#include "mp_argsafe.h" /* M64 P1-3:press_ms 上限 */
|
||||||
#include <pthread.h> /* 按键释放放独立线程:不再用 usleep 阻塞单线程 panel */
|
#include "mp_keyq.h" /* M64 r1 P2-1:串行注入队列(不阻塞 8080,也不合并连续按键) */
|
||||||
|
|
||||||
#define PWR_KEYDEV "/dev/input/event0"
|
#define PWR_KEYDEV "/dev/input/event0"
|
||||||
#define ADC_KEYDEV "/dev/input/event2"
|
#define ADC_KEYDEV "/dev/input/event2"
|
||||||
@@ -49,39 +49,47 @@ int inject_key_event(const char *dev, int code, int down)
|
|||||||
return ok;
|
return ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
typedef struct { int fd; int code; int press_ms; } keyjob_t;
|
/* 队列 worker 调用的注入体:按下→等→抬起,一个键完整走完才轮到下一个。
|
||||||
|
* fd 由本函数负责 close(mp_keyq.h 契约)。 */
|
||||||
/* 按下→等→松开在独立线程里做。修复前是请求线程里 usleep(press_ms)(还无上限):
|
static void ev_sink(int fd, int code, int press_ms)
|
||||||
* 一个键请求就把单线程 8080 服务整段阻塞住。press_ms 已 clamp 到 ≤1000ms,
|
|
||||||
* 线程是 drop 型(detach,自带 job 内存与 fd),请求线程立即返回。
|
|
||||||
* 设备 fd 仍由请求线程同步 open(失败立刻如实报 500,保住原有错误语义)。 */
|
|
||||||
static void *keyjob_run(void *arg)
|
|
||||||
{
|
{
|
||||||
keyjob_t *j = arg;
|
if (write_key_fd(fd, code, 1) == 0) {
|
||||||
if (write_key_fd(j->fd, j->code, 1) == 0) {
|
usleep((useconds_t)(press_ms > 0 ? press_ms : 80) * 1000);
|
||||||
usleep((useconds_t)(j->press_ms > 0 ? j->press_ms : 80) * 1000);
|
write_key_fd(fd, code, 0);
|
||||||
write_key_fd(j->fd, j->code, 0);
|
|
||||||
}
|
}
|
||||||
close(j->fd);
|
close(fd);
|
||||||
free(j);
|
}
|
||||||
return NULL;
|
|
||||||
|
/* 把一个按键序列作为**一个作业**提交到串行队列:
|
||||||
|
* - 作业内按键顺序执行,作业之间按提交顺序执行(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)
|
int inject_key_async(int code, int press_ms)
|
||||||
{
|
{
|
||||||
const char *dev = NULL;
|
return inject_key_seq(&code, &press_ms, 1);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int api_key_single(int sock, int code, const req_t *r)
|
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);
|
int dispatchable = (keycode_to_device(code, &tmpdev) >= 0);
|
||||||
if (!dispatchable)
|
if (!dispatchable)
|
||||||
return send_err(sock, 422, "UNSUPPORTED", "不支持的键码(仅音量/播放/上下曲)");
|
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", "按键注入失败(无法打开输入设备)");
|
return send_err(sock, 500, "INTERNAL", "按键注入失败(无法打开输入设备)");
|
||||||
sbuf_t s = {0};
|
sbuf_t s = {0};
|
||||||
sA(&s, "{\"code\":%d,\"press\":\"%s\",\"dispatched\":true}", code, press);
|
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");
|
if (!root) return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON");
|
||||||
jv_t *keys = jv_obj_get(root, "keys");
|
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 数组"); }
|
if (!keys || keys->type != J_ARR) { jv_free(root); return send_err(sock, 400, "BAD_REQUEST", "缺 keys 数组"); }
|
||||||
sbuf_t s = {0};
|
/* P2-1:整个批次作为**一个作业**提交 → 批次内顺序与提交顺序都保证(修复前每个键起一个
|
||||||
int first = 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) {
|
for (const jv_t *k = keys->child; k; k = k->next) {
|
||||||
if (k->type != J_OBJ) continue;
|
if (k->type != J_OBJ) continue;
|
||||||
jv_t *c = jv_obj_get(k, "code");
|
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:与单键同一上限 */
|
press_ms = mp_key_clamp_ms(press_ms); /* M64 P1-3:与单键同一上限 */
|
||||||
const char *tmpdev = NULL;
|
const char *tmpdev = NULL;
|
||||||
if (keycode_to_device(code, &tmpdev) < 0) continue;
|
if (keycode_to_device(code, &tmpdev) < 0) continue;
|
||||||
if (inject_key_async(code, press_ms) < 0) continue;
|
if (n >= MP_KEYQ_JOB_MAX) { overflow = 1; break; }
|
||||||
if (!first) sA(&s, ",");
|
codes[n] = code;
|
||||||
first = 0;
|
ms[n] = press_ms;
|
||||||
sA(&s, "%d", code);
|
n++;
|
||||||
}
|
}
|
||||||
jv_free(root);
|
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};
|
sbuf_t o = {0};
|
||||||
if (first) sA(&o, "{\"dispatched\":[]}");
|
if (!s.b) sA(&o, "{\"dispatched\":[]}");
|
||||||
else sA(&o, "{\"dispatched\":[%s]}", s.b);
|
else sA(&o, "{\"dispatched\":[%s]}", s.b);
|
||||||
int ret = send_json(sock, 200, o.b ? o.b : "");
|
int ret = send_json(sock, 200, o.b ? o.b : "");
|
||||||
sbuf_free(&s); sbuf_free(&o);
|
sbuf_free(&s); sbuf_free(&o);
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
/* mp_keyq.c — 按键注入串行队列(单 worker + FIFO,见 mp_keyq.h 背景)。
|
||||||
|
* 请求线程只入队;worker 串行执行,保证 launcher 的 EVIOCGKEY 状态采样能看到每一次
|
||||||
|
* press/release 边沿(连续 tap 不再被合并成一次)。 */
|
||||||
|
#include "mp_keyq.h"
|
||||||
|
|
||||||
|
#include <pthread.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
@@ -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 */
|
||||||
@@ -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