feat: M6.10–M6.19 批量收编 + M6.19 推卡验收
- M7 launcher 重写 5 期(虚拟化宫格/meow_theme 单一权威/横幅动画/自定义 PIN 键盘) - OOM 通用化(cachegov 上收 SDK)、自动切歌根治、meowpanel jv_free 泄漏修复、Hello 滚动卡顿修复 - ncm 歌词 UI / 媒体通知封面 / 封面分辨率 640 - 5 bug 修复:ncm 后台化 UI 叠层+点击穿透、再进 ncm 桌面叠层、播放页新旧 UI 叠层抢刷脏区(meow_ui_gfx 恢复边沿整帧重画)、PIN 亮屏亮度跳(keys.c meow_panel_on 按 settings_brightness_clamped 恢复)、通知中心封面显歌单封面(cover.c save_cover_raw 加 is_song + 命中块守卫) - 四 App 统一重链新 meow_ui.a 51caee4a;M6.19=0.8.19 固件 2d2331f2 推卡自刷验收全绿
This commit is contained in:
1 parent
c2ee7626a4
commit
3d70a3dd2c
176 files changed
+15507
-597
No files matched your search
@@ -413,6 +413,24 @@ int main(void)
|
||||
CHECK(cur_cover_hash() == 0, "reset -> cover cleared");
|
||||
g_fail_next = 0;
|
||||
|
||||
/* TX: 缓存命中路径——重请求已下载过的 URL(硬盘还有其 song_cov* 条目,无网络)。
|
||||
* 旧码: 缓存命中分支从不更新 g_cover_path → cover_current_path() 恒 NULL → MSG_META
|
||||
* artwork 恒空 → 通知中心媒体卡无封面路径 → 回退显示歌名(真机症状)。
|
||||
* 修复后: 换入时 g_cover_path 指向已落盘的缓存条目 → artwork 非空且可加载出该封面。 */
|
||||
printf("== TX: cache-hit path -> cover_current_path() non-empty (launcher artwork) ==\n");
|
||||
int bx = g_http_calls;
|
||||
cover_request(URL_A, NULL);
|
||||
CHECK(wait_cover_hash(url_hash(URL_A), 3000), "cache-hit: cover A shown (no download)");
|
||||
CHECK(g_http_calls == bx, "cache-hit: 0 http calls (from cache, calls=%d)", g_http_calls - bx);
|
||||
const char *cpx = cover_current_path();
|
||||
CHECK(cpx && cpx[0], "cache-hit: cover_current_path() non-empty (before fix: NULL)");
|
||||
if (cpx && cpx[0]) {
|
||||
meow_img *im = meow_img_load_file(cpx);
|
||||
CHECK(im && im->hash == url_hash(URL_A), "cache-hit: reported path loads cover A");
|
||||
if (im) meow_img_free(im);
|
||||
}
|
||||
cover_ack();
|
||||
|
||||
printf(g_fail ? "== COVER CHAIN SIM: FAIL ==\n" : "== COVER CHAIN SIM: ALL PASS ==\n");
|
||||
return g_fail;
|
||||
}
|
||||
@@ -0,0 +1,291 @@
|
||||
/* http_truncate_sim.c — 截断守卫宿主测试(0 真实网络)
|
||||
*
|
||||
* 背景(agent-355 挖出):core/ncm_http.c 的 http_run() 在 body 读取循环里,
|
||||
* 对端于 Content-Length 未收满时提前关闭(mbedtls_ssl_read 返回 0 /
|
||||
* MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY),loop 后仅判 overflow,否则一律 NCM_OK,
|
||||
* 于是残缺 body 被调用方当完整成功 → ncm_cache_commit 把半截 .part/.tmp 转正。
|
||||
*
|
||||
* 本 sim mock 整个 mbedtls 传输层(仅符号桩,不链真实 libmbedtls),脚本化喂给
|
||||
* ncm_http.c 一个可分块的对端响应流,验证 http_run(经公开入口
|
||||
* ncm_http_download_status)对以下语义的判定:
|
||||
* 定长(Content-Length) 未收满即关闭 → 截断 → 非 NCM_OK(NCM_ERR_IO)
|
||||
* 定长(Content-Length) 收满 → BS_DONE → NCM_OK
|
||||
* Content-Length: 0(空 body) → BS_DONE → NCM_OK
|
||||
* 无 Content-Length、靠对端关闭结束(BS_CLOSE) → 合法 → NCM_OK
|
||||
* chunked 完整(0 分块末端) → BS_DONE → NCM_OK
|
||||
* chunked 中途关闭 → 截断 → 非 NCM_OK(NCM_ERR_IO)
|
||||
*
|
||||
* 编译(在 ncm_app 下):
|
||||
* gcc -O2 -Wall -Wextra -DJSMN_HEADER -I. -Icore -Ivendor/mbedtls/include \
|
||||
* -c bench/http_truncate_sim.c -o /tmp/t.o
|
||||
* gcc -O2 -Wall -Wextra -DJSMN_HEADER -I. -Icore -Ivendor/mbedtls/include \
|
||||
* -ffunction-sections -c core/ncm_http.c -o /tmp/t_http.o
|
||||
* gcc -O2 -Wall -Wextra -DJSMN_HEADER -I. -Icore -Ivendor/mbedtls/include \
|
||||
* -ffunction-sections -c core/ncm_common.c -o /tmp/t_common.o
|
||||
* gcc -Wl,--gc-sections -o http_truncate_sim /tmp/t.o /tmp/t_http.o /tmp/t_common.o
|
||||
*/
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "ncm_http.h"
|
||||
#include "ncm_common.h"
|
||||
|
||||
#include "mbedtls/net_sockets.h"
|
||||
#include "mbedtls/ssl.h"
|
||||
#include "mbedtls/x509_crt.h"
|
||||
|
||||
/* ============================================================
|
||||
* 传输层桩:脚本化"对端读流"。
|
||||
* 每次 ncm_http_download_status 调用前用 tr_script(...) 装填脚本,
|
||||
* 脚本用尽后 mbedtls_ssl_read 返回 g_close_rc(0=有序关闭;或
|
||||
* MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY 模拟 TLS close_notify)。
|
||||
* ============================================================ */
|
||||
typedef struct { const unsigned char *buf; size_t len; } tr_chunk_t;
|
||||
|
||||
static const tr_chunk_t *g_script;
|
||||
static int g_script_n;
|
||||
static int g_script_i;
|
||||
static int g_close_rc; /* 脚本耗尽后的 ssl_read 返回值 */
|
||||
|
||||
static void tr_script(const tr_chunk_t *chunks, int n, int close_rc) {
|
||||
g_script = chunks; g_script_n = n; g_script_i = 0; g_close_rc = close_rc;
|
||||
}
|
||||
|
||||
int mbedtls_ssl_read(mbedtls_ssl_context *ssl, unsigned char *buf, size_t len) {
|
||||
(void)ssl;
|
||||
if (g_script_i < g_script_n) {
|
||||
const tr_chunk_t *c = &g_script[g_script_i];
|
||||
size_t n = c->len;
|
||||
if (n > len) n = len; /* 脚本块恒 <= 4096(tmp),一般整块发出 */
|
||||
memcpy(buf, c->buf, n);
|
||||
g_script_i++;
|
||||
return (int)n;
|
||||
}
|
||||
return g_close_rc;
|
||||
}
|
||||
int mbedtls_ssl_write(mbedtls_ssl_context *ssl, const unsigned char *buf, size_t len) {
|
||||
(void)ssl; (void)buf; return (int)len; /* 请求头/体全部写成功 */
|
||||
}
|
||||
|
||||
/* 其余 mbedtls 桩:连接/握手/配置全部成功,不触碰任何真实网络。 */
|
||||
void mbedtls_net_init(mbedtls_net_context *ctx) { (void)ctx; }
|
||||
int mbedtls_net_connect(mbedtls_net_context *ctx, const char *host, const char *port, int proto) {
|
||||
(void)ctx; (void)host; (void)port; (void)proto; return 0;
|
||||
}
|
||||
int mbedtls_net_set_nonblock(mbedtls_net_context *ctx) { (void)ctx; return 0; }
|
||||
int mbedtls_net_poll(mbedtls_net_context *ctx, uint32_t rw, uint32_t timeout) {
|
||||
(void)ctx; (void)rw; (void)timeout; return 1; /* 就绪 */
|
||||
}
|
||||
void mbedtls_net_free(mbedtls_net_context *ctx) { (void)ctx; }
|
||||
int mbedtls_net_recv(void *ctx, unsigned char *buf, size_t len) {
|
||||
(void)ctx; (void)buf; (void)len; return MBEDTLS_ERR_SSL_WANT_READ; /* 实际读由 mbedtls_ssl_read 桩接管 */
|
||||
}
|
||||
int mbedtls_net_send(void *ctx, const unsigned char *buf, size_t len) {
|
||||
(void)ctx; (void)buf; return (int)len;
|
||||
}
|
||||
|
||||
void mbedtls_ssl_init(mbedtls_ssl_context *ssl) { (void)ssl; }
|
||||
void mbedtls_ssl_config_init(mbedtls_ssl_config *conf) { (void)conf; }
|
||||
int mbedtls_ssl_config_defaults(mbedtls_ssl_config *conf, int endpoint, int transport, int preset) {
|
||||
(void)conf; (void)endpoint; (void)transport; (void)preset; return 0;
|
||||
}
|
||||
void mbedtls_ssl_conf_authmode(mbedtls_ssl_config *conf, int authmode) { (void)conf; (void)authmode; }
|
||||
void mbedtls_ssl_conf_ca_chain(mbedtls_ssl_config *conf, mbedtls_x509_crt *ca_chain, mbedtls_x509_crl *ca_crl) {
|
||||
(void)conf; (void)ca_chain; (void)ca_crl;
|
||||
}
|
||||
void mbedtls_ssl_conf_rng(mbedtls_ssl_config *conf,
|
||||
int (*f_rng)(void *, unsigned char *, size_t), void *p_rng) {
|
||||
(void)conf; (void)f_rng; (void)p_rng;
|
||||
}
|
||||
int mbedtls_ssl_setup(mbedtls_ssl_context *ssl, const mbedtls_ssl_config *conf) {
|
||||
(void)ssl; (void)conf; return 0;
|
||||
}
|
||||
int mbedtls_ssl_set_hostname(mbedtls_ssl_context *ssl, const char *hostname) {
|
||||
(void)ssl; (void)hostname; return 0;
|
||||
}
|
||||
void mbedtls_ssl_set_bio(mbedtls_ssl_context *ssl, void *p_bio,
|
||||
mbedtls_ssl_send_t *f_send, mbedtls_ssl_recv_t *f_recv,
|
||||
mbedtls_ssl_recv_timeout_t *f_recv_timeout) {
|
||||
(void)ssl; (void)p_bio; (void)f_send; (void)f_recv; (void)f_recv_timeout;
|
||||
}
|
||||
int mbedtls_ssl_handshake(mbedtls_ssl_context *ssl) { (void)ssl; return 0; }
|
||||
void mbedtls_ssl_free(mbedtls_ssl_context *ssl) { (void)ssl; }
|
||||
void mbedtls_ssl_config_free(mbedtls_ssl_config *conf) { (void)conf; }
|
||||
|
||||
void mbedtls_x509_crt_init(mbedtls_x509_crt *crt) { (void)crt; }
|
||||
int mbedtls_x509_crt_parse_file(mbedtls_x509_crt *crt, const char *path) {
|
||||
(void)crt; (void)path; return 0; /* CA 加载成功(validate=0 时不走此径,桩兜底) */
|
||||
}
|
||||
void mbedtls_x509_crt_free(mbedtls_x509_crt *crt) { (void)crt; }
|
||||
|
||||
/* ============================================================
|
||||
* 下载回调 sink:累加接收到的 body 字节。
|
||||
* ============================================================ */
|
||||
typedef struct { unsigned char data[1 << 16]; size_t len; } sink_cb_t;
|
||||
|
||||
static int on_body(void *ctx, const unsigned char *data, size_t len) {
|
||||
sink_cb_t *s = (sink_cb_t *)ctx;
|
||||
if (s->len + len > sizeof s->data) return -1; /* 上限(不触发) */
|
||||
memcpy(s->data + s->len, data, len);
|
||||
s->len += len;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* 运行一个用例:装填脚本 → 发起 GET 下载 → 记录 rc / body_len / content_length */
|
||||
typedef struct { int rc; size_t body_len; size_t content_length; } case_result_t;
|
||||
|
||||
static case_result_t run_case(const tr_chunk_t *script, int script_n, int close_rc) {
|
||||
ncm_http_req_t req;
|
||||
memset(&req, 0, sizeof req);
|
||||
req.url = "https://ex.test/song.mp3";
|
||||
req.method = "GET";
|
||||
req.verify_ca = 0; /* 跳过 CA 校验,聚焦 body 终态判定 */
|
||||
req.timeout_ms = 3000;
|
||||
|
||||
sink_cb_t sink; memset(&sink, 0, sizeof sink);
|
||||
ncm_http_resp_t resp; memset(&resp, 0, sizeof resp);
|
||||
|
||||
tr_script(script, script_n, close_rc);
|
||||
int rc = ncm_http_download_status(&req, on_body, &sink, 0, &resp, NULL);
|
||||
|
||||
case_result_t r;
|
||||
r.rc = rc;
|
||||
r.body_len = sink.len;
|
||||
r.content_length = resp.content_length;
|
||||
return r;
|
||||
}
|
||||
|
||||
static int g_fail = 0;
|
||||
static void check(const char *name, int ok, const char *detail) {
|
||||
if (!ok) g_fail++;
|
||||
printf(" %s %s\n", ok ? "PASS" : "FAIL", name);
|
||||
if (detail) printf(" %s\n", detail);
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
char dbg[160];
|
||||
|
||||
/* ---- 场景 1:定长(CL=100) 对端提前关闭(只发 50 字节)→ 截断 ---- */
|
||||
{
|
||||
static const unsigned char hdrs[] =
|
||||
"HTTP/1.1 200 OK\r\nContent-Length: 100\r\n\r\n";
|
||||
static unsigned char chunk[512];
|
||||
size_t hlen = sizeof hdrs - 1;
|
||||
memcpy(chunk, hdrs, hlen);
|
||||
for (size_t i = 0; i < 50; i++) chunk[hlen + i] = 'A';
|
||||
tr_chunk_t script[1];
|
||||
script[0].buf = chunk; script[0].len = hlen + 50;
|
||||
case_result_t r = run_case(script, 1, 0 /* 对端关闭 */);
|
||||
snprintf(dbg, sizeof dbg, "rc=%d body=%zu content_length=%zu",
|
||||
r.rc, r.body_len, r.content_length);
|
||||
check("truncate_fixed(CL=100, 50B, close) -> NCM_ERR_IO",
|
||||
(r.rc != NCM_OK) && (r.rc == NCM_ERR_IO) && (r.body_len == 50), dbg);
|
||||
}
|
||||
|
||||
/* ---- 场景 2:定长(CL=100) 完整收满 → BS_DONE → NCM_OK ---- */
|
||||
{
|
||||
static const unsigned char hdrs[] =
|
||||
"HTTP/1.1 200 OK\r\nContent-Length: 100\r\n\r\n";
|
||||
static unsigned char chunk[512];
|
||||
size_t hlen = sizeof hdrs - 1;
|
||||
memcpy(chunk, hdrs, hlen);
|
||||
for (size_t i = 0; i < 100; i++) chunk[hlen + i] = 'B';
|
||||
tr_chunk_t script[1];
|
||||
script[0].buf = chunk; script[0].len = hlen + 100;
|
||||
case_result_t r = run_case(script, 1, 0);
|
||||
snprintf(dbg, sizeof dbg, "rc=%d body=%zu", r.rc, r.body_len);
|
||||
check("normal_fixed(CL=100, 100B) -> NCM_OK",
|
||||
(r.rc == NCM_OK) && (r.body_len == 100), dbg);
|
||||
}
|
||||
|
||||
/* ---- 场景 3:定长(CL=100) 两次读到收满(多块)→ NCM_OK ---- */
|
||||
{
|
||||
static const unsigned char hdrs[] =
|
||||
"HTTP/1.1 200 OK\r\nContent-Length: 100\r\n\r\n";
|
||||
static unsigned char part1[512], part2[64];
|
||||
size_t hlen = sizeof hdrs - 1;
|
||||
memcpy(part1, hdrs, hlen);
|
||||
for (size_t i = 0; i < 60; i++) part1[hlen + i] = 'C';
|
||||
for (size_t i = 0; i < 40; i++) part2[i] = 'D';
|
||||
tr_chunk_t script[2];
|
||||
script[0].buf = part1; script[0].len = hlen + 60;
|
||||
script[1].buf = part2; script[1].len = 40;
|
||||
case_result_t r = run_case(script, 2, 0);
|
||||
snprintf(dbg, sizeof dbg, "rc=%d body=%zu", r.rc, r.body_len);
|
||||
check("normal_fixed_multi(CL=100, 60+40B) -> NCM_OK",
|
||||
(r.rc == NCM_OK) && (r.body_len == 100), dbg);
|
||||
}
|
||||
|
||||
/* ---- 场景 4:Content-Length: 0(空 body)→ BS_DONE → NCM_OK ---- */
|
||||
{
|
||||
static const unsigned char chunk[] =
|
||||
"HTTP/1.1 204 No Content\r\nContent-Length: 0\r\n\r\n";
|
||||
static const tr_chunk_t script[] = { { chunk, sizeof chunk - 1 } };
|
||||
case_result_t r = run_case(script, 1, 0);
|
||||
snprintf(dbg, sizeof dbg, "rc=%d body=%zu", r.rc, r.body_len);
|
||||
check("cl_zero(CL=0) -> NCM_OK empty",
|
||||
(r.rc == NCM_OK) && (r.body_len == 0), dbg);
|
||||
}
|
||||
|
||||
/* ---- 场景 5:无 Content-Length、靠对端关闭结束(BS_CLOSE) → 合法 → NCM_OK ---- */
|
||||
{
|
||||
static const unsigned char hdrs[] =
|
||||
"HTTP/1.1 200 OK\r\n\r\n"; /* 无 CL、无 chunked */
|
||||
static unsigned char chunk[512];
|
||||
size_t hlen = sizeof hdrs - 1;
|
||||
memcpy(chunk, hdrs, hlen);
|
||||
for (size_t i = 0; i < 100; i++) chunk[hlen + i] = 'E';
|
||||
tr_chunk_t script[1];
|
||||
script[0].buf = chunk; script[0].len = hlen + 100;
|
||||
case_result_t r = run_case(script, 1, 0);
|
||||
snprintf(dbg, sizeof dbg, "rc=%d body=%zu", r.rc, r.body_len);
|
||||
check("close_delimited(no CL, 100B, close) -> NCM_OK",
|
||||
(r.rc == NCM_OK) && (r.body_len == 100), dbg);
|
||||
}
|
||||
|
||||
/* ---- 场景 6:定长(CL=100) 提前关闭,且用 TLS close_notify 模拟 → 截断 ---- */
|
||||
{
|
||||
static const unsigned char hdrs[] =
|
||||
"HTTP/1.1 200 OK\r\nContent-Length: 100\r\n\r\n";
|
||||
static unsigned char chunk[512];
|
||||
size_t hlen = sizeof hdrs - 1;
|
||||
memcpy(chunk, hdrs, hlen);
|
||||
for (size_t i = 0; i < 50; i++) chunk[hlen + i] = 'F';
|
||||
tr_chunk_t script[1];
|
||||
script[0].buf = chunk; script[0].len = hlen + 50;
|
||||
case_result_t r = run_case(script, 1, MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY);
|
||||
snprintf(dbg, sizeof dbg, "rc=%d body=%zu", r.rc, r.body_len);
|
||||
check("truncate_close_notify(CL=100, 50B) -> NCM_ERR_IO",
|
||||
(r.rc != NCM_OK) && (r.rc == NCM_ERR_IO) && (r.body_len == 50), dbg);
|
||||
}
|
||||
|
||||
/* ---- 场景 7:chunked 完整(0 分块末端)→ BS_DONE → NCM_OK ---- */
|
||||
{
|
||||
static const unsigned char chunk[] =
|
||||
"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n"
|
||||
"A\r\n0123456789\r\n" /* 16 进制 0xA = 10 字节 */
|
||||
"0\r\n\r\n";
|
||||
static const tr_chunk_t script[] = { { chunk, sizeof chunk - 1 } };
|
||||
case_result_t r = run_case(script, 1, 0);
|
||||
snprintf(dbg, sizeof dbg, "rc=%d body=%zu", r.rc, r.body_len);
|
||||
check("chunked_full(10B + 0-chunk) -> NCM_OK",
|
||||
(r.rc == NCM_OK) && (r.body_len == 10), dbg);
|
||||
}
|
||||
|
||||
/* ---- 场景 8:chunked 中途关闭(数据未收完)→ 截断 → 非 NCM_OK ---- */
|
||||
{
|
||||
static const unsigned char chunk[] =
|
||||
"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n"
|
||||
"5\r\nhel"; /* 宣称 5 字节,只发了 3,未到 0 分块 */
|
||||
static const tr_chunk_t script[] = { { chunk, sizeof chunk - 1 } };
|
||||
case_result_t r = run_case(script, 1, 0);
|
||||
snprintf(dbg, sizeof dbg, "rc=%d body=%zu", r.rc, r.body_len);
|
||||
check("chunked_truncated(3/5B, close) -> NCM_ERR_IO",
|
||||
(r.rc != NCM_OK) && (r.rc == NCM_ERR_IO), dbg);
|
||||
}
|
||||
|
||||
printf("\n== HTTP TRUNCATE SIM: %s ==\n", g_fail ? "FAIL" : "ALL PASS");
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
/* bench/mem_release_sim.c — 宿主「彻底释放」路径验证(agent 32bit OOM 专项)。
|
||||
*
|
||||
* 目的:验证「32bit(>16bit)→S32 解码 播完/切歌 是否真正释放」——ncm_dec_open(整文件)→
|
||||
* ncm_dec_read 全量(dr_flac S32 满幅路径)→ ncm_dec_close(dr_flac_close + stream_src_close)
|
||||
* 循环 N 次,断言进程匿名 RSS(RssAnon,OOM 高危口径)**不单调累积**(播完回落基线),
|
||||
* 且解码中峰值有界(32bit 解码缓冲存在但有限,非 40MB 级)。
|
||||
*
|
||||
* 依赖:仅 app/play 解码层(decoder.c + stream_src.c + dr_flac_impl + minimp3_impl),
|
||||
* 无 core/SDK/app.c——本 sim 是解码链路的最小区块,直接量「解码器 buff 播完 free」。
|
||||
* 编译(ncm_app 目录下):
|
||||
* gcc -O2 -Wall -Wextra -DJSMN_HEADER -I. -Icore -I../sdk -I../meow_launcherd \
|
||||
* -Ivendor/jsmn -Ivendor/minimp3 -Ivendor/dr_flac -Ivendor/mbedtls/include \
|
||||
* -c bench/mem_release_sim.c -o /tmp/mr.o
|
||||
* gcc -O2 -Wall -Wextra ... -c app/play/stream_src.c -o /tmp/mr_ss.o
|
||||
* gcc -O2 -Wall -Wextra ... -c app/play/decoder.c -o /tmp/mr_dec.o
|
||||
* gcc -O2 -c -Ivendor/minimp3 app/play/minimp3_impl.c -o /tmp/mr_mp3.o
|
||||
* gcc -O2 -c -Ivendor/dr_flac app/play/dr_flac_impl.c -o /tmp/mr_flac.o
|
||||
* gcc -O2 -o mem_release_sim /tmp/mr.o /tmp/mr_ss.o /tmp/mr_dec.o /tmp/mr_mp3.o /tmp/mr_flac.o -lpthread -lm
|
||||
* 运行:./mem_release_sim <flac 24bit 路径> [iters] (iters 默认 5)
|
||||
* 断言(PASS 才 exit 0):
|
||||
* - RssAnon[最后] - RssAnon[基准] < 256KB(播完回落,无累积);
|
||||
* - 解码中峰值有界(peak - 基准 < 8MB,32bit 解码缓冲不是 40MB 级);
|
||||
* - 总帧数 > 0 且 bits==32(确实走了 S32 满幅路径——本 sim 只受 24bit 输入触发)。
|
||||
*/
|
||||
#include "../app/play/decoder.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#define READ_CHUNK_FRAMES 4096
|
||||
#define ITER_DEFAULT 5
|
||||
#define SETTLE_TOLERANCE_KB (256) /* 播完回落允许抖动(<256KB 视为无累积) */
|
||||
#define PEAK_BOUND_KB (8 * 1024) /* 解码中峰值必须 <8MB(区别于 40MB 级异常) */
|
||||
|
||||
/* 静默 meow_log(本 sim 不链 core;decoder/stream_src 不调它,仅占位防漏链) */
|
||||
void meow_log(int lvl, const char *fmt, ...) { (void)lvl; (void)fmt; }
|
||||
|
||||
static long rss_anon_kb(void)
|
||||
{
|
||||
FILE *f = fopen("/proc/self/status", "r");
|
||||
if (!f) return -1;
|
||||
char line[256];
|
||||
long v = -1;
|
||||
while (fgets(line, sizeof line, f))
|
||||
if (!strncmp(line, "RssAnon:", 8)) { v = atol(line + 8); break; }
|
||||
fclose(f);
|
||||
return v;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
if (argc < 2) {
|
||||
printf("usage: mem_release_sim <flac_path> [iters]\n");
|
||||
return 2;
|
||||
}
|
||||
const char *path = argv[1];
|
||||
int iters = (argc > 2) ? atoi(argv[2]) : ITER_DEFAULT;
|
||||
if (iters < 2) iters = 2;
|
||||
|
||||
long base = rss_anon_kb();
|
||||
if (base < 0) { printf("FAIL cannot read RssAnon\n"); return 1; }
|
||||
printf("mem_release[0] baseline RssAnon=%ldKB\n", base);
|
||||
|
||||
int first_dec_ok = 0;
|
||||
long prev_after = 0, last_after = 0; /* 最近两次播完 close 后的 RssAnon */
|
||||
for (int i = 0; i < iters; i++) {
|
||||
ncm_decoder_t *dec = NULL;
|
||||
int rate = 0, ch = 0, bits = 0;
|
||||
int64_t tf = 0;
|
||||
if (ncm_dec_open(&dec, path, &rate, &ch, &bits, &tf) != 0) {
|
||||
printf("FAIL iter%d ncm_dec_open (%s)\n", i, path);
|
||||
return 1;
|
||||
}
|
||||
if (i == 0) {
|
||||
printf("mem_release: fmt=%s %dHz/%dch/%dbit total_frames=%lld (S32 path=%d)\n",
|
||||
ncm_dec_format(dec), rate, ch, bits, (long long)tf, bits > 16);
|
||||
first_dec_ok = 1;
|
||||
}
|
||||
|
||||
/* 全量读(S32 满幅路径),测解码中匿名峰值 */
|
||||
static int32_t buf[READ_CHUNK_FRAMES * 2];
|
||||
long peak = 0;
|
||||
int64_t frames = 0, got = 0;
|
||||
do {
|
||||
got = ncm_dec_read(dec, buf, READ_CHUNK_FRAMES);
|
||||
frames += got;
|
||||
long r = rss_anon_kb();
|
||||
if (r > peak) peak = r;
|
||||
} while (got > 0);
|
||||
|
||||
printf("mem_release[%d] after decode: frames=%lld peak_RssAnon=%ldKB\n",
|
||||
i + 1, (long long)frames, peak);
|
||||
|
||||
ncm_dec_close(dec); /* drflac_close + stream_src_close(整文件静态源) */
|
||||
long r = rss_anon_kb();
|
||||
prev_after = last_after;
|
||||
last_after = r;
|
||||
printf("mem_release[%d] after close RssAnon=%ldKB (delta=%ldKB)\n",
|
||||
i + 1, r, r - base);
|
||||
|
||||
/* 解码中峰值有界:32bit 解码缓冲不是 40MB 级 */
|
||||
if (peak - base > PEAK_BOUND_KB) {
|
||||
printf("FAIL iter%d decode peak %ldKB - base %ldKB = %ldKB > %dKB (异常高)\n",
|
||||
i, peak, base, peak - base, PEAK_BOUND_KB);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* 累积断言:最后两次播完后的 RssAnon 相对基线的增量都 < 容忍(无单调上升) */
|
||||
long d1 = last_after - base;
|
||||
long d2 = prev_after - base;
|
||||
int no_accum = (d1 < SETTLE_TOLERANCE_KB) && (d2 < SETTLE_TOLERANCE_KB);
|
||||
printf("mem_release: settle delta last=%ldKB prev=%ldKB -> %s\n",
|
||||
d1, d2, no_accum ? "PASS(无累积)" : "FAIL(有残留)");
|
||||
|
||||
int ok = first_dec_ok && no_accum;
|
||||
printf("mem_release: %s\n", ok ? "PASS" : "FAIL");
|
||||
return ok ? 0 : 1;
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
/* bench/mem_watch_sim.c — 宿主「边播监控取证」阈值/峰值验证(agent 32bit OOM 取证)。
|
||||
*
|
||||
* 归属演进(2026-09-02 定案,ARCHITECTURE_PRINCIPLES ③混合):**降载决策上收通用层**——
|
||||
* A + C 分别归 SDK meow_cachegov / meow_svc_perf;本 sim 只验证 mem_watch 的**取证**语义:
|
||||
* 三档 level 标注滞回(normal→high→critical→滞回→normal)+ song_end 重置峰值 + song_begin 起算。
|
||||
* (旧版「dl_paused/prefetch_paused 降载断言」已随降载上收移除——那由 meow_cachegov_sim
|
||||
* 与 svc_perf test_cachegov_monitor 覆盖。)
|
||||
*
|
||||
* 依赖:仅 app/play/mem_watch.c(+ meow_log 桩)。
|
||||
* 编译(ncm_app 目录下):
|
||||
* gcc -O2 -Wall -Wextra -DJSMN_HEADER -I. -I../sdk -I../meow_launcherd \
|
||||
* -c bench/mem_watch_sim.c -o /tmp/mw.o
|
||||
* gcc -O2 -Wall -Wextra -I. -I../sdk -c app/play/mem_watch.c -o /tmp/mw_mod.o
|
||||
* gcc -O2 -o mem_watch_sim /tmp/mw.o /tmp/mw_mod.o -lpthread -lm
|
||||
* 运行:./mem_watch_sim (exit 0=PASS / 1=FAIL)
|
||||
*/
|
||||
#include "../app/play/mem_watch.h"
|
||||
#include <stdarg.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
/* meow_log 桩:打印(便于看阈值过渡;sim 仅验证 mem_watch 逻辑,非真实日志系统) */
|
||||
void meow_log(int lvl, const char *fmt, ...)
|
||||
{
|
||||
(void)lvl;
|
||||
char b[256]; va_list ap; va_start(ap, fmt);
|
||||
vsnprintf(b, sizeof b, fmt, ap); va_end(ap);
|
||||
printf(" [mem.log] %s\n", b);
|
||||
}
|
||||
|
||||
static int fails;
|
||||
#define CHECK(cond, msg) do { \
|
||||
if (!(cond)) { printf("FAIL %s\n", msg); fails++; } \
|
||||
else { printf("ok %s\n", msg); } } while (0)
|
||||
|
||||
/* 等监控线程至少走过 2 个周期(feed 后确保已采样) */
|
||||
static void settle_ms(int m) { usleep((useconds_t)m * 1000u); }
|
||||
|
||||
int main(void)
|
||||
{
|
||||
/* 用 env 设定短周期(50ms)与干净阈值,加速确定性判定 */
|
||||
setenv("NCM_MEM_WATCH_PERIOD_MS", "50", 1);
|
||||
setenv("NCM_MEM_WATCH_HIGH_KB", "30720", 1); /* 30MB */
|
||||
setenv("NCM_MEM_WATCH_LOW_KB", "20480", 1); /* 20MB */
|
||||
setenv("NCM_MEM_WATCH_CRITICAL_KB", "40960", 1); /* 40MB */
|
||||
|
||||
if (mem_watch_start() != 0) { printf("FAIL mem_watch_start\n"); return 1; }
|
||||
printf("started (period=50ms H=30M L=20M C=40M)\n");
|
||||
|
||||
/* 1) 正常区 */
|
||||
mem_watch_set_forced_kb(10000);
|
||||
settle_ms(150);
|
||||
CHECK(mem_watch_level() == NCM_MEM_LVL_NORMAL, "level==NORMAL @10MB");
|
||||
|
||||
/* 2) 升到 HIGH */
|
||||
mem_watch_set_forced_kb(35000);
|
||||
settle_ms(150);
|
||||
CHECK(mem_watch_level() == NCM_MEM_LVL_HIGH, "level==HIGH @35MB");
|
||||
|
||||
/* 3) 升到 CRITICAL */
|
||||
mem_watch_set_forced_kb(45000);
|
||||
settle_ms(150);
|
||||
CHECK(mem_watch_level() == NCM_MEM_LVL_CRITICAL, "level==CRITICAL @45MB");
|
||||
|
||||
/* 4) 滞回:落在 LOW..HIGH 之间 -> 保持 CRITICAL(防抖动) */
|
||||
mem_watch_set_forced_kb(25000);
|
||||
settle_ms(150);
|
||||
CHECK(mem_watch_level() == NCM_MEM_LVL_CRITICAL, "level stays CRITICAL @25MB (hysteresis)");
|
||||
|
||||
/* 5) 回到正常:低于 LOW -> NORMAL */
|
||||
mem_watch_set_forced_kb(15000);
|
||||
settle_ms(150);
|
||||
CHECK(mem_watch_level() == NCM_MEM_LVL_NORMAL, "level==NORMAL @15MB");
|
||||
|
||||
/* 6) 调度器释放钩子:song_end 重置峰值 + song_begin 起算 + rss_anon 反映注入值 */
|
||||
mem_watch_set_forced_kb(20000);
|
||||
settle_ms(100);
|
||||
mem_watch_song_begin();
|
||||
mem_watch_set_forced_kb(37000); /* 本曲峰值冲到 37MB */
|
||||
settle_ms(100);
|
||||
long anon = mem_watch_rss_anon_kb();
|
||||
CHECK(anon == 37000, "rss_anon_kb reflects forced value");
|
||||
mem_watch_song_end(); /* 释放钩子:落峰值+重置 */
|
||||
settle_ms(100);
|
||||
mem_watch_song_begin();
|
||||
mem_watch_set_forced_kb(12000); /* 新曲:峰值应从新起算 */
|
||||
settle_ms(100);
|
||||
CHECK(mem_watch_level() == NCM_MEM_LVL_NORMAL, "after song_end+begin, level resets to NORMAL");
|
||||
|
||||
mem_watch_stop();
|
||||
printf("mem_watch_sim: %s\n", fails ? "FAIL" : "PASS");
|
||||
return fails ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
/* bench/nav_exit_sim.c — 任务2:home 根页左滑「回桌面后台」宿主验证。
|
||||
*
|
||||
* 场景(0 真实请求/0 真实 launcher):fake launcher IPC socket(MEOW_IPC_SOCK)。调
|
||||
* ipc_media_nav_exit(),断言:
|
||||
* ① fake launcher 收到一条 MSG_NAV 帧,nav_type==MEOW_NAV_EXIT、app_id=="dev.player.ncm";
|
||||
* ② 本函数**不**置 g_app_quit(进程保留,媒体会话继续上报 PLAYING,任务2 ③ 保活前提)。
|
||||
* 这验证「从网易云主页面侧滑返回不再退出进程,而请求 launcher 退后台(有歌在播则 M4 保活)」。
|
||||
*
|
||||
* 编译(宿主,ncm_app 目录下):
|
||||
* gcc -O1 -DJSMN_HEADER -I. -Icore -Iapp -I../sdk -I../meow_launcherd \
|
||||
* -c bench/nav_exit_sim.c -o /tmp/ne.o
|
||||
* gcc -O1 -DJSMN_HEADER -I. -Icore -Iapp -I../sdk -I../meow_launcherd \
|
||||
* -c app/play/ipc_media.c -o /tmp/ne_ipc.o
|
||||
* gcc -O1 -o nav_exit_sim /tmp/ne.o /tmp/ne_ipc.o -lpthread
|
||||
* 运行:./nav_exit_sim (0 网络;PASS/FAIL 退出码)
|
||||
*/
|
||||
#include "../app/ncm_app_internal.h"
|
||||
#include "meow_ipc.h"
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
#include <pthread.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
|
||||
ncm_app_state_t g_st;
|
||||
ncm_play_run_t g_play = { .mu = PTHREAD_MUTEX_INITIALIZER, .state = NCM_PC_STOPPED, .cur_song = -1 };
|
||||
volatile int g_app_quit = 0;
|
||||
volatile int g_net_ready = 1;
|
||||
int g_host_mock = 0;
|
||||
|
||||
size_t ncm_strlcpy(char *dst, const char *src, size_t sz)
|
||||
{
|
||||
size_t n = 0;
|
||||
if (sz) { while (src[n] && n < sz - 1) { dst[n] = src[n]; n++; } dst[n] = 0; }
|
||||
while (src[n]) n++;
|
||||
return n;
|
||||
}
|
||||
void meow_log(meow_log_level lvl, const char *fmt, ...) { (void)lvl; (void)fmt; }
|
||||
|
||||
/* ipc_media.c 的其余依赖(本 sim 不驱动播放/封面/歌词,仅服务链接与消息类型) */
|
||||
uint64_t meow_ui_time_ms(void) { return 0; }
|
||||
int player_manager_playing_song(void) { return g_play.cur_song; }
|
||||
int player_manager_current_song(ncm_song_t *out) { (void)out; return -1; }
|
||||
void player_manager_pause(int on) { (void)on; }
|
||||
void player_manager_next(int dir) { (void)dir; }
|
||||
void player_manager_seek(int64_t ms) { (void)ms; }
|
||||
int player_manager_state(void) { return g_play.state; }
|
||||
const char *cover_current_path(void) { return NULL; }
|
||||
int lyric_count(void) { return 0; }
|
||||
|
||||
/* ---- fake launcher ---- */
|
||||
static const char *g_sock = "/tmp/nav_exit_launcher.sock";
|
||||
static uint16_t g_got_type = 0;
|
||||
static int g_got_nav_type = -1;
|
||||
static char g_got_app[MEOW_APP_ID_MAX];
|
||||
static int g_got = 0;
|
||||
|
||||
static void *server(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
unlink(g_sock);
|
||||
int lfd = socket(AF_UNIX, SOCK_STREAM, 0);
|
||||
if (lfd < 0) return NULL;
|
||||
struct sockaddr_un a;
|
||||
memset(&a, 0, sizeof a);
|
||||
a.sun_family = AF_UNIX;
|
||||
strncpy(a.sun_path, g_sock, sizeof a.sun_path - 1);
|
||||
if (bind(lfd, (struct sockaddr *)&a, sizeof a) != 0) { close(lfd); return NULL; }
|
||||
listen(lfd, 1);
|
||||
int fd = accept(lfd, NULL, NULL);
|
||||
if (fd < 0) { close(lfd); return NULL; }
|
||||
uint8_t rbuf[MEOW_IPC_MAX];
|
||||
ssize_t n = read(fd, rbuf, sizeof rbuf);
|
||||
if (n >= (ssize_t)MEOW_IPC_HDR_LEN) {
|
||||
uint32_t magic, len;
|
||||
uint16_t ver, type;
|
||||
memcpy(&magic, rbuf, 4); memcpy(&ver, rbuf + 4, 2); memcpy(&type, rbuf + 6, 2);
|
||||
memcpy(&len, rbuf + 12, 4);
|
||||
if (magic == MEOW_IPC_MAGIC && ver == MEOW_IPC_VERSION && len <= (uint32_t)(n - MEOW_IPC_HDR_LEN)) {
|
||||
g_got_type = type;
|
||||
if (type == MSG_NAV && len >= sizeof(m_nav_t)) {
|
||||
m_nav_t nv;
|
||||
memcpy(&nv, rbuf + MEOW_IPC_HDR_LEN, sizeof nv);
|
||||
g_got_nav_type = (int)nv.nav_type;
|
||||
nv.app_id[MEOW_APP_ID_MAX - 1] = 0;
|
||||
snprintf(g_got_app, sizeof g_got_app, "%s", nv.app_id);
|
||||
}
|
||||
g_got = 1;
|
||||
}
|
||||
}
|
||||
close(fd); close(lfd);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
int fail = 0;
|
||||
setenv("MEOW_IPC_SOCK", g_sock, 1);
|
||||
|
||||
pthread_t th;
|
||||
if (pthread_create(&th, NULL, server, NULL) != 0) { printf("FAIL server thread\n"); return 1; }
|
||||
usleep(50000);
|
||||
|
||||
int rc = ipc_media_nav_exit();
|
||||
pthread_join(th, NULL);
|
||||
|
||||
if (rc != 0) { printf("FAIL ipc_media_nav_exit rc=%d\n", rc); fail = 1; }
|
||||
if (!g_got) { printf("FAIL fake launcher received no frame\n"); fail = 1; }
|
||||
else if (g_got_type != MSG_NAV) {
|
||||
printf("FAIL expected MSG_NAV (0x%04x) got 0x%04x\n", MSG_NAV, g_got_type);
|
||||
fail = 1;
|
||||
} else if (g_got_nav_type != MEOW_NAV_EXIT) {
|
||||
printf("FAIL expected MEOW_NAV_EXIT got %d\n", g_got_nav_type);
|
||||
fail = 1;
|
||||
} else if (strcmp(g_got_app, "dev.player.ncm") != 0) {
|
||||
printf("FAIL expected app_id dev.player.ncm got \"%s\"\n", g_got_app);
|
||||
fail = 1;
|
||||
} else {
|
||||
printf("PASS nav_exit: type=0x%04x nav_type=EXIT app=%s\n", g_got_type, g_got_app);
|
||||
}
|
||||
if (g_app_quit != 0) { printf("FAIL g_app_quit set (process must stay alive for bg keepalive)\n"); fail = 1; }
|
||||
else printf("PASS nav_exit did NOT set g_app_quit (process/alive, media keeps reporting PLAYING)\n");
|
||||
|
||||
unlink(g_sock);
|
||||
if (fail) { printf("nav_exit_sim FAIL\n"); return 1; }
|
||||
printf("nav_exit_sim PASS\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -151,6 +151,7 @@ int ncm_cache_open(const ncm_cache_t *c, const char *id, const char *ext, ncm_ca
|
||||
*w = (ncm_cache_writer_t *)calloc(1, 64); return NCM_OK;
|
||||
}
|
||||
const char *ncm_cache_writer_tmp_path(const ncm_cache_writer_t *w) { (void)w; return g_tmp_path; }
|
||||
const char *ncm_cache_writer_final_path(const ncm_cache_writer_t *w) { (void)w; return g_tmp_path; } /* 链接占位:drop_finished 对 .tmp 无害 */
|
||||
int ncm_cache_write(ncm_cache_writer_t *w, const unsigned char *d, size_t l) { (void)w; (void)d; (void)l; return NCM_OK; }
|
||||
int ncm_cache_commit(ncm_cache_writer_t *w) { free(w); g_cache_ok = 1; g_commits++; return NCM_OK; }
|
||||
int ncm_cache_abort(ncm_cache_writer_t *w)
|
||||
@@ -617,6 +618,32 @@ int main(void)
|
||||
g_eof = 1;
|
||||
wait_state(NCM_PC_STOPPED, 3000);
|
||||
|
||||
printf("== P14: 点「正在播的同曲」-> 只聚焦不重播(任务3 re-entry,非 P12 切档)==\n");
|
||||
usleep(300 * 1000);
|
||||
g_eof = 0; g_reads_left = -1; g_url_rc = NCM_OK; g_fee = 0;
|
||||
g_part_size = 0; g_http_status = 200; g_416_first = 0; g_dl_delay_ms = 0; g_dl_fail = 0;
|
||||
g_vip = NCM_VIP_NONE; g_downloads = 0; g_url_calls = 0; g_last_key[0] = 0; g_last_ext[0] = 0;
|
||||
player_manager_stop(); /* 清 P13 可能残留(STOPPED 或已自动顺延) */
|
||||
player_manager_set_quality(NCM_LEVEL_STANDARD, 10);
|
||||
g_cache_ok = 1; /* standard 缓存命中 */
|
||||
player_manager_play(0);
|
||||
CHECK(wait_state(NCM_PC_PLAYING, 3000), "song0 playing at standard (cache hit)");
|
||||
int opens_before = g_opens;
|
||||
int writes_before = g_writes;
|
||||
/* 任务3:歌单里点「正在播的同曲」(**未** set_quality → pending_replay=0)→ 只聚焦不重播:
|
||||
* 同步返回后状态仍是 PLAYING(若是重播此刻必为 BUFFER),线程窗口内不得新增 audio_out_open。 */
|
||||
player_manager_play(0);
|
||||
CHECK(player_manager_state() == NCM_PC_PLAYING,
|
||||
"re-enter same idx -> state stays PLAYING immediately (no restart to BUFFER)");
|
||||
usleep(300 * 1000); /* 若真发生重播,线程会重跑 run_song 再开 audio */
|
||||
CHECK(g_opens == opens_before,
|
||||
"re-enter same idx -> no new audio_out_open (no restart), opens=%d/%d", g_opens, opens_before);
|
||||
CHECK(g_writes > writes_before,
|
||||
"re-enter same idx -> same stream kept playing (writes %d->%d)", writes_before, g_writes);
|
||||
player_manager_stop();
|
||||
wait_state(NCM_PC_STOPPED, 2000); /* 干净收尾(任务3 复验后置 STOPPED,勿自动顺延) */
|
||||
g_cache_ok = 0; g_cache_real[0] = 0;
|
||||
|
||||
usleep(200 * 1000);
|
||||
player_manager_stop_thread();
|
||||
printf(g_fail ? "== PLAYER FLOW SIM: FAIL ==\n" : "== PLAYER FLOW SIM: ALL PASS ==\n");
|
||||
|
||||
Executable
BIN
Binary file not shown.
@@ -0,0 +1,171 @@
|
||||
/* bench/vol_sync_sim.c — 任务1:开播前「系统当前音量 → audiod」下发宿主验证。
|
||||
*
|
||||
* 场景(0 真实请求/0 真实 audiod):fake audiod socket(AF_UNIX SOCK_STREAM)+ 临时 settings.conf
|
||||
* (audio.volume = <V>,读改写单键同源同键)。调 audio_out_sync_volume(),断言 fake audiod 收到
|
||||
* 一条 MSG_AUDIO_VOLUME 帧,且 pct == settings 的 audio.volume。这验证「打开音频会话时把系统
|
||||
* 音量同步下发到 audiod」的根因修复(未播放时 DAC 停在默认/残留音量 → 动音量键才追设定值)。
|
||||
*
|
||||
* 另验证:音量语义只传 pct(0-100)——反转/真静音(255) 由 audiod dac_vol_set 负责,本层不碰。
|
||||
*
|
||||
* 编译(宿主,ncm_app 目录下):
|
||||
* gcc -O1 -DJSMN_HEADER -I. -Icore -I../sdk -I../meow_launcherd \
|
||||
* -c bench/vol_sync_sim.c -o /tmp/vs.o
|
||||
* gcc -O1 -DJSMN_HEADER -I. -Icore -I../sdk -I../meow_launcherd \
|
||||
* -c app/play/audio_out.c -o /tmp/vs_ao.o
|
||||
* gcc -O1 -o vol_sync_sim /tmp/vs.o /tmp/vs_ao.o -lpthread
|
||||
* 运行:./vol_sync_sim (0 网络;PASS/FAIL 退出码)
|
||||
*/
|
||||
#include "../app/ncm_app_internal.h"
|
||||
#include "meow_ipc.h"
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
#include <pthread.h>
|
||||
#include <fcntl.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
|
||||
ncm_app_state_t g_st;
|
||||
ncm_play_run_t g_play;
|
||||
volatile int g_app_quit = 0;
|
||||
volatile int g_net_ready = 1;
|
||||
int g_host_mock = 0;
|
||||
size_t ncm_strlcpy(char *dst, const char *src, size_t sz)
|
||||
{
|
||||
size_t n = 0;
|
||||
if (sz) { while (src[n] && n < sz - 1) { dst[n] = src[n]; n++; } dst[n] = 0; }
|
||||
while (src[n]) n++;
|
||||
return n;
|
||||
}
|
||||
void meow_log(meow_log_level lvl, const char *fmt, ...) { (void)lvl; (void)fmt; }
|
||||
|
||||
/* audio_out.c 引用但本 sim 不经 SDK 调用的符号:只链不跑就满足链接 */
|
||||
int meow_audio_open(int rate, int ch, int fmt) { (void)rate;(void)ch;(void)fmt; return -1; }
|
||||
int meow_audio_write(const void *pcm, int frames) { (void)pcm; return frames; }
|
||||
int meow_audio_pause(int sid, int paused) { (void)sid;(void)paused; return 0; }
|
||||
int meow_audio_close(int sid) { (void)sid; return 0; }
|
||||
int meow_audio_query(meow_audio_state *out) { (void)out; return -1; }
|
||||
|
||||
/* ---- fake audiod ---- */
|
||||
static const char *g_sock = "/tmp/vol_sync_audiod.sock";
|
||||
static uint16_t g_got_type = 0;
|
||||
static int32_t g_got_pct = -1;
|
||||
static int g_got = 0;
|
||||
static volatile int g_nconn = 0; /* 收到多少条连接 */
|
||||
static volatile int g_nframe = 0; /* 收到多少条 VOLUME 帧 */
|
||||
static volatile int g_stop = 0;
|
||||
|
||||
/* fake audiod:接受多条连接(上限 32,超过即停——失败才触发,正常不该超),每条只读一帧
|
||||
* VOLUME 然后等对端 EOF(ncm 侧 close)。统计连接数与帧数,供「重复 sync 不新建连接」断言。 */
|
||||
static void *server(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
unlink(g_sock);
|
||||
int lfd = socket(AF_UNIX, SOCK_STREAM, 0);
|
||||
if (lfd < 0) return NULL;
|
||||
struct sockaddr_un a;
|
||||
memset(&a, 0, sizeof a);
|
||||
a.sun_family = AF_UNIX;
|
||||
strncpy(a.sun_path, g_sock, sizeof a.sun_path - 1);
|
||||
if (bind(lfd, (struct sockaddr *)&a, sizeof a) != 0) { close(lfd); return NULL; }
|
||||
listen(lfd, 16);
|
||||
int lfl = fcntl(lfd, F_GETFL, 0);
|
||||
if (lfl >= 0) fcntl(lfd, F_SETFL, lfl | O_NONBLOCK); /* 非阻塞:g_stop 能及时退出 */
|
||||
while (!g_stop && g_nconn < 32) {
|
||||
int fd = accept(lfd, NULL, NULL);
|
||||
if (fd < 0) { usleep(20000); continue; } /* EAGAIN:无连接,短睡重试 */
|
||||
g_nconn++;
|
||||
/* 读满一帧(20B)或 EOF:stream socket read 可分段返回,须循环凑满再判。 */
|
||||
uint8_t frame[MEOW_IPC_HDR_LEN + sizeof(int32_t)];
|
||||
ssize_t n = 0;
|
||||
while (n < (ssize_t)sizeof frame) {
|
||||
ssize_t r = read(fd, frame + n, sizeof frame - (size_t)n);
|
||||
if (r <= 0) break; /* EOF / 错 */
|
||||
n += r;
|
||||
}
|
||||
if (n >= (ssize_t)(MEOW_IPC_HDR_LEN + sizeof(int32_t))) {
|
||||
uint32_t magic, len;
|
||||
uint16_t ver, type;
|
||||
memcpy(&magic, frame, 4); memcpy(&ver, frame + 4, 2); memcpy(&type, frame + 6, 2);
|
||||
memcpy(&len, frame + 12, 4);
|
||||
if (magic == MEOW_IPC_MAGIC && ver == MEOW_IPC_VERSION && type == MSG_AUDIO_VOLUME) {
|
||||
g_got_type = type;
|
||||
memcpy(&g_got_pct, frame + 16, sizeof g_got_pct);
|
||||
g_got = 1;
|
||||
g_nframe++;
|
||||
}
|
||||
}
|
||||
close(fd);
|
||||
}
|
||||
close(lfd);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
int fail = 0;
|
||||
/* 临时 settings.conf:audio.volume = 24(读改写单键同源键),另带 max 干扰键 */
|
||||
const char *cfg = "/tmp/vol_sync_settings.conf";
|
||||
FILE *f = fopen(cfg, "w");
|
||||
if (!f) { printf("FAIL cannot write settings\n"); return 1; }
|
||||
fprintf(f, "# meow-settings 1\naudio.volume = 24\naudio.volume_max = 50\ndisplay.brightness = 80\n");
|
||||
fclose(f);
|
||||
setenv("MEOW_SETTINGS_FILE", cfg, 1);
|
||||
setenv("MEOW_AUDIO_SOCK", g_sock, 1);
|
||||
|
||||
pthread_t th;
|
||||
if (pthread_create(&th, NULL, server, NULL) != 0) { printf("FAIL server thread\n"); return 1; }
|
||||
usleep(50000); /* 等 fake audiod listen */
|
||||
|
||||
/* 第一次 sync(首次 open):应下发一帧 VOLUME,pct==settings */
|
||||
audio_out_sync_volume();
|
||||
usleep(50000); /* 等服务线程 accept+读帧(主/服务线程异步) */
|
||||
if (!g_got) { printf("FAIL fake audiod received no frame\n"); fail = 1; }
|
||||
else if (g_got_type != MSG_AUDIO_VOLUME) {
|
||||
printf("FAIL expected MSG_AUDIO_VOLUME (0x%04x) got 0x%04x\n", MSG_AUDIO_VOLUME, g_got_type);
|
||||
fail = 1;
|
||||
} else if (g_got_pct != 24) {
|
||||
printf("FAIL expected pct=24 (settings audio.volume) got %d\n", (int)g_got_pct);
|
||||
fail = 1;
|
||||
} else {
|
||||
printf("PASS audiod sync volume: type=0x%04x pct=%d == settings.audio.volume\n",
|
||||
g_got_type, (int)g_got_pct);
|
||||
}
|
||||
|
||||
/* 连接风暴回归(§2026-09-02 fd=N accept 根因):音量未变时反复 sync 不得新建连接。
|
||||
* 模拟边下边播/切歌期频繁 audio_out_open → audio_out_sync_volume(同值,不重发)。 */
|
||||
for (int i = 0; i < 50; i++) audio_out_sync_volume(); /* 同值 50 次:应零新增连接 */
|
||||
usleep(50000);
|
||||
if (g_nconn != 1) {
|
||||
printf("FAIL unchanged-volume re-sync created %d connections (expect 1)\n", (int)g_nconn);
|
||||
fail = 1;
|
||||
} else {
|
||||
printf("PASS unchanged-volume re-sync: 50 calls -> 1 connection (storm eliminated)\n");
|
||||
}
|
||||
|
||||
/* 值变才重发:settings 改成 50 → 下一次 sync 应新建第 2 条连接 */
|
||||
FILE *g = fopen(cfg, "w");
|
||||
if (!g) { printf("FAIL cannot rewrite settings\n"); fail = 1; }
|
||||
else {
|
||||
fprintf(g, "# meow-settings 1\naudio.volume = 50\naudio.volume_max = 50\ndisplay.brightness = 80\n");
|
||||
fclose(g);
|
||||
}
|
||||
audio_out_sync_volume();
|
||||
usleep(50000);
|
||||
if (g_nconn != 2 || g_got_pct != 50) {
|
||||
printf("FAIL changed-volume re-sync: conns=%d (expect 2) last_pct=%d (expect 50)\n",
|
||||
(int)g_nconn, (int)g_got_pct);
|
||||
fail = 1;
|
||||
} else {
|
||||
printf("PASS changed-volume re-sync: settings=50 -> 2nd connection pct=50\n");
|
||||
}
|
||||
|
||||
g_stop = 1;
|
||||
pthread_join(th, NULL);
|
||||
unlink(g_sock);
|
||||
unlink(cfg);
|
||||
if (fail) { printf("vol_sync_sim FAIL\n"); return 1; }
|
||||
printf("vol_sync_sim PASS\n");
|
||||
return 0;
|
||||
}
|
||||
Reference in new issue
Block a user