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

+91
View File
@@ -2633,3 +2633,94 @@ b731654a06d5ca5746edea8393f8668f ncm_host # 宿主调试二进
- **`parse_url` http→443 潜性缺陷**:`http://` URL 无显式端口时 `url.port` 恒 443(TLS),对 - **`parse_url` http→443 潜性缺陷**:`http://` URL 无显式端口时 `url.port` 恒 443(TLS),对
music.126.net CDN(443 上提供 TLS)恰好可用;语义错误但未阻断,未改(改动有回归风险)。 music.126.net CDN(443 上提供 TLS)恰好可用;语义错误但未阻断,未改(改动有回归风险)。
- **未动 sdk/、vendor/**;`ncm_host`/`test_ncm_core_host` 为宿主调试产物随批重编。 - **未动 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)。
+52 -7
View File
@@ -34,6 +34,29 @@ int appstate_playlist_begin(int idx)
return g; 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 快照)。 */ /* 清窗口/索引(UI 丢弃过期歌单结果时用;播放快照不受影响——播放经 play_song 快照)。 */
void appstate_playlist_reset(void) void appstate_playlist_reset(void)
{ {
@@ -44,6 +67,9 @@ void appstate_playlist_reset(void)
g_st.song_count = 0; g_st.song_count = 0;
g_st.track_ids_count = 0; g_st.track_ids_count = 0;
pthread_mutex_unlock(&g_song_mu); 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 首调) */ static void appstate_srch_hist_init(void); /* §23:读入本地搜索历史(appstate_init 首调) */
@@ -174,19 +200,38 @@ int appstate_ensure_uid(void)
return rc; 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) int appstate_load_playlists(void)
{ {
if (!g_st.logged_in) return NCM_ERR_AUTH; if (!g_st.logged_in) return NCM_ERR_AUTH;
int rc = appstate_ensure_uid(); int rc = appstate_ensure_uid();
if (rc != NCM_OK) return rc; 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; int n = 0;
rc = ncm_api_user_playlist(g_st.ctx.device_id, g_st.ctx.cookie, g_st.uid, int rc2 = 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); NCM_PLAYLIST_MAX, 0, s_pls, NCM_PLAYLIST_MAX, &n);
g_st.playlist_count = (rc == NCM_OK) ? n : 0; if (rc2 == NCM_OK) {
meow_log(MEOW_LOG_INFO, "home: playlists rc=%d count=%d uid=%s", rc, g_st.playlist_count, g_st.uid); int cnt = (n > NCM_PLAYLIST_MAX) ? NCM_PLAYLIST_MAX : n;
if (rc == NCM_OK) appstate_playlists_cache_save(); /* §26:网络成功 → 覆写盘缓存(stale-while-revalidate) */ pthread_mutex_lock(&g_pls_mu);
return rc; 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)---- /* ---- 歌单列表磁盘缓存(§26,NCM_CACHE_DESIGN §2.2)----
+6
View File
@@ -358,6 +358,12 @@ int appstate_playlist_open_gen(void); /* 当前打开代次(0=从未
int appstate_playlist_open_idx(void); /* 当前打开目标下标(-2=搜索歌单哨兵;-1=无) */ int appstate_playlist_open_idx(void); /* 当前打开目标下标(-2=搜索歌单哨兵;-1=无) */
int appstate_playlist_begin(int idx); /* 递增代次并记目标;返回新代次(UI 打开/刷新时调) */ int appstate_playlist_begin(int idx); /* 递增代次并记目标;返回新代次(UI 打开/刷新时调) */
void appstate_playlist_reset(void); /* 清窗口/索引(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 appstate_load_playlist_songs_ctx(const ncm_playlist_t *pl, int is_search,
int ctx_gen, int ctx_idx, int offset, int limit); int ctx_gen, int ctx_idx, int offset, int limit);
/* 快照式加载(歌单详情/曲目物化): /* 快照式加载(歌单详情/曲目物化):
+12
View File
@@ -272,6 +272,10 @@ static void *playlist_worker(void *arg)
} }
g_plloaded_idx = tidx; g_plloaded_idx = tidx;
g_plloaded_gen = tgen; g_plloaded_gen = tgen;
/* §NEW bug B:首屏发布后标记窗口归属=目标歌单(失败/部分也归属,支持同歌单重试不清窗)。
* 过期(STALE)不标记——窗口仍属旧歌单。playlist_refresh 据此换歌单清窗。 */
if (rc != NCM_ERR_STALE)
appstate_playlist_window_set(tpl.id);
} else { } else {
g_ens_err = rc; /* 补载失败 → snack + 占位行保留(滚动/点按再试) */ g_ens_err = rc; /* 补载失败 → snack + 占位行保留(滚动/点按再试) */
g_plloaded_idx = cur_active; g_plloaded_idx = cur_active;
@@ -324,6 +328,14 @@ void playlist_refresh(void)
g_tgt.idx = g_st.active_playlist; g_tgt.idx = g_st.active_playlist;
g_tgt.gen = appstate_playlist_begin(g_tgt.idx); g_tgt.gen = appstate_playlist_begin(g_tgt.idx);
pthread_mutex_unlock(&g_plmu); 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_ens_first = 1; /* 首屏:重建索引 + 物化首段 */
g_pl_stale = 0; g_pl_stale = 0;
g_plloading = 1; g_plloading = 1;
+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; 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(模拟预缓冲前网络失败) */ 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, 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) size_t maxb, ncm_http_resp_t *resp, int *status_out)
+118 -1
View File
@@ -9,7 +9,11 @@
* 窗口写进全局(修【5194 首 3 首歌/串数据/加载失败】);打开时快照 plid 免疫 home_worker * 窗口写进全局(修【5194 首 3 首歌/串数据/加载失败】);打开时快照 plid 免疫 home_worker
* 对 g_st.playlists[] 的并发 memset 污染; * 对 g_st.playlists[] 的并发 memset 污染;
* D. 歌单列表磁盘缓存:appstate_playlists_cache_save/load 回环(断网离线浏览秒开); * 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/不落盘)。 * 风控纪律:0 次真实 NCM 请求(离线 stub;songmeta 全 miss/不落盘)。
* *
* 编译(宿主,ncm_app 目录下): * 编译(宿主,ncm_app 目录下):
@@ -25,6 +29,7 @@
#include <string.h> #include <string.h>
#include <stdlib.h> #include <stdlib.h>
#include <time.h> #include <time.h>
#include <pthread.h>
ncm_app_state_t g_st; ncm_app_state_t g_st;
ncm_play_run_t g_play = { .mu = PTHREAD_MUTEX_INITIALIZER, .state = NCM_PC_STOPPED, .cur_song = -1 }; 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_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; } 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 ---- */ /* ---- 可配置网络 stub ---- */
static int g_pl_total = 92; /* 当前歌单返回的 trackIds 总数 */ static int g_pl_total = 92; /* 当前歌单返回的 trackIds 总数 */
static int64_t g_id_base = 1000000; 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; 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; static int g_fail = 0;
#define CHECK(cond, fmt, ...) do { \ #define CHECK(cond, fmt, ...) do { \
if (cond) printf(" PASS " fmt "\n", ##__VA_ARGS__); \ if (cond) printf(" PASS " fmt "\n", ##__VA_ARGS__); \
else { printf(" FAIL " fmt "\n", ##__VA_ARGS__); g_fail = 1; } \ else { printf(" FAIL " fmt "\n", ##__VA_ARGS__); g_fail = 1; } \
} while (0) } 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) 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); 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); "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"); printf("%s\n", g_fail ? "RESULT: FAIL" : "RESULT: PASS");
return g_fail ? 1 : 0; return g_fail ? 1 : 0;
} }
+1 -1
View File
@@ -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 \ 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_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_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 for f in $SDK_SRC; do
$CC_HOST -O2 -c -DMEOW_UI_HOST_TEST -I"$SDK_DIR" -I"$ROOT/patch_v2/meow_launcherd" \ $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; } "$SDK_DIR/$f.c" -o "$OUT/host/sdk_$f.o" || { echo "sdk $f FAILED"; exit 1; }
Binary file not shown.
Binary file not shown.