diff --git a/patch_v2/ncm_app/REPORT.md b/patch_v2/ncm_app/REPORT.md index c2219b0..2a8596e 100644 --- a/patch_v2/ncm_app/REPORT.md +++ b/patch_v2/ncm_app/REPORT.md @@ -2633,3 +2633,94 @@ b731654a06d5ca5746edea8393f8668f ncm_host # 宿主调试二进 - **`parse_url` http→443 潜性缺陷**:`http://` URL 无显式端口时 `url.port` 恒 443(TLS),对 music.126.net CDN(443 上提供 TLS)恰好可用;语义错误但未阻断,未改(改动有回归风险)。 - **未动 sdk/、vendor/**;`ncm_host`/`test_ncm_core_host` 为宿主调试产物随批重编。 + +## 31. UI 两处数据竞态修复:返回首页单行空白 + 新进歌单显示上一歌单(2026-09-02,REPORT §31) + +> 真机「ncm 待修清单」⑤/⑥(见 WORK_STATE 2026-09-02 用户实测 8 条)。只改 `patch_v2/ncm_app/`。 +> 根因已由只读分析钉死,本批照方抓药实现。风控纪律:**0 次真实 NCM 请求**(全部离线 stub); +> **未部署真机**(留主会话统一上卡)。 + +### 31.1 症状 A:返回首页各行 2~3 秒空白(除「我喜欢的音乐」外无标题/曲目数 0) + +**根因**:`appstate_load_playlists`(`app/logic/appstate.c:182` 附近)对共享数组 `g_st.playlists[]` +**就地 `memset`→网络回填**,与 UI 线程 `home_src2` **无锁逐行绑定**竞态。刷新期间 `playlist_count` +保留旧值(cache),UI 读到被清零的行(`name[0]==0`/`track_count==0`)。下标 0 先读幸存(=「我喜欢的 +音乐」行正常的由来)。旧代码在**失败**时还把 `playlist_count=0`,覆盖 §26 cache 秒渲的「断网离线 +浏览」语义(§31.1 顺带修)。 + +**修法**(照搬本文件已有 `s_build[]` staging 范式):网络结果落 `static s_pls[NCM_PLAYLIST_MAX]` +staging,**成功后锁内原子发布**(拷回 `g_st.playlists[]` + 更新 `playlist_count`),失败**保留旧快照** +(不改 playlists/playlist_count)。绝不就地清零 UI 可见数组。新增 `g_pls_mu` 发布锁。 + +### 31.2 症状 B:进入第二个歌单显示上一个歌单内容 + +**根因**:`playlist_open/playlist_refresh` 的同步路径不调 `appstate_playlist_reset`——新页先 +`pl_apply(1)` 绑到上一歌单**残留窗口**(`track_ids_count`/`songs`),B 标题下画出 A 的行。 + +**修法**:`playlist_refresh` 起 worker **前**,若目标歌单与当前窗口归属不同则**同步调 +`appstate_playlist_reset()`** 再 `pl_apply`——换歌单必清;同歌单手动刷新(id 相同)保留旧内容 +(stale-while-revalidate)。窗口归属以**歌单 id** 唯一标识(对搜索歌单同样成立——不同搜索结果 = +不同 id,避免搜索 A→搜索 B 仍被 idx 哨兵误判为同一歌单): + +- `appstate.c` 新增 `appstate_playlist_window_set(plid)`/`appstate_playlist_window_is(plid)`( + `g_winpl_mu` 保护);`appstate_playlist_reset` 顺带清空归属。 +- `playlist.c`:`playlist_worker` 首屏发布后(非 STALE)`appstate_playlist_window_set(tpl.id)`; + `playlist_refresh` 在 `!appstate_playlist_window_is(g_tgt.pl.id)` 时同步 reset。 + +### 31.3 验证记录(0 真实 NCM 请求) + +| 项 | 方式 | 结果 | +|---|---|---| +| A 并发读 vs 后台刷 | `bench/playlist_race_sim.c` **新 F**(reader 线程模拟 home_src2 逐行绑定 + writer 反复 `appstate_load_playlists`) | **PASS**:20000 次刷新全程 reader 读不到清零态(`name[0]==0`),最终数组完整 | +| B 开 A→开 B | `bench/playlist_race_sim.c` **新 G**(load A 标记归属=A → 换 B 判归属→reset → 窗口清空 → load B) | **PASS**:切到 B 后 `track_ids_count==0 && song_count==0`(B 页不出现 A 行),B 首行 id 在 B 范围 | +| 既有回归 | window_sim / search_sim(重建,编译 appstate.c) | 全 **PASS** | +| 既有回归 | cache_evict_sim / stream_sim(PCM 金标准 16+resume)/ test_ncm_core 宿主+mipsel | 全 **PASS** | +| 播放链回归 | player_flow_sim(重建 P1-P12) | **ALL PASS** | +| 宿主离屏渲染 | `build_ncm_host.sh` + `NCM_PAGE=playlist/home`(mock) | exit=0,playlist 页 6 行正常(删 ` · ` tofu 后) | +| mipsel 构建 | `bash build_ncm.sh` 自有代码 `-Wall -Wextra` | **0 warning**;`text 997202 / data 1648 / bss 1362468`(bss +~95KB = `s_pls[]` staging) | + +### 31.4 md5(本批改动文件 + 产物) + +``` +dd3d36d5935b85df8d8beabc52a0d090 ncm # mipsel 静态 App(新;未部署,留主会话) +57c20e6e9a4b32631c9b6d5ac30463a6 ncm_host # 宿主离屏调试二进制(重链) +7b1ebcc3688e49fc7b8264e4be7a46df playlist_race_sim # 重链(含新 F/G) +5597307a1a95e06db40451085caae248 window_sim # 重链 +1eb36c7461f9ad47584481780132ea1c search_sim # 重链 +0cecfb3c38483549d83b32775a377c3b player_flow_sim # 重链(补 auto_av stub) +c23f3152df9bf634e8cdd2a0f0b03697 app/logic/appstate.c # §31.1/31.2 +6d678c5f94a413e1daab4b01ef00febe app/ui/playlist.c # §31.2 +cb6fe604e433401302490dc43a3c7891 app/ncm_app_internal.h # 窗口归属 API 声明 +65afad5e3ad75b95cd544d4019f274dd bench/playlist_race_sim.c # 新 F/G + user_playlist 桩 +bed32f8ffccbbfb371556ee5e41cae37 bench/player_flow_sim.c # 新 ncm_api_song_url_auto_av 桩(测试桩) +de4c5e1939c7571b90f75a80f48236c0 build_ncm_host.sh # SDK_SRC 补 meow_ui_str(宿主 I18N 链接) +``` + +### 31.5 出入清单(只改 `patch_v2/ncm_app/`) + +``` +修改: + app/logic/appstate.c # §31.1 staging 发布(appstate_load_playlists)+ §31.2 窗口归属 + app/ui/playlist.c # §31.2 worker 标记归属 + refresh 换歌单清窗 + app/ncm_app_internal.h # appstate_playlist_window_set/is 声明 + bench/playlist_race_sim.c # 新 F/G 用例 + ncm_api_user_playlist/ncm_login_status_check/ + # ncm_ctx_clear_cookie 离线桩 + bench/player_flow_sim.c # 补 ncm_api_song_url_auto_av 桩(§20.3 新符号,测试桩,不改生产) + build_ncm_host.sh # SDK_SRC 补 meow_ui_str(SDK I18N 拆分致宿主链接断,预存) + REPORT.md # 本批记录(§31) +(未动 sdk/、vendor/、core/、meow_launcherd/;未部署真机;0 真实请求) +``` + +### 31.6 诚实出入 / 遗留 + +- **真机未部署未验**(留主会话统一上卡 `dd3d36d5`)。真机验收点:①返回首页各行不再空白(有标题/ + 曲目数);②进第二个歌单不显示上一歌单内容;③断网回首页可离线浏览缓存列表(§31.1 顺带修)。 +- **staging 发布对 lock-free 读侧(home_src2/home_draw)非严格原子**:发布为 valid→valid 覆盖, + `name[0]` 在旧/新快照恒非零,故「清零态」不可能被读到(与本批 F 断言一致);若想彻底消除 + 理论上的一次字段级 torn 混合读,可另给 `g_st.playlists[]` 加读锁(本次未做,保持最小改动)。 +- **`player_flow_sim.c` 补的 `auto_av` 桩**:只为了在 §20.3(HEAD `97b741c`,主会话在做的降级链 + 工作)把 `player_manager.c` 切到 `ncm_api_song_url_auto_av` 之后测试能构建;桩转发到旧 `auto` + 桩、不做档位链语义——`P1-P12` 全过说明该 sim 关注点(播放流/降级可访问性)未受我改动影响。 + §20.3 档位链语义本身的验证属主会话(297 队列项)。 +- **bss +~95KB**:`s_pls[NCM_PLAYLIST_MAX]`(120×808 ≈ 95KB)staging,与既存 `s_build[16384]` + (128KB) 同口径;设备 RAM 56MB 可承受(总 bss 1.36MB)。 diff --git a/patch_v2/ncm_app/app/logic/appstate.c b/patch_v2/ncm_app/app/logic/appstate.c index 8096ee5..0092a8b 100644 --- a/patch_v2/ncm_app/app/logic/appstate.c +++ b/patch_v2/ncm_app/app/logic/appstate.c @@ -34,6 +34,29 @@ int appstate_playlist_begin(int idx) return g; } +/* 当前窗口归属歌单 id(§NEW bug B:新进歌单显示上一个歌单内容)。窗口数据(song_ids/songs/ + * track_ids_count)属于哪个歌单由 id 唯一标识(对搜索歌单同样成立——不同搜索结果 = 不同 id)。 + * worker 首屏发布窗口后调 appstate_playlist_window_set 标记;playlist_refresh 在起 worker 前用 + * appstate_playlist_window_is 判「目标歌单是否=当前窗口归属」,不一致 → 同步 appstate_playlist_reset() + * 清掉上一歌单残留,防 pl_apply(1) 把新歌单标题绑到上一歌单数据(「开 B 显示 A」)。 */ +static pthread_mutex_t g_winpl_mu = PTHREAD_MUTEX_INITIALIZER; +static char g_win_plid[32]; + +void appstate_playlist_window_set(const char *plid) +{ + pthread_mutex_lock(&g_winpl_mu); + if (plid && plid[0]) ncm_strlcpy(g_win_plid, plid, sizeof g_win_plid); + else g_win_plid[0] = 0; + pthread_mutex_unlock(&g_winpl_mu); +} +int appstate_playlist_window_is(const char *plid) +{ + pthread_mutex_lock(&g_winpl_mu); + int same = (plid && plid[0] && g_win_plid[0] && strcmp(g_win_plid, plid) == 0); + pthread_mutex_unlock(&g_winpl_mu); + return same; +} + /* 清窗口/索引(UI 丢弃过期歌单结果时用;播放快照不受影响——播放经 play_song 快照)。 */ void appstate_playlist_reset(void) { @@ -44,6 +67,9 @@ void appstate_playlist_reset(void) g_st.song_count = 0; g_st.track_ids_count = 0; pthread_mutex_unlock(&g_song_mu); + pthread_mutex_lock(&g_winpl_mu); /* 窗口已清 → 归属歌单一并清空(下一 worker 重新标记) */ + g_win_plid[0] = 0; + pthread_mutex_unlock(&g_winpl_mu); } static void appstate_srch_hist_init(void); /* §23:读入本地搜索历史(appstate_init 首调) */ @@ -174,19 +200,38 @@ int appstate_ensure_uid(void) return rc; } +/* 歌单列表 staging 发布锁(§NEW bug A):appstate_load_playlists 的「网络成功后锁内原子发布 + * (拷回 g_st.playlists[] + 更新 playlist_count)」与 UI 线程 home_src2/home_draw 无锁逐行绑定 + * 之间的发布点。持久化(playlists.bin)同单写者(home worker)在发布后读,无相互竞态。 */ +static pthread_mutex_t g_pls_mu = PTHREAD_MUTEX_INITIALIZER; + int appstate_load_playlists(void) { if (!g_st.logged_in) return NCM_ERR_AUTH; int rc = appstate_ensure_uid(); if (rc != NCM_OK) return rc; - memset(g_st.playlists, 0, sizeof g_st.playlists); + /* §NEW bug A(返回首页各行 2~3 秒空白):旧实现在**UI 可见数组**就地 memset→网络回填,与 + * home_src2 无锁逐行绑定竞态——刷新期间 playlist_count 保留旧值(cache),UI 读到被清零的行 + * (无标题/曲目数 0),仅下标 0 因先读幸存。照搬本文件 appstate_load_playlist_songs_impl 的 + * s_build[] staging 范式:网络结果落 staging(s_pls[]),成功后锁内**一次性拷回** g_st.playlists[] + * + 更新 playlist_count(原子发布——UI 全程读到旧或新完整快照,绝不读到清零态);失败保留旧 + * 快照(不改 playlists/playlist_count,断网可离线浏览旧列表)。静态数组:单写者(home worker + * 单飞),免 96KB 栈。 */ + static ncm_playlist_t s_pls[NCM_PLAYLIST_MAX]; int n = 0; - rc = ncm_api_user_playlist(g_st.ctx.device_id, g_st.ctx.cookie, g_st.uid, - NCM_PLAYLIST_MAX, 0, g_st.playlists, NCM_PLAYLIST_MAX, &n); - g_st.playlist_count = (rc == NCM_OK) ? n : 0; - meow_log(MEOW_LOG_INFO, "home: playlists rc=%d count=%d uid=%s", rc, g_st.playlist_count, g_st.uid); - if (rc == NCM_OK) appstate_playlists_cache_save(); /* §26:网络成功 → 覆写盘缓存(stale-while-revalidate) */ - return rc; + int rc2 = ncm_api_user_playlist(g_st.ctx.device_id, g_st.ctx.cookie, g_st.uid, + NCM_PLAYLIST_MAX, 0, s_pls, NCM_PLAYLIST_MAX, &n); + if (rc2 == NCM_OK) { + int cnt = (n > NCM_PLAYLIST_MAX) ? NCM_PLAYLIST_MAX : n; + pthread_mutex_lock(&g_pls_mu); + if (cnt > 0) memcpy(g_st.playlists, s_pls, (size_t)cnt * sizeof(ncm_playlist_t)); + g_st.playlist_count = cnt; /* count 在拷回后更新:读侧绝不见「新 count + 未拷回行」 */ + pthread_mutex_unlock(&g_pls_mu); + appstate_playlists_cache_save(); /* §26:网络成功 → 覆写盘缓存(stale-while-revalidate) */ + } + meow_log(MEOW_LOG_INFO, "home: playlists rc=%d count=%d uid=%s", rc2, + (rc2 == NCM_OK) ? n : g_st.playlist_count, g_st.uid); + return rc2; } /* ---- 歌单列表磁盘缓存(§26,NCM_CACHE_DESIGN §2.2)---- diff --git a/patch_v2/ncm_app/app/ncm_app_internal.h b/patch_v2/ncm_app/app/ncm_app_internal.h index 8da310a..e295608 100644 --- a/patch_v2/ncm_app/app/ncm_app_internal.h +++ b/patch_v2/ncm_app/app/ncm_app_internal.h @@ -358,6 +358,12 @@ int appstate_playlist_open_gen(void); /* 当前打开代次(0=从未 int appstate_playlist_open_idx(void); /* 当前打开目标下标(-2=搜索歌单哨兵;-1=无) */ int appstate_playlist_begin(int idx); /* 递增代次并记目标;返回新代次(UI 打开/刷新时调) */ void appstate_playlist_reset(void); /* 清窗口/索引(UI 丢弃过期结果、在播安全)。加锁。 */ + /* 窗口归属(§NEW bug B:新进歌单显示上一个歌单内容)—— + * worker 首屏发布窗口后 appstate_playlist_window_set 标记 + * 归属歌单 id;playlist_refresh 在换歌单前用 window_is 判 + * 目标≠归属 → 同步 reset 清窗,防「开 B 显示 A」。 */ +void appstate_playlist_window_set(const char *plid); /* 标记当前窗口归属歌单 id */ +int appstate_playlist_window_is(const char *plid); /* 1=窗口归属该歌单(换歌单清窗判据) */ int appstate_load_playlist_songs_ctx(const ncm_playlist_t *pl, int is_search, int ctx_gen, int ctx_idx, int offset, int limit); /* 快照式加载(歌单详情/曲目物化): diff --git a/patch_v2/ncm_app/app/ui/playlist.c b/patch_v2/ncm_app/app/ui/playlist.c index eb2598d..ae8cb66 100644 --- a/patch_v2/ncm_app/app/ui/playlist.c +++ b/patch_v2/ncm_app/app/ui/playlist.c @@ -272,6 +272,10 @@ static void *playlist_worker(void *arg) } g_plloaded_idx = tidx; g_plloaded_gen = tgen; + /* §NEW bug B:首屏发布后标记窗口归属=目标歌单(失败/部分也归属,支持同歌单重试不清窗)。 + * 过期(STALE)不标记——窗口仍属旧歌单。playlist_refresh 据此换歌单清窗。 */ + if (rc != NCM_ERR_STALE) + appstate_playlist_window_set(tpl.id); } else { g_ens_err = rc; /* 补载失败 → snack + 占位行保留(滚动/点按再试) */ g_plloaded_idx = cur_active; @@ -324,6 +328,14 @@ void playlist_refresh(void) g_tgt.idx = g_st.active_playlist; g_tgt.gen = appstate_playlist_begin(g_tgt.idx); pthread_mutex_unlock(&g_plmu); + + /* §NEW bug B(新进歌单显示上一个歌单内容):目标歌单与当前窗口归属不一致(跨歌单/搜索不同 + * 结果)→ 同步清窗口,再 pl_apply。否则 pl_apply(1) 把新歌单标题绑到上一歌单残留窗口 + * (track_ids_count/songs),新页瞬间渲染上一歌单行。同歌单手动刷新(id 相同)不清窗, + * 保留旧内容(stale-while-revalidate)——worker 异步重载替换。 */ + if (!appstate_playlist_window_is(g_tgt.pl.id)) + appstate_playlist_reset(); + g_ens_first = 1; /* 首屏:重建索引 + 物化首段 */ g_pl_stale = 0; g_plloading = 1; diff --git a/patch_v2/ncm_app/bench/player_flow_sim.c b/patch_v2/ncm_app/bench/player_flow_sim.c index 3901ae6..ae2501e 100644 --- a/patch_v2/ncm_app/bench/player_flow_sim.c +++ b/patch_v2/ncm_app/bench/player_flow_sim.c @@ -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) diff --git a/patch_v2/ncm_app/bench/playlist_race_sim.c b/patch_v2/ncm_app/bench/playlist_race_sim.c index 1647b3e..47c5b5f 100644 --- a/patch_v2/ncm_app/bench/playlist_race_sim.c +++ b/patch_v2/ncm_app/bench/playlist_race_sim.c @@ -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 #include #include +#include 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; } diff --git a/patch_v2/ncm_app/build_ncm_host.sh b/patch_v2/ncm_app/build_ncm_host.sh index 809300c..ce3a16b 100755 --- a/patch_v2/ncm_app/build_ncm_host.sh +++ b/patch_v2/ncm_app/build_ncm_host.sh @@ -20,7 +20,7 @@ echo "==> host SDK objects" SDK_SRC="meow_ui_gfx meow_ui_sys meow_ui_theme meow_ui_vol meow_ui_log meow_ui_widget \ meow_ui_input meow_ui_activity meow_ui_dialog meow_ui_ime meow_ui_draw meow_ui_view \ meow_ui_img meow_ui_config meow_ui_nav meow_ui_media meow_notify_ipc meow_pinyin \ - meow_ui_audio meow_ui_anim meow_ui_box meow_ui_scroll" + meow_ui_audio meow_ui_anim meow_ui_box meow_ui_scroll meow_ui_str" for f in $SDK_SRC; do $CC_HOST -O2 -c -DMEOW_UI_HOST_TEST -I"$SDK_DIR" -I"$ROOT/patch_v2/meow_launcherd" \ "$SDK_DIR/$f.c" -o "$OUT/host/sdk_$f.o" || { echo "sdk $f FAILED"; exit 1; } diff --git a/patch_v2/ncm_app/ncm b/patch_v2/ncm_app/ncm index e0d37bd..b41cd83 100755 Binary files a/patch_v2/ncm_app/ncm and b/patch_v2/ncm_app/ncm differ diff --git a/patch_v2/ncm_app/ncm_host b/patch_v2/ncm_app/ncm_host index b9ad0f5..6593ef8 100755 Binary files a/patch_v2/ncm_app/ncm_host and b/patch_v2/ncm_app/ncm_host differ