三条 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)。
129 lines
6.7 KiB
Rust
129 lines
6.7 KiB
Rust
//! 快照条目的**现在**:这一条还在不在盘上(M304)。
|
||
//!
|
||
//! ## 为什么需要
|
||
//!
|
||
//! M304 把「点启动」的实例解析从「同步扫一遍 [`super::super::instances`]」改成「查已发布快照」。
|
||
//! 快照是**过去**的一拍:用户可能在别处(文件管理器 / 另一个启动器 / 同一会话的另一条命令)
|
||
//! 把这个实例删了、或者把它绑的版本改了。照旧解析就会「按快照把它拉起来」——拉的是**已经不
|
||
//! 存在的东西**,最坏的一种是 `game_for` 反查不到那个根、退回配置根,于是**启动的是另一个根里
|
||
//! 同名版本**(用户看着 A,起来的却是 B)。
|
||
//!
|
||
//! ## 判据(单实例,不扫 `versions/`)
|
||
//!
|
||
//! 一次只判**这一条**,所以绝不能再用 `instances()`(那是全量扫盘:官方根 200 个版本 236 ms,
|
||
//! 叠加外部档案 1.35 s)——那等于把「点了就受理」换成「点了等一扫」。两条判据各用最便宜的
|
||
//! 那个事实:
|
||
//!
|
||
//! | 实例 | 判据 | 成本 |
|
||
//! |---|---|---|
|
||
//! | 档案实例(id 不是 `ext-`) | `profiles.json` 里还有这条 id,且 `version_id` 没被改过 | 一次小文件读 |
|
||
//! | 自动发现的(`ext-`) | 逐个根重算 id(哈希)定位到 (根, 版本),再用 core 的 `adopt::inspect` 判这一个版本 | #根 次哈希 + 一次单版本检查 |
|
||
//!
|
||
//! ## 反查落空的两种情形,必须分开(r1 复审 P2-1)
|
||
//!
|
||
//! `roots::all` 对非配置根是 `if !extra.is_dir() { continue; }`(只有配置根无条件入列)。于是
|
||
//! 「官方根被删 / 改名 / 盘没挂上 / `canonicalize` 变了」时,那个 `ext-` id 一个根都对不上——
|
||
//! 若照旧放行,`build_request` → `game_for`(`discovered::game_for` 的回落)会退回**配置根**,
|
||
//! 配置根里恰好有同名 `version_id` 就把**另一份**拉起来(gameDir / 存档都在另一边)。这是相对
|
||
//! 改前(同步扫盘 ⇒ 扫不出来 ⇒ 拒绝「找不到实例」)的回归,所以必须分开判:
|
||
//!
|
||
//! - **M273 老档案**(id 只哈希版本目录名 ⇒ `legacy_external_instance_id` 命中):**放行**——
|
||
//! 那种 id 的语义本来就是「按配置根解析」,真没版本时 `build_request` 给具名阶段失败;
|
||
//! - **所属根不见了**(删 / 改名 / 盘没挂上 / `canonicalize` 变了):**拒绝** + 具名原因,并由
|
||
//! 调用方立刻重扫(`Rejection::stale`)。
|
||
//!
|
||
//! 放行老档案不是偷懒:改前那条同步扫盘也不会拦它——`version_present: false` 的磁贴在旧实现里
|
||
//! 照样能点、照样走到同一条具名失败(`VersionManifest` + 路径),进程一个都没起来。
|
||
//!
|
||
//! ## 为什么挂在 [`super::Scan`] 上
|
||
//!
|
||
//! [`super::super::discovered`] / [`super::super::roots`] 是 `game` 的私有子模块,`host` 其它地方
|
||
//! 只够得着 `game` 转出的那几个名字(`Scan` / `instances` / `game_for` …)。`impl` 块可以写在任何
|
||
//! 模块里,于是这一层用关联函数挂在 [`super::Scan`] 上——语义也对:被判的正是「这份扫描账里的
|
||
//! 那一条现在还成不成立」。
|
||
|
||
use unlucky_core::game::adopt;
|
||
use unlucky_core::game::install::instance::read_profiles;
|
||
|
||
use crate::dto::InstanceSummary;
|
||
|
||
use super::super::super::HostEnv;
|
||
use super::super::discovered::{external_instance_id, legacy_external_instance_id};
|
||
use super::super::roots;
|
||
|
||
impl super::Scan {
|
||
/// 这条快照条目**现在**还在不在盘上;不在就给一句给用户看的具名原因。
|
||
///
|
||
/// 只判这一条(成本见模块文档的表格),**不重扫 `versions/`**:调用方是「点启动」那条同步
|
||
/// 路径,它必须便宜到能在同一拍里给出 Busy。
|
||
///
|
||
/// # Errors
|
||
///
|
||
/// 返回的字符串是**给人读的原因**(presenter 没有 i18n 依赖,文案在这里拼,与 `toast` 同一种
|
||
/// 形状):实例清单里没了 / 版本被改过 / 版本目录已不可用。调用方负责拒绝受理并重扫一遍。
|
||
pub(in crate::host) fn revalidate(
|
||
env: &HostEnv,
|
||
instance: &InstanceSummary,
|
||
) -> Result<(), String> {
|
||
if crate::dto::is_discovered_instance(&instance.id) {
|
||
return discovered(env, instance);
|
||
}
|
||
profile(env, instance)
|
||
}
|
||
}
|
||
|
||
/// 档案实例:`profiles.json` 里还有这条 id,且绑的版本没被改过。
|
||
///
|
||
/// 「版本被改过」也算过期:用户改的是**同一个档案**,但屏幕上那条磁贴写着旧版本、启动链也会按
|
||
/// 旧版本装配——那不是他刚才看中的那一条。
|
||
fn profile(env: &HostEnv, instance: &InstanceSummary) -> Result<(), String> {
|
||
let profiles =
|
||
read_profiles(&env.layout).map_err(|error| format!("读实例清单失败:{error}"))?;
|
||
let Some(profile) = profiles
|
||
.profiles
|
||
.iter()
|
||
.find(|profile| profile.id.as_str() == instance.id)
|
||
else {
|
||
return Err(format!(
|
||
"实例 `{}` 已不在实例清单里(可能刚在别处被删掉),列表已过期",
|
||
instance.name
|
||
));
|
||
};
|
||
if profile.version_id != instance.version_id {
|
||
return Err(format!(
|
||
"实例 `{}` 绑的版本已被改成 `{}`(列表里还是 `{}`),列表已过期",
|
||
instance.name, profile.version_id, instance.version_id
|
||
));
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
/// 自动发现的实例:id 是 (根, 版本) 的哈希,逐个根重算一遍定位,再判这一个版本。
|
||
///
|
||
/// 反查不到根 ⇒ 放行(M273 老档案,理由见模块文档):不猜、也不在这里改成「拒绝」,
|
||
/// 否则所有老用户的 `ext-` 磁贴会集体点不动。
|
||
fn discovered(env: &HostEnv, instance: &InstanceSummary) -> Result<(), String> {
|
||
for root in roots::all(env) {
|
||
if external_instance_id(root.key(), &instance.version_id) != instance.id {
|
||
continue;
|
||
}
|
||
return adopt::inspect(root.game(), &env.layout, &instance.version_id)
|
||
.map(|_| ())
|
||
.map_err(|error| {
|
||
format!(
|
||
"实例 `{}` 的版本 `{}` 已不可用({error}),列表已过期",
|
||
instance.name, instance.version_id
|
||
)
|
||
});
|
||
}
|
||
// 反查落空:老档案放行,**根不见了就拒绝**(理由见模块文档——放行会让 `game_for` 退回配置根,
|
||
// 把另一个根里同名的那个版本拉起来)。
|
||
if legacy_external_instance_id(&instance.version_id) == instance.id {
|
||
return Ok(());
|
||
}
|
||
Err(format!(
|
||
"实例 `{}` 所属的游戏目录已不在(被移走 / 改名 / 盘没挂上),列表已过期",
|
||
instance.name
|
||
))
|
||
}
|