fix(ncm §31): 修两处 UI 数据竞态——返回首页行空白(staging发布) + 进第二个歌单显示上一歌单(换歌单清窗)

A. 返回首页各行 2~3s 空白:appstate_load_playlists 就地 memset(g_st.playlists[])
   与 home_src2 无锁逐行绑定竞态,刷新期间 playlist_count 保留旧值 → UI 读清零行。
   照搬 s_build[] staging 范式:网络落 s_pls[] → 成功后锁内原子发布(拷回+更新count),
   失败保留旧快照(断网离线浏览)。绝不就地清零 UI 可见数组。

B. 进第二个歌单显示上一歌单:playlist_open/refresh 同步路径不调 appstate_playlist_reset,
   pl_apply(1) 绑到上一歌单残留窗口(track_ids_count/songs)。
   新增窗口归属(歌单 id 唯一标识, 对搜索歌单同样成立): worker 首屏发布后
   appstate_playlist_window_set;refresh 目标≠归属(窗口is) → 同步 reset 再 pl_apply。
   同歌单手动刷新(id 同)保留旧内容(stale-while-revalidate)。

宿主回归:playlist_race_sim(新 F 并发读无清零态/G 开A→开B 不串)、window/search/
cache_evict/player_flow/stream_sim、test_ncm_core 宿主+mipsel 全 PASS;
build_ncm.sh -Wall -Wextra 0 warning;0 真实请求;未部署。
This commit is contained in:
AskaEth committed 2026-09-02 03:03:48 +08:00
1 parent 97b741cf56
commit 10690bc348
9 files changed
+290 -9

No files matched your search

