Files
MeowOS-For-R3P2/patch_v2/ncm_app/bench/cover_chain_sim.c
T
AskaEth aeb9176101 fix(ncm): 封面链分诊与修复 —— CNMPlayer 头(Referer+UA 解403)/webp 魔数内容型先判不解码走兜底/URL 追加 param=300y300 缩略图/403 与 webp 单独分诊日志不盲重试
- cover.c: 请求头对齐 CNMPlayer(Referer https://music.163.com/ + UA Mozilla/5.0 CNMPlayer/0.1);
  下载后先判 content-type 含 webp 或临时文件魔数 RIFF....WEBP(stb_image 不支持),回 webp=1 不解码,
  worker 打 'webp unsupported' 立即清目标走兜底(保留歌单封面),不再当 fetch failed 盲目指数重试;
  403 单独日志(anti-hotlink/forbidden, no retry),确定性 4xx 不重试(沿用 311)。
- cover_request 入口 cover_norm_url 追加 ?param=300y300(已有 param= 尊重原样, 溢出保原 URL),
  缓存键/去重/下载 URL 同参版(省内存+出 jpeg)。
- ncm_app_internal.h: NCM_COVER_USE_PARAM/NCM_COVER_PARAM 共享宏(cover.c 与 sim 同源)。
- bench/cover_chain_sim.c: 补 ncm_http_download/meow_img_load_file/meow_img_downscale 桩对齐现行
  cover.c(旧 ncm_http_request 桩已过时), url_hash 同参数规则; 新增 T8 webp/T9 403 用例。
- 验证: cover_chain_sim T1-T9 ALL PASS + stream/player_flow/window/search/cache_evict/degrade/mem
  sim 全 PASS + core host+mipsel(qemu) PASS + build_ncm.sh 0 warning(text 1019834/bss 1362500 持平)
  + ncm_host UI dump exit=0。0 新增真实 CDN 请求; 设备端 CDN 矩阵因设备不可达未执行(见 REPORT 34.6)。

新 ncm md5 a7c22bd4ee3bcd02fb24046d27a33242 (未部署, 留主会话上卡)
2026-09-02 10:37:24 +08:00

332 lines
13 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.
/* bench/cover_chain_sim.c — 封面链离线回归(0 真实请求,REPORT §18)。
*
* 驱动真实 app/logic/cover.c(worker 线程/ncm_cache LRU/cover_current.jpg 落盘),
* HTTP 端点为本地合成 stub(按 URL 生成可区分内容,可注入失败次数模拟慢/坏网络),
* meow_img 为假句柄(嵌内容哈希,不真解码 JPEG——测的是链路逻辑不是解码器)。
*
* 用例:
* T1 首次请求 → 封面出现(内容哈希对得上 URL);
* T2 定居后 worker **不忙等**:cover_changed() 在 1.2s 内不得再置位
* (§18 前:成功后不清目标 → 每圈解码+换入+g_gen++ 死循环,CPU 占满且播放页每帧
* 整盘重绘——本用例在旧代码上必 FAIL);
* T3 切歌换 URL → 封面换入新内容 + cover_current.jpg 同步更新(launcher 媒体卡片读它);
* T4 重复请求同一 URL → 去重(0 新增 HTTP,0 换入);
* T5 网络失败 ×2 后退避重试成功(不无声死等);
* T6 连续失败 ≥8 次放弃(目标清除,不永久占 worker);
* T7 空/NULL URL 忽略;cover_cache_reset 清空。
*
* 编译(宿主,ncm_app 目录下;weak cover_put 兼容 §18 前的 cover.c 做对照):
* gcc -O2 -ffunction-sections -DJSMN_HEADER -I. -I../sdk -I../meow_launcherd \
* -Icore -Ivendor/jsmn -c bench/cover_chain_sim.c -o /tmp/cs.o
* gcc -O2 -ffunction-sections -DJSMN_HEADER -I. -I../sdk -I../meow_launcherd \
* -Icore -Ivendor/jsmn -c app/logic/cover.c -o /tmp/cs_cover.o
* gcc -O2 -ffunction-sections -DJSMN_HEADER -Icore -c core/ncm_cache.c -o /tmp/cs_cache.o
* gcc -O2 -ffunction-sections -DJSMN_HEADER -Icore -c core/ncm_store.c -o /tmp/cs_store.o
* gcc -O2 -Wl,--gc-sections -o cover_chain_sim /tmp/cs.o /tmp/cs_cover.o /tmp/cs_cache.o \
* /tmp/cs_store.o -lpthread
* 运行:./cover_chain_sim (0 网络;PASS/FAIL 退出码)
*/
#include "../app/ncm_app_internal.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdarg.h>
#include <unistd.h>
#include <sys/stat.h>
ncm_app_state_t g_st;
volatile int g_app_quit = 0;
int g_host_mock = 0;
/* ---- 公共 stub ---- */
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;
}
static int g_log_warns;
static char g_last_log[512]; /* 最近一条日志文本(webp/403 分诊日志断言用) */
void meow_log(meow_log_level lvl, const char *fmt, ...)
{
if (lvl >= MEOW_LOG_WARN) g_log_warns++;
va_list ap;
va_start(ap, fmt);
vsnprintf(g_last_log, sizeof g_last_log, fmt, ap);
va_end(ap);
}
/* ---- 假 meow_img:嵌内容 FNV 哈希(链路验证用,不真解码) ---- */
struct meow_img { unsigned long hash; int len; int freed; };
static unsigned long fnv1a(const unsigned char *p, size_t n)
{
unsigned long h = 1469598103934665603UL;
for (size_t i = 0; i < n; i++) { h ^= p[i]; h *= 1099511628211UL; }
return h;
}
meow_img *meow_img_load_mem(const void *buf, int len)
{
if (!buf || len <= 0) return NULL;
meow_img *im = (meow_img *)malloc(sizeof *im);
if (!im) return NULL;
im->hash = fnv1a((const unsigned char *)buf, (size_t)len);
im->len = len;
im->freed = 0;
return im;
}
/* §30 起 cover.c 用 meow_img_load_file(从 ncm_cache 写者临时文件解码):读文件内容哈希。
* 与 url_hash() 的期望值对齐(同内容同哈希)。 */
meow_img *meow_img_load_file(const char *path)
{
FILE *f = fopen(path, "rb");
if (!f) return NULL;
unsigned char buf[4096];
size_t n = fread(buf, 1, sizeof buf, f);
fclose(f);
if (!n) return NULL;
meow_img *im = (meow_img *)malloc(sizeof *im);
if (!im) return NULL;
im->hash = fnv1a(buf, n);
im->len = (int)n;
im->freed = 0;
return im;
}
/* meow_img_downscale 桩:不缩放(返回 NULL)→ cover_cap 保留原图(测链逻辑,非缩放器)。 */
meow_img *meow_img_downscale(meow_img *im, int maxdim) { (void)im; (void)maxdim; return NULL; }
void meow_img_free(meow_img *im)
{
if (!im) return;
im->freed = 1;
free(im);
}
/* §18 前的 cover.c 无 cover_put:weak 空实现让同一 sim 可对照旧代码(新代码强符号覆盖)。 */
__attribute__((weak)) void cover_put(void) { }
/* ---- HTTP stub:内容 = djb2(url) 填充 4KB(每 URL 唯一);可注入失败/延迟/webp/403 ---- */
static int g_http_calls;
static int g_fail_next; /* 剩余注入失败次数 */
static int g_delay_ms; /* 模拟慢网络 */
static int g_webp_next; /* 下一次下载返回 WebP 魔数体(200,测「200 解码失败」三分型③) */
static int g_status_next; /* 下一次下载强制返回该 HTTP status(测 403 防盗链;0=200) */
static unsigned long djb2(const char *s)
{
unsigned long h = 5381;
while (*s) h = ((h << 5) + h) + (unsigned char)*s++;
return h;
}
int ncm_http_request(const ncm_http_req_t *req, char *buf, size_t buf_sz, ncm_http_resp_t *resp)
{
g_http_calls++;
if (g_fail_next > 0) { g_fail_next--; return NCM_ERR_IO; }
if (g_delay_ms > 0) usleep((useconds_t)g_delay_ms * 1000);
size_t n = 4096 < buf_sz ? 4096 : buf_sz;
unsigned long h = djb2(req->url);
for (size_t i = 0; i < n; i++) buf[i] = (char)((h >> ((i % 8) * 8)) ^ (unsigned char)i);
memset(resp, 0, sizeof *resp);
resp->status = 200;
resp->body = buf;
resp->body_len = n;
resp->content_length = n;
return NCM_OK;
}
/* 流式下载桩:cover.c 现走 ncm_http_download(§30 改流式写文件,非旧 ncm_http_request)。
* 与 ncm_http_request 同语义:内容 = djb2(req->url) 填充 4KB,经写回调投递(落入 ncm_cache
* 写者临时文件);可注入失败/延迟。 */
int ncm_http_download(const ncm_http_req_t *req, ncm_http_write_cb cb, void *ctx,
size_t max_bytes, ncm_http_resp_t *resp)
{
(void)max_bytes;
g_http_calls++;
if (g_fail_next > 0) { g_fail_next--; return NCM_ERR_IO; }
if (g_delay_ms > 0) usleep((useconds_t)g_delay_ms * 1000);
unsigned char buf[4096];
size_t n = sizeof buf;
unsigned long h = djb2(req->url);
for (size_t i = 0; i < n; i++) buf[i] = (unsigned char)((h >> ((i % 8) * 8)) ^ (unsigned char)i);
/* webp 注入:前 12 字节置 RIFF....WEBP 魔数(stb 不支持 → cover.c 判 webp=1 不解码) */
if (g_webp_next) {
g_webp_next = 0;
buf[0]='R';buf[1]='I';buf[2]='F';buf[3]='F';
buf[4]=0;buf[5]=0;buf[6]=0;buf[7]=0;
buf[8]='W';buf[9]='E';buf[10]='B';buf[11]='P';
}
memset(resp, 0, sizeof *resp);
resp->status = g_status_next ? g_status_next : 200;
g_status_next = 0;
resp->body = (const char *)buf;
resp->body_len = n;
resp->content_length = n;
if (cb && cb(ctx, buf, n) != 0) return NCM_ERR_IO;
return NCM_OK;
}
/* URL 期望内容哈希(与 stub 生成式一致)。cover.c 现对封面 URL 追加 ?param=<NCM_COVER_PARAM>
* 缩略后缀(ncm_app_internal.h,REPORT §NEW 任务2)——实测请求的是参数版 URL,故期望值须按
* 同规则构造,否则「断言 raw URL」与「实测 param URL」哈希错位。已含 param= 不叠。 */
static unsigned long url_hash(const char *url)
{
char n[512];
snprintf(n, sizeof n, "%s", url);
#if NCM_COVER_USE_PARAM
if (!strstr(n, "param=")) {
size_t l = strlen(n), add = strlen("?param=") + strlen(NCM_COVER_PARAM);
if (l + add + 1 < sizeof n) {
strcat(n, strchr(n, '?') ? "&param=" : "?param=");
strcat(n, NCM_COVER_PARAM);
}
}
#endif
unsigned char tmp[4096];
unsigned long h = djb2(n);
for (size_t i = 0; i < sizeof tmp; i++) tmp[i] = (unsigned char)((h >> ((i % 8) * 8)) ^ (unsigned char)i);
return fnv1a(tmp, sizeof tmp);
}
static int g_fail;
#define CHECK(cond, fmt, ...) do { \
if (cond) printf(" PASS " fmt "\n", ##__VA_ARGS__); \
else { printf(" FAIL " fmt "\n", ##__VA_ARGS__); g_fail = 1; } \
} while (0)
/* 轮询等待封面内容变成 want_hash(≤ ms)。返回 1=达成。 */
static int wait_cover_hash(unsigned long want, int ms)
{
for (int t = 0; t < ms; t += 20) {
meow_img *im = cover_get();
unsigned long h = im ? im->hash : 0;
cover_put();
if (h == want) return 1;
usleep(20 * 1000);
}
return 0;
}
static unsigned long cur_cover_hash(void)
{
meow_img *im = cover_get();
unsigned long h = im ? im->hash : 0;
cover_put();
return h;
}
/* 读 cover_current.jpg 并哈希(验证 launcher 媒体卡片读的文件同步更新)。 */
static unsigned long curjpg_hash(void)
{
char path[600];
snprintf(path, sizeof path, "%s/cover_current.jpg", g_st.cache_dir);
FILE *f = fopen(path, "rb");
if (!f) return 0;
unsigned char b[4096];
size_t n = fread(b, 1, sizeof b, f);
fclose(f);
return n ? fnv1a(b, n) : 0;
}
#define URL_A "http://p1.music.126.net/albumA/xxx.jpg"
#define URL_B "http://p1.music.126.net/albumB/yyy.jpg"
#define URL_C "http://p1.music.126.net/albumC/zzz.jpg"
#define URL_D "http://p1.music.126.net/albumD/www.jpg"
#define URL_E "http://p1.music.126.net/albumE/vvv.jpg" /* webp 注入用(未缓存) */
#define URL_F "http://p1.music.126.net/albumF/uuu.jpg" /* 403 注入用(未缓存) */
int main(void)
{
if (system("rm -rf /tmp/ncm_cover_sim") != 0) { printf("FAIL rm /tmp cache\n"); return 1; }
memset(&g_st, 0, sizeof g_st);
snprintf(g_st.cache_dir, sizeof g_st.cache_dir, "/tmp/ncm_cover_sim");
ncm_cache_init(&g_st.ctx.cache, g_st.cache_dir);
printf("== T1: first request -> cover appears ==\n");
cover_request(URL_A);
CHECK(wait_cover_hash(url_hash(URL_A), 3000), "cover A shown (hash match)");
CHECK(g_http_calls == 1, "http calls=%d (expect 1)", g_http_calls);
CHECK(curjpg_hash() == url_hash(URL_A), "cover_current.jpg == A");
printf("== T2: settle -> worker idle (no busy loop) ==\n");
cover_ack();
int churn = 0;
for (int t = 0; t < 1200; t += 10) { if (cover_changed()) churn++; usleep(10 * 1000); }
CHECK(churn == 0, "cover_changed churn in 1.2s = %d (expect 0; §18 前=忙等死循环)", churn);
CHECK(g_http_calls == 1, "http calls=%d (still 1)", g_http_calls);
printf("== T3: switch song -> cover switches + cover_current.jpg updated ==\n");
cover_request(URL_B);
CHECK(wait_cover_hash(url_hash(URL_B), 3000), "cover switched to B");
CHECK(curjpg_hash() == url_hash(URL_B), "cover_current.jpg == B");
CHECK(g_http_calls == 2, "http calls=%d (expect 2)", g_http_calls);
cover_ack();
usleep(400 * 1000);
CHECK(!cover_changed(), "no churn after switch");
printf("== T4: re-request same url -> dedup ==\n");
cover_request(URL_B);
usleep(500 * 1000);
CHECK(g_http_calls == 2, "http calls=%d (still 2, dedup)", g_http_calls);
printf("== T5: flaky network x2 -> backoff retry succeeds ==\n");
g_fail_next = 2;
cover_request(URL_C);
CHECK(wait_cover_hash(url_hash(URL_C), 8000), "cover C after retries");
CHECK(g_http_calls >= 5, "http calls=%d (>=5: 2 fails + 1 ok for C)", g_http_calls);
CHECK(g_log_warns >= 2, "warn logs=%d (failures logged, 不无声)", g_log_warns);
printf("== T6: permanent failure -> give up after %d tries ==\n", 8);
g_fail_next = 100;
int before = g_http_calls;
cover_request(URL_D);
usleep(25 * 1000 * 1000 / 10); /* ~2.5s 不够 8 次退避(0.5+1+2+4*5=23.5s),分段等 */
for (int t = 0; t < 30 && g_http_calls - before < 8; t++) usleep(1000 * 1000);
int tries = g_http_calls - before;
CHECK(tries == 8, "give up after tries=%d (expect 8)", tries);
CHECK(cur_cover_hash() == url_hash(URL_C), "old cover C retained after give-up");
usleep(600 * 1000);
CHECK(g_http_calls - before == 8, "no further retries after give-up (calls=%d)",
g_http_calls - before);
printf("== T8: webp body (200) -> unsupported, clear target, no blind retry ==\n");
g_fail_next = 0;
int b8 = g_http_calls;
g_webp_next = 1;
cover_request(URL_E);
usleep(800 * 1000); /* 等 worker 处理 webp 体 → 判 webp → 清目标 → 转怠速 */
CHECK(g_http_calls == b8 + 1, "webp: single download=%d (no retry)", g_http_calls - b8);
CHECK(cur_cover_hash() == url_hash(URL_C), "webp: 旧封面 C 保留(兜底,不误换)");
CHECK(strstr(g_last_log, "webp unsupported") != NULL, "log: 'webp unsupported' seen");
printf("== T9: HTTP 403 anti-hotlink -> single download, no retry, distinct log ==\n");
int b9 = g_http_calls;
g_status_next = 403;
cover_request(URL_F);
usleep(800 * 1000);
CHECK(g_http_calls == b9 + 1, "403: single download=%d (no retry)", g_http_calls - b9);
CHECK(cur_cover_hash() == url_hash(URL_C), "403: 旧封面 C 保留(兜底)");
CHECK(strstr(g_last_log, "403") != NULL, "log: '403' seen");
g_status_next = 0;
printf("== T7: null/empty + reset ==\n");
int b7 = g_http_calls;
cover_request(NULL);
cover_request("");
CHECK(g_http_calls == b7, "null/empty ignored (calls=%d)", g_http_calls);
cover_cache_reset();
CHECK(cur_cover_hash() == 0, "reset -> cover cleared");
g_fail_next = 0;
printf(g_fail ? "== COVER CHAIN SIM: FAIL ==\n" : "== COVER CHAIN SIM: ALL PASS ==\n");
return g_fail;
}