Files
MeowOS-For-R3P2/patch_v2/ncm_app/app/logic/cover.c
T
AskaEth 44a790dc1d fix(ncm §33): 流媒专项批六项——流式SIGSEGV防御日志/档位保真(无损级必须flac+显示档按实际)/封面4xx不重试/收藏显隐/缓存命名迁移/topbar位图图标
- §2① ncm_api.c level_attempt: 无损级(hires/lossless及以上)命中必须 type=flac,返回 mp3 视为该档不可用续降
  (修「hires 档静默给 mp3 被当命中」)
- §2② player_manager.c eff_level_for_resp: 显示档按响应实际格式/码率——mp3 绝不显示 Hi-Res(≤128k→标准, 否则极高),
  flac 采信链 eff; br 未知钳 ≤EXHIGH
- §1 流式 SIGSEGV(《十面埋伏》sig=11) 审计+UBSAN 探针未复现 → 落地防御日志(dl start/done/interrupt + play done fin/exp/fail/underrun)
- §3 cover.c: download_cover 回填 rc/status; 确定性 4xx(404等)不重试立即跳下一张, 仅瞬时错误退避(rc=%d status=%d 日志)
- §4 playlist.c: pl_show_fav 我创建(is_collected==0)隐藏收藏钮, 我收藏保留, 搜索歌单保守显示(评论槽并入无空洞)
- §5 ncm_cache.c 双点→单点迁移 + player_manager song_local_file 旧键→当前档位键(不重下)
- §6 home.c: topbar 动作 ⚙↻⌕ 迁移 sb_icons 位图(meow_topappbar_set_item_icon, 修 4k 字库豆腐)
- 测试: test_ncm_core 新 test_grade_flac_gate + test_cache 双点迁移断言; degrade_sim/player_flow_sim 桩 flac 化
- 验证: stream/player_flow/window/search/cache_evict/degrade sim 全 PASS + test_ncm_core host+mipsel(qemu) + mem_sim + build_ncm.sh 0 warning
- 新 ncm md5 cb818786 (text 1017110/bss 1362500), 未部署留主会话; 0 真实 NCM 业务请求
2026-09-02 04:20:46 +08:00