+10
View File
@@ -167,6 +167,16 @@ int ncm_api_song_url_auto(const char *dev, const char *ck, const char *id, int l
return NCM_OK;
}
/* §20.3(HEAD 的降级链工作)把 player_manager 切到 ncm_api_song_url_auto_av;本 sim 只测播放流
* 不测档位链语义,故转发到上方 auto 桩(desired→level),0 请求。与本次两处 UI 竞态无关。 */
int ncm_api_song_url_auto_av(const char *device_id, const char *cookie, const char *id,
int account_vip, int fee, int desired,
ncm_play_url_t *out, int *out_level)
{
(void)account_vip; (void)fee;
return ncm_api_song_url_auto(device_id, cookie, id, desired, out, out_level);
}
static int g_dl_fail; /* 阶段3:>0 时下载返回 NCM_ERR_NET(模拟预缓冲前网络失败) */
int ncm_http_download_status(const ncm_http_req_t *req, ncm_http_write_cb cb, void *ctx,
size_t maxb, ncm_http_resp_t *resp, int *status_out)
+118 -1
View File
@@ -9,7 +9,11 @@
* 窗口写进全局(修【5194 首 3 首歌/串数据/加载失败】);打开时快照 plid 免疫 home_worker
* 对 g_st.playlists[] 的并发 memset 污染;
* D. 歌单列表磁盘缓存:appstate_playlists_cache_save/load 回环(断网离线浏览秒开);
* E. 打开上下文 reset 清窗口(UI 丢弃过期结果时不留旧数据)。
* E. 打开上下文 reset 清窗口(UI 丢弃过期结果时不留旧数据);
* F. §NEW bug A:返回首页行空白——UI 读与后台 appstate_load_playlists 并发,断言全程读不到
* memset 就地清零态(修后 staging 发布 = 旧/新完整快照,恒非空 name);
* G. §NEW bug B:开 A→开 B——换歌单清窗(appstate_playlist_window_is 判归属→reset),B 页
* 首行 id 在 B 范围、窗口不再残留 A 的行。
* 风控纪律:0 次真实 NCM 请求(离线 stub;songmeta 全 miss/不落盘)。
*
* 编译(宿主,ncm_app 目录下):
@@ -25,6 +29,7 @@
#include <string.h>
#include <stdlib.h>
#include <time.h>
#include <pthread.h>
ncm_app_state_t g_st;
ncm_play_run_t g_play = { .mu = PTHREAD_MUTEX_INITIALIZER, .state = NCM_PC_STOPPED, .cur_song = -1 };
@@ -58,6 +63,12 @@ int meow_config_int(const char *key, int def) { (void)key; return def; }
int songmeta_load(const char *cd, const char *id, ncm_song_t *out) { (void)cd;(void)id;(void)out; return NCM_ERR_IO; }
int songmeta_save(const char *cd, const char *id, const ncm_song_t *s){ (void)cd;(void)id;(void)s; return NCM_ERR_IO; }
/* core 符号(appstate_ensure_uid 引用;g_st.uid 已设 → 运行时不走这两个分支,仅链接需要)。 */
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; }
/* ---- 可配置网络 stub ---- */
static int g_pl_total = 92; /* 当前歌单返回的 trackIds 总数 */
static int64_t g_id_base = 1000000;
@@ -106,12 +117,56 @@ int ncm_api_song_detail(const char *device_id, const char *cookie, const int64_t
return NCM_OK;
}
/* ---- 用户歌单列表 stub(§NEW bug A:appstate_load_playlists staging 发布用;0 网络)---- */
static int g_user_pl_calls;
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)offset;
g_user_pl_calls++;
int n = 0;
for (int i = 0; i < 2 && i < max && i < limit; i++, n++) {
ncm_strlcpy(out[i].id, i == 0 ? "plFav" : "plDoc", sizeof out[i].id);
ncm_strlcpy(out[i].name, i == 0 ? "我喜欢的音乐" : "通勤歌单", sizeof out[i].name);
snprintf(out[i].cover, sizeof out[i].cover, "http://p1.music.126.net/pl%d.jpg", i);
out[i].track_count = (i == 0) ? 100 : 50;
out[i].is_collected = (i == 0) ? 0 : 1;
}
*out_n = n;
return NCM_OK;
}
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)
/* ---- §NEW bug A 并发读(UI 读 vs 后台刷):模拟 home_src2 逐行绑定 + 断言全程读不到清零态 ---- */
static volatile int g_race_fail, g_race_stop;
static void *home_reader(void *arg)
{
(void)arg;
while (!g_race_stop) {
int cnt = g_st.playlist_count; /* 刷新期间保留旧值(cache):读侧必经的读取路径 */
if (cnt <= 0) continue;
for (int i = 0; i < cnt; i++) {
const ncm_playlist_t *pl = &g_st.playlists[i];
/* 清零态 = memset 就地清零后 playlist_count 仍未变 → 空 name/0 track_count 行。
* 修后 staging 发布(旧或新完整快照)恒非空 name,读不到此态。 */
if (pl->name[0] == 0 || pl->track_count < 0) { g_race_fail = 1; g_race_stop = 1; return NULL; }
}
}
return NULL;
}
static void *home_writer(void *arg)
{
(void)arg;
for (int i = 0; i < 20000 && !g_race_stop; i++)
appstate_load_playlists(); /* 后台刷新(staging 发布;0 网络走 stub) */
return NULL;
}
static void set_pl(int idx, const char *id, const char *name, int total)
{
ncm_strlcpy(g_st.playlists[idx].id, id, sizeof g_st.playlists[idx].id);
@@ -288,6 +343,68 @@ int main(void)
"after reset: count=%d valid=%d base=%d", g_st.track_ids_count, g_st.song_count, g_st.window_base);
}
/* ---- F. §NEW bug A:返回首页空白——UI 读与后台刷并发,断言全程读不到清零态 ---- */
printf("== F. A-race: UI read vs background appstate_load_playlists (no zeroed row) ==\n");
{
g_st.logged_in = 1;
g_st.playlist_count = 2;
set_pl(0, "plFav", "我喜欢的音乐", 92);
set_pl(1, "plDoc", "通勤歌单", 200);
pthread_t rd, wr;
g_race_fail = 0; g_race_stop = 0;
if (pthread_create(&wr, NULL, home_writer, NULL) != 0) { CHECK(0, "spawn writer"); return 1; }
if (pthread_create(&rd, NULL, home_reader, NULL) != 0) { CHECK(0, "spawn reader"); return 1; }
pthread_join(wr, NULL);
g_race_stop = 1; /* 让 reader 退出 */
pthread_join(rd, NULL);
CHECK(g_race_fail == 0, "reader observed NO zeroed row over %d refreshes", g_user_pl_calls);
CHECK(g_user_pl_calls > 0, "background refreshes ran (%d)", g_user_pl_calls);
CHECK(g_st.playlist_count == 2 && g_st.playlists[0].name[0] && g_st.playlists[1].name[0],
"final array intact (count=%d names ok)", g_st.playlist_count);
}
/* ---- G. §NEW bug B:开 A→开 B,断言 B 页不出现 A 的行(换歌单清窗) ---- */
printf("== G. B-race: open A -> open B (B page must not show A rows) ==\n");
{
/* 开 A(load A 后按 playlist_worker 语义标记窗口归属=A) */
reset_ns();
g_st.logged_in = 1;
g_st.playlist_count = 2;
set_pl(0, "plFav", "我喜欢的音乐", 92);
set_pl(1, "plDoc", "通勤歌单", 200);
g_pl_total = 92; g_st.playlists[0].track_count = 92; g_id_base = 1000000;
{
int genA = appstate_playlist_begin(0);
ncm_playlist_t snapA = g_st.playlists[0];
int rc = appstate_load_playlist_songs_ctx(&snapA, 0, genA, 0, 0, NCM_SONG_BATCH);
CHECK(rc == NCM_OK && g_st.track_ids_count == 92, "open A loads (rc=%d count=%d)",
rc, g_st.track_ids_count);
appstate_playlist_window_set(g_st.playlists[0].id); /* 等效 playlist_worker 标记归属 */
ncm_song_t s;
CHECK(appstate_song_copy(0, &s) == 0 && strstr(s.id, "1000") != NULL,
"A first row id in A range (id=%s)", s.id);
}
/* 换歌单到 B:playlist_refresh 起 worker 前的同步判定——目标 B ≠ 窗口归属 A → 清窗 */
g_pl_total = 200; g_st.playlists[1].track_count = 200; g_id_base = 9000000;
{
CHECK(appstate_playlist_window_is(g_st.playlists[1].id) == 0,
"window owned by A (not B) before switch");
int genB = appstate_playlist_begin(1);
if (!appstate_playlist_window_is(g_st.playlists[1].id)) appstate_playlist_reset();
CHECK(g_st.track_ids_count == 0 && g_st.song_count == 0,
"post-switch window cleared: count=%d valid=%d (B page shows no A rows)",
g_st.track_ids_count, g_st.song_count);
/* 开 B 加载:窗口=B,首行 id 在 B 范围 */
ncm_playlist_t snapB = g_st.playlists[1];
int rc = appstate_load_playlist_songs_ctx(&snapB, 0, genB, 1, 0, NCM_SONG_BATCH);
CHECK(rc == NCM_OK && g_st.track_ids_count == 200, "open B loads (rc=%d count=%d)",
rc, g_st.track_ids_count);
ncm_song_t s;
CHECK(appstate_song_copy(0, &s) == 0 && strstr(s.id, "9000") != NULL,
"B first row id in B range (id=%s)", s.id);
}
}
printf("%s\n", g_fail ? "RESULT: FAIL" : "RESULT: PASS");
return g_fail ? 1 : 0;
}