feat: M6.44+M6.45 收编 — 真机 0.8.45 现场保全(reader章节/key_router按键路由/WiFi连接修复/ncm bench+IME修复)
脏工作区现场收编:devdocs/local/m6_44_45_dirty_tracked.patch(46 个已跟踪文本文件,
+3090/-585, 已对 M6.43 基线干净适配) + m6_44_45_untracked_src.tar.gz 中 patch_v2/ 下
13 个新增源文件, 合成单个提交, 内容与容器工作树(R3ProII/patch_v2)逐字节一致。
- reader 章节: 新增 core/rdchapter.{h,c} — TXT 章节规范解析器, 扫描 512KB 码位对齐段文件
逐段流式识别标题行(行首缩进≤4/显示宽≤40/行内标点≤1 三重约束防误判), 输出标题+全书字符
偏移+段号/段内偏移+阿拉伯章号(中文数字归一), 首章前内容归"卷首"; book/reader/rdenc/
rdlayout/settings 接入章节跳转与进度锚点, 附 chapter_smoke/turn_repro/build_turn_repro.sh
- key_router 按键路由: 新增 meow_launcherd/key_router.{h,c} — launcher-UI 页物理键统一路由
(P1 仅音量键), 把读取边沿+防抖+长按 held+ramp 推进从 5 个会话收敛为进程内单开;
各页每圈 key_router_pump() 取防抖后边沿, 动作(调音量/光标导航)仍由调用页决定,
保留 run_menu 的 meow_mode 分支; App 前台 volume_page 武装时边沿抑制;
main_menu/now_playing/notif_center/settings_page/subpage 接入, 附 test_key_router_vol/test_np_keys
- WiFi 连接修复: meow_netmgrd connect 有界等待 wpa_state=COMPLETED(普通 10s/隐藏网络 15s,
env 可调)修设置页连接无响应; list_networks/scan_results 的 \xNN 转义还原始字节, 修中文 SSID
永不命中致新建同名 open 记录顶影; launcher netmgr_ipc 配读超时; 附 test_wifi_connect(桩
wpa_cli 驱动 static wifi_connect_ex), test_netmgrd.sh 补用例; wifi_page 同步
- ncm bench: 新增 bench/lyric_sim.c、bench/plpage_sim.c, vol_sync_sim 与 ncm core/app 同步
- SDK IME 修复: shift 大小写、K_ENTER 只提交不关、阻塞 IME 循环喂 on_tick 不被遮挡、
IME 打开 ~200ms TAP 冷却窗吞 stray-TAP; 附 sdk/test_ime_fix.c
- 同期: meow_audiod 音量异步 ACK 线程(代际防 fd/槽复用竞态)、watchdog 音量收编(首选 audiod
权威, settings.conf 覆盖路径)、panel 文件搜索(F1-④)与蓝牙只读详情(F1-③)、各类 build 脚本同步
范围红线: third_party/legado-E-main 原开发者未跟踪, 本次不入库(保留 devdocs/local tar 参考);
二进制不入库(后续重编); devdocs/ 不入库。59 文件全部在 patch_v2/ 下, 无 third_party/devdocs。
验证: git apply --check 干净; 59 文件 md5 与容器 patch_v2 逐一比对 59/59 一致(含
key_router.c/rdchapter.c/meow_ui_ime.c/test_wifi_connect.c 抽查); 可执行位保守
(check_lgpl_release.sh 100755, build_turn_repro.sh 100644 与容器一致)。
This commit is contained in:
1 parent
e87ccdf061
commit
7c7d891570
59 files changed
+5305
-590
No files matched your search
@@ -537,7 +537,10 @@ int audio_out_pause(int paused); /* 1=暂停(停 PCM 时钟不丢流) 0=恢
|
||||
void audio_out_close(void);
|
||||
int audio_out_sync_volume(void); /* 任务1:读 settings.conf 当前系统音量并下发到 audiod
|
||||
(MSG_AUDIO_VOLUME→写 DAC+持久化)。0=下发成功/-1=失败。
|
||||
audio_out_open SINK_AUDIOD 成功后已自动调;独立导出供宿主验证与复调。 */
|
||||
每音频会话(新 audiod 流 sid)首同步一次 + 值变才重发——切歌/新建
|
||||
会话时 DAC 音量不残留默认响度(修「切歌音量骤增」);同会话内不
|
||||
重复建连(不做 fd 风暴)。audio_out_open SINK_AUDIOD 成功后已自动调;
|
||||
独立导出供宿主验证与复调。 */
|
||||
void audio_out_set_target(int ms); /* 预留:缓冲目标 */
|
||||
int audio_out_buffered_ms(void);
|
||||
|
||||
|
||||
@@ -233,20 +233,27 @@ static void audiod_sync_volume(int pct)
|
||||
close(fd);
|
||||
}
|
||||
|
||||
/* 每进程同步一次 + 值变才重发:杜绝每次 audio_out_open 新建短连接(fd=N accept 风暴根因)。
|
||||
* g_vol_synced=1 即已同步过;g_vol_last=上次下发的 pct,二者相同则零连接直接返回。 */
|
||||
static int g_vol_synced = 0;
|
||||
static int g_vol_last = -1;
|
||||
/* 会话身份 = audiod 流 sid(g_sid):切歌/新建音频会话 = 每次 audio_out_open 都向 audiod 开一条
|
||||
* 新流、audiod 分配新 sid(单调不重用)。此前用进程级 g_vol_synced「首次同步一次」——切歌(新会话)
|
||||
* 时音量不再被重发到 audiod,DAC 落在会话建立/重配后的默认/上次残留值(衰减量 0=最响),造成
|
||||
* 「切歌音量骤增,按一下音量键才回落到设定值」(用户真机 2026-09-05)。audiod 只在 reconfigure
|
||||
* 且参数变化时才 re-apply 音量(_audio_mark_reconfig_needed 同参数跳过),本会话重发是权威兜底。
|
||||
* 修法:**每会话首同步一次**——新 sid(新会话)必重发一次当前音量;同会话内仅值变才重发。
|
||||
* 每会话恰 1 条短连接,不回到「每次 open 都建连」的 fd 风暴(§2026-09-02 根因,vol_sync_sim 60 次
|
||||
* 同值→1 连接)。音量值取自 settings.conf(audiod 权威同源同键),反转/真静音(255) 由 audiod
|
||||
* dac_vol_set 负责,本层只传 pct(0-100)。 */
|
||||
static int g_vol_synced_sid = -1; /* 上次音量同步针对的 audiod 流 sid;-1=从未(必发) */
|
||||
static int g_vol_last = -1;
|
||||
|
||||
int audio_out_sync_volume(void)
|
||||
{
|
||||
int pct = settings_read_volume();
|
||||
if (g_vol_synced && pct == g_vol_last) return 0; /* 已同步且未变:零新建连接 */
|
||||
g_vol_synced = 1;
|
||||
g_vol_last = pct;
|
||||
if (g_vol_synced_sid == g_sid && pct == g_vol_last) return 0; /* 本会话已同步且值未变:零新建连接 */
|
||||
g_vol_synced_sid = g_sid;
|
||||
g_vol_last = pct;
|
||||
audiod_sync_volume(pct);
|
||||
meow_log(MEOW_LOG_INFO, "audio: sync volume -> audiod pct=%d (settings %s)",
|
||||
pct, settings_conf_path());
|
||||
meow_log(MEOW_LOG_INFO, "audio: sync volume -> audiod pct=%d (settings %s, sid=%d)",
|
||||
pct, settings_conf_path(), g_sid);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -379,6 +386,7 @@ int audio_out_pause(int paused)
|
||||
}
|
||||
g_sid = sid;
|
||||
meow_log(MEOW_LOG_INFO, "audio: audiod reopen ok sid=%d", sid);
|
||||
audio_out_sync_volume(); /* 新会话(新 sid):音量权威兜底重发一次(DAC 复位后不残留默认响度) */
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -1032,10 +1032,18 @@ static int g_lyr_thr_on;
|
||||
static volatile int g_lyr_state; /* 0=空闲/未拉 1=加载中 2=已拉完 */
|
||||
static char g_lyr_song_id[24]; /* 歌词归属歌曲 id */
|
||||
|
||||
/* 歌词拉取失败后的重试退避(ms,指数翻倍封顶 OS):掉线瞬间失败的请求**不**写死「无歌词」,
|
||||
* 交回退避重试;网络恢复后下一轮到点即重拉成功(真机症状:掉线一会儿再重连后恒「暂无歌词」)。 */
|
||||
#define LYRIC_RETRY_BASE_MS 1000
|
||||
#define LYRIC_RETRY_MAX_MS (30 * 1000)
|
||||
|
||||
static void *lyric_worker(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
char last_id[24] = "";
|
||||
int last_ok = 1; /* last_id 对应结果是否成功(成功不重拉;失败记退避待重试) */
|
||||
int retry_delay = 0; /* 失败后逐次翻倍的退避基值(ms) */
|
||||
int64_t retry_at = 0; /* 单调时刻:下次重试;0=无待重试 */
|
||||
while (!g_app_quit) {
|
||||
usleep(300 * 1000);
|
||||
char id[24];
|
||||
@@ -1051,14 +1059,28 @@ static void *lyric_worker(void *arg)
|
||||
g_lyr_state = 0;
|
||||
pthread_mutex_unlock(&g_lyrmu);
|
||||
last_id[0] = 0;
|
||||
last_ok = 1;
|
||||
retry_delay = 0;
|
||||
retry_at = 0;
|
||||
continue;
|
||||
}
|
||||
if (!id[0]) continue;
|
||||
if (strcmp(id, last_id) == 0) continue; /* 同一曲已拉/在拉 */
|
||||
if (meow_cachegov_prefetch_paused()) continue; /* page-cache 治理(§2.3/§3):暂停歌词预取
|
||||
* (A 回退硬暂停 OR svc_perf C Cached 降载),
|
||||
* 不置 last_id → 降载恢复后下轮再拉。 */
|
||||
ncm_strlcpy(last_id, id, sizeof last_id);
|
||||
|
||||
int is_new = (strcmp(id, last_id) != 0);
|
||||
if (is_new) {
|
||||
if (meow_cachegov_prefetch_paused()) continue; /* page-cache 治理(§2.3/§3):暂停歌词预取
|
||||
* (A 回退硬暂停 OR svc_perf C Cached 降载),
|
||||
* 不置 last_id → 降载恢复后下轮再拉。 */
|
||||
ncm_strlcpy(last_id, id, sizeof last_id);
|
||||
last_ok = 0;
|
||||
retry_delay = 0;
|
||||
retry_at = 0;
|
||||
} else {
|
||||
if (last_ok) continue; /* 同一曲已成功拉完(含 NCM_OK 空词=真无词),不重拉 */
|
||||
if (meow_cachegov_prefetch_paused()) continue;
|
||||
if (retry_at > ncm_monotonic_ms()) continue; /* 失败退避未到 */
|
||||
/* 到点重试:保留 retry_delay 以便失败时继续翻倍 */
|
||||
}
|
||||
lyric_clear();
|
||||
pthread_mutex_lock(&g_lyrmu);
|
||||
g_lyr_state = 1; /* 加载中 */
|
||||
@@ -1068,22 +1090,32 @@ static void *lyric_worker(void *arg)
|
||||
char *tl = (char *)malloc(32 * 1024);
|
||||
if (!lrc || !tl) {
|
||||
free(lrc); free(tl);
|
||||
retry_delay = retry_delay ? (retry_delay * 2) : LYRIC_RETRY_BASE_MS;
|
||||
if (retry_delay > LYRIC_RETRY_MAX_MS) retry_delay = LYRIC_RETRY_MAX_MS;
|
||||
retry_at = ncm_monotonic_ms() + retry_delay;
|
||||
pthread_mutex_lock(&g_lyrmu); g_lyr_state = 0; pthread_mutex_unlock(&g_lyrmu);
|
||||
continue;
|
||||
}
|
||||
lrc[0] = tl[0] = 0; /* 默认空串(fetch 失败/空词时上报清词,防会话留旧曲歌词) */
|
||||
int rc = ncm_api_lyric(g_st.ctx.device_id, g_st.ctx.cookie, id, lrc, 64 * 1024, tl, 32 * 1024);
|
||||
if (rc == NCM_OK) {
|
||||
last_ok = 1; /* 真正结果:空词(NCM_OK 无词也算「已拉完」) */
|
||||
retry_delay = 0;
|
||||
retry_at = 0;
|
||||
lyric_parse(lrc, tl);
|
||||
ipc_media_lyric_report(lrc, tl); /* 批3:上报 live 歌词到媒体会话 */
|
||||
} else {
|
||||
/* 拉取失败(网络/HTTP/会话):不写死「无词」,交回退避重试;UI 呈「加载中」而非「暂无歌词」 */
|
||||
retry_delay = retry_delay ? (retry_delay * 2) : LYRIC_RETRY_BASE_MS;
|
||||
if (retry_delay > LYRIC_RETRY_MAX_MS) retry_delay = LYRIC_RETRY_MAX_MS;
|
||||
retry_at = ncm_monotonic_ms() + retry_delay;
|
||||
ipc_media_lyric_report("", ""); /* 无词/拉取失败:清 media session 该曲歌词 */
|
||||
}
|
||||
free(lrc); free(tl);
|
||||
|
||||
pthread_mutex_lock(&g_lyrmu);
|
||||
ncm_strlcpy(g_lyr_song_id, id, sizeof g_lyr_song_id);
|
||||
g_lyr_state = 2; /* 拉完(可能 0 行=无词) */
|
||||
g_lyr_state = last_ok ? 2 : 0; /* 2=已拉完(可 0 行=无词);0=失败待重试(UI 呈加载中) */
|
||||
pthread_mutex_unlock(&g_lyrmu);
|
||||
}
|
||||
return NULL;
|
||||
|
||||
@@ -26,8 +26,12 @@ static volatile int g_load_err;
|
||||
static int g_press_row, g_press_delta, g_dragging, g_drag_last_y;
|
||||
|
||||
/* 歌单详情加载后台 worker(大歌单可达数十秒,不能占 UI 线程)。
|
||||
* §17 滑动窗口:worker 两种任务——①首屏(g_ens_first=1):重建 trackIds 索引 + 物化首段;
|
||||
* ②滑窗补载:物化视口区间 [g_ens_lo,g_ens_hi)(ensure_range,盘缓存优先)。 */
|
||||
* §17 滑动窗口:worker 三种任务——①首屏(g_ens_first=1):重建 trackIds 索引 + 物化第 0 页
|
||||
* (≤NCM_SONG_BATCH 首,一次一页——不再首屏连拉多批补满窗口,大歌单阻塞久且触发风控);
|
||||
* ②顺序下一页(g_ens_page=1):仅当用户滑到已分页底部(余量 ≤NCM_PL_PAGE_EDGE 首)时拉一页,
|
||||
* 成功推进 g_paged_hi,拉完该歌单全部曲目即止;③回滚补物化:已分页区间内被窗口挤出的
|
||||
* 失效槽按视口 ±24 补(盘缓存命中为主,不占网络分页额度)。 */
|
||||
#define NCM_PL_PAGE_EDGE 20 /* 懒加载触发余量(首):视口底距已分页底部 ≤20 首才拉下一页 */
|
||||
static pthread_mutex_t g_plmu = PTHREAD_MUTEX_INITIALIZER;
|
||||
static pthread_t g_plthr;
|
||||
static int g_plthr_on;
|
||||
@@ -37,9 +41,11 @@ static volatile int g_plloading;
|
||||
static volatile int g_plloaded_idx;
|
||||
static volatile int g_plloaded_gen; /* worker 启动时代次(§26:过期结果丢弃判据) */
|
||||
static volatile int g_pl_stale; /* 1=本次结果被判过期(未写全局,需重拉当前目标) */
|
||||
static volatile int g_ens_lo, g_ens_hi; /* 滑窗补载目标区间(全局下标) */
|
||||
static volatile int g_ens_lo, g_ens_hi; /* 补载/下一页目标区间(全局下标) */
|
||||
static volatile int g_ens_first; /* 1=首屏重头(offset=0) */
|
||||
static volatile int g_ens_page; /* 1=顺序下一页任务(成功推进 g_paged_hi) */
|
||||
static volatile int g_ens_err; /* 最近一次滑窗补载错误(0=ok;首屏错误走 g_load_err) */
|
||||
static volatile int g_paged_hi; /* 已分页高水位:从顶部连续物化到的全局下标(下一页起点) */
|
||||
|
||||
/* 打开快照(§26 开A→未载完→开B 防串):refresh 在启动 worker 前把「目标歌单」拷进 g_tgt,
|
||||
* worker 启动时再拷到本地——detail 加载不再依赖 g_st.playlists[](会被回家时 home_worker 的
|
||||
@@ -239,12 +245,15 @@ static void pl_draw_footer(void *ctx)
|
||||
ncm_draw_footer(r_cont, foot);
|
||||
}
|
||||
|
||||
/* apply:reset_scroll=1 时列表回顶并清选中(首屏/刷新);0=保持当前滚动(滑窗补载,防跳顶)。 */
|
||||
/* apply:reset_scroll=1 时列表回顶并清选中(首屏/刷新);0=保持当前滚动(滑窗补载,防跳顶)。
|
||||
* 拉到数据后必须让列表项重新绑定/重绘:src2 每帧 live 取行(占位行 ↔ 已物化歌名/歌手),
|
||||
* 失效列表带 + 整帧重画把刚物化的歌名/歌手填进各项(修「拉完曲目行仍 ... 加载中」)。 */
|
||||
static void pl_apply(int reset_scroll)
|
||||
{
|
||||
if (g_list) {
|
||||
meow_listview_set_count(g_list, g_st.track_ids_count); /* 全量行数(占位行兜底未物化区) */
|
||||
if (reset_scroll) { meow_listview_set_scroll(g_list, 0); meow_listview_set_sel(g_list, -1); }
|
||||
meow_ui_invalidate(r_list.x, r_list.y, r_list.w, r_list.h);
|
||||
}
|
||||
const ncm_playlist_t *pcur = pl_cur();
|
||||
if (g_tb && pcur) {
|
||||
@@ -261,30 +270,24 @@ static void *playlist_worker(void *arg)
|
||||
(void)arg;
|
||||
/* 拷贝打开快照 + 任务参数:单飞(g_plthr_on)保证 worker 运行期间 g_tgt 不被改写。 */
|
||||
ncm_playlist_t tpl;
|
||||
int tsearch, tidx, tgen, first, lo, hi;
|
||||
int tsearch, tidx, tgen, first, lo, hi, page;
|
||||
pthread_mutex_lock(&g_plmu);
|
||||
tpl = g_tgt.pl; tsearch = g_tgt.is_search;
|
||||
tidx = g_tgt.idx; tgen = g_tgt.gen;
|
||||
first = g_ens_first; lo = g_ens_lo; hi = g_ens_hi;
|
||||
first = g_ens_first; lo = g_ens_lo; hi = g_ens_hi; page = g_ens_page;
|
||||
pthread_mutex_unlock(&g_plmu);
|
||||
int cur_active = g_st.active_playlist; /* ensure 路径结束校验用(当前目标) */
|
||||
int rc;
|
||||
if (g_host_mock) rc = 0;
|
||||
else if (first) {
|
||||
/* 首屏:快照式加载(detail 全量 trackIds + 物化首段)。ctx 校验代次/目标,过期自动丢弃。 */
|
||||
/* 首屏:快照式加载(detail 全量 trackIds + 物化第 0 页)。ctx 校验代次/目标,过期自动丢弃。
|
||||
* §懒加载:首屏只拉一页(≤NCM_SONG_BATCH 首,一次仅一批)——不再 ensure_range 补满整个
|
||||
* 滑动窗口(旧版首屏连拉 ≤3 批:5k+ 大歌单「我喜欢的音乐」长时间卡住不出列表,连续
|
||||
* 请求还触发风控);后续页由 pl_maybe_ensure 在用户滑到已分页底部时逐页懒加载。 */
|
||||
if (tidx < 0) rc = NCM_ERR_ARG;
|
||||
else rc = appstate_load_playlist_songs_ctx(&tpl, tsearch, tgen, tidx, 0, NCM_SONG_BATCH);
|
||||
/* §22 整张歌单逐渐物化:首屏成功后把「能装进滑动窗口」的歌单一次性补填入窗
|
||||
* (≤NCM_SONG_WINDOW 的歌单整张物化,末叶行不再恒「.../加载中」——真机反馈
|
||||
* 「曲目稍多时行一直加载中,点开播放这首才回填」);超窗口的歌单仍按视口滑动补载
|
||||
* (内存纪律:窗口 ≤256 首)。窗口基址=0 覆盖 [0,min(track,WINDOW)),不滑窗不挤占视口。 */
|
||||
if (rc == NCM_OK) {
|
||||
int full = (g_st.track_ids_count < NCM_SONG_WINDOW) ? g_st.track_ids_count : NCM_SONG_WINDOW;
|
||||
if (full > NCM_SONG_BATCH) {
|
||||
int r2 = appstate_song_ensure_range(0, full);
|
||||
if (r2 != NCM_OK && rc == NCM_OK) rc = r2; /* 补填失败非致命:已物化槽保留 */
|
||||
}
|
||||
}
|
||||
if (rc == NCM_OK)
|
||||
g_paged_hi = (g_st.track_ids_count < NCM_SONG_BATCH) ? g_st.track_ids_count : NCM_SONG_BATCH;
|
||||
/* REPORT §30(真机四连症状①):首屏结果打日志——tidx/track_ids_count/物化数/rc,
|
||||
* 供「162 首歌单所有条目占位 加载中」定位(首屏 song_detail 失败/部分失败=非致命
|
||||
* g_ens_err=rc,窗口空 → 行占位;g_st.song_count 为诊断竞态读,仅日志用途)。 */
|
||||
@@ -292,8 +295,10 @@ static void *playlist_worker(void *arg)
|
||||
"playlist: first-screen idx=%d plid=%s trackIds=%d materialized=%d rc=%d",
|
||||
tidx, tpl.id, g_st.track_ids_count, g_st.song_count, rc);
|
||||
} else {
|
||||
/* 滑窗补载:物化视口区间(盘缓存优先,批网格补载)。 */
|
||||
/* 补载:物化目标区间(顺序下一页 / 回滚补物化;盘缓存优先,批网格补载)。
|
||||
* 顺序下一页成功 → 已分页高水位推进到 hi;部分失败不推进(占位行保留,滑到底部重试本页)。 */
|
||||
rc = (lo < hi) ? appstate_song_ensure_range(lo, hi) : NCM_OK;
|
||||
if (rc == NCM_OK && page && hi > g_paged_hi) g_paged_hi = hi;
|
||||
}
|
||||
pthread_mutex_lock(&g_plmu);
|
||||
if (first) {
|
||||
@@ -324,7 +329,25 @@ static void *playlist_worker(void *arg)
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* 视口驱动滑窗补载:视口 ±24 行有未物化槽位 → 发 ensure worker(已在载/失败态/宿主 mock 不发)。 */
|
||||
/* 发补载 worker(单飞;调用方先判 g_plthr_on)。page=1 为顺序下一页任务。 */
|
||||
static void pl_ensure_post(int lo, int hi, int page)
|
||||
{
|
||||
g_ens_lo = lo; g_ens_hi = hi;
|
||||
g_ens_first = 0;
|
||||
g_ens_page = page;
|
||||
g_plloading = 1;
|
||||
g_plthr_on = 1;
|
||||
if (ncm_thread_create(&g_plthr, playlist_worker, NULL) != 0) {
|
||||
g_plthr_on = 0; g_plloading = 0; ncm_snack("加载失败");
|
||||
}
|
||||
}
|
||||
|
||||
/* 懒加载分页触发(Bug 修①:旧版视口 ±24 触发 + 首屏连拉多批 → 大歌单连续拉多页、阻塞 + 风控):
|
||||
* ① 顺序下一页:仅在视口底滑到「已分页底部」(余量 ≤NCM_PL_PAGE_EDGE 首)时拉下一页,
|
||||
* 一次只拉一页(≤NCM_SONG_BATCH);g_paged_hi 达 track_ids_count = 全部拉完,停止。
|
||||
* 远离已分页底部(快速猛滑到深处占位区)不拉——防连续拉多页触发风控。
|
||||
* ② 回滚补物化:视口 ±24 落在「已分页区间内」的失效槽(窗口挤出)按旧逻辑补——
|
||||
* 盘缓存(songmeta)命中为主,不消耗网络分页额度;区间外(未分页)一律不预拉。 */
|
||||
static void pl_maybe_ensure(void)
|
||||
{
|
||||
if (g_plthr_on || g_host_mock) return;
|
||||
@@ -334,23 +357,32 @@ static void pl_maybe_ensure(void)
|
||||
int scroll = meow_listview_scroll(g_list);
|
||||
int first = scroll / APP_ROW_H;
|
||||
int last = (scroll + r_list.h) / APP_ROW_H + 1;
|
||||
if (last > total) last = total;
|
||||
int paged = g_paged_hi;
|
||||
if (paged > total) paged = total; /* 防御:索引重建后高水位越界 */
|
||||
|
||||
/* ① 顺序下一页(懒加载) */
|
||||
if (paged < total) {
|
||||
int margin = paged - last; /* 视口底距已分页底部余量(首;负=已越过) */
|
||||
if (margin <= NCM_PL_PAGE_EDGE && margin >= -NCM_PL_PAGE_EDGE) {
|
||||
int hi = paged + NCM_SONG_BATCH;
|
||||
if (hi > total) hi = total;
|
||||
pl_ensure_post(paged, hi, 1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
/* ② 回滚补物化(仅已分页区间内) */
|
||||
int lo = first - 24, hi = last + 24;
|
||||
if (lo < 0) lo = 0;
|
||||
if (hi > total) hi = total;
|
||||
if (hi > paged) hi = paged;
|
||||
if (lo >= hi) return;
|
||||
if (appstate_range_loaded(lo, hi)) return; /* 已物化:无操作(窗口原地不动) */
|
||||
g_ens_lo = lo; g_ens_hi = hi;
|
||||
g_ens_first = 0;
|
||||
g_plloading = 1;
|
||||
g_plthr_on = 1;
|
||||
if (ncm_thread_create(&g_plthr, playlist_worker, NULL) != 0) {
|
||||
g_plthr_on = 0; g_plloading = 0; ncm_snack("加载失败");
|
||||
}
|
||||
pl_ensure_post(lo, hi, 0);
|
||||
}
|
||||
|
||||
void playlist_refresh(void)
|
||||
{
|
||||
if (g_host_mock) { g_load_err = 0; g_ens_err = 0; pl_apply(1); return; }
|
||||
if (g_host_mock) { g_load_err = 0; g_ens_err = 0; g_paged_hi = g_st.track_ids_count; pl_apply(1); return; }
|
||||
if (g_plthr_on) return;
|
||||
g_load_err = 0;
|
||||
g_ens_err = 0;
|
||||
@@ -373,7 +405,9 @@ void playlist_refresh(void)
|
||||
if (!appstate_playlist_window_is(g_tgt.pl.id))
|
||||
appstate_playlist_reset();
|
||||
|
||||
g_ens_first = 1; /* 首屏:重建索引 + 物化首段 */
|
||||
g_ens_first = 1; /* 首屏:重建索引 + 物化第 0 页 */
|
||||
g_ens_page = 0;
|
||||
g_paged_hi = 0; /* 分页高水位归零:首屏成功后由 worker 重标 */
|
||||
g_pl_stale = 0;
|
||||
g_plloading = 1;
|
||||
g_plthr_on = 1;
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
/* bench/lyric_sim.c — 歌词「掉线失败 → 重连 → 重试成功」宿主离线回归(0 真实请求)。
|
||||
*
|
||||
* 驱动真实 app/play/player_manager.c 的 lyric_worker(后台歌词线程),网络换成可注入的
|
||||
* ncm_api_lyric 桩:第 1 次调用返回 NCM_ERR_NET(模拟掉线瞬间歌词请求失败),第 2 次返回
|
||||
* NCM_OK + 一段 LRC(模拟网络恢复后重试成功)。
|
||||
*
|
||||
* 复现的真机症状(REPORT/任务):播放中歌词正常;网络掉线一会儿再重连后某曲恒「暂无歌词」。
|
||||
* 根因:旧 lyric_worker 在**发起网络请求前**就把 last_id 记为已拉,且失败也置 lyric_state=2
|
||||
* (=已拉完),故网络失败被写死成「无歌词」,重连后永不重试。本机回归:
|
||||
* ① 第 1 次失败后 lyric_state() 必须**不是** 2(可重试),且无词(lyric_count()==0);
|
||||
* ② 推进单调时钟越过退避点(模拟重连)后必须**自动重试**并成功(第 2 次调用)+ 词展示
|
||||
* (lyric_count()>0、lyric_state()==2)。
|
||||
* 旧代码在 ② 上 FAIL(g_lyr_state 恒 2、第 2 次调用永不发生)。
|
||||
*
|
||||
* 编译(宿主,ncm_app 目录下;gc-sections 只保留歌词路径,避免链整个播放链的符号):
|
||||
* CC=gcc F="-ffunction-sections -Wall -Wextra -DJSMN_HEADER -I. -Icore -I../sdk -I../meow_launcherd -Ivendor/jsmn"
|
||||
* $CC $F -c bench/lyric_sim.c -o /tmp/ly.o
|
||||
* $CC $F -c app/play/player_manager.c -o /tmp/ly_pm.o
|
||||
* $CC $F -c app/logic/lyric.c -o /tmp/ly_lrc.o
|
||||
* $CC -Wl,--gc-sections -o lyric_sim /tmp/ly.o /tmp/ly_pm.o /tmp/ly_lrc.o -lpthread -lm
|
||||
* 运行:./lyric_sim (PASS/FAIL 退出码)
|
||||
*/
|
||||
#include "../app/ncm_app_internal.h"
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h> /* usleep */
|
||||
|
||||
/* ---- 全局状态(app.c 定义;本 sim 提供桩) ---- */
|
||||
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 = 0;
|
||||
int g_host_mock = 1;
|
||||
int g_dark = 0;
|
||||
|
||||
/* ---- 可控单调时钟(lyric_worker 用 ncm_monotonic_ms 做失败退避) ---- */
|
||||
static int64_t g_now_ms = 1000;
|
||||
int64_t ncm_monotonic_ms(void) { return g_now_ms; }
|
||||
|
||||
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; }
|
||||
|
||||
/* appstate 桩:单曲队列(idx 0 恒有效,song id=1000) */
|
||||
int appstate_queue_count(void) { return 1; }
|
||||
|
||||
/* page-cache 治理桩:恒非暂停(不挡歌词预取) */
|
||||
int meow_cachegov_prefetch_paused(void) { return 0; }
|
||||
|
||||
/* 媒体会话歌词上报桩:失败上报空、成功上报 LRC;计数供断言 */
|
||||
static int g_report_empty;
|
||||
static int g_report_lyric;
|
||||
static char g_report_last_lrc[256];
|
||||
void ipc_media_lyric_report(const char *lrc, const char *tl)
|
||||
{
|
||||
(void)tl;
|
||||
if (lrc && lrc[0]) { g_report_lyric++; ncm_strlcpy(g_report_last_lrc, lrc, sizeof g_report_last_lrc); }
|
||||
else { g_report_empty++; }
|
||||
}
|
||||
|
||||
/* 歌词 API 桩:第 1 次=网络失败,其后=成功返回 LRC。 */
|
||||
static int g_fetch_calls;
|
||||
int ncm_api_lyric(const char *device_id, const char *cookie, const char *id,
|
||||
char *lrc, size_t lrc_sz, char *tlrc, size_t tlrc_sz)
|
||||
{
|
||||
(void)device_id; (void)cookie; (void)id; (void)tlrc; (void)tlrc_sz;
|
||||
g_fetch_calls++;
|
||||
if (g_fetch_calls == 1) {
|
||||
if (lrc && lrc_sz) lrc[0] = 0; /* 掉线失败:不写词 */
|
||||
return NCM_ERR_NET; /* 网络错误(掉线) */
|
||||
}
|
||||
static const char body[] =
|
||||
"[00:00.00]Line one\n[00:05.00]Line two\n[00:10.00]Line three\n";
|
||||
if (lrc && lrc_sz) snprintf(lrc, lrc_sz, "%s", body);
|
||||
return NCM_OK;
|
||||
}
|
||||
|
||||
static void wait_ms(int ms)
|
||||
{
|
||||
for (int i = 0; i < ms; i += 20) {
|
||||
usleep(20000);
|
||||
if (g_app_quit) break;
|
||||
}
|
||||
}
|
||||
|
||||
static int wait_until(const volatile int *val, int target, int timeout_ms)
|
||||
{
|
||||
for (int i = 0; i < timeout_ms; i += 10) {
|
||||
if (*val >= target) return 1;
|
||||
usleep(10000);
|
||||
}
|
||||
return (*val >= target);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
memset(&g_st, 0, sizeof g_st);
|
||||
pthread_mutex_init(&g_play.mu, NULL);
|
||||
g_play.state = NCM_PC_PLAYING;
|
||||
g_play.cur_song = 0;
|
||||
g_play.play_song_valid = 1;
|
||||
ncm_strlcpy(g_play.play_song.id, "1000", sizeof g_play.play_song.id);
|
||||
|
||||
if (player_manager_load_lyric() != NCM_OK) { printf("FAIL start worker\n"); return 1; }
|
||||
|
||||
int ok = 1;
|
||||
|
||||
/* ① 第 1 次拉取失败(网络错误):须可重试(state!=2)、无词 */
|
||||
if (!wait_until(&g_fetch_calls, 1, 3000)) { printf("FAIL first fetch never happened\n"); return 1; }
|
||||
wait_ms(400); /* 让 worker 完成失败收尾(state=0, retry_at 已算) */
|
||||
int st1 = lyric_state();
|
||||
int n1 = lyric_count();
|
||||
int ok1 = (g_fetch_calls == 1 && st1 != 2 && n1 == 0);
|
||||
printf("step1 failure: calls=%d state=%d (expect !=2) count=%d rep_empty=%d\n",
|
||||
g_fetch_calls, st1, n1, g_report_empty);
|
||||
printf(" -> %s\n", ok1 ? "PASS" : "FAIL");
|
||||
ok &= ok1;
|
||||
|
||||
/* 模拟网络重连:推进单调时钟越过失败退避点 */
|
||||
g_now_ms = 10000;
|
||||
|
||||
/* ② 重试自动发生并成功:第 2 次调用、state=2、词已展示 */
|
||||
if (!wait_until(&g_fetch_calls, 2, 4000)) {
|
||||
printf("FAIL retry never fired (calls=%d) — old code keeps state=2, no retry\n", g_fetch_calls);
|
||||
return 1;
|
||||
}
|
||||
int st2 = lyric_state();
|
||||
int n2 = lyric_count();
|
||||
int ok2 = (g_fetch_calls == 2 && st2 == 2 && n2 > 0 && g_report_lyric >= 1);
|
||||
printf("step2 retry: calls=%d state=%d count=%d rep_lyric=%d lrc=%s\n",
|
||||
g_fetch_calls, st2, n2, g_report_lyric, g_report_last_lrc);
|
||||
printf(" -> %s\n", ok2 ? "PASS" : "FAIL");
|
||||
ok &= ok2;
|
||||
|
||||
g_app_quit = 1;
|
||||
printf("lyric_sim overall -> %s\n", ok ? "PASS" : "FAIL");
|
||||
fflush(stdout);
|
||||
return ok ? 0 : 1;
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
/* bench/plpage_sim.c — 歌单页「懒加载分页 + 加载后列表刷新」离线回归(0 真实 NCM 请求)。
|
||||
*
|
||||
* 驱动真实 app/ui/playlist.c(页面打开 / 滚动触发 / worker / tick 消费全链路)+ 真实
|
||||
* appstate.c(滑动窗口)+ 真实 songmeta.c(/tmp 盘缓存)+ 宿主 SDK(meow_listview/draw 全真,
|
||||
* 与 ncm_host 同一批 out/host/sdk_*.o)。网络端点(ncm_api_playlist_detail /
|
||||
* ncm_api_song_detail)为本地合成 stub(计调用次数、可注入单批失败)。
|
||||
*
|
||||
* Bug1(大歌单懒加载分页)验证点:
|
||||
* ① 打开 400 首歌单:首屏 = 1 detail + 恰 1 批 song_detail(物化 [0,100),第 100 首未物化)
|
||||
* ——不再首屏连拉多批补满窗口(旧行为 ≤3 批连发,5k+ 歌单阻塞 + 风控);
|
||||
* ② 顶部小幅滚动(视口底距已分页底部 >20 首):不发任何新请求;
|
||||
* ③ 滑到已分页底部(余量 ≤20 首):恰拉 1 页;单批失败时 g_paged_hi 不推进(占位保留),
|
||||
* 再滑到底部重试同一页成功(一次一页,不连拉);
|
||||
* ④ 逐页到底(400 = 4 页);全部拉完后滑到尾部:不再发请求(停止);
|
||||
* ⑤ 回滚到顶部:失效槽(窗口挤出)由盘缓存补物化,song_detail 调用数 0 增长(不占网络额度);
|
||||
* Bug2(加载完列表刷新)验证点:
|
||||
* ⑥ 首屏/每页 worker 完成经 playlist_tick 消费后,整帧重画计数(--wrap=meow_ui_paint_full)
|
||||
* 递增,且新物化行经 appstate_song_copy 取到真实歌名/歌手(= pl_src2 将绑定真实数据,
|
||||
* 不再是「... 加载中」占位)。
|
||||
* 风控纪律:0 次真实 NCM 请求(离线 stub + /tmp 盘缓存)。
|
||||
*
|
||||
* 编译/运行见 build_ncm_sims.sh「plpage_sim」段(宿主,ncm_app 目录下)。
|
||||
*/
|
||||
#include "../app/ncm_app_internal.h"
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
#include <time.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 = 0;
|
||||
int g_host_mock = 0; /* 真实分页链路(不走 mock 跳网分支) */
|
||||
|
||||
/* ---- core 符号最小实现(同 playlist_race_sim 口径) ---- */
|
||||
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;
|
||||
}
|
||||
int64_t ncm_now_ms(void)
|
||||
{
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_REALTIME, &ts);
|
||||
return (int64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
|
||||
}
|
||||
|
||||
/* ---- cover / 播放 stub(本 sim 不测封面与播放;playlist.c 引用,链接需要) ---- */
|
||||
void cover_request(const char *url, const char *song_id) { (void)url; (void)song_id; }
|
||||
meow_img *cover_get(void) { return NULL; }
|
||||
void cover_put(void) {}
|
||||
int cover_changed(void) { return 0; }
|
||||
void cover_ack(void) {}
|
||||
void cover_cache_reset(void) {}
|
||||
void player_play_song(int idx) { (void)idx; }
|
||||
|
||||
/* ---- core 符号(appstate.c 引用;运行时不到达,仅链接需要) ---- */
|
||||
int ncm_login_status_check(const char *device_id, const char *cookie, char *uid, size_t uid_sz,
|
||||
char *nick, size_t nick_sz)
|
||||
{ (void)device_id;(void)cookie;(void)uid;(void)uid_sz;(void)nick;(void)nick_sz; return NCM_ERR_AUTH; }
|
||||
int ncm_ctx_clear_cookie(ncm_ctx_t *ctx) { (void)ctx; return NCM_OK; }
|
||||
int ncm_api_user_playlist(const char *device_id, const char *cookie, const char *uid,
|
||||
int limit, int offset, ncm_playlist_t *out, int max, int *out_n)
|
||||
{ (void)device_id;(void)cookie;(void)uid;(void)limit;(void)offset;(void)out;(void)max;(void)out_n; return NCM_ERR_NET; }
|
||||
|
||||
/* ---- 网络端点 stub(离线,合成数据;计调用次数 + 可注入单批失败) ---- */
|
||||
#define SIM_TOTAL 400 /* 4 页:100 × 4(> NCM_SONG_WINDOW 256,回滚必滑窗) */
|
||||
static int g_detail_calls;
|
||||
static int g_song_calls; /* song_detail 成功批次数 */
|
||||
static int g_song_attempts; /* song_detail 发起次数(含失败批) */
|
||||
static int g_song_fail_n; /* 接下来 N 批 song_detail 网络失败模拟 */
|
||||
|
||||
int ncm_api_playlist_detail(const char *device_id, const char *cookie, const char *playlist_id,
|
||||
char *name, size_t name_sz, char *cover, size_t cover_sz,
|
||||
int64_t *ids, int id_cap, int *out_id_n)
|
||||
{
|
||||
(void)device_id; (void)cookie; (void)playlist_id; (void)cover_sz;
|
||||
g_detail_calls++;
|
||||
if (name) snprintf(name, name_sz, "Sim Playlist");
|
||||
if (cover) cover[0] = 0;
|
||||
int n = (SIM_TOTAL < id_cap) ? SIM_TOTAL : id_cap;
|
||||
for (int i = 0; i < n; i++) ids[i] = 1000000LL + i;
|
||||
*out_id_n = n;
|
||||
return NCM_OK;
|
||||
}
|
||||
|
||||
int ncm_api_song_detail(const char *device_id, const char *cookie, const int64_t *ids, int n,
|
||||
ncm_song_t *out, int max, int *out_n)
|
||||
{
|
||||
(void)device_id; (void)cookie;
|
||||
g_song_attempts++;
|
||||
if (g_song_fail_n > 0) { /* 单批网络失败模拟(失败不推进分页高水位 → 同页重试) */
|
||||
g_song_fail_n--;
|
||||
if (out_n) *out_n = 0;
|
||||
return NCM_ERR_NET;
|
||||
}
|
||||
g_song_calls++;
|
||||
int m = (n < max) ? n : max;
|
||||
for (int i = 0; i < m; i++) {
|
||||
snprintf(out[i].id, sizeof out[i].id, "%lld", (long long)ids[i]);
|
||||
snprintf(out[i].name, sizeof out[i].name, "Song%lld", (long long)ids[i]);
|
||||
snprintf(out[i].art_name, sizeof out[i].art_name, "Artist%lld", (long long)ids[i]);
|
||||
snprintf(out[i].album_name, sizeof out[i].album_name, "Album%lld", (long long)ids[i]);
|
||||
out[i].duration_ms = 200000;
|
||||
out[i].fee = 0;
|
||||
}
|
||||
if (out_n) *out_n = m;
|
||||
return NCM_OK;
|
||||
}
|
||||
|
||||
/* ---- Bug2 证据:整帧重画计数(链接时 --wrap=meow_ui_paint_full 重定向到此) ---- */
|
||||
extern void __real_meow_ui_paint_full(void);
|
||||
static int g_paint_full_calls;
|
||||
void __wrap_meow_ui_paint_full(void)
|
||||
{
|
||||
g_paint_full_calls++;
|
||||
__real_meow_ui_paint_full();
|
||||
}
|
||||
|
||||
/* ---- 驱动辅助:模拟 app_on_tick 周期性消费 worker 完成 ---- */
|
||||
static int pump_until(int (*cond)(void), int timeout_ms)
|
||||
{
|
||||
for (int t = 0; t < timeout_ms; t += 5) {
|
||||
playlist_tick(0);
|
||||
if (cond()) {
|
||||
/* 再喂若干 tick:确保完成态被消费(g_plthr_on 清零),后续滚动可再触发 */
|
||||
for (int k = 0; k < 30; k++) { playlist_tick(0); usleep(2000); }
|
||||
return 1;
|
||||
}
|
||||
usleep(5000);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int g_fail = 0;
|
||||
#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)
|
||||
|
||||
/* 条件(C 无闭包,用全局目标值) */
|
||||
static int g_want_calls;
|
||||
static int cond_calls(void) { return g_song_calls >= g_want_calls; }
|
||||
static int g_want_attempts;
|
||||
static int cond_attempts(void) { return g_song_attempts >= g_want_attempts; }
|
||||
static int cond_first_screen(void)
|
||||
{
|
||||
return g_st.track_ids_count == SIM_TOTAL && g_song_calls >= 1;
|
||||
}
|
||||
|
||||
/* 滚动到指定首行(行高 APP_ROW_H px;swipe 距离按当前行差换算) */
|
||||
static void scroll_to_row(int row)
|
||||
{
|
||||
static int cur;
|
||||
int d = row - cur;
|
||||
cur = row;
|
||||
playlist_swipe(d >= 0 ? MEOW_SWIPE_UP : MEOW_SWIPE_DOWN,
|
||||
(d >= 0 ? d : -d) * APP_ROW_H);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
printf("== plpage_sim: 懒加载分页 + 加载后列表刷新(%d 首,窗口 %d,批 %d,触底余量 20)==\n",
|
||||
SIM_TOTAL, NCM_SONG_WINDOW, NCM_SONG_BATCH);
|
||||
|
||||
/* 宿主 SDK 离屏初始化(与 ncm_host 同口径) */
|
||||
setenv("MEOW_UI_DUMP", "/tmp/plpage_sim.ppm", 0);
|
||||
meow_ui_opts o = { APP_W, APP_H };
|
||||
if (meow_ui_init(&o) != 0) { fprintf(stderr, "init failed\n"); return 1; }
|
||||
meow_theme_set_mode(MEOW_THEME_DARK);
|
||||
ncm_on_theme(MEOW_THEME_DARK);
|
||||
|
||||
/* 歌单 mock:1 张 SIM_TOTAL 首歌单;songmeta 盘缓存用临时目录 */
|
||||
char cdt[] = "/tmp/plpage_cache_XXXXXX";
|
||||
if (!mkdtemp(cdt)) { fprintf(stderr, "mkdtemp failed\n"); return 1; }
|
||||
snprintf(g_st.cache_dir, sizeof g_st.cache_dir, "%s", cdt);
|
||||
g_st.playlist_count = 1;
|
||||
snprintf(g_st.playlists[0].id, sizeof g_st.playlists[0].id, "pl0");
|
||||
snprintf(g_st.playlists[0].name, sizeof g_st.playlists[0].name, "SimPlaylist");
|
||||
g_st.playlists[0].track_count = SIM_TOTAL;
|
||||
|
||||
ncm_song_t sq;
|
||||
int paint0;
|
||||
|
||||
/* ---- ① Bug1:打开歌单 → 首屏恰 1 detail + 1 批 song_detail(物化 [0,100)) ---- */
|
||||
printf("== case 1: 首屏只拉一页 ==\n");
|
||||
paint0 = g_paint_full_calls;
|
||||
playlist_view_create();
|
||||
playlist_open(0);
|
||||
CHECK(pump_until(cond_first_screen, 8000), "first-screen loaded (track_ids=%d calls=%d)",
|
||||
g_st.track_ids_count, g_song_calls);
|
||||
CHECK(g_detail_calls == 1, "detail calls=%d (expect 1)", g_detail_calls);
|
||||
CHECK(g_song_calls == 1, "song_detail calls=%d (expect 1, 不再首屏连拉多批)", g_song_calls);
|
||||
CHECK(g_st.track_ids_count == SIM_TOTAL, "track_ids_count=%d (expect %d)",
|
||||
g_st.track_ids_count, SIM_TOTAL);
|
||||
CHECK(appstate_song_copy(99, &sq) == 0 && strcmp(sq.name, "Song1000099") == 0,
|
||||
"row 99 materialized name='%s'", sq.name);
|
||||
CHECK(appstate_song_copy(100, &sq) != 0, "row 100 NOT materialized yet (placeholder)");
|
||||
/* Bug2:首屏完成 → 重画计数递增 + 行绑定真实歌名/歌手(非占位) */
|
||||
CHECK(g_paint_full_calls > paint0, "paint_full after first-screen %d -> %d (list repainted)",
|
||||
paint0, g_paint_full_calls);
|
||||
CHECK(appstate_song_copy(5, &sq) == 0 && strcmp(sq.art_name, "Artist1000005") == 0,
|
||||
"row 5 binds real artist='%s' (src2 填真实数据,非「... 加载中」)", sq.art_name);
|
||||
|
||||
/* ---- ② 顶部小幅滚动:不触发任何新请求(距已分页底部 >20 首) ---- */
|
||||
printf("== case 2: 顶部滚动不拉页 ==\n");
|
||||
scroll_to_row(10);
|
||||
int a0 = g_song_attempts;
|
||||
for (int k = 0; k < 40; k++) { playlist_tick(0); usleep(3000); }
|
||||
CHECK(g_song_attempts == a0, "song_detail attempts=%d (unchanged, 顶部滚动不拉页)", g_song_attempts);
|
||||
|
||||
/* ---- ③ 滑到已分页底部(余量 ≤20 首):恰拉 1 页;单批失败不推进,再滑重试同页成功 ---- */
|
||||
printf("== case 3: 触底拉下一页(一次一页;失败重试同页) ==\n");
|
||||
g_song_fail_n = 1; /* 注入:下一次 song_detail 网络失败 */
|
||||
int att_before = g_song_attempts;
|
||||
scroll_to_row(90); /* 视口底 ≈97 行,余量 ≤20 → 触发第 2 页(失败) */
|
||||
g_want_attempts = att_before + 1;
|
||||
CHECK(pump_until(cond_attempts, 8000), "page-2 first attempt fired (attempts=%d)", g_song_attempts);
|
||||
CHECK(g_song_calls == 1, "page-2 failed -> calls=%d (g_paged_hi 不推进,占位保留)", g_song_calls);
|
||||
CHECK(appstate_song_copy(199, &sq) != 0, "row 199 still placeholder after failed page");
|
||||
scroll_to_row(91); /* 再滑(滚动事件)→ 重试同一页 */
|
||||
g_want_calls = 2;
|
||||
CHECK(pump_until(cond_calls, 8000), "page 2 retried & loaded (calls=%d attempts=%d)",
|
||||
g_song_calls, g_song_attempts);
|
||||
CHECK(g_song_attempts == att_before + 2,
|
||||
"attempts +%d (1 失败 + 1 重试同页 = 一次一页,未连拉)", g_song_attempts - att_before);
|
||||
CHECK(appstate_song_copy(199, &sq) == 0 && strcmp(sq.name, "Song1000199") == 0,
|
||||
"row 199 materialized name='%s'", sq.name);
|
||||
CHECK(g_paint_full_calls > paint0, "paint_full after page-2 consumed (=%d)", g_paint_full_calls);
|
||||
|
||||
/* ---- ④ 逐页到底;全部拉完后滑到尾部:不再发请求 ---- */
|
||||
printf("== case 4: 逐页到底 + 拉完即止 ==\n");
|
||||
att_before = g_song_attempts;
|
||||
scroll_to_row(190); /* 余量 ≤20 → 触发第 3 页 */
|
||||
g_want_calls = 3;
|
||||
CHECK(pump_until(cond_calls, 8000), "page 3 loaded (calls=%d)", g_song_calls);
|
||||
scroll_to_row(290); /* 余量 ≤20 → 触发第 4 页(末页) */
|
||||
g_want_calls = 4;
|
||||
CHECK(pump_until(cond_calls, 8000), "page 4 (last) loaded (calls=%d)", g_song_calls);
|
||||
CHECK(g_song_attempts == att_before + 2, "attempts +%d (第 3、4 页恰各 1 批)",
|
||||
g_song_attempts - att_before);
|
||||
CHECK(appstate_song_copy(399, &sq) == 0 && strcmp(sq.name, "Song1000399") == 0,
|
||||
"tail row 399 materialized name='%s'", sq.name);
|
||||
att_before = g_song_attempts;
|
||||
scroll_to_row(390); /* 已在尾部:全部拉完 → 不再发请求 */
|
||||
for (int k = 0; k < 40; k++) { playlist_tick(0); usleep(3000); }
|
||||
CHECK(g_song_attempts == att_before, "all loaded -> scroll at tail: attempts=%d (unchanged)",
|
||||
g_song_attempts);
|
||||
|
||||
/* ---- ⑤ 回滚到顶部:失效槽(窗口挤出)盘缓存补物化,song_detail 0 增长 ---- */
|
||||
printf("== case 5: 回滚补物化走盘缓存(0 网络) ==\n");
|
||||
att_before = g_song_attempts;
|
||||
scroll_to_row(0); /* 回顶部:窗口已滑走,行 0-35 失效 → 盘缓存补 */
|
||||
for (int k = 0; k < 400 && !appstate_range_loaded(0, 30); k++) { playlist_tick(0); usleep(5000); }
|
||||
CHECK(appstate_range_loaded(0, 30), "rows 0-29 re-materialized after scroll-back");
|
||||
CHECK(g_song_attempts == att_before, "scroll-back refill: song_detail attempts=%d "
|
||||
"(unchanged, 盘缓存命中不占网络)", g_song_attempts);
|
||||
CHECK(appstate_song_copy(0, &sq) == 0 && strcmp(sq.name, "Song1000000") == 0,
|
||||
"row 0 re-materialized name='%s'", sq.name);
|
||||
|
||||
printf("%s\n", g_fail ? "RESULT: FAIL" : "RESULT: PASS");
|
||||
/* 清理临时盘缓存(≤400 个小文件) */
|
||||
{
|
||||
char cmd[600];
|
||||
snprintf(cmd, sizeof cmd, "rm -rf %s", cdt);
|
||||
if (system(cmd) != 0) fprintf(stderr, "cleanup failed: %s\n", cdt);
|
||||
}
|
||||
meow_ui_shutdown();
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
@@ -40,8 +40,11 @@ size_t ncm_strlcpy(char *dst, const char *src, size_t sz)
|
||||
}
|
||||
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; }
|
||||
/* audio_out.c 引用但本 sim 不经 SDK 调用的符号:只链不跑就满足链接。
|
||||
* 会话建立→音量同步用例(§切歌音量骤增回归)需要 audio_out_open 真正「开新流/分配新 sid」,
|
||||
* 故 meow_audio_open 桩改为返回递增 sid(模拟 audiod 单调分配),供 §切歌 会话切换复现。 */
|
||||
static int g_sim_sid = 0;
|
||||
int meow_audio_open(int rate, int ch, int fmt) { (void)rate;(void)ch;(void)fmt; return ++g_sim_sid; }
|
||||
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; }
|
||||
@@ -161,6 +164,46 @@ int main(void)
|
||||
printf("PASS changed-volume re-sync: settings=50 -> 2nd connection pct=50\n");
|
||||
}
|
||||
|
||||
/* §切歌音量骤增回归:会话建立→音量同步。切歌 = 新 audio_out_open = 新 audiod 流(新 sid),
|
||||
* 必须**每会话重发一次**当前音量——此前 g_vol_synced 进程级「首同步一次」,新会话被跳过,
|
||||
* DAC 停在会话建立后的默认响度(切歌骤增,按音量键才回落)。用真实 audio_out_open/close
|
||||
* (meow_audio_open 桩返回递增 sid)断言:新会话必 +1 连接(重发音量),同会话内重复 sync 不新增。 */
|
||||
{
|
||||
int base = g_nconn; /* 上面值变重发测试后基线 = 2 条连接 */
|
||||
/* 会话 1(首开):应重发 1 条 VOLUME,pct==当前 settings(50) */
|
||||
if (audio_out_open(44100, 2, 16) != 0) { printf("FAIL session1 audio_out_open\n"); fail = 1; }
|
||||
usleep(50000);
|
||||
if (g_nconn != base + 1) {
|
||||
printf("FAIL session1 establish: conns=%d (expect %d) — 新会话未重发音量\n", (int)g_nconn, base + 1);
|
||||
fail = 1;
|
||||
} else if (g_got_pct != 50) {
|
||||
printf("FAIL session1 establish: pct=%d (expect 50)\n", (int)g_got_pct); fail = 1;
|
||||
} else {
|
||||
printf("PASS session1 establish -> 1 volume sync (pct=50)\n");
|
||||
}
|
||||
/* 同会话内反复 sync(同值):不得新建连接(防 fd 风暴) */
|
||||
for (int i = 0; i < 20; i++) audio_out_sync_volume();
|
||||
usleep(50000);
|
||||
if (g_nconn != base + 1) {
|
||||
printf("FAIL same-session repeat sync: conns=%d (expect %d)\n", (int)g_nconn, base + 1);
|
||||
fail = 1;
|
||||
} else {
|
||||
printf("PASS same-session repeat sync -> no new connection (storm-free)\n");
|
||||
}
|
||||
audio_out_close();
|
||||
/* 会话 2(切歌):再开 → 必须再重发 1 条(新 sid),pct==50 */
|
||||
if (audio_out_open(44100, 2, 16) != 0) { printf("FAIL session2 audio_out_open\n"); fail = 1; }
|
||||
usleep(50000);
|
||||
if (g_nconn != base + 2) {
|
||||
printf("FAIL session2 (song switch): conns=%d (expect %d) — 切歌未重发音量\n", (int)g_nconn, base + 2);
|
||||
fail = 1;
|
||||
} else if (g_got_pct != 50) {
|
||||
printf("FAIL session2 switch: pct=%d (expect 50)\n", (int)g_got_pct); fail = 1;
|
||||
} else {
|
||||
printf("PASS session2 (song switch) -> new volume sync (pct=50)\n");
|
||||
}
|
||||
}
|
||||
|
||||
g_stop = 1;
|
||||
pthread_join(th, NULL);
|
||||
unlink(g_sock);
|
||||
|
||||
@@ -75,6 +75,18 @@ gcc -O2 -Wl,--gc-sections -o window_sim /tmp/ws.o /tmp/ws_app.o /tmp/ws_meta.o \
|
||||
out/core-host/ncm_apicache.o -lpthread # appstate.c 调 ncm_apicache_init/clear(自包含模块)
|
||||
run_sim window_sim
|
||||
|
||||
echo "==> build plpage_sim(歌单页懒加载分页 + 加载后列表刷新;真实 playlist.c 全链路 + 宿主 SDK,0 网络。"
|
||||
echo " 依赖 out/host/sdk_*.o:若缺请先跑 build_ncm_host.sh)"
|
||||
PPINC="-DMEOW_UI_HOST_TEST -DJSMN_HEADER -I. -I../sdk -I../meow_launcherd -Icore -Ivendor/jsmn -Ivendor/mbedtls/include -Ivendor/minimp3 -Ivendor/dr_flac"
|
||||
for f in bench/plpage_sim.c app/ui/playlist.c app/logic/appstate.c app/logic/songmeta.c app/ui/chrome.c app/ui/ncm_colors.c; do
|
||||
b=$(basename "$f" .c)
|
||||
gcc -O2 -Wall -Wextra -ffunction-sections -fdata-sections $PPINC -c "$f" -o /tmp/pp_$b.o
|
||||
done
|
||||
gcc -O2 -Wl,--gc-sections -Wl,--wrap=meow_ui_paint_full -o plpage_sim \
|
||||
/tmp/pp_plpage_sim.o /tmp/pp_playlist.o /tmp/pp_appstate.o /tmp/pp_songmeta.o /tmp/pp_chrome.o /tmp/pp_ncm_colors.o \
|
||||
out/host/sdk_*.o -lz -lpthread -lm
|
||||
run_sim plpage_sim
|
||||
|
||||
echo "==> build search_sim"
|
||||
gcc -O2 -ffunction-sections -DJSMN_HEADER -I. -I../sdk -I../meow_launcherd -Icore -Ivendor/jsmn \
|
||||
-c bench/search_sim.c -o /tmp/sh.o
|
||||
|
||||
@@ -12,9 +12,10 @@
|
||||
|
||||
/* 响应缓冲上界(内存瘦身,OOM 修复):多数接口响应远小于 1MB,按需给尺寸避免每次调用
|
||||
* 都 malloc 1MB 堆峰值(播放中 song_url/lyric/song_detail 并发,几 × 1MB 是可砍的瞬态)。
|
||||
* 仅 playlist detail(全量 trackIds,最多 NCM_TRACKID_MAX 条)保留传统 2MB。 */
|
||||
* playlist detail(全量 trackIds,最多 NCM_TRACKID_MAX 条)保留 2MB;
|
||||
* user_playlist(120 首歌单列表)用 NCM_RESP_MAX(1MB),见 ncm_api_user_playlist。 */
|
||||
#define RESP_SMALL (64 * 1024) /* song_url:url+字段,实际 ~1KB */
|
||||
#define RESP_MED (256 * 1024) /* lyric/song_detail/playlist/cloudsearch:实际 ≤100KB */
|
||||
#define RESP_MED (256 * 1024) /* lyric/song_detail/cloudsearch:实际 ≤100KB */
|
||||
|
||||
const char *ncm_api_level_str(int level) {
|
||||
switch (level) {
|
||||
@@ -279,8 +280,11 @@ int ncm_api_user_playlist(const char *device_id, const char *cookie, const char
|
||||
uid, limit > 0 ? limit : 30, offset > 0 ? offset : 0);
|
||||
|
||||
api_json_t c; memset(&c, 0, sizeof c);
|
||||
/* 我的歌单列表:真机 120 首歌单响应可超 RESP_MED(256KB) → ncm_http 回 NCM_ERR_FULL(-11)
|
||||
* (日志 home: playlists rc=-11)。该响应不含 tracks(每首 tracks:null),1MB 足够,
|
||||
* 用 NCM_RESP_MAX(与 vip/info 同口径的堆瞬态,解析后即释,非 bss)。 */
|
||||
int rc = api_post_cached(device_id, cookie, "/api/user/playlist", json, NCM_CRYPT_WEAPI,
|
||||
RESP_MED, &c);
|
||||
NCM_RESP_MAX, &c);
|
||||
if (rc != NCM_OK) return rc;
|
||||
|
||||
int arr = ncm_json_path(&c.j, "playlist");
|
||||
|
||||
@@ -302,10 +302,12 @@ int ncm_req_build(const char *device_id, const char *cookie, const char *path,
|
||||
}
|
||||
}
|
||||
|
||||
/* params 是 full2 的 AES-ECB 大写 hex(≈ plaintext×2)。
|
||||
* 批量 song_detail 的 c 数组转义后 full2 ~1.6KB → hex ~3.2KB,旧 2048 溢出
|
||||
* → ncm_crypto_eapi 返回 NCM_ERR_CRYPTO(实测 batch=64 时 rc=-8)。 */
|
||||
char params[4096];
|
||||
/* params 是 full2 的 AES-ECB 大写 hex(≈ plaintext×2 + 1)。
|
||||
* NCM_SONG_BATCH=100 时:c 数组转义后 full2 ~2.5KB(10 位 id) / ~3.4KB(19 位 id)
|
||||
* → hex ~5.1KB / ~6.9KB。旧 params[4096] 对 100 首批量恒溢出 → aes_ecb_hex
|
||||
* 返回 -1 → ncm_crypto_eapi 返回 NCM_ERR_CRYPTO(-8)(真机 first-screen rc=-8)。
|
||||
* 用 NCM_REQ_BODY_MAX(8192) 兜住 100 首最坏情况(body="params="+hex ≤ 8192)。 */
|
||||
char params[NCM_REQ_BODY_MAX];
|
||||
if (ncm_crypto_eapi(path, full2, params, sizeof params) != 0) return NCM_ERR_CRYPTO;
|
||||
|
||||
char cookie_hdr[3072];
|
||||
|
||||
@@ -234,6 +234,35 @@ static void test_eapi_musicu_hdr(void) {
|
||||
CHECK(strstr(h2, "MUSIC_U=") == NULL, "guest eapi has no MUSIC_U");
|
||||
}
|
||||
|
||||
static void test_eapi_song_detail_batch(void) {
|
||||
/* §NEW:NCM_SONG_BATCH=100 的 song_detail eapi 请求构建(不打网络)。
|
||||
* 旧 params[4096] 对 100 首的 AES-ECB hex(~5.1KB) 溢出 → NCM_ERR_CRYPTO(-8),
|
||||
* 真机 first-screen 因此 rc=-8。这里只构建请求验证 params/body 缓冲够。 */
|
||||
printf("== eapi 100-song detail batch ==\n");
|
||||
const char *device = "AA11BB22CC33DD44EE55FF6600112233445566778899AABB00";
|
||||
char clist[4096] = "[", tmp[64];
|
||||
for (int i = 0; i < 100 && strlen(clist) < sizeof clist - 80; i++) {
|
||||
snprintf(tmp, sizeof tmp, "%s{\\\"id\\\":%lld}", i ? "," : "",
|
||||
(long long)(2048962657LL + (long long)i));
|
||||
ncm_strlcat(clist, tmp, sizeof clist);
|
||||
}
|
||||
ncm_strlcat(clist, "]", sizeof clist);
|
||||
char json[4500];
|
||||
snprintf(json, sizeof json, "{\"c\":\"%s\"}", clist);
|
||||
|
||||
char params[8192];
|
||||
CHECKRC(ncm_crypto_eapi("/api/v3/song/detail", json, params, sizeof params),
|
||||
"eapi crypto 100-song");
|
||||
CHECK(strlen(params) > 4096, "params exceeds old 4096 cap (got %zu)", strlen(params));
|
||||
|
||||
char body[NCM_REQ_BODY_MAX], hdrs[NCM_REQ_HDRS_MAX], url[NCM_REQ_URL_MAX];
|
||||
CHECKRC(ncm_req_build(device, "", "/api/v3/song/detail", json, NCM_CRYPT_EAPI,
|
||||
body, sizeof body, hdrs, sizeof hdrs, url, sizeof url),
|
||||
"eapi req_build 100-song");
|
||||
CHECK(strstr(body, "params=") == body, "body starts with params=");
|
||||
CHECK(strlen(body) < NCM_REQ_BODY_MAX, "body fits NCM_REQ_BODY_MAX (got %zu)", strlen(body));
|
||||
}
|
||||
|
||||
static void test_network(void) {
|
||||
printf("== network (live) ==\n");
|
||||
int net_ok = 0;
|
||||
@@ -515,6 +544,7 @@ int main(int argc, char **argv) {
|
||||
test_qr();
|
||||
test_cookie_merge();
|
||||
test_eapi_musicu_hdr();
|
||||
test_eapi_song_detail_batch();
|
||||
test_network();
|
||||
test_track_all_parse();
|
||||
test_vip_parse();
|
||||
|
||||
Reference in new issue
Block a user