fix(presenter): 回落日志两因并列 + 老 ext 档案覆盖 + 死根档案边界用例(M274 r3)
This commit is contained in:
1 parent
70c6aec6dd
commit
72137dc658
3 files changed
+149
-2
No files matched your search
@@ -238,10 +238,14 @@ pub(in crate::host) fn game_for(env: &HostEnv, id: &str) -> GameLayout {
|
|||||||
return game.clone();
|
return game.clone();
|
||||||
}
|
}
|
||||||
if crate::dto::is_discovered_instance(id) {
|
if crate::dto::is_discovered_instance(id) {
|
||||||
|
// 两因并列,**不要**只写后一种:`ext-` 前缀在 M273 时代就有,那时 id 只哈希版本目录名
|
||||||
|
// (没有根),所以「M274 之前合成的老档案」**永远**反查不到——那是所有老用户的常态,
|
||||||
|
// 不是故障。只写「清单半写/被锁」会让每次点开详情页都往日志里加一条像是在报错的假警报
|
||||||
|
// (M274 r3 复审 P2-1)。
|
||||||
unlucky_core::launch::launcher_log::record(
|
unlucky_core::launch::launcher_log::record(
|
||||||
"multiroot",
|
"multiroot",
|
||||||
&format!(
|
&format!(
|
||||||
"实例 {id} 反查不到所属根(版本此刻不可登记:清单半写/被锁/目录已不在)⇒ 按配置根 {} 解析",
|
"实例 {id} 反查不到所属根(老档案:M273 之前合成,id 不含根;或该版本此刻不可登记:清单半写/被锁/目录已不在)⇒ 按配置根 {} 解析",
|
||||||
env.game.root().display()
|
env.game.root().display()
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
//! | [`the_same_version_in_two_roots_is_two_instances`] | 两个根里的同名版本 = **2 条**磁贴、id 不同、各自指向自己的根、名字分辨得出来 | 真实官方安装的目录形状(`launcher_profiles.json` / 加密的账户文件一概不读) |
|
//! | [`the_same_version_in_two_roots_is_two_instances`] | 两个根里的同名版本 = **2 条**磁贴、id 不同、各自指向自己的根、名字分辨得出来 | 真实官方安装的目录形状(`launcher_profiles.json` / 加密的账户文件一概不读) |
|
||||||
//! | [`a_junction_to_the_configured_root_is_not_a_second_root`] | 同一路径的第二种写法(junction / 符号链接)**不许**让同一批实例列两遍 | 8.3 短名(用例盘上造不出来;判据靠 `canonicalize` 的长名展开,见 `roots::key_of` 的文档) |
|
//! | [`a_junction_to_the_configured_root_is_not_a_second_root`] | 同一路径的第二种写法(junction / 符号链接)**不许**让同一批实例列两遍 | 8.3 短名(用例盘上造不出来;判据靠 `canonicalize` 的长名展开,见 `roots::key_of` 的文档) |
|
||||||
//! | [`saves_come_from_the_instance_own_root`] | 每个实例的存档 / 截图列表只来自**它自己那个根**(不许把外部实例的存档当成当前根的) | 游戏进程正在跑时的文件锁 / 半写状态(未处理,见报告) |
|
//! | [`saves_come_from_the_instance_own_root`] | 每个实例的存档 / 截图列表只来自**它自己那个根**(不许把外部实例的存档当成当前根的) | 游戏进程正在跑时的文件锁 / 半写状态(未处理,见报告) |
|
||||||
//! | [`a_lazily_created_profile_stays_on_its_own_root`] | 懒创建档案(用户第一次改设置)之后 id → 根的反查不退化,且没把配置根的同名版本一并抹掉 | 档案里**没有**根字段这一事实本身(根只从 id 反查,见 `discovered::locate`) |
|
//! | [`a_lazily_created_profile_stays_on_its_own_root`] | 懒创建档案(用户第一次改设置)之后 id → 根的反查不退化,且没把配置根的同名版本一并抹掉 | 档案里**没有**根字段这一事实本身(根只从 id 反查,见 `discovered::Found`) |
|
||||||
//! | [`the_launch_reads_the_manifest_from_the_instance_root`] | 启动装配的清单 / 运行目录走实例自己的根 | 真的把游戏拉起来(要真 jvm / 账号 / 网络) |
|
//! | [`the_launch_reads_the_manifest_from_the_instance_root`] | 启动装配的清单 / 运行目录走实例自己的根 | 真的把游戏拉起来(要真 jvm / 账号 / 网络) |
|
||||||
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|||||||
@@ -161,3 +161,146 @@ fn both_same_named_instances_take_settings_in_either_order() {
|
|||||||
assert_eq!(profiles.profiles.len(), 2, "两条实例各一份档案,不多不少");
|
assert_eq!(profiles.profiles.len(), 2, "两条实例各一份档案,不多不少");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------- 反查回落(M274 r3 复审 P2-1 / ②)
|
||||||
|
|
||||||
|
/// M273 时代的 `ext-` id:**只哈希版本目录名**(不含根)。M274 之后这种 id 永远反查不到。
|
||||||
|
fn m273_external_id(version_id: &str) -> String {
|
||||||
|
let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
|
||||||
|
for byte in version_id.as_bytes() {
|
||||||
|
hash ^= u64::from(*byte);
|
||||||
|
hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
|
||||||
|
}
|
||||||
|
format!("ext-{hash:016x}")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 在**我们自己的日志目录**里数这个实例的回落行(重试到「登记窗口没被并行用例抢走」为止)。
|
||||||
|
///
|
||||||
|
/// `launcher_log` 的目录是**进程级全局**(`set_dir` 以最后一次为准),而 presenter 的用例并行跑、
|
||||||
|
/// 七十多处 `HostEnv::at_root` 都会重新登记。所以这里每次尝试换一个**新目录**:抢到了就能在
|
||||||
|
/// 一个全新文件里看到**恰好一行**;没抢到(文件不存在)就再试。抢不到 64 次视为用例失败(不当跳过)。
|
||||||
|
fn fallback_lines(env: &HostEnv, id: &str) -> Vec<String> {
|
||||||
|
let file = unlucky_core::launch::launcher_log::FILE_NAME;
|
||||||
|
let mut saw_file = false;
|
||||||
|
for attempt in 0..64 {
|
||||||
|
let dir = std::env::temp_dir().join(format!(
|
||||||
|
"ull-host-multiroot3-log-{}-{attempt}-{}",
|
||||||
|
std::process::id(),
|
||||||
|
std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.map(|elapsed| elapsed.as_nanos())
|
||||||
|
.unwrap_or(0)
|
||||||
|
));
|
||||||
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
|
unlucky_core::launch::launcher_log::set_dir(&dir);
|
||||||
|
let _ = game_for(env, id);
|
||||||
|
let Ok(text) = std::fs::read_to_string(dir.join(file)) else {
|
||||||
|
// 文件没建出来:这一拍的登记窗口被并行用例抢走了(它们也会 `set_dir`),再试。
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let lines: Vec<String> = text
|
||||||
|
.lines()
|
||||||
|
.filter(|line| line.contains(id))
|
||||||
|
.map(str::to_string)
|
||||||
|
.collect();
|
||||||
|
std::fs::remove_dir_all(&dir).ok();
|
||||||
|
if !lines.is_empty() {
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
// 文件在、却没有我们的行:可能是别人的行占了这个文件,也可能这条路径**根本没写**。
|
||||||
|
// 再试几次,全都不中就当「0 行」交给断言去红(比在这里 panic 好读)。
|
||||||
|
saw_file = true;
|
||||||
|
}
|
||||||
|
if saw_file {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
panic!("64 次都没看到日志文件:回落日志没写,或登记窗口每次都被并行用例抢走");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// **老 `ext-` 档案**(M273 形状)反查不到 ⇒ 落配置根,并且日志里**恰好一行** `[multiroot]`,
|
||||||
|
/// 文案**两因并列**(老档案 ‖ 此刻不可登记)。
|
||||||
|
///
|
||||||
|
/// 这条路径此前零覆盖(M274 r3 复审指出):它是所有 M273 老用户升级后的**常态**,不是故障;
|
||||||
|
/// 文案只写「清单半写/被锁/目录已不在」会让每次点开详情页都往日志里加一条假警报。
|
||||||
|
///
|
||||||
|
/// 变异证明:把文案只留「此刻不可登记」那半句 ⇒ 下面的关键词断言红;把回落日志整段删掉 ⇒
|
||||||
|
/// 「恰好一行」红(0 行)。
|
||||||
|
#[test]
|
||||||
|
fn an_old_m273_instance_falls_back_with_one_two_cause_log_line() {
|
||||||
|
let (_root, env) = two_roots("ull-host-multiroot3-fallback");
|
||||||
|
write_version(&env.game, "A");
|
||||||
|
let old_id = m273_external_id("A");
|
||||||
|
assert!(
|
||||||
|
crate::dto::is_discovered_instance(&old_id),
|
||||||
|
"老 id 也带 `ext-` 前缀(这正是「永远反查不到」的来源)"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
game_for(&env, &old_id).root(),
|
||||||
|
env.game.root(),
|
||||||
|
"老档案反查不到 ⇒ 只能按配置根解析"
|
||||||
|
);
|
||||||
|
let lines = fallback_lines(&env, &old_id);
|
||||||
|
assert_eq!(
|
||||||
|
lines.len(),
|
||||||
|
1,
|
||||||
|
"一次回落只该写一行(多了会把日志刷成噪声):{lines:?}"
|
||||||
|
);
|
||||||
|
let line = &lines[0];
|
||||||
|
assert!(line.contains("[multiroot]"), "分类名要可 grep:{line}");
|
||||||
|
assert!(
|
||||||
|
line.contains("老档案"),
|
||||||
|
"M273 老 id 是**常态**,文案必须并列这一因,不能只说「清单半写/被锁」:{line}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
line.contains("不可登记"),
|
||||||
|
"另一因(官方启动器正在更新)也要在:{line}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// **已知边界**(不是设计意图):官方根的版本目录消失后,那条死档案会「吞掉」配置根的同名版本。
|
||||||
|
///
|
||||||
|
/// 现场(M274 r3 复审 ②):两根各一个 `A`,官方那条先懒创建;官方根的 `versions/` 随后不见
|
||||||
|
/// (官方启动器升级 / 清理 / 卸载 / 换盘)。此时:
|
||||||
|
/// - 列表只剩 **1 条**,名字还写着 `A · …\official\.minecraft`(指向官方根);
|
||||||
|
/// - 而 `version_present` / 读盘路径却全从**配置根**读出来(`game_for` 回落);
|
||||||
|
/// - ⇒ 真正的配置根那条 `A` **从列表消失**(被 `bound` 判成「已被档案占住」)。
|
||||||
|
///
|
||||||
|
/// 这是 r1 判 P1 的那种「显示与行为分叉」的反向形态。**这轮不改结构**:真正的修法是给
|
||||||
|
/// `InstanceProfile` 加「根」字段(`profiles.json` schema + 迁移),代价超出本 mission。
|
||||||
|
/// 所以这条用例钉的是**现状**——将来做 schema 迁移时它会红,届时要跟着改成「两条都在」。
|
||||||
|
#[test]
|
||||||
|
fn a_dead_official_root_is_swallowed_by_its_stale_profile() {
|
||||||
|
let (_root, env) = two_roots("ull-host-multiroot3-deadroot");
|
||||||
|
write_version(&env.game, "A");
|
||||||
|
write_version(&GameLayout::new(official(&env)), "A");
|
||||||
|
|
||||||
|
let official_id = tile_id(&env, |tile| tile.name.contains("official"));
|
||||||
|
set_isolation(&env, &official_id, "shared").expect("官方那条先懒创建档案");
|
||||||
|
// 官方启动器把那个版本清理掉了(目录整个不见)。
|
||||||
|
std::fs::remove_dir_all(GameLayout::new(official(&env)).versions_dir())
|
||||||
|
.expect("删官方 versions");
|
||||||
|
|
||||||
|
let snapshot = instances(&env);
|
||||||
|
assert_eq!(
|
||||||
|
snapshot.len(),
|
||||||
|
1,
|
||||||
|
"现状:只有档案那一条(配置根的同名版本被 bound 吞掉):{snapshot:?}"
|
||||||
|
);
|
||||||
|
let tile = &snapshot[0];
|
||||||
|
assert_eq!(tile.id, official_id);
|
||||||
|
assert!(
|
||||||
|
tile.name.contains("official"),
|
||||||
|
"名字还指着**已经不存在的**官方根:{tile:?}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
tile.version_present && game_for(&env, &tile.id).root() == env.game.root(),
|
||||||
|
"而读盘/显示已经悄悄落回配置根(显示与行为分叉的反向形态):{tile:?}"
|
||||||
|
);
|
||||||
|
let dir = super::super::saves::run_dir(&env, tile).expect("派生运行目录");
|
||||||
|
assert_eq!(
|
||||||
|
dir,
|
||||||
|
env.game.root(),
|
||||||
|
"存档也落回配置根——这就是「死档案吞掉同名版本」的后果"
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in new issue
Block a user