318 lines
15 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.
/* logic/cover.c — 封面下载(HTTPS)+ 限幅 + 解码缓存(单张,URL 变更即换;后台线程)。
*
* 内存纪律:下载限幅 NCM_COVER_MAX(字节上限);解码后的 RGBA 保持单张(换封面先 free 旧再解码)。
* 线程模型:一个持久的 cover 工作线程负责「目标 URL → 下载 → 解码 → 换入」;UI 线程只经
* cover_request(url)(提交目标)与 cover_get()/cover_changed()(读取/判变)+ cover_ack()(消费)。
* 工作线程不碰 meow_ui(绘制在 UI 线程 tick 判 cover_changed 后触发 invalidate)。
*
* §18 修复(切歌封面不更新/CPU 忙等根因):
* - worker 换入成功后**清目标**(g_tgt_url[0]=0)转怠速——旧版成功后不清目标,每圈
* 缓存读+JPEG 解码+换入+g_gen++ 无 sleep 死循环:CPU 占满、cover_changed() 恒真导致
* 播放页每帧整盘重绘,慢网络下新封面请求被饿到表现如「切歌封面不更新」。
* - 下载/解码失败**指数退避**重试(0.5s→4s),连续 8 次放弃(UI 重进页会重新提交目标);
* 失败写 WARN 日志(排查资产),不再无声死等。
* - 绘制安全:cover_get() 持锁返回、cover_put() 解锁——旧版 UI 绘制期 worker 可 free
* 同一 meow_img(use-after-free);调用方须在 cover_get 后配对 cover_put。
*
* §30 修复(真机封面全加载失败):旧版 `static g_buf[NCM_COVER_MAX(288KB)]` 作为下载缓冲,
* 网易云封面全尺寸实测 ~1.34MB(>288KB)→ ncm_http 回 NCM_ERR_FULL → 下载失败×8 放弃。
* 且设备 RAM 仅 56MB,不能再涨静态 bss。改为**流式写文件**:ncm_http_download 的写回调直接
* 把 body 写进 ncm_cache 写者临时文件(write(2) 直写,无 stdio 缓冲),meow_img_load_file 从
* 临时文件解码,commit 落盘。全程零封面大缓冲、无 bss 上涨;NCM_COVER_MAX 仅作下载字节上限。
*/
#include "../ncm_app_internal.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
static pthread_mutex_t g_cm = PTHREAD_MUTEX_INITIALIZER;
static meow_img *g_cover_img;
static char g_cover_url[512]; /* g_cover_img 对应的 URL */
static char g_tgt_url[512]; /* 目标 URL(worker 拉取) */
static pthread_t g_thr;
static int g_thr_on;
static int g_quit;
static volatile int g_gen; /* 每次换入 +1 */
static int g_acked; /* UI 已消费的版本 */
static char g_cover_path[512]; /* 封面原始文件路径(供 launcher) */
/* 封面缓存键:URL 的 djb2 哈希(稳定性由 URL 决定——同一专辑封面 URL 名即可命中)。
* 走 ncm_cache(LRU 口径沿用 ncm_cache,见 REPORT §14.2):键进 ncm_cache_path => 文件
* `song_cov<hex>.jpg`,与 mp3(song_<id>.mp3)同池,lookup/commit 刷新 mtime(LRU)。 */
static void cover_cache_id(const char *url, char *out, size_t out_sz)
{
unsigned long h = 5381;
for (const char *p = url; *p; p++) h = ((h << 5) + h) + (unsigned char)*p;
snprintf(out, out_sz, "cov%lx", h);
}
/* 文件存在且非空(封面缓存命中用;streaming 后无需整读进内存)。 */
static int file_nonempty(const char *path)
{
struct stat st;
return (path && stat(path, &st) == 0 && st.st_size > 0) ? 1 : 0;
}
/* 流式写回调:body 字节直接写进 ncm_cache 写者临时文件(write(2) 直写)。ctx=cover_sink_t*。 */
typedef struct { ncm_cache_writer_t *w; size_t total; } cover_sink_t;
static int cover_sink_write(void *ctx, const unsigned char *data, size_t len)
{
cover_sink_t *s = (cover_sink_t *)ctx;
if (ncm_cache_write(s->w, data, len) != 0) return -1;
s->total += len;
return 0;
}
/* 下载 url 到 cache 写者 w(流式,写进其临时文件),成功后从临时文件解码并返回 img。
* 失败返回 NULL(不 commit,调用方 abort)。*out_len=body 字节数;*out_rc 与 *out_status
* 回填下载 rc 与 HTTP status(封面 4xx 加固判定:确定性 4xx 不重试,REPORT §NEW 任务3)。 */
static meow_img *download_cover(const char *url, ncm_cache_writer_t *w, size_t *out_len,
int *out_rc, int *out_status)
{
ncm_http_req_t req;
memset(&req, 0, sizeof req);
req.url = url;
req.method = "GET";
req.extra_hdrs = "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36";
req.verify_ca = NCM_HTTP_VERIFY_DEFAULT;
req.timeout_ms = 15000;
ncm_http_resp_t resp;
cover_sink_t sink = { w, 0 };
int rc = ncm_http_download(&req, cover_sink_write, &sink, NCM_COVER_MAX, &resp);
if (out_rc) *out_rc = rc;
if (out_status) *out_status = resp.status;
if (rc != NCM_OK || resp.status != 200 || sink.total == 0) return NULL;
if (out_len) *out_len = sink.total;
/* 从写者临时文件解码(未 commit,文件仍在 .tmp;解码到内存 RGBA,与落盘无关) */
const char *tmp = ncm_cache_writer_tmp_path(w);
if (!tmp) return NULL;
return meow_img_load_file(tmp);
}
/* 封面原始文件路径写入(供 launcher 媒体卡片读;兼容保留)。调用方须已持有 g_cm。
* 从 cache 写者临时文件把已下载字节复制到 cover_current.jpg(早前版本在此内再 lock g_cm
* → 非递归锁自锁死——cover worker 永久卡死、封面永不出现)。 */
static void save_cover_raw(const char *path, size_t len)
{
if (!path || len == 0) return;
if (g_st.cache_dir[0]) {
char dst[NCM_CACHE_DIR_MAX + 32];
snprintf(dst, sizeof dst, "%s/cover_current.jpg", g_st.cache_dir);
/* 文件复制(源=临时文件,目标=cover_current.jpg)。 */
FILE *in = fopen(path, "rb");
FILE *out = NULL;
if (in) {
out = fopen(dst, "wb");
if (out) {
char buf[4096];
size_t n;
while ((n = fread(buf, 1, sizeof buf, in)) > 0)
if (fwrite(buf, 1, n, out) != n) { fclose(out); out = NULL; break; }
if (out) fclose(out);
}
fclose(in);
}
ncm_strlcpy(g_cover_path, dst, sizeof g_cover_path);
}
}
#define COVER_RETRY_MAX 8 /* 连续失败放弃次数(UI 重进页/切歌会重新提交目标) */
/* 内存瘦身:封面解码后按显示上限降采样驻留。网易云全尺寸封面(如 1448×1448)解码成
* RGBA = 8MB,而应用显示只需 ≤480px(播放页黑胶方图+歌词页模糊铺底均 ≤ 屏幕 480 宽)——
* 缩到 480×480 = 0.9MB,省 ~7MB anon(设备 RAM 仅 56MB,OOM 真机被杀,最高优先级)。
* 若已 ≤上限(小缩略图)则原样保留。返回本函数处理后的图(可能替换/释放旧图)。 */
#define NCM_COVER_DECODE_MAX 480
static meow_img *cover_cap(meow_img *im)
{
if (!im) return NULL;
meow_img *small = meow_img_downscale(im, NCM_COVER_DECODE_MAX);
if (small) { meow_img_free(im); return small; } /* 已缩放:释放原始大图,换小图驻留 */
return im; /* 本就够小:保留原图 */
}
static void *cover_worker(void *arg)
{
(void)arg;
int backoff_ms = 0, fails = 0;
while (!g_quit) {
pthread_mutex_lock(&g_cm);
if (!g_tgt_url[0]) {
pthread_mutex_unlock(&g_cm);
backoff_ms = 0; fails = 0;
usleep(250 * 1000);
continue;
}
char url[512];
ncm_strlcpy(url, g_tgt_url, sizeof url);
pthread_mutex_unlock(&g_cm);
char key[32];
cover_cache_id(url, key, sizeof key);
size_t blen = 0;
meow_img *img = NULL;
int drc = NCM_ERR_NET, dstatus = 0; /* 本目标最近一次下载 rc/HTTP status(4xx 判定用) */
/* ① 缓存命中直接解码(免下载、离线可看;LRU:lookup 刷新 mtime) */
char cpath[512];
if (g_st.ctx.cache.dir[0] && ncm_cache_lookup(&g_st.ctx.cache, key, ".jpg",
cpath, sizeof cpath) == NCM_OK &&
file_nonempty(cpath)) {
img = meow_img_load_file(cpath);
}
/* ② 未命中 → 流式下载解码;成功后落进 ncm_cache(LRU 池,超限由 ncm_cache_evict 淘汰) */
if (!img) {
if (g_st.ctx.cache.dir[0]) {
ncm_cache_evict(&g_st.ctx.cache);
ncm_cache_writer_t *w = NULL;
if (ncm_cache_open(&g_st.ctx.cache, key, ".jpg", &w) == NCM_OK) {
blen = 0;
img = download_cover(url, w, &blen, &drc, &dstatus);
if (img) {
pthread_mutex_lock(&g_cm);
save_cover_raw(ncm_cache_writer_tmp_path(w), blen);
pthread_mutex_unlock(&g_cm);
if (ncm_cache_commit(w) != 0) { ncm_cache_abort(w); }
} else {
ncm_cache_abort(w);
}
ncm_cache_evict(&g_st.ctx.cache);
} else {
/* 打开写者失败(罕见):退避重试,不空转 */
img = NULL;
}
}
}
/* 内存瘦身:全尺寸封面解码后降采样到 ≤480px 驻留(省 ~7MB anon,OOM 修复)。 */
if (img) img = cover_cap(img);
if (!img) {
/* 失败分诊(REPORT §NEW 任务3):确定性 4xx(404 死链等)**不重试**,立即清目标
* 跳下一张(重试也白费);仅瞬时错误(超时/网络/TLS/5xx)指数退避重试。
* 日志打 rc/status(排查资产:先前只见 "fetch failed" 不知 HTTP 层返了什么)。 */
int deterministic = (dstatus >= 400 && dstatus < 500 && dstatus != 408 && dstatus != 429);
meow_log(MEOW_LOG_WARN, "cover: fetch fail url=%.64s rc=%d status=%d%s",
url, drc, dstatus, deterministic ? " (4xx, no retry)" : "");
if (deterministic) {
pthread_mutex_lock(&g_cm);
if (strcmp(g_tgt_url, url) == 0) g_tgt_url[0] = 0; /* 立即跳下一张 */
pthread_mutex_unlock(&g_cm);
backoff_ms = 0; fails = 0;
continue;
}
/* 瞬时错误:指数退避重试,连续 COVER_RETRY_MAX 次放弃(不无声死等;
* 目标保留期间 UI 每帧重请求由 cover_request 去重挡住)。 */
fails++;
if (fails >= COVER_RETRY_MAX) {
meow_log(MEOW_LOG_WARN, "cover: give up after %d fails url=%.64s", fails, url);
pthread_mutex_lock(&g_cm);
if (strcmp(g_tgt_url, url) == 0) g_tgt_url[0] = 0;
pthread_mutex_unlock(&g_cm);
backoff_ms = 0; fails = 0;
continue;
}
meow_log(MEOW_LOG_WARN, "cover: fetch failed (%d) url=%.64s retry in %dms",
fails, url, backoff_ms ? backoff_ms : 500);
if (g_app_quit) return NULL;
usleep(backoff_ms ? backoff_ms * 1000 : 500 * 1000);
backoff_ms = backoff_ms ? (backoff_ms * 2 > 4000 ? 4000 : backoff_ms * 2) : 500;
continue;
}
backoff_ms = 0; fails = 0;
pthread_mutex_lock(&g_cm);
/* 目标仍是 url 才换入(期间未提交更早) */
if (g_tgt_url[0] && strcmp(g_tgt_url, url) == 0) {
if (g_cover_img) meow_img_free(g_cover_img);
g_cover_img = img;
ncm_strlcpy(g_cover_url, url, sizeof g_cover_url);
g_gen++;
g_tgt_url[0] = 0; /* 目标达成即清:worker 转怠速(修忙等死循环,§18) */
} else {
meow_img_free(img);
}
pthread_mutex_unlock(&g_cm);
}
return NULL;
}
/* UI 线程:提交目标封面 URL(无网络/无 URL 则忽略)。 */
void cover_request(const char *url)
{
if (!url || !url[0]) return;
if (g_host_mock) return; /* 宿主 mock = 0 网络纪律(mock URL 不得发真实 HTTPS) */
pthread_mutex_lock(&g_cm);
/* 去重:已在展示 或 已在拉取途中的同一 URL 不重复提交。 */
if ((g_cover_url[0] && strcmp(g_cover_url, url) == 0) ||
(g_tgt_url[0] && strcmp(g_tgt_url, url) == 0)) { pthread_mutex_unlock(&g_cm); return; }
ncm_strlcpy(g_tgt_url, url, sizeof g_tgt_url);
pthread_mutex_unlock(&g_cm);
if (!g_thr_on) {
g_thr_on = 1;
if (ncm_thread_create(&g_thr, cover_worker, NULL) == 0)
pthread_detach(g_thr);
else
g_thr_on = 0;
}
}
/* UI 线程:读取当前封面——**持锁返回**,绘制完必须配对 cover_put()(§18 修 UAF:
* 旧版 get 后无锁绘制,worker 换入时 free 同一张图 → use-after-free)。 */
meow_img *cover_get(void)
{
pthread_mutex_lock(&g_cm);
return g_cover_img;
}
void cover_put(void)
{
pthread_mutex_unlock(&g_cm);
}
/* 是否有新封面待绘制(UI tick 判变)。 */
int cover_changed(void) { return g_gen != g_acked; }
/* UI 消费一次:标记已见当前版本。 */
void cover_ack(void) { g_acked = g_gen; }
/* 封面原始文件路径(供 launcher 媒体卡片读);NULL=无。 */
const char *cover_current_path(void)
{
const char *p;
pthread_mutex_lock(&g_cm);
p = g_cover_path[0] ? g_cover_path : NULL;
pthread_mutex_unlock(&g_cm);
return p;
}
void cover_cache_reset(void)
{
pthread_mutex_lock(&g_cm);
if (g_cover_img) { meow_img_free(g_cover_img); g_cover_img = NULL; }
g_cover_url[0] = 0;
g_tgt_url[0] = 0;
g_cover_path[0] = 0;
pthread_mutex_unlock(&g_cm);
g_gen++;
g_acked = g_gen;
}
#ifdef MEOW_UI_HOST_TEST
/* 宿主测试桩(仅宿主构建;产品构建无此符号):注入一张本地封面图(不经网络),
* 验证封面页方形圆角大图与歌词页模糊置底路径(§22)。0 真实请求纪律不变。 */
void cover_host_inject(const char *path)
{
if (!path || !path[0]) return;
meow_img *img = meow_img_load_file(path);
if (!img) { meow_log(MEOW_LOG_WARN, "cover host inject: load failed %s", path); return; }
pthread_mutex_lock(&g_cm);
if (g_cover_img) meow_img_free(g_cover_img);
g_cover_img = img;
ncm_strlcpy(g_cover_url, path, sizeof g_cover_url);
g_cover_path[0] = 0;
g_gen++;
pthread_mutex_unlock(&g_cm);
}
#endif