Commit Graph
2 Commits
Author SHA1 Message Date
AskaEth 2fb61d8006 fix(launch): M304 r1 —— 根不见了必须拒绝 + 运行时态落进快照 + 真 host 端到端同拍判据
三条 r1 复审 P2,全部按代码修(不是只改注释)。

**P2-1(唯一实质问题)**:`ext-` 实例「所属根消失 / 改名」被静默放行 ⇒ 可能起错副本。
`live.rs::discovered` 反查落空时原来一律 `Ok(())`;而 `roots::all` 对非配置根是
`if !extra.is_dir() { continue; }`,于是官方根被删 / 改名 / 盘没挂上时反查必然落空 ——
放行之后 `build_request` → `game_for` 会退回配置根,配置根里恰好有同名 `version_id`
就把**另一份**拉起来(gameDir / 存档都在另一边),相对改前(同步扫盘 ⇒ 扫不出来 ⇒ 拒绝)
是回归。改法:把 M273 的「只哈希版本目录名」搬进 `discovered.rs`
(`legacy_external_instance_id`,与 `external_instance_id` 共用同一个多段 FNV),反查落空时
**老档案放行、其余一律拒绝**并给具名原因(+ `stale` 触发重扫)。
另加 `game::tests::instance_id_hashes_are_the_released_ones`:两个哈希公式的字面量钉死
——这次把哈希抽成公共函数,公式一漂就会误杀所有存量 `ext-` 档案。

**P2-2**:`send()` 只认 `InstancesChanged`,而 app 侧 `state.rs:274` 把
`InstanceRuntimeChanged` **就地**写进自己的列表 ⇒ `runtime` 这一位必定分叉,而
`accept::running_pid` 的兜底读的正是它。改:`send()` 同时把运行时态落到 `published`
对应条目,并加用例 `runtime_events_land_in_the_published_snapshot`。

**P2-3**:补真 `CoreHost` + 真 app 的端到端判据(`app/update/instances/scan_phase.rs`):
临时数据根 + 真 host,`advance` 到首份快照落地后点启动,**`advance` 之前**详情页必须已
`can_stop`。变异(把 Busy 挪进启动线程、只有回程才发)实测必红。

判据(新增 3 条 + 1 条钉公式):`revalidate_tests`(根不见了 ⇒ 拒绝)、
`target_tests`(运行时态落快照)、`scan_phase`(真 host 同一拍)、`game::tests`(哈希公式)。
用例夹具按 400 行硬线拆到 `launch/fixtures.rs`。

门禁:`cargo test -p unlucky-presenter -p unlucky-app-iced --all-targets` 全绿
(426 + 1076)· `clippy --workspace --all-targets -- -D warnings` 0 ·
`fmt --all --check` 0 · 行数全 ≤400(最大 366;未碰的 `app/update/instances.rs` 399)。
2026-11-13 20:46:00 +08:00
AskaEth 583a6cf24b perf(launch): M304 —— 点启动/停止不再在 UI 线程扫盘
M302 把 `refresh_instances` 的扫盘搬进了后台线程,但「点 Play」那条路上还留着两次
`game::instances(&self.env)`(`launch_target` 与 `running_pid`):官方根 200 个版本
236 ms、叠加 10 份外部档案 1.35 s,全部落在 UI 线程上。

改法:两条查询都读**已发布快照**(`CoreHost::send` 发 `InstancesChanged` 时留一份),
并在受理那一刻补一道**单实例**复核(`game::Scan::revalidate`),于是

- 调用方线程一次盘都不扫(一次内存查表 + 微秒级复核);
- 快照过期不会拉错东西:档案实例查 `profiles.json` 是否还在、版本有没有被改;
  自动发现的 `ext-` 实例用「逐个根重算 id」定位 (根, 版本),再让 `adopt::inspect`
  只判这一个版本(不扫 `versions/`)。复核不过 ⇒ 拒绝 + 具名原因 + 立刻重扫。
- 同名不猜:名字兜底只在唯一命中时用,两个同名实例直接拒绝(多根之后很常见)。
- 「点下去立刻有反馈」不变:`Busy` 仍在 `dispatch` 返回前发出(`launch::accept`)。

拆分:`launch_target` / `in_flight` / `mark_pending` / `start_launch` / `running_pid`
从 `host/mod.rs`(原 395/400 行)搬进 `host/launch/accept.rs`;解析与拒绝形状在
`host/launch/target.rs`,单实例复核在 `host/game/refresh/live.rs`。

判据(`host/launch/target_tests.rs`,5 条):
- 同一拍进 Busy(无 tick、无后台回程),且受理不派扫描(代次不变);
- 快照落地后**新增**的实例点启动必须被拒绝(解析改回现场扫盘 ⇒ 红,实测);
- 快照落地后**删掉**的实例点启动必须拒绝并给具名原因(去掉复核 ⇒ 红,实测);
- 两个同名实例不猜;停止读快照里的 pid。

门禁:`cargo test -p unlucky-presenter -p unlucky-app-iced --all-targets`(423 + 1075 全绿)、
`cargo clippy --workspace --all-targets -- -D warnings`(0)、`cargo fmt --all --check`(0)、
文件行数全 ≤400(`host/mod.rs` 340、`refresh.rs` 382、`launch/` 90/114/242/280)。
2026-11-13 20:14:37 +08:00